Add knowledge stale-discard characterization test
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user