- 窗口声明:models.json 组级 contextWindow + env LLM_CONTEXT_WINDOW(per-group materialize 播种,0 不写盘、负数报错)。 - 预算三态:CONTEXT_MAX_TOKENS 未设置 ⇒ 按窗口推导;>0 显式覆盖;0|负 关闭压缩(不变)。推导 budget = min(窗口×比例, 窗口−输出保留),比例默认 2/3(CONTEXT_BUDGET_RATIO,1M ⇒ 699050 tokens ≈ 683Ki);输出保留 = max(32768, 该组 maxTokens) 且封顶窗口/2(防配置矛盾时退化成 1 token);窗口未声明 ⇒ 回落 131072 + 恰好一条 WARN。接线在组合根 wire_context.go(唯一决策点)。 - usage/缓存可观测(此前**完全不可观测**):Response 增 CacheHit/CacheMiss/Reasoning tokens + CacheHitRate;解析 OpenAI 兼容(prompt_cache_hit_tokens/prompt_cache_miss_tokens/cached_tokens/reasoning_tokens)与 anthropic(cache_read_input_tokens/cache_creation_input_tokens)两种形状;记录点在适配器 record(换入口不丢成本日志),日志无凭据;单次输入 ≥预算 90% 记 WARN(不阻断)。 - 根治清单「续读」假承诺:抽取期不再按注入预算截断(sidecar 存全文,只受 MaxSidecarBytes=8 MiB 安全上限,ExtractorVersion v1→v2 旧 sidecar 失效重抽);注入预算改在装配期生效 ⇒ ref.Chars/Truncated 是注入侧事实、游标真是继续读的起点;document-read 的 limit 上限改为独立具名常量。 - 顺手修越界:config.go 505 行(超 500)拆出 llm.go。 - 主会话独立验收:隔离 worktree(HEAD + 本切片 39 文件,排除在途 gateway)build/vet 绿、全树 59 包 0 FAIL、linux+darwin 交叉编译绿;变异独立复现(比例改 0.25 ⇒ 推导用例红 262144 vs 699050)。
408 lines
17 KiB
Go
408 lines
17 KiB
Go
package io
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"errors"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"testing"
|
||
"unicode/utf8"
|
||
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/capability/registry"
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/capability/restrict"
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/config"
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/documents"
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/history"
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/tool"
|
||
)
|
||
|
||
// docName 造一个形态合法的服务端存储名:32 位小写十六进制主名 + 扩展名。
|
||
func docName(ext string) string { return strings.Repeat("ab12", 8) + ext }
|
||
|
||
// docReadFixture 是一份隔离的数据目录 + **真实上游解析器**。夹具里的 sidecar 一律由
|
||
// documents.Resolver(S3-A 的生产者)写出,测试因此不复制「首行元数据 + 正文自第二行
|
||
// 起」的第二份实现:生产者与消费者在同一份夹具里对接,sidecar 形状一漂移测试就红。
|
||
type docReadFixture struct {
|
||
dataDir string
|
||
uploads string
|
||
resolver *documents.Resolver
|
||
}
|
||
|
||
func newDocReadFixture(t *testing.T) *docReadFixture {
|
||
t.Helper()
|
||
dataDir := t.TempDir()
|
||
uploads := filepath.Join(dataDir, "uploads", "documents")
|
||
if err := os.MkdirAll(uploads, 0o700); err != nil {
|
||
t.Fatalf("建上传目录: %v", err)
|
||
}
|
||
return &docReadFixture{
|
||
dataDir: dataDir,
|
||
uploads: uploads,
|
||
resolver: documents.NewResolver(documents.Config{DataDir: dataDir, UploadsDir: uploads}),
|
||
}
|
||
}
|
||
|
||
// extract 落一份上传件并让装配期解析器跑一次(写出 sidecar),返回结论与**注入正文**:
|
||
// 后者就是模型在清单行下面看到的正文,也是工具 offset/limit 的坐标系。
|
||
func (f *docReadFixture) extract(t *testing.T, name, content string) (history.DocumentRef, string) {
|
||
t.Helper()
|
||
if err := os.WriteFile(filepath.Join(f.uploads, name), []byte(content), 0o600); err != nil {
|
||
t.Fatalf("写上传件: %v", err)
|
||
}
|
||
msg := model.DocumentMessage(model.RoleUser, "", model.Document{Name: name, MIME: "text/plain", Size: int64(len(content))})
|
||
refs, bodies, err := f.resolver.Resolve(context.Background(), msg)
|
||
if err != nil {
|
||
t.Fatalf("解析 %q: %v", name, err)
|
||
}
|
||
return refs[name], bodies[name]
|
||
}
|
||
|
||
// sidecarPath 是某份文档的 sidecar 路径(与上游同一命名规则,供断言用)。
|
||
func (f *docReadFixture) sidecarPath(name string) string {
|
||
return filepath.Join(f.dataDir, "extracted", name+".txt")
|
||
}
|
||
|
||
// tool 返回指向同一份数据目录的 document-read 工具。
|
||
func (f *docReadFixture) tool(t *testing.T) *documentReadTool {
|
||
t.Helper()
|
||
got, ok := findTool(t, buildTools(t, &config.Config{DataDir: f.dataDir}), documentReadToolName).(*documentReadTool)
|
||
if !ok {
|
||
t.Fatalf("%q 工具类型不符", documentReadToolName)
|
||
}
|
||
return got
|
||
}
|
||
|
||
// callDocRead 执行一次 document-read,并把返回的 JSON 文本解回结果结构。工具返回的是
|
||
// **字符串**(循环的 formatToolResult 对字符串直通,模型看到裸 JSON),这里因此必须反
|
||
// 序列化才能断言字段 —— 它同时钉住「返回值是 JSON 文本」这条约定。
|
||
func callDocRead(t *testing.T, tl *documentReadTool, args map[string]any) (documentReadResult, error) {
|
||
t.Helper()
|
||
raw, err := tl.Execute(context.Background(), args)
|
||
if err != nil {
|
||
return documentReadResult{}, err
|
||
}
|
||
text, ok := raw.(string)
|
||
if !ok {
|
||
t.Fatalf("返回值类型 = %T, want string(JSON 文本)", raw)
|
||
}
|
||
var out documentReadResult
|
||
if err := json.Unmarshal([]byte(text), &out); err != nil {
|
||
t.Fatalf("返回值不是合法 JSON: %v(%q)", err, text)
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
// TestDocumentReadTool_PaginatesWithNextOffset 钉住①:首读 + 用 nextOffset 续读,两段
|
||
// 拼接逐字等于装配期注入的正文(两边同一坐标系),游标单位为字符而非字节。
|
||
func TestDocumentReadTool_PaginatesWithNextOffset(t *testing.T) {
|
||
t.Parallel()
|
||
fx := newDocReadFixture(t)
|
||
name := docName(".txt")
|
||
body := "季度报告:营收增长 12%。\n" + strings.Repeat("第二段:成本下降 3%。\n", 12)
|
||
_, injected := fx.extract(t, name, body)
|
||
tool := fx.tool(t)
|
||
total := utf8.RuneCountInString(injected)
|
||
half := total / 2
|
||
|
||
first, err := callDocRead(t, tool, map[string]any{"name": name, "limit": half})
|
||
if err != nil {
|
||
t.Fatalf("首读: %v", err)
|
||
}
|
||
if first.Status != "extracted" || first.Offset != 0 || first.TotalChars != total {
|
||
t.Fatalf("首读 = %+v, want extracted / offset 0 / totalChars %d", first, total)
|
||
}
|
||
if !first.Truncated || first.NextOffset != half || utf8.RuneCountInString(first.Text) != half {
|
||
t.Fatalf("首读截断信息 = %+v, want truncated + nextOffset %d + 正文 %d 字符", first, half, half)
|
||
}
|
||
second, err := callDocRead(t, tool, map[string]any{"name": name, "offset": first.NextOffset})
|
||
if err != nil {
|
||
t.Fatalf("续读: %v", err)
|
||
}
|
||
if second.Offset != half || second.Truncated || second.NextOffset != 0 {
|
||
t.Fatalf("续读 = %+v, want 读到末尾(offset %d、无续读游标)", second, half)
|
||
}
|
||
if first.Text+second.Text != injected {
|
||
t.Fatalf("两段拼接 %q + %q != 注入正文 %q", first.Text, second.Text, injected)
|
||
}
|
||
if second.Text != string([]rune(injected)[half:]) {
|
||
t.Fatalf("续读正文 = %q, want 注入正文的后半段", second.Text)
|
||
}
|
||
}
|
||
|
||
// TestDocumentReadTool_ContinuesAcrossPages 钉住「续读之后**仍然**被 limit 截断」这条
|
||
// 路径:游标必须一页页往前走(既不原地打转,也不吞掉中间的内容),多页拼接逐字等于
|
||
// 注入正文。
|
||
func TestDocumentReadTool_ContinuesAcrossPages(t *testing.T) {
|
||
t.Parallel()
|
||
fx := newDocReadFixture(t)
|
||
name := docName(".txt")
|
||
_, injected := fx.extract(t, name, strings.Repeat("一行正文,用于多页续读。\n", 20))
|
||
tool := fx.tool(t)
|
||
total := utf8.RuneCountInString(injected)
|
||
step := total/4 + 1 // 至少 3 页,因此中间那些页后面还有内容
|
||
|
||
var joined string
|
||
offset, pages := 0, 0
|
||
for {
|
||
if pages++; pages > 8 {
|
||
t.Fatalf("分页不收敛:offset 停在 %d", offset)
|
||
}
|
||
page, err := callDocRead(t, tool, map[string]any{"name": name, "offset": offset, "limit": step})
|
||
if err != nil {
|
||
t.Fatalf("第 %d 页: %v", pages, err)
|
||
}
|
||
if page.Offset != offset {
|
||
t.Fatalf("第 %d 页 offset = %d, want %d", pages, page.Offset, offset)
|
||
}
|
||
// limit 是**上限**:任何一页都不得超出(多给会让模型的上下文预算失控)。
|
||
if n := utf8.RuneCountInString(page.Text); n > step {
|
||
t.Fatalf("第 %d 页返回 %d 字符, 超过 limit %d", pages, n, step)
|
||
}
|
||
joined += page.Text
|
||
if page.Truncated && utf8.RuneCountInString(page.Text) != step {
|
||
t.Fatalf("第 %d 页被截断却只给了 %d 字符, want %d", pages, utf8.RuneCountInString(page.Text), step)
|
||
}
|
||
if !page.Truncated {
|
||
if page.NextOffset != 0 {
|
||
t.Fatalf("末页仍带续读游标 %d", page.NextOffset)
|
||
}
|
||
break
|
||
}
|
||
if want := offset + utf8.RuneCountInString(page.Text); page.NextOffset != want {
|
||
t.Fatalf("第 %d 页续读游标 = %d, want %d", pages, page.NextOffset, want)
|
||
}
|
||
offset = page.NextOffset
|
||
}
|
||
if pages < 3 {
|
||
t.Fatalf("只读到 %d 页,夹具没有覆盖「续读后仍被截断」", pages)
|
||
}
|
||
if joined != injected {
|
||
t.Fatalf("多页拼接 = %d 字符, want 注入正文 %d 字符", utf8.RuneCountInString(joined), total)
|
||
}
|
||
}
|
||
|
||
// TestDocumentReadTool_LimitDefaultAndCap 钉住 limit 的缺省与上限:缺省取
|
||
// defaultDocumentReadChars,超过上限的请求夹到 maxDocumentReadChars —— 正文本身就在同一
|
||
// 上限内(抽取期预算),因此夹取只会削掉「多要了」的部分,不会少给已存在的正文。
|
||
func TestDocumentReadTool_LimitDefaultAndCap(t *testing.T) {
|
||
t.Parallel()
|
||
fx := newDocReadFixture(t)
|
||
name := docName(".txt")
|
||
_, injected := fx.extract(t, name, "短正文:只有这一行。\n")
|
||
tool := fx.tool(t)
|
||
|
||
got, err := callDocRead(t, tool, map[string]any{"name": name})
|
||
if err != nil {
|
||
t.Fatalf("缺省读取: %v", err)
|
||
}
|
||
if got.Limit != defaultDocumentReadChars || got.Truncated || got.NextOffset != 0 || got.Text != injected {
|
||
t.Fatalf("缺省读取 = %+v, want limit %d、无截断、正文 %q", got, defaultDocumentReadChars, injected)
|
||
}
|
||
capped, err := callDocRead(t, tool, map[string]any{"name": name, "limit": maxDocumentReadChars * 2})
|
||
if err != nil {
|
||
t.Fatalf("超大 limit 读取: %v", err)
|
||
}
|
||
if capped.Limit != maxDocumentReadChars {
|
||
t.Fatalf("limit 夹取 = %d, want %d", capped.Limit, maxDocumentReadChars)
|
||
}
|
||
}
|
||
|
||
// TestDocumentReadTool_OffsetOutOfRange 钉住②:负 offset 在碰盘之前就被拒,超过正文长度
|
||
// 的 offset 也不静默给空,而「恰好等于正文长度」是合法的「读到末尾」。
|
||
func TestDocumentReadTool_OffsetOutOfRange(t *testing.T) {
|
||
t.Parallel()
|
||
fx := newDocReadFixture(t)
|
||
name := docName(".txt")
|
||
_, injected := fx.extract(t, name, "abcdefghij\n")
|
||
tool := fx.tool(t)
|
||
total := utf8.RuneCountInString(injected)
|
||
|
||
// 负数:用一份**从未抽取过**的名字证明它在读盘之前就被拒 —— 若实现先读盘,这里得到的
|
||
// 会是「没有可读的抽取结果」而不是 offset 错误。
|
||
if _, err := callDocRead(t, tool, map[string]any{"name": docName(".log"), "offset": -1}); !errors.Is(err, ErrDocumentOffset) {
|
||
t.Fatalf("负 offset → %v, want ErrDocumentOffset(应在碰盘前被拒)", err)
|
||
}
|
||
if _, err := callDocRead(t, tool, map[string]any{"name": name, "offset": total + 1}); !errors.Is(err, ErrDocumentOffset) {
|
||
t.Fatalf("越界 offset → %v, want ErrDocumentOffset", err)
|
||
}
|
||
last, err := callDocRead(t, tool, map[string]any{"name": name, "offset": total})
|
||
if err != nil {
|
||
t.Fatalf("末尾读取: %v", err)
|
||
}
|
||
if last.Text != "" || last.Truncated || last.NextOffset != 0 || last.TotalChars != total {
|
||
t.Fatalf("末尾读取 = %+v, want 空 text + 无续读 + totalChars %d", last, total)
|
||
}
|
||
}
|
||
|
||
// TestDocumentReadTool_RejectsIllegalName 钉住④(含「不碰盘」):形态非法的名字必须在触达
|
||
// 文件系统之前被拒。数据目录在这里被指向一条**不存在**的路径:若实现先读盘,得到的会是
|
||
// 「没有可读的抽取结果」,因此 errors.Is(ErrInvalidName) 本身就是「没碰盘」的证据。
|
||
func TestDocumentReadTool_RejectsIllegalName(t *testing.T) {
|
||
t.Parallel()
|
||
dataDir := filepath.Join(t.TempDir(), "missing")
|
||
tool, ok := findTool(t, buildTools(t, &config.Config{DataDir: dataDir}), documentReadToolName).(*documentReadTool)
|
||
if !ok {
|
||
t.Fatalf("%q 工具类型不符", documentReadToolName)
|
||
}
|
||
illegal := []string{
|
||
"", // 空名字
|
||
"../secret.txt", // 路径穿越
|
||
"..", // 父目录
|
||
"a.txt", // 主名不足 32 位
|
||
"ab12ab12ab12ab12ab12ab12ab12ab1.txt", // 31 位主名
|
||
docName(".TXT"), // 大写扩展名
|
||
strings.Repeat("g", 32) + ".txt", // 主名不是十六进制
|
||
docName("." + strings.Repeat("x", 13)), // 扩展名超长(>12)
|
||
"extracted/" + docName(".txt"), // 含目录分隔符
|
||
`C:\Windows\x.txt`, // 绝对路径
|
||
"ab12ab12ab12ab12ab12ab12ab12ab12..txt", // 扩展名里带点
|
||
}
|
||
for _, name := range illegal {
|
||
if _, err := callDocRead(t, tool, map[string]any{"name": name}); !errors.Is(err, documents.ErrInvalidName) {
|
||
t.Fatalf("name %q → %v, want documents.ErrInvalidName", name, err)
|
||
}
|
||
}
|
||
if _, err := tool.Execute(context.Background(), "not-an-object"); err == nil {
|
||
t.Fatal("非对象参数应当报错")
|
||
}
|
||
entries, err := os.ReadDir(filepath.Join(dataDir, "extracted"))
|
||
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||
t.Fatalf("读 extracted 目录: %v", err)
|
||
}
|
||
if len(entries) != 0 {
|
||
t.Fatalf("非法名字竟然产生了 %d 个文件", len(entries))
|
||
}
|
||
// 扩展名白名单是 S1 存储侧的裁决(design/24 §4.2 末:不重复存储侧校验、不扩大访问面):
|
||
// 形态合法但 S1 永不会签发的扩展名因此走到「没有可读的抽取结果」这条诚实错误。
|
||
if _, err := callDocRead(t, tool, map[string]any{"name": docName(".exe")}); !errors.Is(err, documents.ErrSidecarUnavailable) {
|
||
t.Fatalf("形态合法但无对应 sidecar → %v, want documents.ErrSidecarUnavailable", err)
|
||
}
|
||
}
|
||
|
||
// TestDocumentReadTool_MissingSidecarIsHonest 钉住③(缺失)与「只读 sidecar,绝不重抽」:
|
||
// 没有 sidecar 时给的是诚实错误,而不是空正文、也不是回头重抽原文件。
|
||
func TestDocumentReadTool_MissingSidecarIsHonest(t *testing.T) {
|
||
t.Parallel()
|
||
fx := newDocReadFixture(t)
|
||
name := docName(".txt")
|
||
tool := fx.tool(t)
|
||
|
||
if _, err := callDocRead(t, tool, map[string]any{"name": name}); !errors.Is(err, documents.ErrSidecarUnavailable) {
|
||
t.Fatalf("无 sidecar → %v, want documents.ErrSidecarUnavailable", err)
|
||
}
|
||
// 抽取过、随后 sidecar 被清掉:原文件仍在,但工具**绝不**因此回头重抽。
|
||
_, injected := fx.extract(t, name, "正文在 sidecar 里。\n")
|
||
if err := os.Remove(fx.sidecarPath(name)); err != nil {
|
||
t.Fatalf("删除 sidecar: %v", err)
|
||
}
|
||
if _, err := callDocRead(t, tool, map[string]any{"name": name}); !errors.Is(err, documents.ErrSidecarUnavailable) {
|
||
t.Fatalf("sidecar 被清掉后 → %v, want documents.ErrSidecarUnavailable(不得回退重抽)", err)
|
||
}
|
||
if injected == "" {
|
||
t.Fatal("夹具前提:抽取应当产出正文")
|
||
}
|
||
}
|
||
|
||
// TestDocumentReadTool_UnreadableIsHonest 钉住③(unreadable):sidecar 的结论是读不到正文
|
||
// 时,工具给的是**带原因的**诚实错误,而不是空正文假装成功。
|
||
func TestDocumentReadTool_UnreadableIsHonest(t *testing.T) {
|
||
t.Parallel()
|
||
fx := newDocReadFixture(t)
|
||
name := docName(".pdf")
|
||
ref, _ := fx.extract(t, name, "%PDF-1.7\n")
|
||
if ref.Status != history.DocumentStatusUnreadable {
|
||
t.Fatalf("夹具前提不成立:status = %q, want %q", ref.Status, history.DocumentStatusUnreadable)
|
||
}
|
||
_, err := callDocRead(t, fx.tool(t), map[string]any{"name": name})
|
||
if !errors.Is(err, ErrDocumentUnreadable) {
|
||
t.Fatalf("unreadable → %v, want ErrDocumentUnreadable", err)
|
||
}
|
||
// 抽取侧给的原因短句必须抵达错误文案(那是模型/用户唯一能看到的解释);这里断言的
|
||
// 是全部原因文案共有的前缀,不是某一句具体措辞。
|
||
if !strings.Contains(err.Error(), "无法抽取正文") {
|
||
t.Fatalf("错误文案没带上抽取侧的原因:%v", err)
|
||
}
|
||
}
|
||
|
||
// TestDocumentReadTool_TextExcludesMetadataLine 钉住⑥:sidecar 第一行的元数据绝不混进
|
||
// text(正文自第二行起),返回的正文与装配期注入的正文逐字相同。
|
||
func TestDocumentReadTool_TextExcludesMetadataLine(t *testing.T) {
|
||
t.Parallel()
|
||
fx := newDocReadFixture(t)
|
||
name := docName(".txt")
|
||
body := "正文第一行\n正文第二行\n"
|
||
_, injected := fx.extract(t, name, body)
|
||
if injected != body {
|
||
t.Fatalf("夹具前提:注入正文 = %q, want %q", injected, body)
|
||
}
|
||
got, err := callDocRead(t, fx.tool(t), map[string]any{"name": name})
|
||
if err != nil {
|
||
t.Fatalf("读取: %v", err)
|
||
}
|
||
if got.Text != injected {
|
||
t.Fatalf("text = %q, want 注入正文 %q", got.Text, injected)
|
||
}
|
||
// 磁盘上的 sidecar 确实带一行元数据(否则上面的相等只是「本来就没有元数据」的假绿)。
|
||
raw, err := os.ReadFile(fx.sidecarPath(name))
|
||
if err != nil {
|
||
t.Fatalf("读 sidecar: %v", err)
|
||
}
|
||
if !strings.HasPrefix(string(raw), "{") || !strings.Contains(string(raw), "\n") {
|
||
t.Fatalf("sidecar 首行不是元数据:%q", string(raw))
|
||
}
|
||
for _, leak := range []string{`"status"`, `"v":`, `"chars"`} {
|
||
if strings.Contains(got.Text, leak) {
|
||
t.Fatalf("text 混进了 sidecar 元数据(含 %s):%q", leak, got.Text)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestDocumentReadTool_OwnerOnlyByDefault 钉住⑤:工具以**默认最严**的可达性标记注册
|
||
// (tool_policies 无行 = 两者皆 false = owner-only),因此群聊/陌生人会话在默认策略下既
|
||
// 看不到也调不到它(design/24 §5 红线)。注册走的是既有工具同一条路径
|
||
// (dispatcher.Register → registry.RegisterTool),因此它同样受 CAPACITY_TOOL_SETS 裁剪。
|
||
func TestDocumentReadTool_OwnerOnlyByDefault(t *testing.T) {
|
||
t.Parallel()
|
||
fx := newDocReadFixture(t)
|
||
reg := registry.New()
|
||
dispatcher, err := tool.New(reg)
|
||
if err != nil {
|
||
t.Fatalf("tool.New: %v", err)
|
||
}
|
||
for _, tl := range buildTools(t, &config.Config{DataDir: fx.dataDir}) {
|
||
if rerr := dispatcher.Register(tl); rerr != nil {
|
||
t.Fatalf("注册 %q: %v", tl.Name(), rerr)
|
||
}
|
||
}
|
||
cap, err := reg.Get(documentReadToolName)
|
||
if err != nil {
|
||
t.Fatalf("%q 未注册: %v", documentReadToolName, err)
|
||
}
|
||
if cap.Kind != registry.KindTool {
|
||
t.Fatalf("kind = %q, want %q", cap.Kind, registry.KindTool)
|
||
}
|
||
if cap.Policy.AllowInGroup || cap.Policy.AllowInStranger {
|
||
t.Fatalf("默认可达性标记 = %+v, want 两者皆 false(最严)", cap.Policy)
|
||
}
|
||
for _, tc := range []struct {
|
||
name string
|
||
env restrict.ActorEnv
|
||
want bool
|
||
}{
|
||
{"owner", restrict.ActorOwner, true},
|
||
{"group", restrict.ActorGroupMember, false},
|
||
{"stranger", restrict.ActorStranger, false},
|
||
} {
|
||
if got := restrict.SetContext(reg, tc.env).Allow(documentReadToolName); got != tc.want {
|
||
t.Fatalf("%s 环境 Allow = %v, want %v", tc.name, got, tc.want)
|
||
}
|
||
// 循环真正用的那道门(dispatcher 的策略引擎)必须与策略引擎同一结论。
|
||
if got := dispatcher.PolicyEngine(tc.env).Allow(documentReadToolName); got != tc.want {
|
||
t.Fatalf("%s 环境 PolicyEngine().Allow = %v, want %v", tc.name, got, tc.want)
|
||
}
|
||
}
|
||
}
|