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) workContext string 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) WorkContext() string { return c.workContext } func (c *projectRunCtx) ProjectEvidence(_ context.Context, ref string) (extension.ProjectEvidence, error) { return c.evidence(ref) } func TestProjectContextKeepsVisionRoutingAndImageInputs(t *testing.T) { var gotModel string var gotMessages []agentllm.Message runner := &LLMRunner{Stream: func(ctx context.Context, model string, msgs []agentllm.Message, tools []agentllm.ToolDecl, opts agentllm.StreamOptions) <-chan agentllm.Event { gotModel = model gotMessages = append([]agentllm.Message(nil), msgs...) return scriptStream(textTurns("done"), nil)(ctx, model, msgs, tools, opts) }} c := &projectRunCtx{ fakeRunCtx: &fakeRunCtx{ ctx: context.Background(), projectID: "project-1", prof: core.AgentProfile{Model: core.ModelRoute{Chat: "plain-text"}}, history: []core.Message{{ ID: "m-image", Role: core.RoleUser, Parts: []core.Part{{Type: core.PartText, Text: "inspect this http://127.0.0.1/private.png"}}, }}, attachments: []core.ParsedDocument{{ Meta: core.DocMeta{Filename: "chart.png", MIME: "image/png"}, Blocks: []core.DocBlock{{ Type: core.BlockImageRef, Ref: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==", }}, }}, }, bundle: extension.ProjectContextBundle{ Status: extension.ProjectContextReady, ProjectID: "project-1", Items: []extension.ProjectContextItem{{SourceID: "source-1", Revision: "r1", Hash: "h1", Text: "CURRENT_PROJECT_FACT", Required: true}}, }, budgetSettings: core.EffectiveSettings{ "models": map[string]any{ "chat": map[string]any{"model": "plain-text", "baseUrl": "http://test"}, "vision": map[string]any{"model": "vision-model", "capable": "vision-model"}, }, "runtime": map[string]any{"verify": false, "composeFinal": false}, }, } if err := runner.Execute(c); err != nil { t.Fatal(err) } if gotModel != "vision-model" { t.Fatalf("project image request model=%q, want vision-model", gotModel) } var images int for _, msg := range gotMessages { images += len(msg.Images) } wire := joinReq(gotMessages) if images != 1 || !strings.Contains(wire, "CURRENT_PROJECT_FACT") || !strings.Contains(wire, "地址不在允许范围") { t.Fatalf("project image request lost context or image behavior: images=%d request=%q", images, wire) } } func TestProjectContextRefreshesEachTurnAndWorkContextAppearsOnce(t *testing.T) { turns := [][]agentllm.Event{ {{Kind: agentllm.KindMessageEnd, Message: agentllm.Message{Role: agentllm.RoleAssistant, StopReason: agentllm.StopToolCalls, ToolCalls: []agentllm.ToolCall{{ID: "tool-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 c := &projectRunCtx{ fakeRunCtx: &fakeRunCtx{ctx: context.Background(), projectID: "project-1", prof: core.AgentProfile{Model: core.ModelRoute{Chat: "test-model"}}}, bundle: extension.ProjectContextBundle{Status: extension.ProjectContextReady, ProjectID: "project-1", Version: "v1", AuthorizationEpoch: "auth-1", PublicationEpoch: "pub-1", Items: []extension.ProjectContextItem{{SourceID: "source-1", Revision: "r1", Hash: "h1", Text: "PROJECT_V1", Required: true}}}, workContext: "SAVED_WORK_CONTEXT", } 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.Version = "v2" c.bundle.AuthorizationEpoch = "auth-2" c.bundle.PublicationEpoch = "pub-2" c.bundle.Items[0].Text = "PROJECT_V2" } 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 at least 2", len(requests)) } first := joinReq(requests[0]) all := first for _, request := range requests[1:] { all += "\n" + joinReq(request) } if strings.Count(all, "SAVED_WORK_CONTEXT") != 1 { t.Fatalf("work context must appear once on turn 0: requests=%q", all) } if !strings.Contains(first, "PROJECT_V1") { t.Fatalf("first request lost v1 project context: %q", first) } for i, request := range requests[1:] { wire := joinReq(request) if !strings.Contains(wire, "PROJECT_V2") || strings.Contains(wire, "PROJECT_V1") { t.Fatalf("request %d did not refresh without stale carryover: %q", i+2, wire) } } } func TestProjectContextSecondTurnRefreshFailureBlocksModel(t *testing.T) { turns := [][]agentllm.Event{{ {Kind: agentllm.KindMessageEnd, Message: agentllm.Message{Role: agentllm.RoleAssistant, StopReason: agentllm.StopToolCalls, ToolCalls: []agentllm.ToolCall{{ID: "tool-1", Name: "knowledge_search", Arguments: `{}`}}}}, }} base := scriptStream(turns, nil) modelCalls := 0 c := &projectRunCtx{ fakeRunCtx: &fakeRunCtx{ctx: context.Background(), projectID: "project-1", prof: core.AgentProfile{Model: core.ModelRoute{Chat: "test-model"}}}, bundle: extension.ProjectContextBundle{Status: extension.ProjectContextReady, ProjectID: "project-1", Version: "v1", Items: []extension.ProjectContextItem{{SourceID: "source-1", Revision: "r1", Hash: "h1", Text: "PROJECT_V1", Required: true}}}, workContext: "SAVED_WORK_CONTEXT", } runner := &LLMRunner{Stream: func(ctx context.Context, model string, msgs []agentllm.Message, tools []agentllm.ToolDecl, opts agentllm.StreamOptions) <-chan agentllm.Event { modelCalls++ c.readErr = errors.New("project authorization revoked") return base(ctx, model, msgs, tools, opts) }} err := runner.Execute(c) if err == nil || !strings.Contains(err.Error(), "project authorization revoked") || modelCalls != 1 || c.reads != 2 { t.Fatalf("second-turn refresh failure was swallowed: err=%v modelCalls=%d reads=%d", err, modelCalls, c.reads) } } 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 TestSquadEntryProjectRequestUsesContextBeforeWorkspaceFailClosed(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}}}} err := runner.Execute(c) if err == nil || !strings.Contains(err.Error(), "missing target deliverable output.md") { t.Fatalf("project squad did not fail closed without audited workspace tools: %v", err) } if calls < 1 || !strings.Contains(joinReq(sent), "squad project fact") || len(c.manifests) < 2 || c.manifests[len(c.manifests)-1].Delivery != "responded" { t.Fatalf("squad model missed project context: input=%q manifests=%+v", joinReq(sent), c.manifests) } }