package io import ( "strings" "testing" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/documents" ) // 本文件钉住 document-read 与**装配期注入预算**之间的成对契约(切片 T1 §7.2): // sidecar 存抽取全文,注入只在装配期按预算截断,因此清单行给出的续读游标真的是 // 「继续读的起点」。工具自身的分页/边界/诚实失败用例在 document_read_test.go。 // TestDocumentReadTool_CursorContinuesIntoTextBeyondTheInjectionBudget 钉住切片 T1 // §7.2 修好的断裂(S4 实测:清单游标恒等于 sidecar 正文长度 ⇒ 照它续读必然读到空): // // - CSV 的摘要行属于正文坐标系(offset 0 = 摘要行开头),与装配期注入的正文一致; // - sidecar 存的是**抽取全文**:正文远超旧的 64K 注入预算时,`Chars` 仍是全文长度、 // `Truncated=false`,因此模型照清单给的游标(= 已注入字符数)续读,拿到的是 // 注入预算**之外**的真实正文,而不是空。 func TestDocumentReadTool_CursorContinuesIntoTextBeyondTheInjectionBudget(t *testing.T) { t.Parallel() fx := newDocReadFixture(t) tool := fx.tool(t) csv := docName(".csv") _, injectedCSV := fx.extract(t, csv, "a,b\n1,2\n3,4\n") if !strings.HasPrefix(injectedCSV, "[CSV") { t.Fatalf("CSV 夹具应当带摘要行:%q", injectedCSV) } gotCSV, err := callDocRead(t, tool, map[string]any{"name": csv}) if err != nil { t.Fatalf("读 CSV: %v", err) } if gotCSV.Text != injectedCSV { t.Fatalf("CSV text = %q, want 含摘要行的注入正文 %q", gotCSV.Text, injectedCSV) } // 正文 20 万字符:远超旧的 64K 注入预算,但远在 sidecar 安全上限内。 body := strings.Repeat("正", 200*1024) big := docName(".txt") ref, injected := fx.extract(t, big, body) if ref.Truncated || ref.Chars != len([]rune(body)) { t.Fatalf("夹具前提:ref = %+v, want 全文 %d 字符、未撞安全上限", ref, len([]rune(body))) } if injected != body { t.Fatalf("注入正文长度 = %d, want 全文 %d", len([]rune(injected)), len([]rune(body))) } // 装配期若按旧预算(64K 字符)注入,游标就是 64K;按它续读必须拿到预算之外的正文。 const injectedByOldBudget = 64 * 1024 atCursor, err := callDocRead(t, tool, map[string]any{"name": big, "offset": injectedByOldBudget}) if err != nil { t.Fatalf("按清单游标读取: %v", err) } if atCursor.TotalChars != len([]rune(body)) { t.Fatalf("totalChars = %d, want sidecar 全文 %d", atCursor.TotalChars, len([]rune(body))) } if atCursor.Text == "" { t.Fatal("按游标续读读到了空正文 —— 清单给的承诺是假的(S4 的断裂没有修好)") } if !atCursor.Truncated || atCursor.NextOffset != injectedByOldBudget+len([]rune(atCursor.Text)) { t.Fatalf("续读窗口 = %+v, want 被 limit 截断且带 nextOffset", atCursor) } if want := body[injectedByOldBudget*len("正"):][:len(atCursor.Text)]; atCursor.Text != want { t.Fatalf("续读正文不是注入预算之外的那一段:前 40 字符 %q", firstRunes(atCursor.Text, 40)) } if atCursor.ExtractTruncated { t.Fatal("未撞安全上限的文档不该报 extractTruncated") } } // firstRunes 返回 s 的前 n 个 rune(只用于失败信息,避免把整段正文打进日志)。 func firstRunes(s string, n int) string { r := []rune(s) if len(r) <= n { return s } return string(r[:n]) + "…" } // TestDocumentReadTool_SafetyCapTruncationIsHonest 钉住与抽取侧的另一半成对契约: // 正文撞上 sidecar 安全上限(documents.MaxSidecarBytes)时,sidecar 只有那段前缀, // 工具照实把「抽取期就到头了」告诉模型(extractTruncated=true),使「读到末尾」 // 不会被误当成「文件只有这么长」。 func TestDocumentReadTool_SafetyCapTruncationIsHonest(t *testing.T) { t.Parallel() fx := newDocReadFixture(t) name := docName(".txt") ref, injected := fx.extract(t, name, strings.Repeat("宽", documents.MaxSidecarBytes/len("宽")+16)) if !ref.Truncated { t.Fatalf("夹具前提:ref = %+v, want 撞上安全上限并截断", ref) } if ref.Chars != len([]rune(injected)) || ref.Chars > documents.MaxSidecarBytes/len("宽") { t.Fatalf("撞上限后的字符数 = %d, want 不超过 %d 且与正文一致", ref.Chars, documents.MaxSidecarBytes/len("宽")) } atEnd, err := callDocRead(t, fx.tool(t), map[string]any{"name": name, "offset": ref.Chars}) if err != nil { t.Fatalf("按末尾游标读取: %v", err) } if atEnd.TotalChars != ref.Chars || atEnd.Text != "" || atEnd.Truncated { t.Fatalf("末尾读取 = %+v, want 读到 sidecar 末尾(totalChars %d)", atEnd, ref.Chars) } if !atEnd.ExtractTruncated { t.Fatal("抽取期的安全上限截断没有如实回给模型(extractTruncated=false)") } }