Add knowledge stale-discard characterization test

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-01 17:35:25 -04:00
parent 465938906e
commit f13f33cf74
+81
View File
@@ -949,6 +949,83 @@ func TestTasksViewAbsentOutsidePlanning(t *testing.T) {
} }
} }
// TestStaleKnowledgeFetchDiscardedAfterLeavingPlanning exercises the
// "left planning" arm of the discard guard in startKnowledgeFetchLocked:
// a slow knowledge load that returns after the user has left planning must
// not clobber the fresh state produced by a later planning entry.
//
// Sequence:
// 1. Enter planning with a gated source (load in flight, blocked on the gate).
// 2. StartManualCommitment moves Planning -> Active, leaving planning.
// 3. Complete + End the session to return to Locked.
// 4. Install a second (ungated) source and re-enter planning.
// 5. Wait for the fresh load to reach "ready" with the new source's profile.
// 6. Release the gate — the stale goroutine from step 1 now runs and must be
// discarded (knowledgeGen has moved on / runtimeState differs at commit).
// 7. Assert, through the now-visible Planning-state projection, that the stale
// result did not clobber the fresh knowledge view. The "/stale" vs "/fresh"
// Path distinguishes the two (both texts are 5 chars, so Chars cannot).
func TestStaleKnowledgeFetchDiscardedAfterLeavingPlanning(t *testing.T) {
c, _ := newTestController(t)
// Step 1: gated source whose result ("/stale") must never surface.
staleGate := make(chan struct{})
stale := &fakeSource{
profile: knowledge.Profile{Text: "STALE", Path: "/stale"},
gate: staleGate,
}
c.SetKnowledge(stale)
if err := c.EnterPlanning(); err != nil { // launches the gated load (gen 1)
t.Fatalf("enter planning: %v", err)
}
// Step 2: leave planning via a commitment — the stale load is still blocked.
if err := c.StartManualCommitment("write report", "report drafted", 25*time.Minute, []string{"code"}, domain.EnforcementWarn); err != nil {
t.Fatalf("start commitment: %v", err)
}
if c.State().RuntimeState != domain.RuntimeActive {
t.Fatalf("expected Active after StartManualCommitment")
}
// Step 3: end the session to return to Locked.
if err := c.Complete(); err != nil {
t.Fatalf("complete: %v", err)
}
if err := c.End(); err != nil {
t.Fatalf("end: %v", err)
}
// Step 4: fresh, ungated source whose result ("/fresh") is the expected state.
fresh := &fakeSource{profile: knowledge.Profile{Text: "FRESH", Path: "/fresh"}}
c.SetKnowledge(fresh)
if err := c.EnterPlanning(); err != nil {
t.Fatalf("second enter planning: %v", err)
}
// Step 5: wait for the fresh load to complete and be visible.
st := waitKnowledgeStatus(t, c, "ready")
if st.Knowledge.Path != "/fresh" || st.Knowledge.Chars != len("FRESH") {
t.Fatalf("expected FRESH knowledge after second planning entry, got %+v", st.Knowledge)
}
// Step 6: release the stale goroutine — it must detect that its generation is
// stale and discard rather than overwrite the fresh view.
close(staleGate)
time.Sleep(50 * time.Millisecond)
// Step 7: the fresh view must still be intact; "/stale" must not have landed.
st = c.State()
if st.Knowledge == nil {
t.Fatalf("knowledge view should still be present in planning")
}
if st.Knowledge.Status != "ready" {
t.Fatalf("knowledge status should still be ready, got %q", st.Knowledge.Status)
}
if st.Knowledge.Path != "/fresh" || st.Knowledge.Chars != len("FRESH") {
t.Fatalf("stale knowledge fetch clobbered the fresh view: %+v", st.Knowledge)
}
}
// TestStaleTasksFetchDiscardedAfterLeavingPlanning exercises the // TestStaleTasksFetchDiscardedAfterLeavingPlanning exercises the
// "left planning" arm of the discard guard in startTasksFetchLocked. // "left planning" arm of the discard guard in startTasksFetchLocked.
// //
@@ -1027,9 +1104,13 @@ func TestStaleTasksFetchDiscardedAfterLeavingPlanning(t *testing.T) {
type fakeSource struct { type fakeSource struct {
profile knowledge.Profile profile knowledge.Profile
err error err error
gate chan struct{} // if non-nil, Load blocks until it receives
} }
func (f *fakeSource) Load(ctx context.Context, path string) (knowledge.Profile, error) { func (f *fakeSource) Load(ctx context.Context, path string) (knowledge.Profile, error) {
if f.gate != nil {
<-f.gate
}
return f.profile, f.err return f.profile, f.err
} }