Files
Agent-System-Backend/internal/runmgr/project_context_test.go
T
weichen 7a00b20cd4 Bind workflow judge inputs to trusted step attempts
Why: Compiled workflow critics could resend a prior prompt output after document duty or publication changed across separate model guards.

Evidence: RED cross-guard revocation and retry tests; go test ./... -count=1 and targeted go test -race passed locally before commit.

Impact: Bind materialized prompt bytes to the engine step attempt and responded model receipt; recheck current project sources and epochs before judging, failing closed for unproven or ambiguous output.

Refs: RDM two-trees stage5 W6; stage6 X1 public extension request; W6 independent review P1.
2026-10-14 04:55:14 +00:00

283 lines
14 KiB
Go

package runmgr
import (
"context"
"sync"
"testing"
"time"
agentllm "agent-native/runtime/llm"
"agent-system-backend/internal/projectcontext"
"agent-system-backend/pkg/extension"
core "agent-system-core"
)
func TestProjectContextUsesPersistedRunAndRefreshesAuthorization(t *testing.T) {
st := openRecoveryStore(t)
m := newRecoveryManager(t, st)
conv := recoveryConversation(t, st)
run := recoveryRun(t, st, conv, core.RunPending)
allowed := true
m.SetProjectWorkAuthorization(func(context.Context, string, core.ID) (bool, error) { return allowed, nil })
called := 0
m.SetProjectContextResolver(func(_ context.Context, req extension.ProjectContextRequest) (extension.ProjectContextBundle, error) {
called++
if req.Actor != string(run.Owner) || req.ProjectID != run.ProjectID || req.ConversationID != run.ConversationID || req.RunID != run.ID {
t.Errorf("context scope did not come from persisted run: %+v", req)
}
return extension.ProjectContextBundle{Status: extension.ProjectContextReady, ProjectID: req.ProjectID}, nil
})
rc := &runCtx{mgr: m, run: run, ctx: context.Background()}
if _, active, err := rc.ProjectContext(context.Background(), "work goal", 900); err != nil || !active || called != 1 {
t.Fatalf("valid persisted scope rejected: active=%v called=%d err=%v", active, called, err)
}
allowed = false
if _, _, err := rc.ProjectContext(context.Background(), "work goal", 900); err == nil || called != 1 {
t.Fatal("revoked project still reached context provider")
}
allowed = true
rc.run.ProjectID = "forged"
if _, _, err := rc.ProjectContext(context.Background(), "work goal", 900); err == nil || called != 1 {
t.Fatal("in-memory project override reached context provider")
}
}
func TestProjectContextResumeReloadsCurrentVersionAndAuthorization(t *testing.T) {
st := openRecoveryStore(t)
m := newRecoveryManager(t, st)
conv := recoveryConversation(t, st)
run := recoveryRun(t, st, conv, core.RunInterrupted)
allowed, version := true, "v1"
m.SetProjectWorkAuthorization(func(context.Context, string, core.ID) (bool, error) { return allowed, nil })
m.SetProjectContextResolver(func(_ context.Context, req extension.ProjectContextRequest) (extension.ProjectContextBundle, error) {
return extension.ProjectContextBundle{Status: extension.ProjectContextReady, ProjectID: req.ProjectID, Version: version}, nil
})
first := &runCtx{mgr: m, run: run, ctx: context.Background()}
if b, _, err := first.ProjectContext(context.Background(), "work", 900); err != nil || b.Version != "v1" {
t.Fatalf("initial context: %+v err=%v", b, err)
}
reloaded, err := st.GetRunByID(string(run.ID))
if err != nil {
t.Fatal(err)
}
resumed := &runCtx{mgr: m, run: reloaded, ctx: context.Background()}
version = "v2"
if b, _, err := resumed.ProjectContext(context.Background(), "work", 900); err != nil || b.Version != "v2" {
t.Fatalf("resumed context reused stale version: %+v err=%v", b, err)
}
allowed = false
if _, _, err := resumed.ProjectContext(context.Background(), "work", 900); err == nil {
t.Fatal("resumed context ignored current revocation")
}
}
func TestProjectJudgeChecksPersistedSourceEpochAcrossGuards(t *testing.T) {
st := openRecoveryStore(t)
m := newRecoveryManager(t, st)
conv := recoveryConversation(t, st)
run := recoveryRun(t, st, conv, core.RunPending)
m.SetProjectWorkAuthorization(func(context.Context, string, core.ID) (bool, error) { return true, nil })
epoch := "duty-1"
publication := "pub-1"
items := []extension.ProjectContextItem{{SourceID: "restricted-doc", Revision: "r1", Hash: "h1", Text: "OLD_RESTRICTED_BODY", Required: true}}
m.SetProjectContextResolver(func(_ context.Context, req extension.ProjectContextRequest) (extension.ProjectContextBundle, error) {
return extension.ProjectContextBundle{Status: extension.ProjectContextReady, ProjectID: req.ProjectID, AuthorizationEpoch: epoch, PublicationEpoch: publication, Watermark: "commit-1", Items: items}, nil
})
rc := &runCtx{mgr: m, run: run, ctx: context.Background()}
started := time.Now().Add(-time.Second)
bundle, _, err := rc.ProjectContext(context.Background(), "write output", 900)
if err != nil {
t.Fatal(err)
}
_, manifest, err := projectcontext.Assemble(bundle, 900)
if err != nil {
t.Fatal(err)
}
manifest.InputSHA256, manifest.AttemptID, manifest.Delivery = "input-hash", "step-attempt", "attempted"
manifest.WorkflowStepAttemptID = "engine-run/draft/1"
if err := rc.SaveContextManifest(context.Background(), manifest); err != nil {
t.Fatal(err)
}
manifest.Delivery = "responded"
if err := rc.SaveContextManifest(context.Background(), manifest); err != nil {
t.Fatal(err)
}
if err := rc.BindProjectStepOutput(context.Background(), "draft", "engine-run/draft/1", "prompt", started, []byte("OLD_RESTRICTED_BODY")); err != nil {
t.Fatal(err)
}
if err := rc.VerifyProjectStepOutput(context.Background(), "draft", "engine-run/draft/1", "prompt", []byte("OLD_RESTRICTED_BODY")); err != nil {
t.Fatalf("same epoch output rejected: %v", err)
}
if err := rc.ValidateProjectCrossGuard(context.Background(), bundle); err != nil {
t.Fatalf("same epoch cross-guard request rejected: %v", err)
}
if err := rc.VerifyProjectStepOutput(context.Background(), "draft", "engine-run/draft/1", "prompt", []byte("replaced bytes")); err == nil {
t.Fatal("replaced output accepted")
}
if err := rc.VerifyProjectStepOutput(context.Background(), "draft", "engine-run/draft/2", "prompt", []byte("OLD_RESTRICTED_BODY")); err == nil {
t.Fatal("second execution reused first attempt's identical output")
}
if err := rc.VerifyProjectStepOutput(context.Background(), "other", "engine-run/other/1", "prompt", []byte("OLD_RESTRICTED_BODY")); err == nil {
t.Fatal("another step reused output binding")
}
items = nil // provider withdraws the document while retaining project membership and epoch
if err := rc.VerifyProjectStepOutput(context.Background(), "draft", "engine-run/draft/1", "prompt", []byte("OLD_RESTRICTED_BODY")); err == nil {
t.Fatal("withdrawn document accepted with unchanged authorization epoch")
}
items = []extension.ProjectContextItem{{SourceID: "restricted-doc", Revision: "r1", Hash: "h1", Text: "OLD_RESTRICTED_BODY", Required: true}}
epoch = "duty-2"
current, _, err := rc.ProjectContext(context.Background(), "judge", 900)
if err != nil {
t.Fatal(err)
}
if err := rc.ValidateProjectCrossGuard(context.Background(), current); err == nil {
t.Fatal("cross-guard request ignored persisted epoch change")
}
if err := rc.VerifyProjectStepOutput(context.Background(), "draft", "engine-run/draft/1", "prompt", []byte("OLD_RESTRICTED_BODY")); err == nil {
t.Fatal("revoked source epoch accepted for workflow judge")
}
reloaded, err := st.GetRunByID(string(run.ID))
if err != nil {
t.Fatal(err)
}
resumed := &runCtx{mgr: m, run: reloaded, ctx: context.Background()}
if err := resumed.VerifyProjectStepOutput(context.Background(), "draft", "engine-run/draft/1", "prompt", []byte("OLD_RESTRICTED_BODY")); err == nil {
t.Fatal("resumed judge ignored revocation")
}
epoch, publication = "duty-1", "pub-2"
if err := resumed.VerifyProjectStepOutput(context.Background(), "draft", "engine-run/draft/1", "prompt", []byte("OLD_RESTRICTED_BODY")); err == nil {
t.Fatal("resumed judge ignored publication epoch change")
}
other := recoveryRun(t, st, conv, core.RunPending)
noReceipt := &runCtx{mgr: m, run: other, ctx: context.Background()}
if err := noReceipt.BindProjectStepOutput(context.Background(), "draft", "engine-run/draft/1", "prompt", started, []byte("tool body")); err == nil {
t.Fatal("tool-only output without model provenance accepted")
}
if err := noReceipt.VerifyProjectStepOutput(context.Background(), "draft", "engine-run/draft/1", "prompt", []byte("tool body")); err == nil {
t.Fatal("another run reused output binding")
}
}
func TestContextManifestPersistsByOwnerAndRun(t *testing.T) {
st := openRecoveryStore(t)
m := newRecoveryManager(t, st)
conv := recoveryConversation(t, st)
run := recoveryRun(t, st, conv, core.RunPending)
rc := &runCtx{mgr: m, run: run, ctx: context.Background()}
manifest := extension.ProjectContextManifest{ProjectID: run.ProjectID, Version: "v3", Model: "model-1", AttemptID: "attempt-1", Delivery: "attempted", InputSHA256: "abc", Included: []extension.ProjectContextManifestEntry{{SourceID: "s1", Revision: "r2", InputSHA256: "def"}}}
if err := rc.SaveContextManifest(context.Background(), manifest); err != nil {
t.Fatal(err)
}
got, err := st.ListContextManifests(string(run.Owner), string(run.ID))
if err != nil || len(got) != 1 || got[0].Version != "v3" || got[0].Included[0].SourceID != "s1" {
t.Fatalf("manifest missing from persisted run audit: got=%+v err=%v", got, err)
}
other, err := st.ListContextManifests("other", string(run.ID))
if err != nil || len(other) != 0 {
t.Fatalf("cross-owner manifest visible: got=%+v err=%v", other, err)
}
}
func TestContextManifestRejectsDocumentDutyRevocationBeforeSend(t *testing.T) {
st := openRecoveryStore(t)
m := newRecoveryManager(t, st)
conv := recoveryConversation(t, st)
run := recoveryRun(t, st, conv, core.RunPending)
m.SetProjectWorkAuthorization(func(context.Context, string, core.ID) (bool, error) { return true, nil })
epoch := "duty-1"
m.SetProjectContextResolver(func(_ context.Context, req extension.ProjectContextRequest) (extension.ProjectContextBundle, error) {
return extension.ProjectContextBundle{Status: extension.ProjectContextReady, ProjectID: req.ProjectID, Version: "v1", AuthorizationEpoch: epoch,
Items: []extension.ProjectContextItem{{SourceID: "restricted-doc", Text: "private text", Required: true}}}, nil
})
rc := &runCtx{mgr: m, run: run, ctx: context.Background()}
bundle, active, err := rc.ProjectContext(context.Background(), "work", 900)
if err != nil || !active {
t.Fatalf("initial context: active=%v err=%v", active, err)
}
_, manifest, err := projectcontext.Assemble(bundle, 900)
if err != nil {
t.Fatal(err)
}
manifest.InputSHA256, manifest.AttemptID, manifest.Delivery = "request-hash", "attempt-revoked", "attempted"
epoch = "duty-2" // project work remains allowed; document duty changed
if err := rc.SaveContextManifest(context.Background(), manifest); err == nil {
t.Fatal("stale document duty accepted at model send")
}
stored, err := st.ListContextManifests(string(run.Owner), string(run.ID))
if err != nil || len(stored) != 0 {
t.Fatalf("revoked context wrote a send receipt: %+v err=%v", stored, err)
}
}
func TestProjectEvidenceOnlyReadsCurrentBundleReference(t *testing.T) {
st := openRecoveryStore(t)
m := newRecoveryManager(t, st)
conv := recoveryConversation(t, st)
run := recoveryRun(t, st, conv, core.RunPending)
m.SetProjectWorkAuthorization(func(context.Context, string, core.ID) (bool, error) { return true, nil })
listed := true
m.SetProjectContextResolver(func(_ context.Context, req extension.ProjectContextRequest) (extension.ProjectContextBundle, error) {
b := extension.ProjectContextBundle{Status: extension.ProjectContextReady, ProjectID: req.ProjectID}
if listed {
b.Evidence = []extension.ProjectEvidenceRef{{ID: "ref-1", SourceID: "doc-1", Revision: "r3", Hash: "source-hash"}}
}
return b, nil
})
read := 0
m.SetProjectEvidenceReader(func(_ context.Context, req extension.ProjectEvidenceRequest) (extension.ProjectEvidence, error) {
read++
if req.Scope.RunID != run.ID || req.Reference.ID != "ref-1" {
t.Errorf("untrusted evidence request: %+v", req)
}
return extension.ProjectEvidence{Reference: req.Reference, Text: "authorized detail"}, nil
})
rc := &runCtx{mgr: m, run: run, ctx: context.Background()}
if _, err := rc.ProjectEvidence(context.Background(), "forged-ref"); err == nil || read != 0 {
t.Fatal("arbitrary reference reached provider")
}
evidence, err := rc.ProjectEvidence(context.Background(), "ref-1")
if err != nil || evidence.Text != "authorized detail" || read != 1 {
t.Fatalf("current reference rejected: evidence=%+v read=%d err=%v", evidence, read, err)
}
if got := rc.ContextEvidenceInRequest([]agentllm.Message{{Role: agentllm.RoleTool, Content: "authorized detail"}}); len(got) != 1 || got[0].SourceID != "doc-1" {
t.Fatalf("actual evidence read missing from request manifest: %+v", got)
}
if got := rc.ContextEvidenceInRequest([]agentllm.Message{{Role: agentllm.RoleTool, Content: "cleared old tool result"}}); len(got) != 0 {
t.Fatalf("cleared evidence still counted as model input: %+v", got)
}
listed = false
if _, err := rc.ProjectEvidence(context.Background(), "ref-1"); err == nil || read != 1 {
t.Fatal("revoked reference reached provider")
}
if err := rc.SaveContextManifest(context.Background(), extension.ProjectContextManifest{ProjectID: run.ProjectID, InputSHA256: "new-request-hash", AttemptID: "attempt-revoked", Delivery: "attempted"}); err == nil {
t.Fatal("new model request accepted after an earlier evidence read was revoked")
}
}
func TestConcurrentProjectEvidenceReadsKeepTrustedScope(t *testing.T) {
st := openRecoveryStore(t)
m := newRecoveryManager(t, st)
conv := recoveryConversation(t, st)
run := recoveryRun(t, st, conv, core.RunPending)
m.SetProjectWorkAuthorization(func(context.Context, string, core.ID) (bool, error) { return true, nil })
ref := extension.ProjectEvidenceRef{ID: "r1", SourceID: "doc", Revision: "v1"}
m.SetProjectContextResolver(func(_ context.Context, req extension.ProjectContextRequest) (extension.ProjectContextBundle, error) {
return extension.ProjectContextBundle{Status: extension.ProjectContextReady, ProjectID: req.ProjectID, Evidence: []extension.ProjectEvidenceRef{ref}}, nil
})
m.SetProjectEvidenceReader(func(_ context.Context, req extension.ProjectEvidenceRequest) (extension.ProjectEvidence, error) {
return extension.ProjectEvidence{Reference: req.Reference, Text: "body"}, nil
})
rc := &runCtx{mgr: m, run: run, ctx: context.Background()}
var wg sync.WaitGroup
for i := 0; i < 8; i++ {
wg.Add(1)
go func() {
defer wg.Done()
if _, err := rc.ProjectEvidence(context.Background(), "r1"); err != nil {
t.Error(err)
}
}()
}
wg.Wait()
}