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:
@ -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,
|
||||
|
||||
Reference in New Issue
Block a user