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}, Build: known(false, nil)}, 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, Build: known(false, nil)}, 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}, Build: known(false, nil)}, 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", Build: known(false, nil)}, 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"}}, Build: known(false, nil)}, 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"}}, Build: known(false, nil)}, 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}}}) } // R-TARGET-UNKNOWN: with the build state unread the watermark sees only the // consumed file, so a request that already completed would look pending on a // fresh box. Target then does no request bookkeeping at all. func TestAnUnreadBuildStateDoesNoRequestBookkeeping(t *testing.T) { runTargetCases(t, []tcase{ {"held, completed request looks pending: unknown, nothing resolved", TargetInput{Pointer: head("v7"), Settings: UpdateSettings{Updates: UpdatesManual, InstallRequest: 5}, Build: BuildStateCopy{}}, TargetDecision{Reason: ReasonUnknown}}, {"auto, FastTrack, baking head: today's adoption, nothing resolved", TargetInput{Pointer: head("v7"), HeadHeld: "held 20h", Settings: UpdateSettings{Updates: UpdatesAuto, InstallRequest: 5, InstallFastTrack: true}, Build: BuildStateCopy{}}, TargetDecision{Version: "v7", Reason: ReasonAdopt, Evidence: EvidencePointer, Bake: true}}, {"pin, pending request: the pin, nothing resolved", TargetInput{Pointer: head("v7"), Settings: UpdateSettings{Updates: UpdatesAuto, Pin: "v1", InstallRequest: 5}, Build: BuildStateCopy{}}, TargetDecision{Version: "v1", Reason: ReasonPin, Evidence: EvidenceRecord}}, {"resolved T failed, build state unread: no failure completion", TargetInput{Pointer: head("v7"), Settings: UpdateSettings{Updates: UpdatesAuto, Pin: "v1", InstallRequest: 5}, Consumed: &Consumed{Resolved: &ResolvedRequest{ID: 5, Target: "v9"}}, Failed: func(v string) bool { return v == "v9" }}, TargetDecision{Version: "v1", Reason: ReasonPin, Evidence: EvidenceRecord}}, }) } // R-TARGET-FAILEDHEAD: a pending request whose eligible head this box holds // as failed is resolved to that head and completed failed in the same pass. // It installs nothing itself; the later rules decide what runs, and the // failed head never becomes the Version. func TestAPendingRequestOnAFailedHeadCompletesFailed(t *testing.T) { failedV9 := func(v string) bool { return v == "v9" } runTargetCases(t, []tcase{ {"manual: the held build runs, request resolved and failed", TargetInput{Pointer: head("v9"), Failed: failedV9, Settings: UpdateSettings{Updates: UpdatesManual, InstallRequest: 2}, Build: known(true, &BuildState{LastExecuted: "v1", Completed: 1}), Installed: "v1"}, TargetDecision{Version: "v1", Reason: ReasonManual, Evidence: EvidenceRecord, Resolve: &ResolvedRequest{ID: 2, Target: "v9"}, Fail: 2}}, {"auto: nothing new, request resolved and failed", TargetInput{Pointer: head("v9"), Failed: failedV9, Settings: UpdateSettings{Updates: UpdatesAuto, InstallRequest: 2}, Build: known(true, nil), Installed: "v2"}, TargetDecision{Reason: ReasonFailed, Resolve: &ResolvedRequest{ID: 2, Target: "v9"}, Fail: 2}}, {"pin: the pin runs, request resolved and failed", TargetInput{Pointer: head("v9"), Failed: failedV9, Settings: UpdateSettings{Updates: UpdatesAuto, Pin: "v1", InstallRequest: 2}, Build: known(true, nil)}, TargetDecision{Version: "v1", Reason: ReasonPin, Evidence: EvidenceRecord, Resolve: &ResolvedRequest{ID: 2, Target: "v9"}, Fail: 2}}, {"baking failed head: still unresolved", TargetInput{Pointer: head("v9"), Failed: failedV9, HeadHeld: "held 20h", Settings: UpdateSettings{Updates: UpdatesManual, InstallRequest: 2}, Build: known(true, &BuildState{LastExecuted: "v1", Completed: 1}), Installed: "v1"}, TargetDecision{Version: "v1", Reason: ReasonManual, Evidence: EvidenceRecord}}, }) } // Ruling R-RESOLUTION-STATE-WINS (Task 11 sweep, seed 531): box A resolved R // to v1 and lost the instance to a move before any wake; box B resolved R // again, to v4, and a wake carried that into the build state; back on A, the // file and the build state name different builds for the same R. The build // state's T is followed, root's file is rewritten to it, and the wake that // runs it completes R. Following the file ran v1 forever with R pending. func TestTheBuildStatesResolutionOutranksTheBoxsFile(t *testing.T) { in := TargetInput{Pointer: head("v5"), HeadTag: "1.5.0", Settings: UpdateSettings{Updates: UpdatesManual, InstallRequest: 1, InstallFastTrack: true}, Consumed: &Consumed{Host: "a.example.com", Resolved: &ResolvedRequest{ID: 1, Target: "v1"}}, Build: known(true, &BuildState{LastExecuted: "v1", Resolved: &ResolvedRequest{ID: 1, Target: "v4"}}), Installed: "v1"} runTargetCases(t, []tcase{{"same request, two builds: the build state's T", in, TargetDecision{Version: "v4", Reason: ReasonRequest, Evidence: EvidenceRecord}}}) dec := Target(in) c := NextConsumed(in, dec) if c.Resolved == nil || c.Resolved.ID != 1 || c.Resolved.Target != "v4" { t.Fatalf("consumed after the pass = %+v; want the build state's R1->v4", c.Resolved) } st := NextBuildState(in.Build.State, &c, dec.Version) if st.LastExecuted != "v4" || st.Completed != 1 || st.CompletedFailed { t.Fatalf("build state after the wake = %+v; want R1 completed by v4", st) } }