test(memory): 补 377e02c 的判据假绿 —— multiCarrier 名不符实,真多块零覆盖

`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")如实为红。
This commit is contained in:
2026-11-04 18:32:16 +08:00
parent 377e02c943
commit c1f7e06b22

View File

@ -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,