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)) } }