Why: - Independent review found trusted-boundary, semantic parity, replay-budget, comparison, persistence, cancellation, provenance, and routing gaps. - Storage migration must include the new durable delivery table. Evidence: - go test -count=1 ./... - go test -race ./internal/docgen ./internal/tools ./internal/runmgr - go test ./... (deps/agent-document-renderer) - go vet ./... - go build ./... - CGO_ENABLED=0 go build -trimpath ./cmd/agent-system - Python house-style unittest suite and git diff --check Impact: - Word delivery now uses host-owned freezes, Markdown-first resumable persistence, closed registration boundaries, semantic shared rendering, and real comparison. - Controlled/project documents still fail closed to RDM and non-Word flows remain unchanged. Refs: - Unified Document Renderer implementation plan Task 10 - /tmp/task10-backend-review-last.md - Renderer d5cd8c126045a27d5a5cc5da23f64d3c737e116a
171 lines
7.5 KiB
Go
171 lines
7.5 KiB
Go
package runmgr
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
|
|
"agent-system-backend/internal/bus"
|
|
"agent-system-backend/internal/docgen"
|
|
"agent-system-backend/internal/settings"
|
|
"agent-system-backend/internal/store"
|
|
"agent-system-backend/internal/tools"
|
|
core "agent-system-core"
|
|
)
|
|
|
|
func TestDocumentDeliveryReportsPartialAndUnknownReceiptsWithStableRetry(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
stage string
|
|
wantOutcome string
|
|
wantCount int
|
|
}{
|
|
{"derived staging", store.DocumentDeliveryFaultAfterManifestBlob, "partial", 1},
|
|
{"post commit", store.DocumentDeliveryFaultAfterDerivedCommit, "unknown", 4},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
run, st := newDocumentDeliveryRun(t)
|
|
st.SetDocumentDeliveryFault(test.stage, errors.New("injected "+test.stage))
|
|
arguments := json.RawMessage(`{"operationId":"receipt-operation","name":"Receipt","documentId":"receipt","documentType":"general","language":"en","markdown":"# Receipt\n\nbody\n","mode":"preview","controlled":false,"projectDocument":false}`)
|
|
failed := run.ExecuteTool(core.ToolCallRequest{CallID: "delivery-failed", Name: "render_document", Arguments: arguments})
|
|
if failed.Status != "error" {
|
|
t.Fatalf("fault result = %#v", failed)
|
|
}
|
|
var receipt docgen.OperationReceipt
|
|
if err := json.Unmarshal([]byte(strings.TrimPrefix(failed.Output, "render_document: ")), &receipt); err != nil {
|
|
t.Fatalf("decode failure receipt: %v\n%s", err, failed.Output)
|
|
}
|
|
if receipt.Outcome != test.wantOutcome || len(receipt.Artifacts) != test.wantCount || receipt.FrozenAt.IsZero() {
|
|
t.Fatalf("failure receipt = %#v", receipt)
|
|
}
|
|
firstIDs := receiptArtifactIdentity(receipt.Artifacts)
|
|
|
|
retry := run.ExecuteTool(core.ToolCallRequest{CallID: "delivery-retry", Name: "render_document", Arguments: arguments})
|
|
if retry.Status != "ok" {
|
|
t.Fatalf("retry failed: %s", retry.Output)
|
|
}
|
|
var completed docgen.OperationReceipt
|
|
if err := json.Unmarshal([]byte(retry.Output), &completed); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if completed.Outcome != "complete" || len(completed.Artifacts) != 4 || !completed.FrozenAt.Equal(receipt.FrozenAt) {
|
|
t.Fatalf("completed receipt = %#v", completed)
|
|
}
|
|
for role, identity := range firstIDs {
|
|
if got := receiptArtifactIdentity(completed.Artifacts)[role]; got != identity {
|
|
t.Errorf("artifact %s changed on retry: %s -> %s", role, identity, got)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDocumentMigrationComparisonRequiresTrustedManagerCapability(t *testing.T) {
|
|
run, st := newDocumentDeliveryRun(t)
|
|
arguments := json.RawMessage(`{"operationId":"comparison-operation","name":"Comparison","documentId":"comparison","documentType":"general","language":"en","markdown":"# Comparison\n\nBody with **bold** and [safe](https://example.com/private).\n","mode":"preview","controlled":false,"projectDocument":false,"migrationComparison":true}`)
|
|
rejected := run.ExecuteTool(core.ToolCallRequest{CallID: "comparison-model", Name: "render_document", Arguments: arguments})
|
|
if rejected.Status != "error" || !strings.Contains(rejected.Output, "unknown field") {
|
|
t.Fatalf("model enabled comparison: %#v", rejected)
|
|
}
|
|
|
|
run.mgr.documentMigrationComparison = true
|
|
arguments = json.RawMessage(`{"operationId":"comparison-operation","name":"Comparison","documentId":"comparison","documentType":"general","language":"en","markdown":"# Comparison\n\nBody with **bold** and [safe](https://example.com/private).\n","mode":"preview","controlled":false,"projectDocument":false}`)
|
|
result := run.ExecuteTool(core.ToolCallRequest{CallID: "comparison-host", Name: "render_document", Arguments: arguments})
|
|
if result.Status != "ok" {
|
|
t.Fatalf("trusted comparison: %s", result.Output)
|
|
}
|
|
var receipt docgen.OperationReceipt
|
|
if err := json.Unmarshal([]byte(result.Output), &receipt); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var manifestID string
|
|
for _, artifact := range receipt.Artifacts {
|
|
if artifact.Role == string(store.DocumentArtifactManifest) {
|
|
manifestID = artifact.ID
|
|
}
|
|
}
|
|
raw, err := st.ArtifactContent("alice", manifestID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var manifest docgen.Manifest
|
|
if err := json.Unmarshal(raw, &manifest); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !manifest.Comparison.Enabled || manifest.Comparison.Assessment == "" || len(manifest.Comparison.Dimensions) == 0 {
|
|
t.Fatalf("trusted comparison was not assessed: %#v", manifest.Comparison)
|
|
}
|
|
}
|
|
|
|
func TestDocumentRendererFailureKeepsAuthoritativeMarkdownForRetry(t *testing.T) {
|
|
run, st := newDocumentDeliveryRun(t)
|
|
arguments := json.RawMessage(`{"operationId":"renderer-failure","name":"Failure","documentId":"failure","documentType":"general","language":"en","markdown":"# Failure\n\n\n","mode":"preview","controlled":false,"projectDocument":false}`)
|
|
first := run.ExecuteTool(core.ToolCallRequest{CallID: "renderer-failure-1", Name: "render_document", Arguments: arguments})
|
|
if first.Status != "error" {
|
|
t.Fatalf("renderer failure result = %#v", first)
|
|
}
|
|
var firstReceipt docgen.OperationReceipt
|
|
if err := json.Unmarshal([]byte(strings.TrimPrefix(first.Output, "render_document: ")), &firstReceipt); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if firstReceipt.Outcome != "partial" || len(firstReceipt.Artifacts) != 1 || firstReceipt.Artifacts[0].Role != string(store.DocumentArtifactMarkdown) {
|
|
t.Fatalf("renderer failure lost Markdown receipt: %#v", firstReceipt)
|
|
}
|
|
markdown, err := st.ArtifactContent("alice", firstReceipt.Artifacts[0].ID)
|
|
if err != nil || !strings.Contains(string(markdown), "<!-- node-id:") {
|
|
t.Fatalf("authoritative Markdown missing markers: %v %q", err, markdown)
|
|
}
|
|
|
|
second := run.ExecuteTool(core.ToolCallRequest{CallID: "renderer-failure-2", Name: "render_document", Arguments: arguments})
|
|
var secondReceipt docgen.OperationReceipt
|
|
if second.Status != "error" || json.Unmarshal([]byte(strings.TrimPrefix(second.Output, "render_document: ")), &secondReceipt) != nil ||
|
|
len(secondReceipt.Artifacts) != 1 || secondReceipt.Artifacts[0].ID != firstReceipt.Artifacts[0].ID || secondReceipt.Artifacts[0].SHA256 != firstReceipt.Artifacts[0].SHA256 {
|
|
t.Fatalf("renderer failure retry changed Markdown identity: first=%#v second=%#v result=%#v", firstReceipt, secondReceipt, second)
|
|
}
|
|
}
|
|
|
|
func newDocumentDeliveryRun(t *testing.T) (*runCtx, *store.Store) {
|
|
t.Helper()
|
|
st := storeForRunManagerTest(t)
|
|
if err := st.EnsureUser("alice", "Alice"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
project, err := st.CreateProject("alice", "delivery")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
conversation, err := st.CreateConversation("alice", project.ID, "delivery", "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
registry := tools.NewRegistry()
|
|
if err := registry.RegisterBuiltin(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
eventBus := bus.New()
|
|
events := newEventBuffer(st, eventBus)
|
|
t.Cleanup(events.stop)
|
|
manager := &Manager{st: st, bus: eventBus, registry: registry, settings: settings.NewService(st), events: events}
|
|
return &runCtx{
|
|
mgr: manager, run: core.Run{ID: "run-delivery", ProjectID: project.ID, ConversationID: conversation.ID, Owner: "alice"},
|
|
prof: core.AgentProfile{}, eff: manager.settings.Effective("alice", ""), ctx: context.Background(),
|
|
}, st
|
|
}
|
|
|
|
func storeForRunManagerTest(t *testing.T) *store.Store {
|
|
t.Helper()
|
|
_, st := newExecEnv(t)
|
|
return st
|
|
}
|
|
|
|
func receiptArtifactIdentity(artifacts []docgen.ReceiptArtifact) map[string]string {
|
|
result := make(map[string]string, len(artifacts))
|
|
for _, artifact := range artifacts {
|
|
result[artifact.Role] = artifact.ID + ":" + artifact.SHA256
|
|
}
|
|
return result
|
|
}
|