Files
Agent-System-Backend/internal/tools/document_renderer_test.go
weichenandCursor 1afe164798 Pin published company default renderer and drop candidate wording
Why: agent-document-renderer 31fbe54 publishes company.default@1.0.0 with
review status approved and publicationAllowed true (owner decision
2026-10-16: A4, ~2.5cm margins, SimSun + Times New Roman 12pt body, SimHei
headings, no logo). Backend still hard-coded Candidate=true and every prompt,
skill and tool text called the default a pending-human-visual-review
candidate.

Evidence: go vet ./... exit 0; gofmt -l on changed packages empty;
go test -count=1 ./internal/docgen/... ./internal/prompts/...
./internal/skills/... ./internal/tools/... ok; go test -count=1 ./... exit 0
(42 packages ok); go build ./... and CGO_ENABLED=0 go build ./cmd/agent-system
ok; git diff --cached --check clean. New tests failed before the change.

Impact: bump deps/agent-document-renderer 0d1c512 -> 31fbe54 and regenerate
the renderer identity. The manifest profile receipt now reports the
renderer's own review status and derives Candidate from the snapshot state
(false for the published default). Prompts (base.deliverables v12,
tool.render_document v2, task.report v4, task.doc-minutes v4), docx-author
and house-style skills, the render_document tool description and comparison
exception details describe the default as published (approved by owner
2026-10-16) while still limiting it to ordinary uncontrolled workspace
working/preview Word. Routing is unchanged: project, controlled,
special-template, review and approved outputs still go to RDM, with no
Backend default fallback and no legacy Word builder fallback.

Refs: unified renderer plan Tasks 7/10
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-16 05:07:19 +00:00

206 lines
8.2 KiB
Go

