test(memory-b): make F8 mutants bite the positive criteria
This commit is contained in:
@ -2645,6 +2645,21 @@ test('D5†: dropping the unavailable sentence makes D5 red', async () => {
|
||||
|
||||
// ── F8: a mis-sized vector is a HARD error when the width is declared ──────
|
||||
//
|
||||
// ★ The positive predicate below is shared by the unmutated case and by every
|
||||
// F8 mutant, so a mutant can only pass by satisfying the SAME requirement the
|
||||
// real code satisfies. Re-writing the requirement inside the mutant's body
|
||||
// ("under the mutation the bad vector lands") is a self-test: it asserts the
|
||||
// mutated behaviour and therefore stays green exactly when the criterion should
|
||||
// fail.
|
||||
function assertExplicitWidthRefused(store) {
|
||||
assert.throws(
|
||||
() => store.putBlock({ text: 'x', vector: [1, 2], fingerprint: 'fp-a', expectedDimension: 3 }),
|
||||
(err) => /dimension 2 does not match the active space's 3/.test(err.message),
|
||||
'a 2-wide vector in a 3-wide space must be refused, not stored and counted later',
|
||||
)
|
||||
assert.equal(store.countBlocks(), 0, 'nothing was written')
|
||||
}
|
||||
//
|
||||
// criteria:165 says "provider returns a dimension ≠ the configured one ⇒ hard
|
||||
// error, never truncate or pad". Until now only the adapter layer enforced it
|
||||
// (H3b); the write path silently degraded, so a bad vector could land and be
|
||||
@ -2655,12 +2670,7 @@ test('D5†: dropping the unavailable sentence makes D5 red', async () => {
|
||||
test('F8: putBlock refuses a vector whose width differs from the declared space', () => {
|
||||
const store = new BlockStore({ path: ':memory:' }).open()
|
||||
try {
|
||||
assert.throws(
|
||||
() => store.putBlock({ text: 'x', vector: [1, 2], fingerprint: 'fp-a', expectedDimension: 3 }),
|
||||
/dimension 2 does not match the active space's 3/,
|
||||
'a 2-wide vector in a 3-wide space must be refused, not stored and counted later',
|
||||
)
|
||||
assert.equal(store.countBlocks(), 0, 'nothing was written')
|
||||
assertExplicitWidthRefused(store)
|
||||
|
||||
// The matching width is accepted.
|
||||
assert.equal(
|
||||
@ -2710,13 +2720,28 @@ test('F8: a provider returning the wrong width still degrades (not a caller bug)
|
||||
try {
|
||||
const r = await mem.commit({ text: 'x' })
|
||||
assert.equal(r.embedded, false, 'a misbehaving provider degrades, it does not throw')
|
||||
// ★ Counting rows, not picking one: `allBlocks(1)[0]` is satisfied by any
|
||||
// single row, so a regression that stopped writing on the degraded path
|
||||
// entirely would still be green here. F8/W4 require the block to LAND.
|
||||
assert.equal(mem.store.countBlocks(), 1, 'the block is still written (degrade ≠ drop)')
|
||||
assert.equal(mem.store.allBlocks(1)[0].vector, '', 'and no bad vector is stored')
|
||||
} finally {
|
||||
await done()
|
||||
}
|
||||
})
|
||||
|
||||
test('F8†: softening the write-side width check lets a bad vector land', async () => {
|
||||
test('F8†: softening the write-side width check bites the positive criterion red', async (t) => {
|
||||
// ★ The anchor must appear EXACTLY ONCE in lib/store.js. `withMutatedCopy`
|
||||
// already fails loudly when it is missing; the count check makes "appears
|
||||
// twice, first match mutated, second silently left intact" impossible too.
|
||||
const { readFileSync } = await import('node:fs')
|
||||
const source = readFileSync(new URL('../lib/store.js', import.meta.url), 'utf8')
|
||||
assert.equal(
|
||||
source.split(' if (actual !== expectedDimension) {').length - 1,
|
||||
1,
|
||||
'the F8† anchor must occur exactly once in lib/store.js',
|
||||
)
|
||||
|
||||
await withMutatedCopy(
|
||||
'store.js',
|
||||
[[
|
||||
@ -2726,14 +2751,19 @@ test('F8†: softening the write-side width check lets a bad vector land', async
|
||||
async (mod) => {
|
||||
const store = new mod.BlockStore({ path: ':memory:' }).open()
|
||||
try {
|
||||
// Under the mutation the bad vector is accepted, which is the state F8 forbids.
|
||||
const written = store.putBlock({ text: 'x', vector: [1, 2], fingerprint: 'fp-a', expectedDimension: 3 })
|
||||
assert.equal(written.created, true, 'the mutation really admits the wrong width')
|
||||
assert.equal(
|
||||
JSON.parse(store.allBlocks(1)[0].vector).length,
|
||||
2,
|
||||
'and stores it — so F8 (which requires a throw) fails on the mutated module',
|
||||
)
|
||||
// ★ The SAME positive criterion, run on the mutant. It must throw: on the
|
||||
// mutant the write is admitted, so `assert.throws` sees no error and
|
||||
// fails with ERR_ASSERTION.
|
||||
assert.throws(() => assertExplicitWidthRefused(store), (err) => {
|
||||
assert.equal(err.code, 'ERR_ASSERTION', 'the criterion must fail as an assertion')
|
||||
t.diagnostic(`F8† RAW RED >>> ${String(err.message).split('\n')[0]}`)
|
||||
return true
|
||||
}, 'F8† must break the unchanged F8 criterion')
|
||||
// Corroboration: the mutation really does admit the bad vector. This is
|
||||
// reported as a diagnostic, never as the thing that "bites" — asserting
|
||||
// it would be the self-test that made the previous version useless.
|
||||
const probe = store.putBlock({ text: 'probe', vector: [1, 2], fingerprint: 'fp-a', expectedDimension: 3 })
|
||||
t.diagnostic(`F8† mutant admitted the bad vector: created=${probe.created}, blocks=${store.countBlocks()}`)
|
||||
} finally {
|
||||
store.close()
|
||||
}
|
||||
@ -2741,6 +2771,60 @@ test('F8†: softening the write-side width check lets a bad vector land', async
|
||||
)
|
||||
})
|
||||
|
||||
test('F8†b: a degraded provider write that drops the block bites countBlocks red', async (t) => {
|
||||
// ★ Mutation on lib/index.js: the degraded branch (provider returned a wrong
|
||||
// width) stops persisting. `embedded === false` and `vector === ''` both stay
|
||||
// green under this mutation — only the row count catches it, which is exactly
|
||||
// what the `allBlocks(1)[0]` shape could not do.
|
||||
await withMutatedCopy(
|
||||
'index.js',
|
||||
[[
|
||||
` if (!Array.isArray(vector) || vector.length !== space.dimension) {
|
||||
return { ...this.store.putBlock(input), embedded: false }
|
||||
}`,
|
||||
` if (!Array.isArray(vector) || vector.length !== space.dimension) {
|
||||
return { id: '', created: false, embedded: false } // MUTATED: degrade by dropping
|
||||
}`,
|
||||
]],
|
||||
async (mod) => {
|
||||
const { Context } = await import('@deepseek-ai/cordis')
|
||||
const { SystemPrompt } = await import('@deepseek-ai/dsh-system-prompt')
|
||||
const { ToolRuntime } = await import('@deepseek-ai/dsh-tools')
|
||||
const { mkdtempSync, rmSync } = await import('node:fs')
|
||||
const { tmpdir } = await import('node:os')
|
||||
const { join } = await import('node:path')
|
||||
const dir = mkdtempSync(join(tmpdir(), 'f8b-'))
|
||||
const ctx = new Context()
|
||||
new SystemPrompt(ctx, {})
|
||||
new ToolRuntime(ctx, {})
|
||||
try {
|
||||
const mem = mod.apply(ctx, {
|
||||
path: join(dir, 'm.db'),
|
||||
embedder: { async embedText() { return [1, 2] }, info: () => ({ fingerprint: 'fp-x', dimension: 3 }) },
|
||||
})
|
||||
const r = await mem.commit({ text: 'x' })
|
||||
// The two assertions that survive the mutation — reported, not relied on.
|
||||
t.diagnostic(`F8†b still-green under mutation: embedded=${r.embedded}, blocks=${mem.store.countBlocks()}`)
|
||||
assert.throws(() => {
|
||||
assert.equal(
|
||||
mem.store.countBlocks(),
|
||||
1,
|
||||
'the block must still be written on a degraded provider write',
|
||||
)
|
||||
assert.equal(mem.store.allBlocks(1)[0]?.vector, '', 'and carry no bad vector')
|
||||
}, (err) => {
|
||||
assert.equal(err.code, 'ERR_ASSERTION', 'only the row-count criterion may bite')
|
||||
t.diagnostic(`F8†b RAW RED >>> ${String(err.message).split('\n')[0]}`)
|
||||
return true
|
||||
}, 'F8†b must break the unchanged provider-degradation criterion')
|
||||
} finally {
|
||||
await ctx.fiber.dispose()
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
test('F3 performance guard: blockStates source does not parse vector payloads on reads', async () => {
|
||||
// The old implementation did `JSON.parse(row.vector).length` for every row;
|
||||
// at 2000 blocks x 2048 dims that measured ~354ms per renderIndex call, ~255ms
|
||||
|
||||
Reference in New Issue
Block a user