447 lines
20 KiB
Go
447 lines
20 KiB
Go
package runmgr
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"agent-system-backend/internal/projectcontext"
|
|
"agent-system-backend/pkg/extension"
|
|
core "agent-system-core"
|
|
)
|
|
|
|
type recheckFixture struct {
|
|
rc *runCtx
|
|
mgr *Manager
|
|
run core.Run
|
|
receipt extension.ProjectContextManifest
|
|
current extension.ProjectContextBundle
|
|
ref extension.ProjectEvidenceRef
|
|
}
|
|
|
|
func newRecheckFixture(t *testing.T) recheckFixture {
|
|
t.Helper()
|
|
st := openRecoveryStore(t)
|
|
mgr := newRecoveryManager(t, st)
|
|
conv := recoveryConversation(t, st)
|
|
run := recoveryRun(t, st, conv, core.RunPending)
|
|
mgr.SetProjectWorkAuthorization(func(context.Context, string, core.ID) (bool, error) { return true, nil })
|
|
item := extension.ProjectContextItem{SourceID: "source-a", Revision: "r1", Hash: "ha", Text: "AUTHORIZED_A", Required: true}
|
|
ref := extension.ProjectEvidenceRef{ID: "ref-a", SourceID: "doc-a", Revision: "r1", Hash: "hd"}
|
|
bundle := extension.ProjectContextBundle{Status: extension.ProjectContextReady, ProjectID: run.ProjectID, Version: "v1", Watermark: "w1",
|
|
AuthorizationEpoch: "duty-1", PublicationEpoch: "pub-1", Items: []extension.ProjectContextItem{item}, Evidence: []extension.ProjectEvidenceRef{ref}}
|
|
_, receipt, err := projectcontext.Assemble(bundle, 900)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
receipt.Delivery = "responded"
|
|
receipt.ReadEvidence = []extension.ProjectContextManifestEntry{{ReferenceID: ref.ID, SourceID: ref.SourceID, Revision: ref.Revision, SourceHash: ref.Hash,
|
|
InputSHA256: fmt.Sprintf("%x", sha256.Sum256([]byte("DETAIL_A")))}}
|
|
mgr.SetProjectEvidenceReader(func(_ context.Context, req extension.ProjectEvidenceRequest) (extension.ProjectEvidence, error) {
|
|
return extension.ProjectEvidence{Reference: req.Reference, Text: "DETAIL_A"}, nil
|
|
})
|
|
return recheckFixture{rc: &runCtx{mgr: mgr, run: run, ctx: context.Background()}, mgr: mgr, run: run, receipt: receipt, current: bundle, ref: ref}
|
|
}
|
|
|
|
func (f recheckFixture) answer(bundle extension.ProjectContextBundle) {
|
|
f.mgr.SetProjectContextSourceRechecker(func(context.Context, extension.ProjectContextSourceRecheckRequest) (extension.ProjectContextSourceRecheckResult, error) {
|
|
return extension.ProjectContextSourceRecheckResult{Bundle: bundle}, nil
|
|
})
|
|
}
|
|
|
|
func TestProjectSourceRecheckRequestCarriesOnlyTrustedIdentities(t *testing.T) {
|
|
f := newRecheckFixture(t)
|
|
f.mgr.SetProjectContextResolver(func(_ context.Context, req extension.ProjectContextRequest) (extension.ProjectContextBundle, error) {
|
|
return f.current, nil
|
|
})
|
|
// A live request goal must not leak into the later source recheck.
|
|
if _, _, err := f.rc.ProjectContext(context.Background(), "SECRET_GOAL_TEXT", 900); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var got extension.ProjectContextSourceRecheckRequest
|
|
f.mgr.SetProjectContextSourceRechecker(func(_ context.Context, req extension.ProjectContextSourceRecheckRequest) (extension.ProjectContextSourceRecheckResult, error) {
|
|
got = req
|
|
return extension.ProjectContextSourceRecheckResult{Bundle: f.current}, nil
|
|
})
|
|
if err := f.rc.recheckManifest(context.Background(), f.receipt, "draft"); err != nil {
|
|
t.Fatalf("authorized recheck rejected: %v", err)
|
|
}
|
|
scope := got.Scope
|
|
if scope.Actor != string(f.run.Owner) || scope.ProjectID != f.run.ProjectID || scope.ConversationID != f.run.ConversationID ||
|
|
scope.RunID != f.run.ID || scope.StepID != "draft" || scope.Goal != "" || scope.TargetID != "" {
|
|
t.Fatalf("recheck scope is not the persisted run identity: %+v", scope)
|
|
}
|
|
if len(got.Included) != 1 || got.Included[0] != f.receipt.Included[0] || len(got.OfferedReferences) != 1 || got.OfferedReferences[0] != f.ref ||
|
|
len(got.ReadEvidence) != 1 || got.ReadEvidence[0] != f.receipt.ReadEvidence[0] {
|
|
t.Fatalf("recheck request does not name exactly the recorded inputs: %+v", got)
|
|
}
|
|
raw, err := json.Marshal(got)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, body := range []string{"SECRET_GOAL_TEXT", "AUTHORIZED_A", "DETAIL_A"} {
|
|
if strings.Contains(string(raw), body) {
|
|
t.Fatalf("recheck request carried body text %q", body)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestProjectSourceRecheckRejectsUnrelatedOrDuplicateSource(t *testing.T) {
|
|
f := newRecheckFixture(t)
|
|
receipt := f.receipt
|
|
receipt.Included = append(receipt.Included, receipt.Included[0])
|
|
padded := f.current
|
|
padded.Items = []extension.ProjectContextItem{f.current.Items[0], {SourceID: "source-x", Revision: "r9", Hash: "hx", Text: "UNRELATED"}}
|
|
f.answer(padded)
|
|
if err := f.rc.recheckManifest(context.Background(), receipt, "draft"); err == nil {
|
|
t.Fatal("unrelated source filled the slot of a recorded input")
|
|
}
|
|
echoed := f.current
|
|
echoed.Items = []extension.ProjectContextItem{{SourceID: "source-a", Revision: "r1", Hash: "ha", Text: "REPLACED"}}
|
|
f.answer(echoed)
|
|
if err := f.rc.recheckManifest(context.Background(), f.receipt, "draft"); err == nil {
|
|
t.Fatal("provider echoed recorded identity over replaced text")
|
|
}
|
|
republished := f.current
|
|
republished.PublicationEpoch = "pub-2"
|
|
f.answer(republished)
|
|
if err := f.rc.recheckManifest(context.Background(), f.receipt, "draft"); err == nil {
|
|
t.Fatal("publication change accepted")
|
|
}
|
|
}
|
|
|
|
func TestProjectSourceRecheckFailsClosedOnRefusalDenialAndCancellation(t *testing.T) {
|
|
f := newRecheckFixture(t)
|
|
f.answer(f.current)
|
|
if err := f.rc.recheckManifest(context.Background(), f.receipt, "draft"); err != nil {
|
|
t.Fatalf("authorized baseline rejected: %v", err)
|
|
}
|
|
f.mgr.SetProjectContextSourceRechecker(func(context.Context, extension.ProjectContextSourceRecheckRequest) (extension.ProjectContextSourceRecheckResult, error) {
|
|
return extension.ProjectContextSourceRecheckResult{Bundle: f.current}, errors.New("provider refused")
|
|
})
|
|
if err := f.rc.recheckManifest(context.Background(), f.receipt, "draft"); err == nil {
|
|
t.Fatal("provider refusal accepted")
|
|
}
|
|
denied := f.current
|
|
denied.Status = extension.ProjectContextDenied
|
|
f.answer(denied)
|
|
if err := f.rc.recheckManifest(context.Background(), f.receipt, "draft"); err == nil {
|
|
t.Fatal("denied bundle accepted")
|
|
}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
f.mgr.SetProjectContextSourceRechecker(func(context.Context, extension.ProjectContextSourceRecheckRequest) (extension.ProjectContextSourceRecheckResult, error) {
|
|
cancel()
|
|
return extension.ProjectContextSourceRecheckResult{Bundle: f.current}, nil
|
|
})
|
|
if err := f.rc.recheckManifest(ctx, f.receipt, "draft"); !errors.Is(err, context.Canceled) {
|
|
t.Fatalf("source recheck cancelled in flight was accepted: %v", err)
|
|
}
|
|
f.answer(f.current)
|
|
ctx, cancel = context.WithCancel(context.Background())
|
|
f.mgr.SetProjectEvidenceReader(func(_ context.Context, req extension.ProjectEvidenceRequest) (extension.ProjectEvidence, error) {
|
|
cancel()
|
|
return extension.ProjectEvidence{Reference: req.Reference, Text: "DETAIL_A"}, nil
|
|
})
|
|
if err := f.rc.recheckManifest(ctx, f.receipt, "draft"); !errors.Is(err, context.Canceled) {
|
|
t.Fatalf("evidence re-read cancelled in flight was accepted: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestProjectSourceRecheckConcurrentCancellationFailsEveryWaiter(t *testing.T) {
|
|
f := newRecheckFixture(t)
|
|
const workers = 6
|
|
entered := make(chan struct{}, workers)
|
|
release := make(chan struct{})
|
|
f.mgr.SetProjectContextSourceRechecker(func(context.Context, extension.ProjectContextSourceRecheckRequest) (extension.ProjectContextSourceRecheckResult, error) {
|
|
entered <- struct{}{}
|
|
<-release
|
|
return extension.ProjectContextSourceRecheckResult{Bundle: f.current}, nil
|
|
})
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
errs := make(chan error, workers)
|
|
for i := 0; i < workers; i++ {
|
|
go func() { errs <- f.rc.recheckManifest(ctx, f.receipt, "draft") }()
|
|
}
|
|
timeout := time.After(10 * time.Second)
|
|
for i := 0; i < workers; i++ {
|
|
select {
|
|
case <-entered:
|
|
case <-timeout:
|
|
t.Fatal("concurrent rechecks did not reach the provider")
|
|
}
|
|
}
|
|
cancel()
|
|
close(release)
|
|
for i := 0; i < workers; i++ {
|
|
if err := <-errs; !errors.Is(err, context.Canceled) {
|
|
t.Fatalf("concurrent recheck survived cancellation: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestProjectCrossGuardRechecksEarlierRoundAfterLaterSource(t *testing.T) {
|
|
st := openRecoveryStore(t)
|
|
mgr := newRecoveryManager(t, st)
|
|
conv := recoveryConversation(t, st)
|
|
run := recoveryRun(t, st, conv, core.RunPending)
|
|
mgr.SetProjectWorkAuthorization(func(context.Context, string, core.ID) (bool, error) { return true, nil })
|
|
items := map[string]extension.ProjectContextItem{
|
|
"source-a": {SourceID: "source-a", Revision: "r1", Hash: "ha", Text: "AUTHORIZED_A", Required: true},
|
|
"source-b": {SourceID: "source-b", Revision: "r1", Hash: "hb", Text: "PUBLIC_B", Required: true},
|
|
}
|
|
withdrawn := map[string]bool{}
|
|
bundleWith := func(projectID core.ID, sources ...string) extension.ProjectContextBundle {
|
|
b := extension.ProjectContextBundle{Status: extension.ProjectContextReady, ProjectID: projectID, AuthorizationEpoch: "duty-1", PublicationEpoch: "pub-1", Watermark: "w1"}
|
|
for _, id := range sources {
|
|
if !withdrawn[id] {
|
|
b.Items = append(b.Items, items[id])
|
|
}
|
|
}
|
|
return b
|
|
}
|
|
mgr.SetProjectContextResolver(func(_ context.Context, req extension.ProjectContextRequest) (extension.ProjectContextBundle, error) {
|
|
if strings.Contains(req.Goal, "use A") {
|
|
return bundleWith(req.ProjectID, "source-a"), nil
|
|
}
|
|
return bundleWith(req.ProjectID, "source-b"), nil
|
|
})
|
|
var rechecked []string
|
|
mgr.SetProjectContextSourceRechecker(func(_ context.Context, req extension.ProjectContextSourceRecheckRequest) (extension.ProjectContextSourceRecheckResult, error) {
|
|
var ids []string
|
|
for _, entry := range req.Included {
|
|
ids = append(ids, entry.SourceID)
|
|
rechecked = append(rechecked, entry.SourceID)
|
|
}
|
|
return extension.ProjectContextSourceRecheckResult{Bundle: bundleWith(req.Scope.ProjectID, ids...)}, nil
|
|
})
|
|
rc := &runCtx{mgr: mgr, run: run, ctx: context.Background()}
|
|
respond := func(goal, attempt string) extension.ProjectContextBundle {
|
|
t.Helper()
|
|
bundle, _, err := rc.ProjectContext(context.Background(), goal, 900)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, m, err := projectcontext.Assemble(bundle, 900)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
m.InputSHA256, m.AttemptID, m.Delivery = "input-"+attempt, attempt, "attempted"
|
|
if err := rc.SaveContextManifest(context.Background(), m); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
m.Delivery = "responded"
|
|
if err := rc.SaveContextManifest(context.Background(), m); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return bundle
|
|
}
|
|
respond("use A", "round-a")
|
|
bundleB := respond("now B", "round-b")
|
|
if err := rc.ValidateProjectCrossGuard(context.Background(), bundleB); err != nil {
|
|
t.Fatalf("authorized multi-round run rejected: %v", err)
|
|
}
|
|
slices.Sort(rechecked)
|
|
if strings.Join(rechecked, ",") != "source-a,source-b" {
|
|
t.Fatalf("cross guard did not recheck every earlier round: %q", rechecked)
|
|
}
|
|
withdrawn["source-a"] = true
|
|
if err := rc.ValidateProjectCrossGuard(context.Background(), bundleB); err == nil {
|
|
t.Fatal("withdrawn early-round source A passed because the last round used B")
|
|
}
|
|
}
|
|
|
|
func TestProjectStepOutputRejectsUnsettledAttemptInSameStep(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 })
|
|
bundle := func(projectID core.ID) extension.ProjectContextBundle {
|
|
return extension.ProjectContextBundle{Status: extension.ProjectContextReady, ProjectID: projectID, AuthorizationEpoch: "duty-1", PublicationEpoch: "pub-1", Watermark: "w1",
|
|
Items: []extension.ProjectContextItem{{SourceID: "source-a", Revision: "r1", Hash: "ha", Text: "AUTHORIZED_A", Required: true}}}
|
|
}
|
|
m.SetProjectContextResolver(func(_ context.Context, req extension.ProjectContextRequest) (extension.ProjectContextBundle, error) {
|
|
return bundle(req.ProjectID), nil
|
|
})
|
|
m.SetProjectContextSourceRechecker(func(_ context.Context, req extension.ProjectContextSourceRecheckRequest) (extension.ProjectContextSourceRecheckResult, error) {
|
|
return extension.ProjectContextSourceRecheckResult{Bundle: bundle(req.Scope.ProjectID)}, nil
|
|
})
|
|
rc := &runCtx{mgr: m, run: run, ctx: context.Background()}
|
|
started := time.Now().Add(-time.Second)
|
|
current, _, err := rc.ProjectContext(context.Background(), "draft", 900)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, receipt, err := projectcontext.Assemble(current, 900)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
receipt.InputSHA256, receipt.AttemptID, receipt.WorkflowStepAttemptID = "input-1", "model-1", "engine-run/draft/1"
|
|
scoped := projectcontext.WithResolveScope(context.Background(), "draft", "")
|
|
for _, delivery := range []string{"attempted", "responded"} {
|
|
receipt.Delivery = delivery
|
|
if err := rc.SaveContextManifest(scoped, receipt); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
output := []byte("AUTHORIZED_A report")
|
|
if err := rc.BindProjectStepOutput(context.Background(), "draft", "engine-run/draft/1", "prompt", started, output); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := rc.VerifyProjectStepOutput(context.Background(), "draft", "engine-run/draft/1", "prompt", output); err != nil {
|
|
t.Fatalf("settled step rejected: %v", err)
|
|
}
|
|
later := receipt
|
|
later.InputSHA256, later.AttemptID, later.Delivery = "input-2", "model-2", "attempted"
|
|
if err := rc.SaveContextManifest(scoped, later); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := rc.VerifyProjectStepOutput(context.Background(), "draft", "engine-run/draft/1", "prompt", output); err == nil {
|
|
t.Fatal("step with an unanswered model attempt passed")
|
|
}
|
|
later.Delivery = "unknown"
|
|
if err := rc.SaveContextManifest(scoped, later); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := rc.VerifyProjectStepOutput(context.Background(), "draft", "engine-run/draft/1", "prompt", output); err == nil {
|
|
t.Fatal("step mixing responded and unknown model attempts passed")
|
|
}
|
|
}
|
|
|
|
func TestProjectSourceRecheckComparesMissingSourcesAsASet(t *testing.T) {
|
|
f := newRecheckFixture(t)
|
|
receipt := f.receipt
|
|
receipt.Missing = []string{"source-b", "source-a"}
|
|
// Order carries no authorization meaning, so a reordered list of the same
|
|
// unresolved sources stays a pass.
|
|
reordered := f.current
|
|
reordered.Missing = []string{"source-a", "source-b"}
|
|
f.answer(reordered)
|
|
if err := f.rc.recheckManifest(context.Background(), receipt, "draft"); err != nil {
|
|
t.Fatalf("reordered missing sources treated as a changed source set: %v", err)
|
|
}
|
|
if strings.Join(receipt.Missing, ",") != "source-b,source-a" || strings.Join(reordered.Missing, ",") != "source-a,source-b" {
|
|
t.Fatalf("missing-source comparison mutated a caller slice: recorded=%q current=%q", receipt.Missing, reordered.Missing)
|
|
}
|
|
// Any difference other than order still fails closed.
|
|
for name, missing := range map[string][]string{
|
|
"dropped": {"source-a"},
|
|
"added": {"source-a", "source-b", "source-c"},
|
|
"replaced": {"source-a", "source-z"},
|
|
"masked-by-dup": {"source-a", "source-a"},
|
|
"withdrawn": nil,
|
|
} {
|
|
changed := f.current
|
|
changed.Missing = missing
|
|
f.answer(changed)
|
|
if err := f.rc.recheckManifest(context.Background(), receipt, "draft"); err == nil {
|
|
t.Fatalf("changed missing-source set %q was accepted", name)
|
|
}
|
|
}
|
|
f.answer(reordered)
|
|
if err := f.rc.recheckManifest(context.Background(), receipt, "draft"); err != nil {
|
|
t.Fatalf("unrelated failure left the authorized baseline broken: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestProjectStepOutputRejectsEvidenceRefWithChangedHash(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: "ref-a", SourceID: "doc-a", Revision: "r1", Hash: "hd"}
|
|
evidenceHash := ref.Hash
|
|
bundle := func() extension.ProjectContextBundle {
|
|
return extension.ProjectContextBundle{Status: extension.ProjectContextReady, ProjectID: run.ProjectID, Version: "v1", Watermark: "w1",
|
|
AuthorizationEpoch: "duty-1", PublicationEpoch: "pub-1",
|
|
Items: []extension.ProjectContextItem{{SourceID: "source-a", Revision: "r1", Hash: "ha", Text: "AUTHORIZED_A", Required: true}},
|
|
Evidence: []extension.ProjectEvidenceRef{{ID: ref.ID, SourceID: ref.SourceID, Revision: ref.Revision, Hash: evidenceHash}}}
|
|
}
|
|
m.SetProjectContextResolver(func(context.Context, extension.ProjectContextRequest) (extension.ProjectContextBundle, error) {
|
|
return bundle(), nil
|
|
})
|
|
m.SetProjectContextSourceRechecker(func(context.Context, extension.ProjectContextSourceRecheckRequest) (extension.ProjectContextSourceRecheckResult, error) {
|
|
return extension.ProjectContextSourceRecheckResult{Bundle: bundle()}, nil
|
|
})
|
|
m.SetProjectEvidenceReader(func(_ context.Context, req extension.ProjectEvidenceRequest) (extension.ProjectEvidence, error) {
|
|
return extension.ProjectEvidence{Reference: req.Reference, Text: "DETAIL_A"}, nil
|
|
})
|
|
rc := &runCtx{mgr: m, run: run, ctx: context.Background()}
|
|
current, _, err := rc.ProjectContext(context.Background(), "draft", 900)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, receipt, err := projectcontext.Assemble(current, 900)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
receipt.InputSHA256, receipt.AttemptID, receipt.WorkflowStepAttemptID = "input-1", "model-1", "engine-run/draft/1"
|
|
receipt.ReadEvidence = []extension.ProjectContextManifestEntry{{ReferenceID: ref.ID, SourceID: ref.SourceID, Revision: ref.Revision, SourceHash: ref.Hash,
|
|
InputSHA256: fmt.Sprintf("%x", sha256.Sum256([]byte("DETAIL_A")))}}
|
|
scoped := projectcontext.WithResolveScope(context.Background(), "draft", "")
|
|
for _, delivery := range []string{"attempted", "responded"} {
|
|
receipt.Delivery = delivery
|
|
if err := rc.SaveContextManifest(scoped, receipt); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
output := []byte("AUTHORIZED_A report")
|
|
if err := rc.BindProjectStepOutput(context.Background(), "draft", "engine-run/draft/1", "prompt", time.Now().Add(-time.Second), output); err != nil {
|
|
t.Fatalf("unchanged evidence source rejected at binding: %v", err)
|
|
}
|
|
if err := rc.VerifyProjectStepOutput(context.Background(), "draft", "engine-run/draft/1", "prompt", output); err != nil {
|
|
t.Fatalf("unchanged evidence source rejected at the critic boundary: %v", err)
|
|
}
|
|
// Same ReferenceID and revision, different source hash: a re-published or
|
|
// substituted document must not inherit the recorded grant.
|
|
evidenceHash = "hd2"
|
|
if err := rc.VerifyProjectStepOutput(context.Background(), "draft", "engine-run/draft/1", "prompt", output); err == nil {
|
|
t.Fatal("same reference id with a changed source hash passed the critic boundary")
|
|
}
|
|
evidenceHash = ref.Hash
|
|
if err := rc.VerifyProjectStepOutput(context.Background(), "draft", "engine-run/draft/1", "prompt", output); err != nil {
|
|
t.Fatalf("restoring the recorded hash did not restore the verdict: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSaveContextManifestRejectsUnrecheckableModelInput(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()}
|
|
base := extension.ProjectContextManifest{ProjectID: run.ProjectID, AttemptID: "attempt-1", Delivery: "attempted", InputSHA256: "request-hash"}
|
|
cases := map[string]func(*extension.ProjectContextManifest){
|
|
"included without source hash": func(m *extension.ProjectContextManifest) {
|
|
m.Included = []extension.ProjectContextManifestEntry{{SourceID: "s1", Revision: "r1", InputSHA256: "in"}}
|
|
},
|
|
"included without revision": func(m *extension.ProjectContextManifest) {
|
|
m.Included = []extension.ProjectContextManifestEntry{{SourceID: "s1", SourceHash: "h1", InputSHA256: "in"}}
|
|
},
|
|
"offered reference without id": func(m *extension.ProjectContextManifest) {
|
|
m.OfferedReferences = []extension.ProjectEvidenceRef{{SourceID: "doc", Revision: "r1", Hash: "h1"}}
|
|
},
|
|
"read evidence without reference": func(m *extension.ProjectContextManifest) {
|
|
m.ReadEvidence = []extension.ProjectContextManifestEntry{{SourceID: "doc", Revision: "r1", SourceHash: "h1", InputSHA256: "in"}}
|
|
},
|
|
}
|
|
for name, mutate := range cases {
|
|
manifest := base
|
|
mutate(&manifest)
|
|
if err := rc.SaveContextManifest(context.Background(), manifest); err == nil {
|
|
t.Fatalf("%s admitted as recoverable model input", name)
|
|
}
|
|
}
|
|
stored, err := st.ListContextManifests(string(run.Owner), string(run.ID))
|
|
if err != nil || len(stored) != 0 {
|
|
t.Fatalf("unrecheckable model input wrote a receipt: %+v err=%v", stored, err)
|
|
}
|
|
}
|