Files
Cyrene_v2/internal/runtime/tool/legfile_test.go
T
AskaEth 89e13cf53d
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(gateway,loop): 忙时并行回合基础管线(I1′ 有条件放宽)
- turnId 由编排器生成、loop Run 复用(硬前提:否则 turn_begin 与流帧各铸一个,客户端两级分桶立即失配)
- StateSink 增带 turnId 的变体(未接线时退化为现签名,不丢状态帧)
- 上行 chat.parallel(WS chat 帧 + REST 三入口同形;省略 = 逐字不变)
- 单会话额度默认 2 / 硬上限 3 / <2 关闭;额度满或未声明 ⇒ 落回既有排队语义
- I1′:checkpoint 归属、历史配对、忙态计数三处从会话级单例升为路级
- session_idle 收紧为「所有在途路收尾且 pending 空」(仅并行时生效;单路逐字不变)
- 预留接口(形状定死、无实现无接线):tool/legfile.go 的 FileClaimer / ErrFileBusy / WithLegID
- 顺手修一个真 bug:并行路 ctx 派生自提交请求作用域,REST 同步入口一写响应就把它连坐杀死(改 context.WithoutCancel 派生)
- 23 个新用例(含 -race -count=3);变异 6 条(turnId 复用 / idle 时点 / 并行升级 / checkpoint 键 / 编排器交 id / pending_ack reason)
- 契约变更点 10 条待监工升 v0.65(本人未改 Docs/**)
2026-10-05 09:18:50 +08:00

98 lines
3.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}
}