Files
weichen fec2d16cc6 Close Task 10 renderer delivery gaps
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
2026-10-15 21:44:03 +00:00

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![missing](asset:missing@1.0.0)\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
}