Files
Agent-System-Backend/internal/runtime/project_context_test.go
T
weichen ecb86f4309 Recheck project workflow sources without storing prompts
Why: Goal-sensitive workflow verification persisted model prompts and prior output text in trusted receipt rows, while cross-step model inputs could reuse stale machine output.

Evidence: RED receipt privacy test; full Go suite, targeted race, isolated PostgreSQL store/runmgr/storage-migrate tests, vet, and static build passed locally before commit. A known unrelated TempDir cleanup flake failed once and passed on targeted and serial full replay.

Impact: Add optional same-provider source recheck, verify every included item and evidence body before reuse, gate cross-step outputs, reject unsettled attempts, remove raw scope columns, scrub isolated candidate v34 SQLite/PG schemas, and reject unsafe backups.

Refs: Approved RDM stage6 W6 source-recheck design adjustment r1; W6 independent review.
2026-10-14 05:38:11 +00:00

364 lines
22 KiB
Go

package runtime
import (
"context"
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
"strings"
"testing"
agentllm "agent-native/runtime/llm"
"agent-squad/squad"
"agent-system-backend/pkg/extension"
core "agent-system-core"
)
type projectRunCtx struct {
*fakeRunCtx
bundle extension.ProjectContextBundle
readErr error
reads int
manifests []extension.ProjectContextManifest
evidence func(string) (extension.ProjectEvidence, error)
budgetSettings core.EffectiveSettings
sessionNote string
injections []ContextInjection
surfaces []core.SurfaceSnapshot
toolList []core.ToolDesc
}
func (c *projectRunCtx) Tools() []core.ToolDesc { return c.toolList }
func (c *projectRunCtx) SessionNote() string { return c.sessionNote }
func (c *projectRunCtx) Injections() []ContextInjection { return c.injections }
func (c *projectRunCtx) Surfaces() []core.SurfaceSnapshot { return c.surfaces }
func (c *projectRunCtx) Settings() core.EffectiveSettings {
if c.budgetSettings != nil {
return c.budgetSettings
}
return c.fakeRunCtx.Settings()
}
func (c *projectRunCtx) ProjectContext(context.Context, string, int) (extension.ProjectContextBundle, bool, error) {
c.reads++
return c.bundle, true, c.readErr
}
func (c *projectRunCtx) SaveContextManifest(_ context.Context, m extension.ProjectContextManifest) error {
c.manifests = append(c.manifests, m)
return nil
}
func (c *projectRunCtx) ProjectContextEnabled() bool { return true }
func (c *projectRunCtx) ProjectEvidence(_ context.Context, ref string) (extension.ProjectEvidence, error) {
return c.evidence(ref)
}
func TestProjectContextRefreshesPerModelRequestWithoutPersistingOldSummary(t *testing.T) {
var requests [][]agentllm.Message
base := scriptStream(textTurns("private old model summary", "ok"), nil)
runner := &LLMRunner{Stream: func(ctx context.Context, model string, msgs []agentllm.Message, tools []agentllm.ToolDecl, opts agentllm.StreamOptions) <-chan agentllm.Event {
requests = append(requests, append([]agentllm.Message(nil), msgs...))
return base(ctx, model, msgs, tools, opts)
}}
c := &projectRunCtx{fakeRunCtx: &fakeRunCtx{ctx: context.Background(), sandbox: t.TempDir(), projectID: "project-1", prof: core.AgentProfile{Model: core.ModelRoute{Chat: "test-model"}}}}
c.bundle = extension.ProjectContextBundle{Status: extension.ProjectContextReady, ProjectID: "project-1", Version: "v1", Items: []extension.ProjectContextItem{{SourceID: "s1", Revision: "r1", Hash: "h1", Text: "summary-v1", Required: true}}}
if err := runner.Execute(c); err != nil {
t.Fatal(err)
}
c.bundle.Version = "v2"
c.bundle.Items[0].Text = "summary-v2"
if err := runner.Execute(c); err != nil {
t.Fatal(err)
}
if len(requests) != 2 || !strings.Contains(joinReq(requests[0]), "summary-v1") || !strings.Contains(joinReq(requests[1]), "summary-v2") || strings.Contains(joinReq(requests[1]), "summary-v1") || strings.Contains(joinReq(requests[1]), "private old model summary") {
t.Fatalf("project context not refreshed: first=%q second=%q", joinReq(requests[0]), joinReq(requests[1]))
}
if c.reads != 2 || len(c.manifests) != 4 || c.manifests[0].Delivery != "attempted" || c.manifests[1].Delivery != "responded" || c.manifests[2].Version != "v2" || c.manifests[3].Delivery != "responded" || c.manifests[2].AttemptID != c.manifests[3].AttemptID || c.manifests[3].InputSHA256 == "" {
t.Fatalf("actual request manifests missing: reads=%d manifests=%+v", c.reads, c.manifests)
}
wire, err := json.Marshal(requests[1])
if err != nil {
t.Fatal(err)
}
if want := fmt.Sprintf("%x", sha256.Sum256(wire)); c.manifests[3].InputSHA256 != want {
t.Fatalf("manifest hash %q does not match actual model request %q", c.manifests[3].InputSHA256, want)
}
}
func TestProjectContextResolutionFailureBlocksModelRequest(t *testing.T) {
called := false
runner := &LLMRunner{Stream: func(ctx context.Context, model string, msgs []agentllm.Message, tools []agentllm.ToolDecl, opts agentllm.StreamOptions) <-chan agentllm.Event {
called = true
return scriptStream(textTurns("bad"), nil)(ctx, model, msgs, tools, opts)
}}
c := &projectRunCtx{fakeRunCtx: &fakeRunCtx{ctx: context.Background(), projectID: "project-1", prof: core.AgentProfile{Model: core.ModelRoute{Chat: "test-model"}}}, readErr: errors.New("permission revoked")}
if err := runner.Execute(c); err == nil || called {
t.Fatalf("context resolution failure reached model: err=%v called=%v", err, called)
}
}
func TestProjectRevocationScrubsOldMachineContextBeforeSecondTurn(t *testing.T) {
const old = "OLD_RESTRICTED_BODY"
turns := [][]agentllm.Event{
{{Kind: agentllm.KindTextDelta, Text: old}, {Kind: agentllm.KindMessageEnd, Message: agentllm.Message{Role: agentllm.RoleAssistant, Content: old, StopReason: agentllm.StopToolCalls, ToolCalls: []agentllm.ToolCall{{ID: "c1", Name: "knowledge_search", Arguments: `{}`}}}}},
{{Kind: agentllm.KindTextDelta, Text: "done"}, {Kind: agentllm.KindMessageEnd, Message: agentllm.Message{Role: agentllm.RoleAssistant, Content: "done", StopReason: agentllm.StopEnd}}},
}
base := scriptStream(turns, nil)
var requests [][]agentllm.Message
c := &projectRunCtx{fakeRunCtx: &fakeRunCtx{ctx: context.Background(), projectID: "p1", prof: core.AgentProfile{Model: core.ModelRoute{Chat: "test-model"}}, memEntries: []memEntry{{typ: "note", content: "STALE_MEMORY"}}, execOutput: old},
bundle: extension.ProjectContextBundle{Status: extension.ProjectContextReady, ProjectID: "p1", Version: "v1", AuthorizationEpoch: "duty-1", PublicationEpoch: "pub-1", Items: []extension.ProjectContextItem{{SourceID: "doc", Revision: "r1", Hash: "h1", Text: old, Required: true}}},
budgetSettings: core.EffectiveSettings{"models": map[string]any{"chat": map[string]any{"baseUrl": "http://test"}}, "context": map[string]any{"userInstructions": "STALE_INSTRUCTIONS"}, "runtime": map[string]any{"verify": false, "composeFinal": false}},
sessionNote: "STALE_SESSION_NOTE", injections: []ContextInjection{{Content: "STALE_INJECTION", Constant: true}},
surfaces: []core.SurfaceSnapshot{{SemanticText: "STALE_SURFACE"}},
}
runner := &LLMRunner{Stream: func(ctx context.Context, model string, msgs []agentllm.Message, tools []agentllm.ToolDecl, opts agentllm.StreamOptions) <-chan agentllm.Event {
requests = append(requests, append([]agentllm.Message(nil), msgs...))
if len(requests) == 1 {
c.bundle = extension.ProjectContextBundle{Status: extension.ProjectContextReady, ProjectID: "p1", Version: "v2", AuthorizationEpoch: "duty-2", PublicationEpoch: "pub-2", Items: []extension.ProjectContextItem{{SourceID: "public", Revision: "r1", Hash: "h2", Text: "CURRENT_PUBLIC_FACT", Required: true}}}
}
return base(ctx, model, msgs, tools, opts)
}}
if err := runner.Execute(c); err != nil {
t.Fatal(err)
}
if len(requests) != 2 {
t.Fatalf("model requests=%d, want 2", len(requests))
}
if c.fakeRunCtx.toolRuns != 1 {
t.Fatalf("expected restricted tool output before revocation, tool runs=%d", c.fakeRunCtx.toolRuns)
}
first := joinReq(requests[0])
for _, unproven := range []string{"STALE_MEMORY", "STALE_INSTRUCTIONS", "STALE_SESSION_NOTE", "STALE_INJECTION", "STALE_SURFACE"} {
if strings.Contains(first, unproven) {
t.Fatalf("unproven machine context %q reached first project request: %s", unproven, first)
}
}
second := joinReq(requests[1])
for _, forbidden := range []string{old, "STALE_MEMORY", "STALE_INSTRUCTIONS", "STALE_SESSION_NOTE", "STALE_INJECTION", "STALE_SURFACE"} {
if strings.Contains(second, forbidden) {
t.Fatalf("revoked machine context %q reached second model request: %s", forbidden, second)
}
}
if !strings.Contains(second, "CURRENT_PUBLIC_FACT") {
t.Fatalf("current context absent from second model request: %s", second)
}
}
func TestProjectSameScopeKeepsCurrentRunToolContext(t *testing.T) {
turns := [][]agentllm.Event{
{{Kind: agentllm.KindMessageEnd, Message: agentllm.Message{Role: agentllm.RoleAssistant, StopReason: agentllm.StopToolCalls, ToolCalls: []agentllm.ToolCall{{ID: "c1", Name: "knowledge_search", Arguments: `{}`}}}}},
{{Kind: agentllm.KindMessageEnd, Message: agentllm.Message{Role: agentllm.RoleAssistant, Content: "done", StopReason: agentllm.StopEnd}}},
}
base := scriptStream(turns, nil)
var requests [][]agentllm.Message
runner := &LLMRunner{Stream: func(ctx context.Context, model string, msgs []agentllm.Message, tools []agentllm.ToolDecl, opts agentllm.StreamOptions) <-chan agentllm.Event {
requests = append(requests, append([]agentllm.Message(nil), msgs...))
return base(ctx, model, msgs, tools, opts)
}}
c := &projectRunCtx{fakeRunCtx: &fakeRunCtx{ctx: context.Background(), projectID: "p1", prof: core.AgentProfile{Model: core.ModelRoute{Chat: "test-model"}}, execOutput: "AUTHORIZED_TOOL_RESULT"},
bundle: extension.ProjectContextBundle{Status: extension.ProjectContextReady, ProjectID: "p1", AuthorizationEpoch: "duty-1", PublicationEpoch: "pub-1", Items: []extension.ProjectContextItem{{SourceID: "fact", Revision: "r1", Hash: "h1", Text: "CURRENT_FACT", Required: true}}}}
if err := runner.Execute(c); err != nil {
t.Fatal(err)
}
if len(requests) < 2 || !strings.Contains(joinReq(requests[1]), "AUTHORIZED_TOOL_RESULT") || !strings.Contains(joinReq(requests[1]), "CURRENT_FACT") {
t.Fatalf("same-scope direct run lost current tool context: requests=%+v", requests)
}
}
func TestReadProjectEvidenceToolUsesScopedReader(t *testing.T) {
c := &projectRunCtx{fakeRunCtx: &fakeRunCtx{projectID: "p1"}, evidence: func(ref string) (extension.ProjectEvidence, error) {
if ref != "ref-1" {
return extension.ProjectEvidence{}, errors.New("reference denied")
}
return extension.ProjectEvidence{Reference: extension.ProjectEvidenceRef{ID: ref}, Text: "current authorized detail"}, nil
}}
st := &directRun{l: &LLMRunner{}, rc: c, eff: c.Settings()}
if got := st.runToolCtx(context.Background(), toolReadProjectEvidence, `{"reference":"ref-1"}`); got.IsError || !strings.Contains(got.Content, "current authorized detail") {
t.Fatalf("current evidence unavailable: %+v", got)
}
if got := st.runToolCtx(context.Background(), toolReadProjectEvidence, `{"reference":"../../arbitrary"}`); !got.IsError {
t.Fatalf("arbitrary evidence reference accepted: %+v", got)
}
}
func TestSubagentModelRequestUsesProjectContextAndManifest(t *testing.T) {
var sent []agentllm.Message
base := scriptStream(textTurns("done"), nil)
sub := &SubLLMRunner{Stream: func(ctx context.Context, model string, msgs []agentllm.Message, tools []agentllm.ToolDecl, opts agentllm.StreamOptions) <-chan agentllm.Event {
sent = append([]agentllm.Message(nil), msgs...)
return base(ctx, model, msgs, tools, opts)
}}
c := &projectRunCtx{fakeRunCtx: &fakeRunCtx{ctx: context.Background(), projectID: "project-1", prof: core.AgentProfile{Model: core.ModelRoute{Chat: "test-model", Lightweight: "test-lite"}}},
bundle: extension.ProjectContextBundle{Status: extension.ProjectContextReady, ProjectID: "project-1", Items: []extension.ProjectContextItem{{SourceID: "s1", Revision: "r1", Hash: "h1", Text: "current project fact", Required: true}}}}
if err := sub.Execute(c); err != nil {
t.Fatal(err)
}
if !strings.Contains(joinReq(sent), "current project fact") || len(c.manifests) != 2 || c.manifests[1].Delivery != "responded" {
t.Fatalf("background subagent missed project context: input=%q manifests=%+v", joinReq(sent), c.manifests)
}
}
func TestSubagentSameScopeKeepsNewToolResultAcrossTurns(t *testing.T) {
turns := [][]agentllm.Event{
{{Kind: agentllm.KindMessageEnd, Message: agentllm.Message{Role: agentllm.RoleAssistant, StopReason: agentllm.StopToolCalls, ToolCalls: []agentllm.ToolCall{{ID: "search-1", Name: "knowledge_search", Arguments: `{}`}}}}},
{{Kind: agentllm.KindMessageEnd, Message: agentllm.Message{Role: agentllm.RoleAssistant, Content: "done", StopReason: agentllm.StopEnd}}},
}
base := scriptStream(turns, nil)
var requests [][]agentllm.Message
sub := &SubLLMRunner{Stream: func(ctx context.Context, model string, msgs []agentllm.Message, tools []agentllm.ToolDecl, opts agentllm.StreamOptions) <-chan agentllm.Event {
requests = append(requests, append([]agentllm.Message(nil), msgs...))
return base(ctx, model, msgs, tools, opts)
}}
c := &projectRunCtx{fakeRunCtx: &fakeRunCtx{ctx: context.Background(), projectID: "p1", prof: core.AgentProfile{Model: core.ModelRoute{Chat: "test-model", Lightweight: "test-lite"}}, execOutput: "NEW_AUTHORIZED_TOOL"},
bundle: extension.ProjectContextBundle{Status: extension.ProjectContextReady, ProjectID: "p1", AuthorizationEpoch: "duty-1", PublicationEpoch: "pub-1", Items: []extension.ProjectContextItem{{SourceID: "fact", Revision: "r1", Hash: "h1", Text: "CURRENT_FACT", Required: true}}},
toolList: []core.ToolDesc{{Name: "knowledge_search", Description: "Search", Parameters: json.RawMessage(`{"type":"object"}`)}},
}
if err := sub.Execute(c); err != nil {
t.Fatal(err)
}
if len(requests) != 2 || !strings.Contains(joinReq(requests[1]), "NEW_AUTHORIZED_TOOL") || !strings.Contains(joinReq(requests[1]), "CURRENT_FACT") || len(c.manifests) != 4 {
t.Fatalf("same-scope subagent lost authorized tool context: requests=%+v manifests=%+v", requests, c.manifests)
}
}
func TestProjectStreamRejectsProjectRunWithoutTrustedContextPort(t *testing.T) {
called := false
base := func(ctx context.Context, model string, msgs []agentllm.Message, tools []agentllm.ToolDecl, opts agentllm.StreamOptions) <-chan agentllm.Event {
called = true
return scriptStream(textTurns("bad"), nil)(ctx, model, msgs, tools, opts)
}
stream := GuardProjectStream(&fakeRunCtx{projectID: "p1"}, "goal", base)
for ev := range stream(context.Background(), "test", []agentllm.Message{{Role: agentllm.RoleUser, Content: "goal"}}, nil, agentllm.StreamOptions{}) {
if ev.Kind != agentllm.KindError {
t.Fatalf("missing scope port did not fail closed: %+v", ev)
}
}
if called {
t.Fatal("model reached without a trusted project context port")
}
}
func TestProjectStreamBlocksRequestExceedingFinalModelBudget(t *testing.T) {
called := false
base := func(ctx context.Context, model string, msgs []agentllm.Message, tools []agentllm.ToolDecl, opts agentllm.StreamOptions) <-chan agentllm.Event {
called = true
return scriptStream(textTurns("bad"), nil)(ctx, model, msgs, tools, opts)
}
c := &projectRunCtx{fakeRunCtx: &fakeRunCtx{projectID: "p1"}, bundle: extension.ProjectContextBundle{Status: extension.ProjectContextReady, ProjectID: "p1", Items: []extension.ProjectContextItem{{SourceID: "large", Revision: "r1", Hash: "h1", Text: strings.Repeat("x", 11000), Required: true}}}, budgetSettings: core.EffectiveSettings{"models": map[string]any{"chat": map[string]any{"contextWindow": float64(4096), "outputReserve": float64(512), "contextBuffer": float64(512)}}}}
for ev := range GuardProjectStream(c, "goal", base)(context.Background(), "test", []agentllm.Message{{Role: agentllm.RoleUser, Content: "goal"}}, nil, agentllm.StreamOptions{}) {
if ev.Kind != agentllm.KindError {
t.Fatalf("oversized request did not fail: %+v", ev)
}
}
if called || len(c.manifests) != 0 {
t.Fatalf("oversized project request reached model or recorded an attempt: called=%v manifests=%+v", called, c.manifests)
}
}
func TestProjectStreamBlocksStaleMachineHistoryAfterDutyRevocation(t *testing.T) {
c := &projectRunCtx{fakeRunCtx: &fakeRunCtx{projectID: "p1"}, bundle: extension.ProjectContextBundle{Status: extension.ProjectContextReady, ProjectID: "p1", Version: "v1", AuthorizationEpoch: "duty-1", PublicationEpoch: "pub-1"}}
calls := 0
base := scriptStream(textTurns("done"), nil)
guarded := GuardProjectStream(c, "goal", func(ctx context.Context, model string, msgs []agentllm.Message, tools []agentllm.ToolDecl, opts agentllm.StreamOptions) <-chan agentllm.Event {
calls++
return base(ctx, model, msgs, tools, opts)
})
initial := []agentllm.Message{{Role: agentllm.RoleSystem, Content: "base"}, {Role: agentllm.RoleUser, Content: "goal"}}
for range guarded(context.Background(), "model", initial, nil, agentllm.StreamOptions{}) {
}
c.bundle = extension.ProjectContextBundle{Status: extension.ProjectContextReady, ProjectID: "p1", Version: "v2", AuthorizationEpoch: "duty-2", PublicationEpoch: "pub-2"}
stale := append(append([]agentllm.Message(nil), initial...), agentllm.Message{Role: agentllm.RoleAssistant, Content: "OLD_RESTRICTED_BODY"}, agentllm.Message{Role: agentllm.RoleTool, Content: "OLD_RESTRICTED_BODY"})
sawError := false
for ev := range guarded(context.Background(), "model", stale, nil, agentllm.StreamOptions{}) {
sawError = sawError || ev.Kind == agentllm.KindError
}
if !sawError || calls != 1 {
t.Fatalf("revoked history reached generic model stream: error=%v calls=%d", sawError, calls)
}
}
func TestProjectStreamRebuildsFirstRequestWithPriorMachineHistory(t *testing.T) {
c := &projectRunCtx{fakeRunCtx: &fakeRunCtx{projectID: "p1"}, bundle: extension.ProjectContextBundle{Status: extension.ProjectContextReady, ProjectID: "p1", AuthorizationEpoch: "duty-1", PublicationEpoch: "pub-1"}}
called := false
var requests [][]agentllm.Message
baseTurns := scriptStream(textTurns("ok", "done"), nil)
base := func(ctx context.Context, model string, msgs []agentllm.Message, tools []agentllm.ToolDecl, opts agentllm.StreamOptions) <-chan agentllm.Event {
called = true
requests = append(requests, append([]agentllm.Message(nil), msgs...))
return baseTurns(ctx, model, msgs, tools, opts)
}
msgs := []agentllm.Message{{Role: agentllm.RoleSystem, Content: "old system"}, {Role: agentllm.RoleUser, Content: "old goal"}, {Role: agentllm.RoleAssistant, Content: "OLD_RESTRICTED_BODY"}, {Role: agentllm.RoleTool, Content: "OLD_RESTRICTED_BODY"}, {Role: agentllm.RoleUser, Content: "current goal"}}
guarded := GuardProjectStream(c, "goal", base)
sawError := false
for ev := range guarded(context.Background(), "model", msgs, nil, agentllm.StreamOptions{}) {
sawError = sawError || ev.Kind == agentllm.KindError
}
if sawError || !called || strings.Contains(joinReq(requests[0]), "OLD_RESTRICTED_BODY") || !strings.Contains(joinReq(requests[0]), "current goal") {
t.Fatalf("first request was not safely rebuilt: error=%v called=%v sent=%q", sawError, called, joinReq(requests[0]))
}
second := append(append([]agentllm.Message(nil), msgs...), agentllm.Message{Role: agentllm.RoleAssistant, Content: "NEW_AUTHORIZED_STEP"}, agentllm.Message{Role: agentllm.RoleTool, Content: "NEW_AUTHORIZED_TOOL"})
for ev := range guarded(context.Background(), "model", second, nil, agentllm.StreamOptions{}) {
sawError = sawError || ev.Kind == agentllm.KindError
}
if sawError || len(requests) != 2 || strings.Contains(joinReq(requests[1]), "OLD_RESTRICTED_BODY") || !strings.Contains(joinReq(requests[1]), "NEW_AUTHORIZED_TOOL") {
t.Fatalf("second request failed to keep only same-scope new tool history: error=%v requests=%+v", sawError, requests)
}
}
func TestProjectStreamRejectsPriorMachineHistoryWithoutFreshGoal(t *testing.T) {
c := &projectRunCtx{fakeRunCtx: &fakeRunCtx{projectID: "p1"}, bundle: extension.ProjectContextBundle{Status: extension.ProjectContextReady, ProjectID: "p1", AuthorizationEpoch: "duty-1", PublicationEpoch: "pub-1"}}
called := false
base := func(ctx context.Context, model string, msgs []agentllm.Message, tools []agentllm.ToolDecl, opts agentllm.StreamOptions) <-chan agentllm.Event {
called = true
return scriptStream(textTurns("bad"), nil)(ctx, model, msgs, tools, opts)
}
msgs := []agentllm.Message{{Role: agentllm.RoleSystem, Content: "base"}, {Role: agentllm.RoleUser, Content: "old goal"}, {Role: agentllm.RoleAssistant, Content: "OLD_RESTRICTED_BODY"}}
sawError := false
for ev := range GuardProjectStream(c, "goal", base)(context.Background(), "model", msgs, nil, agentllm.StreamOptions{}) {
sawError = sawError || ev.Kind == agentllm.KindError
}
if !sawError || called {
t.Fatalf("machine history without a fresh goal reached model: error=%v called=%v", sawError, called)
}
}
type projectSquadCtx struct {
*entryFake
manifests []extension.ProjectContextManifest
}
func (c *projectSquadCtx) Run() core.Run {
return core.Run{ID: "run-squad", Owner: "alice", ProjectID: "project-1"}
}
func (c *projectSquadCtx) ProjectContext(context.Context, string, int) (extension.ProjectContextBundle, bool, error) {
return extension.ProjectContextBundle{Status: extension.ProjectContextReady, ProjectID: "project-1", AuthorizationEpoch: "duty-1", PublicationEpoch: "pub-1", Items: []extension.ProjectContextItem{{SourceID: "squad-source", Revision: "r1", Hash: "h1", Text: "squad project fact", Required: true}}}, true, nil
}
func (c *projectSquadCtx) SaveContextManifest(_ context.Context, m extension.ProjectContextManifest) error {
c.manifests = append(c.manifests, m)
return nil
}
func TestSquadEntryModelRequestUsesProjectContext(t *testing.T) {
var sent []agentllm.Message
calls := 0
base := writeThenStop(nil, nil)
runner := &SquadRunner{Router: fixedRoute{mode: squad.RouteDirect}, Checker: squad.MinimalChecker{}, Hitl: squad.HitlPolicy{Mode: squad.HitlDisabled},
Stream: func(ctx context.Context, model string, msgs []agentllm.Message, tools []agentllm.ToolDecl, opts agentllm.StreamOptions) <-chan agentllm.Event {
calls++
sent = append([]agentllm.Message(nil), msgs...)
return base(ctx, model, msgs, tools, opts)
}}
dir := t.TempDir()
c := &projectSquadCtx{entryFake: &entryFake{text: "write report", sandbox: dir, settings: core.EffectiveSettings{"models": map[string]any{"chat": map[string]any{"model": "test-model", "baseUrl": "http://test"}}, "storage": map[string]any{"dataDir": dir}}}}
if err := runner.Execute(c); err != nil {
t.Fatal(err)
}
if calls < 2 || !strings.Contains(joinReq(sent), "squad project fact") || !strings.Contains(joinReq(sent), "output.md") || len(c.manifests) < 4 || c.manifests[len(c.manifests)-1].Delivery != "responded" {
t.Fatalf("squad model missed project context: input=%q manifests=%+v", joinReq(sent), c.manifests)
}
}