Files
Agent-System-Backend/internal/runmgr/exec_tool_artifact_test.go
T
weichen c8f8076579
ci / vet-and-test (pull_request) Successful in 1m16s
Make artifact backfill fixture a structurally valid docx
Why: The backfill contract test used a fake PK-magic docx as its fixture;
the new registration structural gate correctly refuses that shape, so the
contract was no longer exercised.

Evidence: The three affected runmgr tests pass locally with -count=1;
the full combined suite receipt follows on the task card.

Impact: Test fixture only; the gate behavior under test is unchanged.

Refs: Dev task feature/pptx-register-structural-gate
2026-10-14 07:42:28 +00:00

149 lines
5.0 KiB
Go

// ExecuteTool 工件回填契约(交付物卡片根因修复的验收):注册表工具经
// rc.WriteArtifact 写入的工件必须出现在 ToolCallResult.Artifact 上——
// llm.go 据此生成对话内 artifact_ref 卡片部件。此前只有超长截断路径会设置它。
package runmgr
import (
"context"
"encoding/json"
"os"
"path/filepath"
"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 newExecEnv(t *testing.T) (*runCtx, *store.Store) {
t.Helper()
st, err := store.OpenSQLite(filepath.Join(t.TempDir(), "exec.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { st.Close() })
b := bus.New()
reg := tools.NewRegistry()
if err := reg.RegisterBuiltin(); err != nil {
t.Fatal(err)
}
proj, err := st.CreateProject("alice", "p1")
if err != nil {
t.Fatal(err)
}
conv, err := st.CreateConversation("alice", proj.ID, "c1", "")
if err != nil {
t.Fatal(err)
}
svc := settings.NewService(st)
m := &Manager{st: st, bus: b, registry: reg, settings: svc, events: newEventBuffer(st, b)}
eff := svc.Effective("alice", "")
c := &runCtx{
mgr: m, run: core.Run{ProjectID: proj.ID, ConversationID: conv.ID, Owner: "alice"},
prof: core.AgentProfile{}, eff: eff, ctx: context.Background(),
}
return c, st
}
func TestExecuteToolBackfillsArtifact(t *testing.T) {
c, _ := newExecEnv(t)
res := c.ExecuteTool(core.ToolCallRequest{
CallID: "call1", Name: "write_artifact",
Arguments: json.RawMessage(`{"name":"summary.md","kind":"document","content":"# 摘要正文"}`),
})
if res.Status != "ok" {
t.Fatalf("write_artifact: %s", res.Output)
}
if res.Artifact == nil {
t.Fatal("ToolCallResult.Artifact 未回填——对话内交付物卡片不会出现(回归)")
}
if res.Artifact.Name != "summary.md" {
t.Errorf("回填工件名不符: %+v", res.Artifact)
}
}
// run_python 生成文件后 register_artifact 必须把工件回填到 ToolCallResult.Artifact。
func TestRunPythonRegisterArtifactBackfill(t *testing.T) {
c, _ := newExecEnv(t)
root := t.TempDir()
c.mgr.SetWorkspaceRoot(root)
if err := tools.RegisterExecTools(c.mgr.registry); err != nil {
t.Fatal(err)
}
if err := tools.RegisterInterpreterTools(c.mgr.registry); err != nil {
t.Fatal(err)
}
// 夹具必须是结构合法的最小 docx:登记口会做 OPC 结构自检,
// 伪 zip(PK 魔数+垃圾字节)现在会被拒绝而非登记。
py := c.ExecuteTool(core.ToolCallRequest{
CallID: "py1", Name: "run_python",
Arguments: json.RawMessage(`{"script":"import zipfile\nwith zipfile.ZipFile('note.docx','w') as z:\n z.writestr('[Content_Types].xml','<?xml version=\"1.0\"?><Types/>')\n z.writestr('word/document.xml','<?xml version=\"1.0\"?><doc/>')\n"}`),
})
if py.Status != "ok" || !strings.Contains(py.Output, "exit=0") {
t.Fatalf("run_python: %s %s", py.Status, py.Output)
}
res := c.ExecuteTool(core.ToolCallRequest{
CallID: "reg1", Name: "register_artifact",
Arguments: json.RawMessage(`{"path":"note.docx"}`),
})
if res.Status != "ok" {
t.Fatalf("register_artifact: %s", res.Output)
}
if res.Artifact == nil || res.Artifact.Name != "note.docx" {
t.Fatalf("ToolCallResult.Artifact 未回填: %+v", res.Artifact)
}
if res.Artifact.MIME != "application/vnd.openxmlformats-officedocument.wordprocessingml.document" {
t.Errorf("docx mime: %s", res.Artifact.MIME)
}
b, err := os.ReadFile(filepath.Join(root, "alice", c.run.ConversationID, "note.docx"))
if err != nil {
t.Fatal(err)
}
if string(b[:4]) != "PK\x03\x04" {
t.Fatalf("stored bytes: %q", b)
}
}
// 空手工具(如 time_now)不得凭空挂 Artifact。
func TestExecuteToolNoArtifactByDefault(t *testing.T) {
c, _ := newExecEnv(t)
res := c.ExecuteTool(core.ToolCallRequest{CallID: "c1", Name: "time_now", Arguments: json.RawMessage(`{}`)})
if res.Status != "ok" || res.Artifact != nil {
t.Errorf("无产物工具不应携带 Artifact: %+v", res)
}
}
func TestGeneratedDocxIsSearchable(t *testing.T) {
c, st := newExecEnv(t)
const marker = "zebraindexword"
docx, err := docgen.Build("纪要", "结论:"+marker)
if err != nil {
t.Fatal(err)
}
mime := "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
if _, err := c.WriteArtifact("note.docx", "document", mime, docx); err != nil {
t.Fatal(err)
}
hits, err := st.SearchKnowledge("alice", core.RetrievalQuery{Text: marker, ProjectID: c.run.ProjectID, TopK: 5})
if err != nil {
t.Fatal(err)
}
if len(hits) == 0 {
t.Fatal("generated docx text was not indexed")
}
if _, err := c.WriteArtifact("bad.docx", "document", mime, []byte("notazipmarker")); err != nil {
t.Fatal(err)
}
hits, err = st.SearchKnowledge("alice", core.RetrievalQuery{Text: "notazipmarker", ProjectID: c.run.ProjectID, TopK: 5})
if err != nil {
t.Fatal(err)
}
if len(hits) != 0 {
t.Fatalf("unparseable docx bytes should not be indexed, got %d hits", len(hits))
}
}