carlos / platform Public

settings-4 task 2: release.Target, settings spec §4.3

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 9ac7317d1ee6c01fb624bed8d080565913ff4112 parent fb1dc99
5 files changed, +385 −10
  • internal/release/buildstate_test.go +2 −3
  • internal/release/updatetarget.go +187 −0
  • internal/release/updatetarget_test.go +166 −0
  • internal/updatewords/words_approved_test.go +13 −7
  • internal/updatewords/words_box.go +17 −0
diff --git a/internal/release/buildstate_test.go b/internal/release/buildstate_test.go
index 06acc9315..366d4d687 100644
--- a/internal/release/buildstate_test.go
+++ b/internal/release/buildstate_test.go
@@ -19,14 +19,13 @@ func TestConsumedRoundTrip(t *testing.T) {
if c, err := ReadConsumed(opt, "a.example.com"); c != nil || err != nil {
t.Fatalf("absent = %+v, %v", c, err)
}
- // Reason is the plain string "request"; Task 2 gives it a named constant.
want := Consumed{Schema: 1, Host: "a.example.com", Resolved: &ResolvedRequest{ID: 3, Target: "v2"}, Completed: 2,
- Desired: "v2", Reason: "request", At: time.Now().UTC().Truncate(time.Second)}
+ Desired: "v2", Reason: string(ReasonRequest), At: time.Now().UTC().Truncate(time.Second)}
if err := WriteConsumed(opt, want); err != nil {
t.Fatal(err)
}
got, err := ReadConsumed(opt, "a.example.com")
- if err != nil || got.Resolved.Target != "v2" || got.Completed != 2 || got.Reason != "request" {
+ if err != nil || got.Resolved.Target != "v2" || got.Completed != 2 || got.Reason != string(ReasonRequest) {
t.Fatalf("round trip = %+v, %v", got, err)
}
if ConsumedPath(opt, "a.example.com") != filepath.Join(opt, "consumed", "a.example.com") {
diff --git a/internal/release/updatetarget.go b/internal/release/updatetarget.go
new file mode 100644
index 000000000..1f54ae604
--- /dev/null
+++ b/internal/release/updatetarget.go
@@ -0,0 +1,187 @@
+package release
+
+import "github.com/carlosframework/platform/internal/registry"
+
+// TargetReason is why Target chose what it did; plan 5 renders each.
+type TargetReason string
+
+const (
+ ReasonAdopt TargetReason = "adopt"
+ ReasonPin TargetReason = "pin"
+ ReasonRequest TargetReason = "request"
+ ReasonManual TargetReason = "manual"
+ ReasonMajor TargetReason = "major"
+ ReasonUnknown TargetReason = "unknown"
+ ReasonNew TargetReason = "new"
+ ReasonBaking TargetReason = "baking"
+ ReasonFailed TargetReason = "failed"
+ ReasonUnauthorized TargetReason = "unauthorized"
+)
+
+// Evidence is what authorizes an install (settings spec §4.1).
+type Evidence int
+
+const (
+ EvidencePointer Evidence = iota // the signed head (and its plan-1 digest): today's adoption only
+ EvidenceRecord // a signed promotion record with a matching manifest digest
+ EvidenceStaged // the box's own staged, verified copy (adoptHost's fallback from Record)
+)
+
+// TargetInput is everything §4.3 decides on, read by the caller so Target
+// stays pure.
+type TargetInput struct {
+ Route registry.Route
+ Pointer *ChannelPointer // the verified head; nil when unresolved
+ HeadTag string // the head's semver tag, "" when it has none
+ Settings UpdateSettings
+ Build BuildStateCopy
+ Consumed *Consumed
+ Installed string
+ HeadHeld string // Hold(...) for the head on this box: "" = eligible
+ Failed func(version string) bool // the zero-downtime failure records for this host
+}
+
+// TargetDecision is root's one answer per route per pass.
+type TargetDecision struct {
+ Version string // "" installs nothing
+ Reason TargetReason
+ Evidence Evidence
+ Bake bool // the caller applies today's Hold to Version
+ Resolve *ResolvedRequest // record before staging: this pass resolves R to T
+ Fail uint64 // record request Fail as completed, failed
+ Detail string // the build or number a reason names
+}
+
+// Target is settings spec §4.3, rules in order, with the zero-downtime
+// failure records outranking them all. The console never calls it; root is
+// the only caller that installs (§4).
+func Target(in TargetInput) TargetDecision {
+ u := in.Settings
+ head := ""
+ if in.Pointer != nil {
+ head = in.Pointer.Version
+ }
+ failed := func(v string) bool { return v != "" && in.Failed != nil && in.Failed(v) }
+ state := in.Build.State
+ wm := Watermark(in.Consumed, state)
+
+ // Request bookkeeping comes first, before any selection rule, pins
+ // included (§4.3 rule 2: "resolved at the start of every pass"; Astra I3
+ // #6). A pending R is resolved onto the head eligible now; a resolved R
+ // whose T failed completes failed. A pin then decides what runs, and the
+ // resolution waits, so clearing the pin later installs this T, not a
+ // head that moved on. A completed request never enters here again.
+ var (
+ resolve *ResolvedRequest
+ failedReq uint64
+ reqTarget string // a resolved T, not failed, above the watermark
+ )
+ if u.InstallRequest > wm {
+ if res := resolvedFor(in.Consumed, state, u.InstallRequest); res != nil {
+ if failed(res.Target) {
+ failedReq = res.ID // completes failed, once; the rules below decide what runs
+ } else {
+ reqTarget = res.Target
+ }
+ } else if head != "" && (in.HeadHeld == "" || u.InstallFastTrack) && !failed(head) {
+ resolve = &ResolvedRequest{ID: u.InstallRequest, Target: head}
+ reqTarget = head
+ }
+ // Unresolved (the head is baking here): it blocks nothing.
+ }
+ // A completed request whose target failed after it first ran (a later
+ // health, settle or hold failure) is recorded completed-failed, once,
+ // without reopening it (Astra I1 #10, §4.3 rule 2); also before the pin.
+ if failedReq == 0 && u.InstallRequest != 0 && u.InstallRequest <= wm && !completedFailed(in.Consumed, state, wm) {
+ if res := resolvedFor(in.Consumed, state, wm); res != nil && failed(res.Target) {
+ failedReq = res.ID
+ }
+ }
+ carry := func(d TargetDecision) TargetDecision {
+ d.Resolve, d.Fail = resolve, failedReq
+ return d
+ }
+
+ // Rule 1: a pin. A person chose it, so bake does not apply.
+ if u.Pin != "" {
+ if failed(u.Pin) {
+ return carry(TargetDecision{Reason: ReasonFailed, Detail: u.Pin})
+ }
+ return carry(TargetDecision{Version: u.Pin, Reason: ReasonPin, Evidence: EvidenceRecord, Detail: u.Pin})
+ }
+
+ // Rule 2: a resolved request above the watermark (this pass's resolution
+ // included).
+ if reqTarget != "" {
+ ev := EvidenceRecord
+ if reqTarget == head {
+ ev = EvidencePointer
+ }
+ return carry(TargetDecision{Version: reqTarget, Reason: ReasonRequest, Evidence: ev, Detail: reqTarget})
+ }
+
+ // Rule 3: held.
+ majorHeld := u.Major != nil && !tagInMajor(in.HeadTag, *u.Major)
+ if u.Updates == UpdatesManual || majorHeld {
+ reason := ReasonManual
+ if u.Updates != UpdatesManual {
+ reason = ReasonMajor
+ }
+ var d TargetDecision
+ switch {
+ case !in.Build.Known:
+ d = TargetDecision{Reason: ReasonUnknown}
+ case state != nil && state.LastExecuted != "":
+ d = TargetDecision{Version: state.LastExecuted, Reason: reason, Evidence: EvidenceRecord, Detail: state.LastExecuted}
+ case in.Installed != "":
+ d = TargetDecision{Version: in.Installed, Reason: reason, Evidence: EvidenceRecord, Detail: in.Installed}
+ case in.Build.Replica:
+ d = TargetDecision{Reason: ReasonUnknown}
+ case head != "" && !failed(head):
+ d = TargetDecision{Version: head, Reason: ReasonNew, Evidence: EvidencePointer, Bake: true, Detail: head}
+ case head == "":
+ d = TargetDecision{Reason: ReasonNew} // new, and no readable head to give it: nothing yet
+ default:
+ d = TargetDecision{Reason: ReasonFailed, Detail: head}
+ }
+ if failed(d.Version) {
+ d = TargetDecision{Reason: ReasonFailed, Detail: d.Version}
+ }
+ d.Fail = failedReq
+ return d
+ }
+
+ // Rule 4: today's adoption.
+ if head == "" {
+ return TargetDecision{Fail: failedReq}
+ }
+ if failed(head) {
+ return TargetDecision{Reason: ReasonFailed, Detail: head, Fail: failedReq}
+ }
+ return TargetDecision{Version: head, Reason: ReasonAdopt, Evidence: EvidencePointer, Bake: true, Detail: head, Fail: failedReq}
+}
+
+// completedFailed reports whether either source already records request id
+// as completed, failed.
+func completedFailed(c *Consumed, b *BuildState, id uint64) bool {
+ return (c != nil && c.Completed == id && c.CompletedFailed) || (b != nil && b.Completed == id && b.CompletedFailed)
+}
+
+// resolvedFor is R's T from either source (§4.3 rule 2: "locally or in the
+// build state"), or nil.
+func resolvedFor(c *Consumed, b *BuildState, id uint64) *ResolvedRequest {
+ if c != nil && c.Resolved != nil && c.Resolved.ID == id {
+ return c.Resolved
+ }
+ if b != nil && b.Resolved != nil && b.Resolved.ID == id {
+ return b.Resolved
+ }
+ return nil
+}
+
+// tagInMajor: a head whose semver tag is absent or has another major is
+// held (§3.1).
+func tagInMajor(tag string, major int) bool {
+ v, ok := ParseSemVer(tag)
+ return ok && v.Major == major
+}
diff --git a/internal/release/updatetarget_test.go b/internal/release/updatetarget_test.go
new file mode 100644
index 000000000..5ce21bf17
--- /dev/null
+++ b/internal/release/updatetarget_test.go
@@ -0,0 +1,166 @@
+package release
+
+import (
+ "testing"
+)
+
+func ip(n int) *int { return &n }
+
+type tcase struct {
+ name string
+ in TargetInput
+ want TargetDecision
+}
+
+func head(v string) *ChannelPointer { return &ChannelPointer{Version: v} }
+
+func known(replica bool, b *BuildState) BuildStateCopy {
+ return BuildStateCopy{Known: true, Replica: replica, State: b}
+}
+
+func runTargetCases(t *testing.T, cases []tcase) {
+ t.Helper()
+ for _, c := range cases {
+ got := Target(c.in)
+ w := c.want
+ if got.Version != w.Version || got.Reason != w.Reason || got.Evidence != w.Evidence || got.Bake != w.Bake ||
+ got.Fail != w.Fail || (got.Resolve == nil) != (w.Resolve == nil) ||
+ (got.Resolve != nil && *got.Resolve != *w.Resolve) {
+ t.Errorf("%s:\n got %+v\nwant %+v", c.name, got, w)
+ }
+ }
+}
+
+func auto() UpdateSettings { return UpdateSettings{Updates: UpdatesAuto, Channel: "stable"} }
+func manual() UpdateSettings { return UpdateSettings{Updates: UpdatesManual, Channel: "stable"} }
+
+func TestTargetRules(t *testing.T) {
+ failedV9 := func(v string) bool { return v == "v9" }
+ runTargetCases(t, []tcase{
+ {"auto: today's adoption", TargetInput{Pointer: head("v3"), Settings: auto(), Installed: "v2"},
+ TargetDecision{Version: "v3", Reason: ReasonAdopt, Evidence: EvidencePointer, Bake: true}},
+ {"pin: the pinned build, a record, no bake", TargetInput{Pointer: head("v3"), Settings: UpdateSettings{Updates: UpdatesAuto, Pin: "v1"}, Installed: "v3"},
+ TargetDecision{Version: "v1", Reason: ReasonPin, Evidence: EvidenceRecord}},
+ // Astra I3 #6: the request is still resolved at the start of the pass,
+ // pin or no pin, onto the head eligible now; the pin decides what runs.
+ {"pin outranks manual and a request, which still resolves", TargetInput{Pointer: head("v3"), Settings: UpdateSettings{Updates: UpdatesManual, Pin: "v1", InstallRequest: 5}},
+ TargetDecision{Version: "v1", Reason: ReasonPin, Evidence: EvidenceRecord, Resolve: &ResolvedRequest{ID: 5, Target: "v3"}}},
+ {"pin, request already resolved to T: the pin runs, T is kept for later", TargetInput{Pointer: head("v4"), Settings: UpdateSettings{Updates: UpdatesAuto, Pin: "v1", InstallRequest: 2},
+ Consumed: &Consumed{Resolved: &ResolvedRequest{ID: 2, Target: "v3"}, Completed: 1}},
+ TargetDecision{Version: "v1", Reason: ReasonPin, Evidence: EvidenceRecord}},
+ {"pin, the request's T failed: completes failed under the pin", TargetInput{Pointer: head("v3"), Settings: UpdateSettings{Updates: UpdatesAuto, Pin: "v1", InstallRequest: 2},
+ Consumed: &Consumed{Resolved: &ResolvedRequest{ID: 2, Target: "v9"}, Completed: 1}, Failed: failedV9},
+ TargetDecision{Version: "v1", Reason: ReasonPin, Evidence: EvidenceRecord, Fail: 2}},
+ {"pin, request waiting on bake: unresolved", TargetInput{Pointer: head("v3"), HeadHeld: "held 20h", Settings: UpdateSettings{Updates: UpdatesAuto, Pin: "v1", InstallRequest: 2}},
+ TargetDecision{Version: "v1", Reason: ReasonPin, Evidence: EvidenceRecord}},
+ {"pending request, head eligible: resolves to the head", TargetInput{Pointer: head("v3"), Settings: UpdateSettings{Updates: UpdatesManual, InstallRequest: 2},
+ Build: known(true, &BuildState{LastExecuted: "v1", Completed: 1}), Installed: "v1"},
+ TargetDecision{Version: "v3", Reason: ReasonRequest, Evidence: EvidencePointer, Resolve: &ResolvedRequest{ID: 2, Target: "v3"}}},
+ {"pending request, head baking: unresolved, held instance recovers", TargetInput{Pointer: head("v3"), HeadHeld: "held 20h", Settings: UpdateSettings{Updates: UpdatesManual, InstallRequest: 2},
+ Build: known(true, &BuildState{LastExecuted: "v1", Completed: 1}), Installed: "v0"},
+ TargetDecision{Version: "v1", Reason: ReasonManual, Evidence: EvidenceRecord}},
+ {"pending request, head baking, FastTrack: resolves anyway", TargetInput{Pointer: head("v3"), HeadHeld: "held 20h", Settings: UpdateSettings{Updates: UpdatesManual, InstallRequest: 2, InstallFastTrack: true},
+ Build: known(true, &BuildState{Completed: 1}), Installed: "v1"},
+ TargetDecision{Version: "v3", Reason: ReasonRequest, Evidence: EvidencePointer, Resolve: &ResolvedRequest{ID: 2, Target: "v3"}}},
+ {"resolved request, T no longer the head: a record", TargetInput{Pointer: head("v4"), Settings: UpdateSettings{Updates: UpdatesManual, InstallRequest: 2},
+ Consumed: &Consumed{Resolved: &ResolvedRequest{ID: 2, Target: "v3"}, Completed: 1}, Installed: "v1"},
+ TargetDecision{Version: "v3", Reason: ReasonRequest, Evidence: EvidenceRecord}},
+ {"resolved request ignores manual and major", TargetInput{Pointer: head("v3"), HeadTag: "2.0.0", Settings: UpdateSettings{Updates: UpdatesManual, Major: ip(1), InstallRequest: 2},
+ Consumed: &Consumed{Resolved: &ResolvedRequest{ID: 2, Target: "v3"}}},
+ TargetDecision{Version: "v3", Reason: ReasonRequest, Evidence: EvidencePointer}},
+ {"resolved request whose T failed: completes failed, installs nothing", TargetInput{Pointer: head("v9"), Settings: UpdateSettings{Updates: UpdatesManual, InstallRequest: 2},
+ Consumed: &Consumed{Resolved: &ResolvedRequest{ID: 2, Target: "v9"}}, Failed: failedV9, Build: known(true, &BuildState{LastExecuted: "v1"}), Installed: "v1"},
+ TargetDecision{Version: "v1", Reason: ReasonManual, Evidence: EvidenceRecord, Fail: 2}},
+ {"manual, build state differs from installed: install last_executed", TargetInput{Pointer: head("v3"), Settings: manual(),
+ Build: known(true, &BuildState{LastExecuted: "v1"}), Installed: "v2"},
+ TargetDecision{Version: "v1", Reason: ReasonManual, Evidence: EvidenceRecord}},
+ {"manual, no build state, something installed: keep it", TargetInput{Pointer: head("v3"), Settings: manual(), Build: known(true, nil), Installed: "v2"},
+ TargetDecision{Version: "v2", Reason: ReasonManual, Evidence: EvidenceRecord}},
+ {"manual, no build state, nothing installed, a replica: unknown", TargetInput{Pointer: head("v3"), Settings: manual(), Build: known(true, nil)},
+ TargetDecision{Reason: ReasonUnknown}},
+ {"manual, none of these: new, gets the eligible head", TargetInput{Pointer: head("v3"), Settings: manual(), Build: known(false, nil)},
+ TargetDecision{Version: "v3", Reason: ReasonNew, Evidence: EvidencePointer, Bake: true}},
+ {"major matching head tag: today's adoption", TargetInput{Pointer: head("v3"), HeadTag: "2.1.0", Settings: UpdateSettings{Updates: UpdatesAuto, Channel: "stable", Major: ip(2)}, Installed: "v2"},
+ TargetDecision{Version: "v3", Reason: ReasonAdopt, Evidence: EvidencePointer, Bake: true}},
+ {"major, head of another major: held", TargetInput{Pointer: head("v3"), HeadTag: "3.0.0", Settings: UpdateSettings{Updates: UpdatesAuto, Channel: "stable", Major: ip(2)},
+ Build: known(true, &BuildState{LastExecuted: "v2"}), Installed: "v2"},
+ TargetDecision{Version: "v2", Reason: ReasonMajor, Evidence: EvidenceRecord}},
+ {"major, untagged head: held", TargetInput{Pointer: head("v3"), Settings: UpdateSettings{Updates: UpdatesAuto, Channel: "stable", Major: ip(2)},
+ Build: known(true, &BuildState{LastExecuted: "v2"}), Installed: "v2"},
+ TargetDecision{Version: "v2", Reason: ReasonMajor, Evidence: EvidenceRecord}},
+ {"auto onto a failed head: nothing new", TargetInput{Pointer: head("v9"), Settings: auto(), Installed: "v2", Failed: failedV9},
+ TargetDecision{Reason: ReasonFailed}},
+ })
+}
+
+// Review Focus 3.
+func TestFailureRecordsOutrankAPin(t *testing.T) {
+ runTargetCases(t, []tcase{{"pin of a failed build", TargetInput{Pointer: head("v3"), Settings: UpdateSettings{Updates: UpdatesAuto, Pin: "v9"},
+ Installed: "v2", Failed: func(v string) bool { return v == "v9" }}, TargetDecision{Reason: ReasonFailed}}})
+}
+
+// Review Focus 2.
+func TestARequestBelowTheWatermarkNeverRuns(t *testing.T) {
+ runTargetCases(t, []tcase{{"completed in the build state, older local file", TargetInput{Pointer: head("v4"),
+ Settings: UpdateSettings{Updates: UpdatesManual, InstallRequest: 3},
+ Consumed: &Consumed{Resolved: &ResolvedRequest{ID: 3, Target: "v3"}, Completed: 2},
+ Build: known(true, &BuildState{LastExecuted: "v3", Completed: 3}), Installed: "v3"},
+ TargetDecision{Version: "v3", Reason: ReasonManual, Evidence: EvidenceRecord}}})
+}
+
+// Review Focus 4.
+func TestAnUnreadBuildStateIsNeverNew(t *testing.T) {
+ runTargetCases(t, []tcase{{"held, build state never read", TargetInput{Pointer: head("v3"), Settings: manual(), Build: BuildStateCopy{}},
+ TargetDecision{Reason: ReasonUnknown}}})
+}
+
+// Review Focus 5.
+func TestAResolutionInTheBuildStateIsReused(t *testing.T) {
+ runTargetCases(t, []tcase{{"resolved on another box", TargetInput{Pointer: head("v5"),
+ Settings: UpdateSettings{Updates: UpdatesManual, InstallRequest: 4},
+ Build: known(true, &BuildState{LastExecuted: "v2", Resolved: &ResolvedRequest{ID: 4, Target: "v3"}, Completed: 3})},
+ TargetDecision{Version: "v3", Reason: ReasonRequest, Evidence: EvidenceRecord}}})
+}
+
+// Astra I1 #10: T ran (the request completed at the pre-exec write), then
+// failed its health check. The completed request is marked failed once,
+// and is not reopened.
+func TestAFailureAfterCompletionMarksTheRequestFailed(t *testing.T) {
+ failedV3 := func(v string) bool { return v == "v3" }
+ runTargetCases(t, []tcase{
+ {"completed, then T failed", TargetInput{Pointer: head("v3"), Settings: UpdateSettings{Updates: UpdatesManual, InstallRequest: 2},
+ Build: known(true, &BuildState{LastExecuted: "v1", Resolved: &ResolvedRequest{ID: 2, Target: "v3"}, Completed: 2}),
+ Installed: "v1", Failed: failedV3},
+ TargetDecision{Version: "v1", Reason: ReasonManual, Evidence: EvidenceRecord, Fail: 2}},
+ {"already recorded as failed: not again", TargetInput{Pointer: head("v3"), Settings: UpdateSettings{Updates: UpdatesManual, InstallRequest: 2},
+ Build: known(true, &BuildState{LastExecuted: "v1", Resolved: &ResolvedRequest{ID: 2, Target: "v3"}, Completed: 2, CompletedFailed: true}),
+ Installed: "v1", Failed: failedV3},
+ TargetDecision{Version: "v1", Reason: ReasonManual, Evidence: EvidenceRecord}},
+ })
+}
+
+// Astra I1 #9: rules 1-3 do not need a readable head.
+func TestIndependentTargetsNeedNoPointer(t *testing.T) {
+ runTargetCases(t, []tcase{
+ {"pin, no pointer", TargetInput{Settings: UpdateSettings{Updates: UpdatesAuto, Pin: "v1"}},
+ TargetDecision{Version: "v1", Reason: ReasonPin, Evidence: EvidenceRecord}},
+ {"resolved request, no pointer", TargetInput{Settings: UpdateSettings{Updates: UpdatesManual, InstallRequest: 2},
+ Consumed: &Consumed{Resolved: &ResolvedRequest{ID: 2, Target: "v3"}}},
+ TargetDecision{Version: "v3", Reason: ReasonRequest, Evidence: EvidenceRecord}},
+ {"held recovery, no pointer", TargetInput{Settings: manual(), Build: known(true, &BuildState{LastExecuted: "v1"})},
+ TargetDecision{Version: "v1", Reason: ReasonManual, Evidence: EvidenceRecord}},
+ {"new held instance, no pointer: nothing yet", TargetInput{Settings: manual(), Build: known(false, nil)},
+ TargetDecision{Reason: ReasonNew}},
+ {"auto, no pointer: nothing", TargetInput{Settings: auto()}, TargetDecision{}},
+ })
+}
+
+// Spec §8: a held sleeper with a pending request wakes onto T, which the
+// wake check reads from this same decision.
+func TestAPendingTOutranksLastExecuted(t *testing.T) {
+ runTargetCases(t, []tcase{{"resolved T pending while last_executed is older", TargetInput{Pointer: head("v3"),
+ Settings: UpdateSettings{Updates: UpdatesManual, InstallRequest: 2},
+ Consumed: &Consumed{Resolved: &ResolvedRequest{ID: 2, Target: "v3"}},
+ Build: known(true, &BuildState{LastExecuted: "v1", Completed: 1}), Installed: "v3"},
+ TargetDecision{Version: "v3", Reason: ReasonRequest, Evidence: EvidencePointer}}})
+}
diff --git a/internal/updatewords/words_approved_test.go b/internal/updatewords/words_approved_test.go
index 7e73ab107..627d8725d 100644
--- a/internal/updatewords/words_approved_test.go
+++ b/internal/updatewords/words_approved_test.go
@@ -29,14 +29,17 @@ func loadApproved(t *testing.T, path string) approvedRecord {
return rec
}
-// The copy Paul approved lives in two records: plan 2's (2026-10-09, the
-// authorize strings) and plan 3's (2026-10-11, the settings.console.*
-// strings). Each record must name exactly its own IDs, each text must be what
-// Text returns byte for byte, each entry must name the file that holds it, and
-// together the two records must name exactly the IDs of All(), so a new draft
-// with no approved record fails.
+// The copy Paul approved lives in three records: plan 2's (2026-10-09, the
+// authorize strings), plan 3's (2026-10-11, the settings.console.* strings)
+// and plan 4's (the settings.box.* strings; the record is written only after
+// Paul approves them, so this test fails with the file missing until then).
+// Each record must name exactly its own IDs, each text must be what Text
+// returns byte for byte, each entry must name the file that holds it, and
+// together the three records must name exactly the IDs of All(), so a new
+// draft with no approved record fails.
func TestApprovedCopyIsApplied(t *testing.T) {
const console = "settings.console."
+ const box = "settings.box."
records := []struct {
path string
file string
@@ -44,10 +47,13 @@ func TestApprovedCopyIsApplied(t *testing.T) {
}{
{"../../docs/superpowers/plans/2026-10-07-instance-settings-2-copy.json",
"internal/updatewords/words_authorize.go",
- func(id string) bool { return !strings.HasPrefix(id, console) }},
+ func(id string) bool { return !strings.HasPrefix(id, console) && !strings.HasPrefix(id, box) }},
{"../../docs/superpowers/plans/2026-10-07-instance-settings-3-copy.json",
"internal/updatewords/words_console.go",
func(id string) bool { return strings.HasPrefix(id, console) }},
+ {"../../docs/superpowers/plans/2026-10-07-instance-settings-4-copy.json",
+ "internal/updatewords/words_box.go",
+ func(id string) bool { return strings.HasPrefix(id, box) }},
}
var union []string
docs := ""
diff --git a/internal/updatewords/words_box.go b/internal/updatewords/words_box.go
new file mode 100644
index 000000000..86934e875
--- /dev/null
+++ b/internal/updatewords/words_box.go
@@ -0,0 +1,17 @@
+package updatewords
+
+func init() {
+ // TODO-COPY: draft, awaiting Paul's copy review (settings plan 4)
+ add(
+ Draft{ID: "settings.box.reason.adopt", Text: "following its channel", Context: "Why an instance runs the build it does: it takes its channel's builds as they arrive. Shown in carlos instances list and on the instance page."},
+ Draft{ID: "settings.box.reason.pin", Text: "pinned to %s", Context: "The instance is pinned. %s is the build."},
+ Draft{ID: "settings.box.reason.request", Text: "updating to %s (Update now)", Context: "An Update now request was resolved to this build and is being installed or waits for the next start. %s is the build."},
+ Draft{ID: "settings.box.reason.manual", Text: "manual updates: stays on %s", Context: "Updates are manual, so the instance stays on the build its data last ran with. %s is the build."},
+ Draft{ID: "settings.box.reason.major", Text: "held at major %d: stays on %s", Context: "The channel's newest build is another major version, so the instance stays put. Major number, build."},
+ Draft{ID: "settings.box.reason.unknown", Text: "build unknown: use Update now or set a pin", Context: "The box cannot tell which build last used this instance's data, so it starts nothing rather than guess. The member chooses."},
+ Draft{ID: "settings.box.reason.new", Text: "new instance: takes the channel's build", Context: "A held instance that has never run gets the channel's current build once."},
+ Draft{ID: "settings.box.reason.baking", Text: "Update now waits for %s to finish its safety delay on this box", Context: "An Update now request is not resolved yet because the newest build is still inside its bake window. %s is the build."},
+ Draft{ID: "settings.box.reason.failed", Text: "%s failed on this box, so it is not installed", Context: "The build the settings ask for failed here before, so the instance keeps what it has. %s is the build."},
+ Draft{ID: "settings.box.request-failed", Text: "Update now to %s failed: the instance stayed on its previous build", Context: "Shown once an Update now request completed as failed. %s is the build."},
+ )
+}