173 lines
9.6 KiB
JavaScript
173 lines
9.6 KiB
JavaScript
const { test } = require('node:test')
|
|||
|
|
const assert = require('node:assert/strict')
|
||
|
|
const fs = require('node:fs')
|
||
|
|
const path = require('node:path')
|
||
|
|
const vm = require('node:vm')
|
||
|
|
const { historicalPageOnlyReceipts, isHistoricalPageOnlyReceipt, hasSourceReadingProof, recoverySchedulingGate, remainingRecoveryCandidates } = require('./wiki_recovery_scheduler_policy.cjs')
|
||
|
|
|
||
|
|
const pageResult = { recovery_status: 'wiki_recovered', pending: false, pending_finalize: 0, new_failures: 0, failed_page_spans: 0, span_status: 'done', full_read_status: 'not_verified' }
|
||
|
|
const legacy = historicalPageOnlyReceipts.map(receipt => ({ ...receipt, ...pageResult }))
|
||
|
|
const current = { ...pageResult, tenant_id: 10059, dead_letter_id: 10, attempt_number: 4, pending_op_id: 9999,
|
||
|
|
read_coverage_version: 'wiki-full-read-v1', source_read_complete: true, full_read_status: 'source_processed' }
|
||
|
|
const report = (recoveries, remaining = 21) => ({ recoveries, remaining_not_attempted: remaining })
|
||
|
|
|
||
|
|
test('the eleven historical page receipts stay unverified and do not block proven new recovery', () => {
|
||
|
|
const before = structuredClone(legacy)
|
||
|
|
const result = recoverySchedulingGate(report([...legacy, current]))
|
||
|
|
assert.equal(result.state, 'ready_for_next_batch')
|
||
|
|
assert.deepEqual(result.missing_full_read_ids, [])
|
||
|
|
assert.deepEqual(result.historical_full_read_unverified_ids, [2, 5, 6, 15, 16, 17, 28, 30, 31, 32, 33])
|
||
|
|
assert.equal(result.source_reading_complete, false)
|
||
|
|
assert.deepEqual(legacy, before)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('new page recovery without full reading stops before another document can be scheduled', () => {
|
||
|
|
for (const change of [
|
||
|
|
{ full_read_status: 'not_verified' }, { full_read_status: 'in_progress' }, { full_read_status: 'paused_budget' },
|
||
|
|
{ source_read_complete: false }, { source_read_complete: undefined }, { read_coverage_version: 'old-prefix' },
|
||
|
|
{ read_coverage_version: undefined },
|
||
|
|
]) {
|
||
|
|
const result = recoverySchedulingGate(report([...legacy, { ...current, ...change }]))
|
||
|
|
assert.equal(result.state, 'stopped_for_review')
|
||
|
|
assert.deepEqual(result.missing_full_read_ids, [10])
|
||
|
|
assert.deepEqual(result.unresolved_ids, [10])
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
test('complete candidate and summary windows alone do not establish source reading proof', () => {
|
||
|
|
const row = { ...pageResult, dead_letter_id: 1, read_progress: {
|
||
|
|
candidates: { stage_complete: true, source_windows_complete: true, status: 'complete' },
|
||
|
|
summary: { stage_complete: true, source_windows_complete: true, status: 'complete' },
|
||
|
|
} }
|
||
|
|
assert.equal(hasSourceReadingProof(row), false)
|
||
|
|
assert.equal(recoverySchedulingGate(report([row])).state, 'stopped_for_review')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('the legacy exemption cannot extend to a new attempt, pending op, source, model or tenant', () => {
|
||
|
|
for (const change of [{ attempt_number: 3 }, { pending_op_id: 9000 }, { tenant_id: 10060 },
|
||
|
|
{ knowledge_id: 'other-doc' }, { knowledge_base_id: 'other-kb' }, { model_id: 'other-model' },
|
||
|
|
{ queued_at: '2026-09-20T00:00:00Z' }]) {
|
||
|
|
const row = { ...legacy[0], ...change }
|
||
|
|
assert.equal(isHistoricalPageOnlyReceipt(row), false)
|
||
|
|
assert.equal(recoverySchedulingGate(report([row])).state, 'stopped_for_review')
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
test('pending reading and finalization wait without prematurely failing coverage', () => {
|
||
|
|
for (const change of [{ pending: true, recovery_status: 'running' }, { pending_finalize: 2 }]) {
|
||
|
|
const row = { ...current, source_read_complete: undefined, full_read_status: 'in_progress', ...change }
|
||
|
|
assert.equal(recoverySchedulingGate(report([...legacy, row])).state, 'waiting_for_current_batch')
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
test('terminal page recovery explicitly retains historical full-reading debt', () => {
|
||
|
|
const result = recoverySchedulingGate(report([...legacy, current], 0))
|
||
|
|
assert.equal(result.state, 'all_original_pages_recovered')
|
||
|
|
assert.equal(result.source_reading_complete, false)
|
||
|
|
assert.equal(result.historical_full_read_unverified_ids.length, 11)
|
||
|
|
assert.notEqual(result.state, 'all_originals_verified')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('terminal state requires source reading proof for every new receipt, including after restart', () => {
|
||
|
|
const rows = [...legacy, { ...current, full_read_status: 'not_verified', source_read_complete: undefined }]
|
||
|
|
for (const remaining of [0, 21]) {
|
||
|
|
const serialized = JSON.parse(JSON.stringify(report(rows, remaining)))
|
||
|
|
assert.equal(recoverySchedulingGate(serialized).state, 'stopped_for_review')
|
||
|
|
}
|
||
|
|
const complete = recoverySchedulingGate(report([current], 0))
|
||
|
|
assert.equal(complete.state, 'all_original_pages_and_source_reading_complete')
|
||
|
|
assert.equal(complete.source_reading_complete, true)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('historical and newly recovered records are never candidates for automatic requeue', () => {
|
||
|
|
const state = report([...legacy, current])
|
||
|
|
state.candidates = [...legacy.map(row => ({ id: row.dead_letter_id, eligible: true, chunks: 1, pending: 0 })),
|
||
|
|
{ id: 10, eligible: true, chunks: 624, pending: 0 }, { id: 1, eligible: true, chunks: 100, pending: 0 },
|
||
|
|
{ id: 3, eligible: false, chunks: 100, pending: 0 }, { id: 4, eligible: true, chunks: 100, pending: 1 }]
|
||
|
|
assert.deepEqual(remainingRecoveryCandidates(state).map(row => row.id), [1])
|
||
|
|
})
|
||
|
|
|
||
|
|
test('full-reading proof never bypasses page failures or an unknown settled outcome', () => {
|
||
|
|
for (const change of [{ recovery_status: 'partial' }, { recovery_status: 'unexpected' },
|
||
|
|
{ span_status: 'failed' }, { new_failures: 1 }, { failed_page_spans: 1 }]) {
|
||
|
|
assert.equal(recoverySchedulingGate(report([{ ...current, ...change }], 0)).state, 'stopped_for_review')
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
// Evaluate the actual runner with all I/O replaced. No recovery command,
|
||
|
|
// filesystem write, timer or model call can escape this fixture.
|
||
|
|
function runIsolatedScheduler(reports, hasReview = true) {
|
||
|
|
const directory = path.resolve(__dirname, '../../acceptance/wiki-deadletter-recovery-20260919')
|
||
|
|
const reportFile = path.join(directory, 'isolated-status.json')
|
||
|
|
const states = []
|
||
|
|
const commands = []
|
||
|
|
let inspected
|
||
|
|
let unlocked = false
|
||
|
|
const context = {
|
||
|
|
__dirname,
|
||
|
|
__filename: path.join(__dirname, 'run_wiki_deadletter_recovery.cjs'),
|
||
|
|
process: { argv: ['node', 'run_wiki_deadletter_recovery.cjs', '--apply', '--review-directory=/reviewed'], execPath: 'isolated-node', pid: 123, on() {} },
|
||
|
|
console: { log(value) { states.push(JSON.parse(value)) }, error() {} },
|
||
|
|
setTimeout() { throw new Error('Unexpected scheduler timer') },
|
||
|
|
require(name) {
|
||
|
|
if (name === 'node:fs') return {
|
||
|
|
mkdirSync() {}, openSync() { return 1 }, closeSync() {}, writeFileSync() {}, renameSync() {}, appendFileSync() {},
|
||
|
|
existsSync(file) { assert.match(file, /^\/reviewed\/dead-letter-\d+-attempt-1\.json$/); return hasReview },
|
||
|
|
readFileSync(file) { assert.equal(file, reportFile); return JSON.stringify(inspected) },
|
||
|
|
unlinkSync(file) { assert.equal(path.basename(file), 'runner.lock'); unlocked = true },
|
||
|
|
}
|
||
|
|
if (name === 'node:child_process') return {
|
||
|
|
execFileSync(command, args) {
|
||
|
|
assert.equal(command, 'isolated-node')
|
||
|
|
assert.equal(path.basename(args[0]), 'recover_wiki_deadletters.cjs')
|
||
|
|
commands.push(args.slice(1))
|
||
|
|
inspected = reports[commands.length - 1]
|
||
|
|
assert.ok(inspected, 'Unexpected recovery command')
|
||
|
|
return JSON.stringify({ report_file: reportFile })
|
||
|
|
},
|
||
|
|
spawn() { throw new Error('Unexpected subprocess') },
|
||
|
|
}
|
||
|
|
if (name === './wiki_recovery_scheduler_policy.cjs') return require(name)
|
||
|
|
if (name === 'node:path' || name === 'node:assert/strict') return require(name)
|
||
|
|
throw new Error(`Unexpected runner dependency: ${name}`)
|
||
|
|
},
|
||
|
|
}
|
||
|
|
vm.runInNewContext(fs.readFileSync(context.__filename, 'utf8'), context)
|
||
|
|
return { states, commands, unlocked }
|
||
|
|
}
|
||
|
|
|
||
|
|
test('actual runner stops before apply when a new result lacks source proof', () => {
|
||
|
|
const fixture = runIsolatedScheduler([report([...legacy, { ...current, source_read_complete: false }])])
|
||
|
|
assert.equal(fixture.commands.length, 1)
|
||
|
|
assert.equal(fixture.commands[0].length, 0)
|
||
|
|
assert.equal(fixture.states.at(-1).state, 'stopped_for_review')
|
||
|
|
assert.deepEqual(fixture.states.at(-1).missing_full_read_ids, [10])
|
||
|
|
assert.equal(fixture.unlocked, true)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('actual runner queues only an untouched original and preserves historical debt on completion', () => {
|
||
|
|
const next = { ...current, dead_letter_id: 1, attempt_number: 1, pending_op_id: 10000 }
|
||
|
|
const initial = { ...report([...legacy, current], 1), candidates: [
|
||
|
|
{ id: 2, eligible: true, chunks: 100, pending: 0 },
|
||
|
|
{ id: 10, eligible: true, chunks: 624, pending: 0 },
|
||
|
|
{ id: 1, eligible: true, chunks: 100, pending: 0 },
|
||
|
|
] }
|
||
|
|
const complete = report([...legacy, current, next], 0)
|
||
|
|
const fixture = runIsolatedScheduler([initial, complete, complete])
|
||
|
|
assert.deepEqual(fixture.commands.map(args => Array.from(args)), [[], ['--apply', '--ids=1', '--review-file=/reviewed/dead-letter-1-attempt-1.json'], []])
|
||
|
|
assert.equal(fixture.states.at(-1).state, 'all_original_pages_recovered')
|
||
|
|
assert.equal(fixture.states.at(-1).source_reading_complete, false)
|
||
|
|
assert.equal(fixture.states.at(-1).historical_full_read_unverified_ids.length, 11)
|
||
|
|
assert.equal(fixture.states.at(-1).newly_enqueued, 1)
|
||
|
|
assert.equal(fixture.unlocked, true)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('runner stops before enqueueing an untouched record without a durable review', () => {
|
||
|
|
const initial = { ...report([...legacy, current], 1), candidates: [{ id: 1, eligible: true, chunks: 100, pending: 0 }] }
|
||
|
|
const fixture = runIsolatedScheduler([initial], false)
|
||
|
|
assert.equal(fixture.commands.length, 1)
|
||
|
|
assert.equal(fixture.states.at(-1).state, 'stopped_for_review')
|
||
|
|
assert.match(fixture.states.at(-1).reason, /request limit/)
|
||
|
|
assert.equal(fixture.unlocked, true)
|
||
|
|
})
|