package main import ( "context" "testing" "github.com/google/uuid" ) func TestExtractExecutionIDs(t *testing.T) { // Real tool-result phrasings that should yield an execution id. pos := map[string]string{ `"pct_create on host:strong auto-approved via assent window — execution 019f4b19-eafd-74ed-baa6-d24a27b3f52c running."`: "019f4b19-eafd-74ed-baa6-d24a27b3f52c", `"run on lxc:caddy requires approval (risk: config_mutation) — execution 019f4af7-7eff-7723-b38c-b540b267f407 queued."`: "019f4af7-7eff-7723-b38c-b540b267f407", `"apt_upgrade on host:hubris auto-approved via assent window — execution 019f4b58-c88c-7767-87dd-044608ced913 running."`: "019f4b58-c88c-7767-87dd-044608ced913", } for in, want := range pos { ids := extractExecutionIDs(in) if len(ids) != 1 || ids[0].String() != want { t.Errorf("extractExecutionIDs(%q) = %v, want [%s]", in, ids, want) } } // Synchronous auto-run and read-only results carry no "execution " // phrasing — they've already completed inline and must NOT be linked for // continuation. neg := []string{ `"run on host:strong (read_only, auto): 09:30 up 8 days"`, `"run on lxc:caddy (config_mutation, auto via assent window): done"`, `[{"slug":"lxc:caddy","health":"healthy"}]`, `"target not found: lxc:nope"`, } for _, in := range neg { if ids := extractExecutionIDs(in); len(ids) != 0 { t.Errorf("extractExecutionIDs(%q) = %v, want none", in, ids) } } // De-dupes repeated ids in one result. dup := `execution 019f4b19-eafd-74ed-baa6-d24a27b3f52c queued ... execution 019f4b19-eafd-74ed-baa6-d24a27b3f52c running` if ids := extractExecutionIDs(dup); len(ids) != 1 { t.Errorf("expected de-dup to 1 id, got %v", ids) } } // TestResumeSession_SkipsWhenBusy guards the P0 fix // (plans/2026-08-03-nomos-chat-changes-review.md): resumeSession must skip — // return false, body never executed — when a turn is already active for the // session. continueSession relies on this so it only marks a continuation // "continued" after a turn really ran (otherwise the result is lost: marked // continued, never re-queued by pendingContinuations). // // A minimal agent with only a gate is enough: if the body ever ran, chatWith // would dereference the nil provider and panic. Returning false cleanly proves // the body was skipped. func TestResumeSession_SkipsWhenBusy(t *testing.T) { a := &agent{gate: newTurnGate()} if !a.gate.acquire("sess", 0) { t.Fatal("precondition: initial acquire should succeed on a free session") } ran := a.resumeSession(context.Background(), "sess", "note") if ran { t.Fatal("resumeSession must return false (skip) while a turn is active for the session") } } // TestContinueSession_DefersWhenBusy guards the other half of P0: when the // session is busy, continueSession defers (leaves the execution pending for the // next worker tick) instead of running or marking it. It must return cleanly // without reaching resumeSession's body (nil provider → panic) or markContinued. func TestContinueSession_DefersWhenBusy(t *testing.T) { a := &agent{gate: newTurnGate()} if !a.gate.acquire("sess", 0) { t.Fatal("precondition: initial acquire should succeed on a free session") } p := pendingContinuation{ExecID: uuid.New(), SessionID: "sess", Status: "completed"} a.continueSession(context.Background(), p) // must not panic; must not run/mark }