package memory import ( "testing" "time" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/embedding" ) // measuredReviewChain* 是**实测参照值**(2026-10-13,本机空闲;正文取自活库 // cyrene/user 分区 201 条,均长 34.7 汉字;完整曲线与复跑命令见 // internal/runtime/embedding/budget.go 的文件头)。它们只用于本文件的断言 —— // 与 embedding 侧 api_url_test.go 的 reviewBudgetRef 同一手法:把「拿实测数字做的 // 决定」留成可复核的常量,而不是消失在注释里。 // // 链的形状 = ⌈(ScanLimit + 1) / embedding.DefaultBatchLimit⌉ 个请求: // // 改前 4 条/片:51 个请求 ⇒ 墙钟 23.070 / 21.860 s // 改后 8 条/片:26 个请求 ⇒ 墙钟 21.435 / 21.597 s // // loaded 那一档取自 M14 只读勘测报告(同一条链与整理链并发时测得 56.7s)—— // 它是按**改前**的 4 条/片形状测的;链路墙钟由「条数 × 每条成本」主导、与批次 // 形状几乎无关,因此拿它当改后代码的最坏参照是**偏保守**的一侧。 const ( measuredReviewChainIdle = 22 * time.Second measuredReviewChainLoaded = 57 * time.Second ) // TestEmbeddingTimeoutFitsReviewBudget 钉住两条**跨包**口径: // // 1. embedding 单次调用的独立超时必须严格小于入库前审查的单条总预算; // 2. 反过来,审查的单条总预算必须严格大于「实测最坏召回链长 + T1 自己的超时」。 // // 第 1 条的理由(2026-10-07 实测的故障形态):端点主机写成 localhost → 先试 IPv6 // ::1 → 连接等系统超时(Windows 上约 21s),一次 embedding 失败正好吃光预算, // 整条审查超时(`memory/review: context deadline exceeded`),排在后面的 T1 // 灰区判定连执行机会都没有。地址规范化(embedding.normalizeEndpointURL)治的是根, // 第 1 条守的是「就算再有新的卡死来源,也得先把预算留给后续步骤」。 // // 第 2 条是本次修复(2026-10-13)的核心断言,**变异自证**:把 defaultReviewTimeout // 调回 20s ⇒ 这一条必红。它守的是「预算 ≥ 链长 + T1」这条不可让步的下界 —— // 链长随 ScanLimit / 批上限漂移,T1 超时随 defaultT1Timeout 漂移,任一侧动到把 // 预算吃掉都会被它抓到。旧值 20s 正是踩在这条线上的:实测链长 21~23s(空闲), // 于是每一条候选满额的审查都必然超时,7 天里 2274 条审查因此降级为空 recall_mode。 // // embedding 侧同一口径也有一条钉值测试(api_url_test.go),两边互为护栏。 func TestEmbeddingTimeoutFitsReviewBudget(t *testing.T) { t.Parallel() if embedding.DefaultRequestTimeout <= 0 { t.Fatalf("embedding.DefaultRequestTimeout = %s, 必须为正", embedding.DefaultRequestTimeout) } if embedding.DefaultRequestTimeout >= defaultReviewTimeout { t.Fatalf("embedding 单次超时 %s 必须严格小于审查总预算 %s", embedding.DefaultRequestTimeout, defaultReviewTimeout) } // 第 2 条:预算必须盖得住实测最坏链长,还要留得下 T1 那一步自己的超时 // (T1 在召回**之后**跑,同一条 ctx)。空闲余量倍数由 budget.go 的注释列明, // 这里只钉住「盖得住」这条下界。 worst := measuredReviewChainLoaded + defaultT1Timeout if defaultReviewTimeout <= worst { t.Fatalf("审查单条预算 %s 必须严格大于「实测最坏召回链 %s + T1 超时 %s = %s」—— "+ "否则链还没跑完预算就用光(2026-10-13 的 2274 条超时降级正是这条)", defaultReviewTimeout, measuredReviewChainLoaded, defaultT1Timeout, worst) } if measuredReviewChainIdle >= defaultReviewTimeout { t.Fatalf("空闲实测链长 %s 已不小于审查预算 %s(余量必须为正)", measuredReviewChainIdle, defaultReviewTimeout) } }