Why: Integrate the exact Dev main pin while preserving trusted project context, vision routing, work-result context, and distinct migration identities.
Evidence: go vet ./...; go test ./... -count=1; go build ./...; CGO_ENABLED=0 go build ./cmd/agent-system; isolated PostgreSQL migration history tests.
Impact: Keep exec_env_layers at schema v35, move the feature migrations to v36-v46 in order, and fail closed on the old unpublished numbering.
Refs: 14e780c6fa32fdd4742b6232150c5efcb0d766ff; ab12fea474
489 lines
28 KiB
Go
489 lines
28 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)
|
|
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)
|
|
}
|
|
}
|