package tool import ( "context" "errors" "testing" ) // 本文件钉住 design/15 §2.5.7(主人 2026-10-05 裁决 3)为**后续切片**预留的 // 文件级互斥通道:本切片不实现、不接线,但通道形状与判定口径必须现在定死, // 否则实现方以后还得改工具接口。 // // 两件事缺一不可:① 「我是哪一路」只能来自 ctx(绝不从模型输出推断); // ② 「该文件正被另一条路操作」必须是一条**能被 errors.Is 识别**的哨兵错误, // 让写类工具把它当成正常返回值显式交给 LLM,而不是静默吞掉或排队死等。 // TestLegIDContextRoundTrip 钉住路标识的 ctx 通道:空 legID 是空操作(单路 / // 子代理 / 未接线),nil ctx 不 panic。 func TestLegIDContextRoundTrip(t *testing.T) { t.Parallel() ctx := WithLegID(context.Background(), "turn-a") if got := LegIDFrom(ctx); got != "turn-a" { t.Fatalf("LegIDFrom = %q, want turn-a", got) } if got := LegIDFrom(WithLegID(context.Background(), "")); got != "" { t.Fatalf("空 legID 应是不挂载的 ctx 副本,得到 %q", got) } if got := LegIDFrom(nil); got != "" { t.Fatalf("nil ctx 应返回空串(单路 ⇒ 文件级互斥无需生效),得到 %q", got) } } // TestFileBusyErrorWrapsSentinel 钉住「同一文件正在被别的路操作」的错误形状: // 它必须能被 errors.Is 识别(调用方按它决定「让模型稍后再试」而不是「执行失败」), // 且文案里带规范化后的目标,便于模型理解为何重试。 func TestFileBusyErrorWrapsSentinel(t *testing.T) { t.Parallel() err := FileBusyError("reports/2026.csv") if !errors.Is(err, ErrFileBusy) { t.Fatalf("errors.Is(err, ErrFileBusy) = false,错误形状已断: %v", err) } // 它绝不是执行失败:调用方不得把它当成「工具坏了」上报。 if errors.Is(err, ErrUnknownTool) { t.Fatal("文件占用被误判成了未知工具") } if got := err.Error(); got == "" { t.Fatal("面向模型的错误文案不能为空") } } // fakeFileClaimer 是接口预留的形状检验:它证明 FileClaimer 只要两个方法 // (Claim / Release)就能表达「文件级、按路登记、被占用即明确拒绝」的口径, // 不需要工具侧再改签名 —— 本切片到此为止,不接线任何实现。 type fakeFileClaimer struct { owner map[string]string } func (f *fakeFileClaimer) Claim(_ context.Context, legID, target string) error { if holder, ok := f.owner[target]; ok && holder != legID { return FileBusyError(target) } f.owner[target] = legID return nil } func (f *fakeFileClaimer) Release(_ context.Context, legID, target string) { if f.owner[target] == legID { delete(f.owner, target) } } // TestFileClaimerShapeFileLevel 钉住接口预留的**粒度**语义(主人裁决 3): // 同一目标被别的路占住 ⇒ 明确拒绝;**不同目标** ⇒ 允许并行;同一路重复 Claim // 自己的目标 ⇒ 幂等;Release 之后别的路可以接手。 func TestFileClaimerShapeFileLevel(t *testing.T) { t.Parallel() var claimer FileClaimer = &fakeFileClaimer{owner: map[string]string{}} ctx := context.Background() if err := claimer.Claim(ctx, "turn-a", "a.txt"); err != nil { t.Fatalf("首次占用: %v", err) } if err := claimer.Claim(ctx, "turn-a", "a.txt"); err != nil { t.Fatalf("同一路重复占用同一目标必须是幂等的: %v", err) } if err := claimer.Claim(ctx, "turn-b", "b.txt"); err != nil { t.Fatalf("不同文件必须允许并行操作: %v", err) } err := claimer.Claim(ctx, "turn-b", "a.txt") if !errors.Is(err, ErrFileBusy) { t.Fatalf("同一文件被另一路占用时必须明确拒绝,得到 %v", err) } claimer.Release(ctx, "turn-a", "a.txt") if err := claimer.Claim(ctx, "turn-b", "a.txt"); err != nil { t.Fatalf("占用释放后应可接手: %v", err) } }