From c1f7e06b22eb6926213cd32157dbed4117832c3d Mon Sep 17 00:00:00 2001 From: jianf <2198972886@qq.com> Date: Wed, 4 Nov 2026 18:32:16 +0800 Subject: [PATCH] =?UTF-8?q?test(memory):=20=E8=A1=A5=20377e02c=20=E7=9A=84?= =?UTF-8?q?=E5=88=A4=E6=8D=AE=E5=81=87=E7=BB=BF=20=E2=80=94=E2=80=94=20mul?= =?UTF-8?q?tiCarrier=20=E5=90=8D=E4=B8=8D=E7=AC=A6=E5=AE=9E=EF=BC=8C?= =?UTF-8?q?=E7=9C=9F=E5=A4=9A=E5=9D=97=E9=9B=B6=E8=A6=86=E7=9B=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `multiCarrier(texts, convention)` 名叫"多块",实际 `content: [{ type: 'text', text: joined }]` 恒为 1 个 block。所以 S2a/S2b 读起来是"多块路径已覆盖", 实际一条都没走多块分支: M1 在 `const rendered = ...` 前插 `if (blocks.length > 1) return null` (即 V2 那个错误实现)→ 22 条判据 pass 20 / fail 0,全绿。 这正是上一轮 375→377e02c 留下的假绿:不是缺测试,是 helper 名字把人骗了。 名字夸大覆盖范围比没有测试更毒,因为它读起来像有。 改法(只动 test,lib/loop.js 零改动,sha256 f28c5564… 前后一致): - `multiCarrier` → `singleBlockCarrier`,实现不动,JSDoc 写明 "产出:1 个 text block 承载 N 个 section(lib/index.js:932 的真实形状)" 并记下它曾叫 multiCarrier 这段历史。 - 新增 `trueMultiCarrier(texts, kind)`:N 个 text block,每个 block 一个 section,rendered = texts.join('')(glued)。注释写明真多块没有 spaced 变体。 - S2a/S2b → singleBlockCarrier;S3 标题改成 "single content block carrying N sections != those sections …" 并改用 singleBlockCarrier。 - S2c/S2d → trueMultiCarrier(['a','b']),断言不变。 变异自证(两条都咬住,且撤净后 sha256 回到基线): M1 `if (blocks.length > 1) return null` → S2c 红("got 1 message(s)") M2 删掉 glued 分支(只剩 `rendered !== spaced`)→ S2b 红("got 1") 对 5c6aaf5 的干净 archive 跑本判据:tests 22 / pass 10 / fail 12, 其中 S2c("got 1 message(s)")与 S2d("got 2")如实为红。 --- test/loop.test.js | 59 +++++++++++++++++++++++++++++------------------ 1 file changed, 37 insertions(+), 22 deletions(-) diff --git a/test/loop.test.js b/test/loop.test.js index 0512e47..c82af44 100644 --- a/test/loop.test.js +++ b/test/loop.test.js @@ -216,14 +216,20 @@ function carrier(text, kind = 'memory') { /** * A carrier whose N sections are rendered into ONE content block. * - * ★ Note the shape: this is "single content block carrying N sections", NOT - * "N content blocks". It models the two producers' joining conventions - * ('\n\n' and '') on a single block — which is what `lib/index.js:932` - * actually emits. True multi-block carriers are S2c/S2d below. + * The name states the shape: ONE text block. It models the two joining + * conventions ('\n\n' and '') on that single block, which is what + * `lib/index.js:932` actually emits. + * + * ★ This helper was previously called `multiCarrier` while producing a single + * block, and that name is exactly what let the V2 bug (`if (blocks.length > 1) + * return null`) pass untouched: S2a/S2b read as multi-block coverage while + * exercising none of it. A helper whose name overstates its shape is worse than + * a missing test, because it looks like coverage. Real multi-block carriers come + * from `trueMultiCarrier` below and are exercised by S2c/S2d. * @param {string[]} texts one entry per section * @param {'glued'|'spaced'} convention how the sections were concatenated */ -function multiCarrier(texts, convention = 'spaced', kind = 'memory') { +function singleBlockCarrier(texts, convention = 'spaced', kind = 'memory') { const joined = convention === 'glued' ? texts.join('') : texts.join('\n\n') return { role: 'user', @@ -232,6 +238,21 @@ function multiCarrier(texts, convention = 'spaced', kind = 'memory') { } } +/** + * 产出:N 个 text block,每个 block 一个 section 的文本(rendered = texts.join(''))。 + * 这不是本插件产出的形状,是"其它 producer 逐块发文本"的形状;只用来证明多块路径有牙。 + * 注意:真多块时 rendered 恒为 glued 拼法,没有 spaced 变体 —— spaced 只由单块承载。 + * @param {string[]} texts one entry per block AND per section + * @param {string} kind source kind + */ +function trueMultiCarrier(texts, kind = 'memory') { + return { + role: 'user', + content: texts.map((text) => ({ type: 'text', text })), + source: { kind, form: 'snapshot', sections: texts.map((text, i) => ({ name: `${kind}-${i}`, text })) }, + } +} + /** Readable "expected vs actual" rendering for the assertion messages. */ const show = (value) => JSON.stringify(value) @@ -256,7 +277,7 @@ test('S1: single block == single section is promoted, text byte-identical', () = }) test('S2a: single content block carrying N sections ("\\n\\n" convention) is promoted', () => { - const out = promoteSnapshotSections(decisionWith([multiCarrier(['a', 'b'], 'spaced')])) + const out = promoteSnapshotSections(decisionWith([singleBlockCarrier(['a', 'b'], 'spaced')])) assert.equal( out.messages.length, 0, @@ -275,7 +296,7 @@ test('S2a: single content block carrying N sections ("\\n\\n" convention) is pro }) test('S2b: single content block carrying N sections ("" glued convention) is promoted', () => { - const out = promoteSnapshotSections(decisionWith([multiCarrier(['a', 'b'], 'glued')])) + const out = promoteSnapshotSections(decisionWith([singleBlockCarrier(['a', 'b'], 'glued')])) assert.equal( out.messages.length, 0, @@ -294,33 +315,27 @@ test('S2b: single content block carrying N sections ("" glued convention) is pro }) test('S2c: two content blocks == two sections (glued) is promoted', () => { - const msg = { - role: 'user', - content: [{ type: 'text', text: 'a' }, { type: 'text', text: 'b' }], // 2 blocks, rendered='ab' - source: { kind: 'memory', form: 'snapshot', sections: [{ name: 'm0', text: 'a' }, { name: 'm1', text: 'b' }] }, - } + const msg = trueMultiCarrier(['a', 'b']) // 2 blocks, rendered='ab' const out = promoteSnapshotSections(decisionWith([msg])) assert.equal(out.messages.length, 0, `two blocks == two sections must be promoted; got ${out.messages.length} message(s)`) assert.equal(out.assembly.sections[1].text, 'ab', `promoted text must be the rendered content; got ${JSON.stringify(out.assembly.sections[1].text)}`) }) test('S2d: two content blocks != sections is NOT promoted (control: the match is)', () => { + // rendered = 'ab' (glued), while the single section says 'a\n\nb' — neither + // canonical join matches, so this must stay in history. The control beside it + // is the same two blocks with matching sections, which must be promoted. const drifted = { - role: 'user', - content: [{ type: 'text', text: 'a' }, { type: 'text', text: 'b' }], // rendered='ab' - source: { kind: 'memory', form: 'snapshot', sections: [{ name: 'm0', text: 'a\n\nb' }] }, // glued/spaced='a\n\nb' - } - const control = { - role: 'user', - content: [{ type: 'text', text: 'a' }, { type: 'text', text: 'b' }], - source: { kind: 'memory', form: 'snapshot', sections: [{ name: 'm0', text: 'a' }, { name: 'm1', text: 'b' }] }, + ...trueMultiCarrier(['a', 'b']), + source: { kind: 'memory', form: 'snapshot', sections: [{ name: 'm0', text: 'a\n\nb' }] }, } + const control = trueMultiCarrier(['a', 'b']) const out = promoteSnapshotSections(decisionWith([control, drifted])) assert.equal(out.messages.length, 1, `only the drifted one may stay; got ${out.messages.length}`) assert.equal(out.messages[0], drifted, 'the surviving message must be the drifted one') }) -test('S3: multi block != multi section is NOT promoted (control: the match is)', () => { +test('S3: single content block carrying N sections != those sections is NOT promoted (control: the match is)', () => { // Sections say "a"+"b", the content block says "zzz": editing the text would // not be a move, so this one must stay in history — while the matching // carrier beside it must go. The two must be told apart by CONTENT, not by @@ -334,7 +349,7 @@ test('S3: multi block != multi section is NOT promoted (control: the match is)', sections: [{ name: 'memory-0', text: 'a' }, { name: 'memory-1', text: 'b' }], }, } - const control = multiCarrier(['a', 'b'], 'spaced') + const control = singleBlockCarrier(['a', 'b'], 'spaced') const out = promoteSnapshotSections(decisionWith([control, drifted])) assert.equal( out.messages.length,