package tools
import (
"context"
"encoding/json"
"strings"
"testing"
"time"
"agent-system-backend/internal/docgen"
core "agent-system-core"
)
type capturedArtifact struct {
metadata core.Artifact
content []byte
}
type documentRendererCtx struct {
ctx context.Context
artifacts []capturedArtifact
receipt docgen.OperationReceipt
err error
}
func (c *documentRendererCtx) ProjectID() string { return "conversation-project" }
func (c *documentRendererCtx) ConversationID() string { return "conversation" }
func (c *documentRendererCtx) OwnerID() string { return "alice" }
func (c *documentRendererCtx) WorkspaceRoot() string { return "" }
func (c *documentRendererCtx) SettingBool(string, bool) bool {
return false
}
func (c *documentRendererCtx) Context() context.Context {
if c.ctx != nil {
return c.ctx
}
return context.Background()
}
func (c *documentRendererCtx) Retrieve(core.RetrievalQuery) ([]core.RetrievalHit, error) {
return nil, nil
}
func (c *documentRendererCtx) PeerConversations() ([]core.Conversation, error) { return nil, nil }
func (c *documentRendererCtx) PeerConversationMessages(string, int) ([]core.Message, error) {
return nil, nil
}
func (c *documentRendererCtx) WriteArtifact(name, kind, mime string, content []byte) (core.Artifact, error) {
artifact := core.Artifact{ID: core.ID("art-" + name), ProjectID: "conversation-project", Owner: "alice", Name: name, Kind: kind, MIME: mime, Size: int64(len(content))}
c.artifacts = append(c.artifacts, capturedArtifact{metadata: artifact, content: append([]byte(nil), content...)})
return artifact, nil
}
func (c *documentRendererCtx) ExecuteDocumentDelivery(context.Context, docgen.OperationRequest) (docgen.OperationReceipt, error) {
return c.receipt, c.err
}
func TestRenderDocumentToolWritesMarkdownFirstAndReportsPackageHashes(t *testing.T) {
registry := NewRegistry()
if err := registry.RegisterBuiltin(); err != nil {
t.Fatal(err)
}
arguments, _ := json.Marshal(map[string]any{
"operationId": "operation-1", "name": "研发纪要", "documentId": "workspace-minutes", "documentType": "general", "language": "zh-CN",
"markdown": "# 研发纪要\n\n正文。\n", "mode": "preview",
"controlled": false, "projectDocument": false,
})
ctx := &documentRendererCtx{receipt: docgen.OperationReceipt{
OperationID: "operation-1", Outcome: "complete", State: "complete", Renderer: docgen.RendererIdentity,
FrozenAt: time.Date(2026, 10, 15, 12, 0, 0, 0, time.UTC),
Artifacts: []docgen.ReceiptArtifact{
{Role: "markdown", ID: "md", Name: "研发纪要.md", SHA256: strings.Repeat("a", 64)},
{Role: "docx", ID: "docx", Name: "研发纪要.docx", SHA256: strings.Repeat("b", 64)},
{Role: "manifest", ID: "manifest", Name: "研发纪要.manifest.json", SHA256: strings.Repeat("c", 64)},
{Role: "archive", ID: "archive", Name: "研发纪要.zip", SHA256: strings.Repeat("d", 64)},
},
}}
result := registry.Execute(ctx, core.ToolCallRequest{CallID: "render-1", Name: "render_document", Arguments: arguments})
if result.Status != "ok" {
t.Fatalf("render_document: %s", result.Output)
}
var receipt docgen.OperationReceipt
if err := json.Unmarshal([]byte(result.Output), &receipt); err != nil {
t.Fatal(err)
}
if receipt.OperationID != "operation-1" || receipt.Outcome != "complete" || len(receipt.Artifacts) != 4 || receipt.FrozenAt.IsZero() {
t.Fatalf("durable receipt = %#v", receipt)
}
for _, artifact := range receipt.Artifacts {
if !strings.Contains(result.Output, artifact.SHA256) {
t.Errorf("tool output omitted %s hash %s", artifact.Role, artifact.SHA256)
}
}
}
func TestRenderDocumentToolDescriptionReportsPublishedWorkspaceDefault(t *testing.T) {
registry := NewRegistry()
if err := registry.RegisterBuiltin(); err != nil {
t.Fatal(err)
}
description, _, found := registry.Get("render_document")
if !found {
t.Fatal("render_document is not registered")
}
for _, required := range []string{"已发布", "RDM", "非受控工作区"} {
if !strings.Contains(description.Description, required) {
t.Errorf("render_document description missing %q: %s", required, description.Description)
}
}
if strings.Contains(description.Description, "候选") {
t.Errorf("render_document description still calls the organization default a candidate: %s", description.Description)
}
}
func TestRenderDocumentToolSchemaIsClosedAndUsesOnlyOperationIdentity(t *testing.T) {
registry := NewRegistry()
if err := registry.RegisterBuiltin(); err != nil {
t.Fatal(err)
}
description, _, found := registry.Get("render_document")
if !found {
t.Fatal("render_document is not registered")
}
var schema struct {
AdditionalProperties *bool `json:"additionalProperties"`
Properties map[string]json.RawMessage `json:"properties"`
Required []string `json:"required"`
}
if err := json.Unmarshal(description.Parameters, &schema); err != nil {
t.Fatal(err)
}
if schema.AdditionalProperties == nil || *schema.AdditionalProperties {
t.Fatal("render_document schema is not closed")
}
if _, found := schema.Properties["operationId"]; !found {
t.Fatal("render_document schema omitted operationId")
}
if _, found := schema.Properties["frozenAt"]; found {
t.Fatal("render_document still accepts model-supplied frozenAt")
}
wantRequired := map[string]bool{"operationId": true, "name": true, "documentId": true, "markdown": true, "controlled": true, "projectDocument": true}
for _, name := range schema.Required {
delete(wantRequired, name)
}
if len(wantRequired) != 0 {
t.Fatalf("render_document required fields missing: %#v", wantRequired)
}
arguments := json.RawMessage(`{"operationId":"closed-schema","name":"Report","documentId":"report","markdown":"# Report\n","controlled":false,"projectDocument":false,"frozenAt":"2000-01-01T00:00:00Z"}`)
result := registry.Execute(&documentRendererCtx{}, core.ToolCallRequest{CallID: "closed-schema", Name: "render_document", Arguments: arguments})
if result.Status != "error" || !strings.Contains(result.Output, `unknown field "frozenAt"`) {
t.Fatalf("model-supplied frozenAt was accepted: %#v", result)
}
}
func TestPythonToolDescriptionDoesNotAdvertiseWordBypass(t *testing.T) {
registry := NewRegistry()
if err := RegisterInterpreterTools(registry); err != nil {
t.Fatal(err)
}
description, _, found := registry.Get("run_python")
if !found {
t.Fatal("run_python is not registered")
}
for _, forbidden := range []string{"python-docx", "生成文档", "docx_kit"} {
if strings.Contains(description.Description, forbidden) {
t.Fatalf("run_python description advertises Word bypass %q: %s", forbidden, description.Description)
}
}
}
func TestRenderDocumentToolRequiresExplicitRouteDeclaration(t *testing.T) {
registry := NewRegistry()
if err := registry.RegisterBuiltin(); err != nil {
t.Fatal(err)
}
arguments, _ := json.Marshal(map[string]any{
"operationId": "operation-ambiguous", "name": "未声明路由", "documentId": "ambiguous", "markdown": "# 文档\n",
"mode": "preview",
})
ctx := &documentRendererCtx{}
result := registry.Execute(ctx, core.ToolCallRequest{CallID: "render-3", Name: "render_document", Arguments: arguments})
if result.Status != "error" || !strings.Contains(result.Output, "controlled and projectDocument must be explicit") {
t.Fatalf("ambiguous route result = %#v", result)
}
if len(ctx.artifacts) != 0 {
t.Fatalf("ambiguous route wrote %d artifacts", len(ctx.artifacts))
}
}
func TestRenderDocumentToolRejectsControlledRouteWithoutArtifacts(t *testing.T) {
registry := NewRegistry()
if err := registry.RegisterBuiltin(); err != nil {
t.Fatal(err)
}
arguments, _ := json.Marshal(map[string]any{
"operationId": "operation-controlled", "name": "受控文档", "documentId": "controlled", "documentType": "dhf", "language": "zh-CN",
"markdown": "# 受控文档\n", "mode": "preview",
"controlled": true, "projectDocument": false,
})
ctx := &documentRendererCtx{}
result := registry.Execute(ctx, core.ToolCallRequest{CallID: "render-2", Name: "render_document", Arguments: arguments})
if result.Status != "error" || !strings.Contains(result.Output, "RDM") {
t.Fatalf("controlled render result = %#v", result)
}
if len(ctx.artifacts) != 0 {
t.Fatalf("controlled fallback wrote %d artifacts", len(ctx.artifacts))
}
}