diff --git a/test/phase-b-h.test.js b/test/phase-b-h.test.js index 56d6fee..32703f8 100644 --- a/test/phase-b-h.test.js +++ b/test/phase-b-h.test.js @@ -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