package io import ( "os" "path/filepath" "testing" ) // 本文件是**文件级互斥的机制级回归**(声明面单元见 file_targets_test.go,夹具见 // file_claims_rig_test.go):把**真实插件**注册进真实的 tool.Dispatcher,用可控闸门把 // 「一路持有占用」变成确定性事件(占用登记在 Execute 之前、释放在其后,因此停在闸门里就 // 等于持有),逐条钉住主人口径: // // - 同一文件的两个读**并行放行**(检索类并行不该被互斥拖住); // - 读与写、写与写**判忙**,且回灌给模型的文案带文件名与文件状态; // - 不同文件**放行**; // - 未实现 FileTargeter 的工具**完全不受影响**(占用表 0 键)。 // // 这一层走的是派发器真实的登记 / 释放路径与真实的进程内占用表 —— 声明面一错就红。 // TestFileClaims_TwoReadsOfTheSameFileRunParallel 钉住①:同一文件的两个读并行放行 // (主人「优先考虑性能而不是预算」的口径)。 func TestFileClaims_TwoReadsOfTheSameFileRunParallel(t *testing.T) { t.Parallel() rig := newClaimRig(t) file := rig.seed(t, "notes.txt", "hello from disk") holder := rig.gated(t, "file", "file-read-holder") first := runLeg(rig.d, "turn-a", holder.Name(), map[string]any{"action": "read", "path": file}) awaitEnter(t, holder.entered) val, err := invokeLeg(rig.d, "turn-b", "file", map[string]any{"action": "read", "path": file}) if err != nil { t.Fatalf("同一文件的两个读必须允许并行,得到 %v", err) } if content := toolContent(t, val); content != "hello from disk" { t.Fatalf("第二路读到的正文 = %q", content) } // 两路登记的是**同一个键**(同类别、同路径):这才叫「同一个文件的读读并行」, // 而不是两路各自登记了不同的键。 targets := rig.rec.targets() if len(targets) != 2 || targets[0] != targets[1] { t.Fatalf("两路读应登记同一个目标键,得到 %#v", targets) } if got := rig.rec.liveCount(); got != 1 { t.Fatalf("第二路退场后应只剩持锁那一路的键,实际 %d 个", got) } holder.release() if res := awaitDone(t, first); res.err != nil { t.Fatalf("持锁那一路应正常完成: %v", res.err) } assertNoClaims(t, rig.rec, 2) } // TestFileClaims_ReadIsBusyWhileAnotherLegWrites 钉住②:读与写同一文件 ⇒ 判忙,且面向 // 模型的文案带文件名与文件状态(谁在写、本次读不了);持锁那一路退场后立刻可再进。 func TestFileClaims_ReadIsBusyWhileAnotherLegWrites(t *testing.T) { t.Parallel() rig := newClaimRig(t) file := rig.seed(t, "notes.txt", "v1") holder := rig.gated(t, "file", "file-write-holder") first := runLeg(rig.d, "turn-a", holder.Name(), map[string]any{"action": "write", "path": file, "content": "v2"}) awaitEnter(t, holder.entered) _, err := invokeLeg(rig.d, "turn-b", "file", map[string]any{"action": "read", "path": file}) assertBusy(t, err, "notes.txt", "正在被另一路写入", "本次读取无法进行") holder.release() if res := awaitDone(t, first); res.err != nil { t.Fatalf("持锁那一路应正常完成: %v", res.err) } if data, err := os.ReadFile(file); err != nil || string(data) != "v2" { t.Fatalf("持锁那一路的写入没有落盘: %q, %v", data, err) } if _, err := invokeLeg(rig.d, "turn-c", "file", map[string]any{"action": "read", "path": file}); err != nil { t.Fatalf("前一路退场后必须已释放占用,得到 %v", err) } // 三次尝试:持锁那一路的写、被判忙的那次读、退场后的第二次读。 assertNoClaims(t, rig.rec, 3) } // TestFileClaims_WriteVsWriteIsBusy 钉住②(写写):第二路写同一文件被判忙、**根本没有执行** // (闸门未被进入),文件里留下的是持锁那一路的内容。 func TestFileClaims_WriteVsWriteIsBusy(t *testing.T) { t.Parallel() rig := newClaimRig(t) file := rig.seed(t, "notes.txt", "seed") holder := rig.gated(t, "file", "file-write-holder") peer := rig.gated(t, "file", "file-write-peer") first := runLeg(rig.d, "turn-a", holder.Name(), map[string]any{"action": "write", "path": file, "content": "held"}) awaitEnter(t, holder.entered) _, err := invokeLeg(rig.d, "turn-b", peer.Name(), map[string]any{"action": "write", "path": file, "content": "peer"}) assertBusy(t, err, "notes.txt", "正在被另一路写入", "本次写入无法进行") select { case <-peer.entered: t.Fatal("被判忙的调用绝不该执行") default: } holder.release() if res := awaitDone(t, first); res.err != nil { t.Fatalf("持锁那一路应正常完成: %v", res.err) } if data, err := os.ReadFile(file); err != nil || string(data) != "held" { t.Fatalf("文件内容应只剩持锁那一路写的 held: %q, %v", data, err) } // 两次尝试:持锁那一路的写,以及第二路被判忙的写(判忙那一路也问过表,只是没占到键)。 assertNoClaims(t, rig.rec, 2) } // TestFileClaims_CrossToolReadOfAWrittenFileIsBusy 钉住②(跨工具):file 正在写一个文件时, // image-crop 读**同一个文件**同样判忙 —— 这正是文件级互斥要盖住的场景(读到的会是撕开的 // 内容),也是「同一份路径口径被两个工具共用」的证据。 func TestFileClaims_CrossToolReadOfAWrittenFileIsBusy(t *testing.T) { t.Parallel() rig := newClaimRig(t) file := rig.seed(t, "notes.txt", "v1") holder := rig.gated(t, "file", "file-write-holder") first := runLeg(rig.d, "turn-a", holder.Name(), map[string]any{"action": "write", "path": file, "content": "v2"}) awaitEnter(t, holder.entered) _, err := invokeLeg(rig.d, "turn-b", "image-crop", map[string]any{ "image": file, "x": 0, "y": 0, "width": 1, "height": 1, }) assertBusy(t, err, "notes.txt", "正在被另一路写入", "本次读取无法进行") holder.release() if res := awaitDone(t, first); res.err != nil { t.Fatalf("持锁那一路应正常完成: %v", res.err) } // 两次尝试:持锁那一路的写,以及 image-crop 读同一文件被判忙的那次。 assertNoClaims(t, rig.rec, 2) } // TestFileClaims_DifferentFilesRunParallel 钉住③:不是同一个文件 ⇒ 放行(且真的写成功), // 占用表里两路各占一个键、退场后清空。 func TestFileClaims_DifferentFilesRunParallel(t *testing.T) { t.Parallel() rig := newClaimRig(t) held := rig.seed(t, "a.txt", "a") other := filepath.Join(rig.dir, "b.txt") holder := rig.gated(t, "file", "file-write-holder") first := runLeg(rig.d, "turn-a", holder.Name(), map[string]any{"action": "write", "path": held, "content": "held"}) awaitEnter(t, holder.entered) val, err := invokeLeg(rig.d, "turn-b", "file", map[string]any{"action": "write", "path": other, "content": "bee"}) if err != nil { t.Fatalf("不同文件必须允许并行操作,得到 %v", err) } if m, ok := val.(map[string]any); !ok || m["size"] != len("bee") { t.Fatalf("第二路的结果异常: %#v", val) } if data, err := os.ReadFile(other); err != nil || string(data) != "bee" { t.Fatalf("第二路的写入没有落盘: %q, %v", data, err) } if got := rig.rec.liveCount(); got != 1 { t.Fatalf("第二路退场后应只剩持锁那一路的键,实际 %d 个", got) } holder.release() if res := awaitDone(t, first); res.err != nil { t.Fatalf("持锁那一路应正常完成: %v", res.err) } assertNoClaims(t, rig.rec, 2) } // TestFileClaims_ToolWithoutDeclarationIsUnaffected 钉住④:未实现 FileTargeter 的工具完全 // 不受影响 —— (a) 它一次都不碰占用表;(b) 真实插件里不碰文件的 web-search 在别的路持有 // 写占用时照常调用,不判忙、不登记。 func TestFileClaims_ToolWithoutDeclarationIsUnaffected(t *testing.T) { t.Parallel() rig := newClaimRig(t) // (a) 两路并发调用一个只实现 sdk.Tool 的工具:都成功,占用表 0 键、登记 0 次。 plain := &plainTool{name: "plain-tool"} if err := rig.d.Register(plain); err != nil { t.Fatalf("Register %q: %v", plain.Name(), err) } for _, leg := range []string{"turn-a", "turn-b"} { val, err := invokeLeg(rig.d, leg, plain.Name(), map[string]any{"echo": "hi"}) if err != nil { t.Fatalf("未声明目标的工具不该受影响(leg=%s): %v", leg, err) } if echo, _ := val.(string); echo != "hi" { t.Fatalf("结果 = %#v", val) } } assertNoClaims(t, rig.rec, 0) // (b) 另一路持有某个文件的写占用时,未声明目标的真实工具照常可调用。 file := rig.seed(t, "notes.txt", "v1") holder := rig.gated(t, "file", "file-write-holder") first := runLeg(rig.d, "turn-a", holder.Name(), map[string]any{"action": "write", "path": file, "content": "v2"}) awaitEnter(t, holder.entered) _, err := invokeLeg(rig.d, "turn-b", "web-search", map[string]any{"query": "x"}) assertNotBusy(t, err) if got := rig.rec.claimAttempts(); got != 1 { t.Fatalf("只有持锁那一路该问占用表,实际问了 %d 次", got) } holder.release() if res := awaitDone(t, first); res.err != nil { t.Fatalf("持锁那一路应正常完成: %v", res.err) } assertNoClaims(t, rig.rec, 1) }