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>
206 lines
8.2 KiB
Go
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))
|
|
}
|
|
}
|