mirror of
https://github.com/stablyai/orca.git
synced 2026-10-12 08:02:37 +00:00
test(ai-vault-search): prove the three conclusions a broken pass must not reach
Three round-10 mutations survived the first pass of tests, all of them the same shape: a pass that failed still reached a verdict, and nothing checked. The drop reset moves into the sweep itself, where a test holding the store can overflow the queue and watch a completed sweep clear it; from the indexer the call was unreachable without twenty thousand files. A sweep whose held-file read throws now has a test that the failure travels rather than being folded into an empty list, and a sweep that threw part way has one that the flag saying a sweep is owed comes back.
This commit is contained in:
@@ -12,7 +12,7 @@ import {
|
||||
type SessionSearchIndexerHarness
|
||||
} from './session-search-indexer-test-fixture'
|
||||
import { SessionSearchIndexingStatus } from './session-search-indexing-status'
|
||||
import { SessionSearchStore } from './session-search-store'
|
||||
import { STALE_PATH_LIMIT, SessionSearchStore } from './session-search-store'
|
||||
|
||||
let harness: SessionSearchIndexerHarness
|
||||
let store: SessionSearchStore
|
||||
@@ -117,3 +117,44 @@ it('reports the real roots it listed transcripts under', async () => {
|
||||
const result = await sweep()
|
||||
expect([...result.rootsWithFiles]).toEqual([harness.roots.claudeProjectsDir])
|
||||
})
|
||||
|
||||
// A drop means the queue is knowingly missing something. The sweep is what
|
||||
// makes that stop being true, because it re-enumerates every root; left as a
|
||||
// lifetime tally the count described an incompleteness that had been resolved
|
||||
// minutes earlier and never returned to zero for the life of the process.
|
||||
it('forgets the queue drops it just re-enumerated past', async () => {
|
||||
await writeTranscripts(1)
|
||||
for (let index = 0; index < STALE_PATH_LIMIT + 3; index++) {
|
||||
store.markStale({
|
||||
agent: 'claude',
|
||||
codexHome: null,
|
||||
file: {
|
||||
path: join(harness.claudeProjectDir, `dropped-${index}.jsonl`),
|
||||
mtimeMs: index,
|
||||
modifiedAt: new Date(index).toISOString(),
|
||||
sizeBytes: 1
|
||||
}
|
||||
})
|
||||
}
|
||||
expect(store.droppedPendingFileCount).toBe(3)
|
||||
|
||||
await sweep()
|
||||
|
||||
expect(store.droppedPendingFileCount).toBe(0)
|
||||
})
|
||||
|
||||
// A sweep decides which rows nothing rediscovered by reading the whole file
|
||||
// table. A handle that cannot answer that must not be read as "nothing is
|
||||
// missing": an empty list is the one conclusion an unreadable handle cannot
|
||||
// earn, so the failure travels rather than being folded into a verdict.
|
||||
it('refuses to conclude anything from a sweep whose held-file read failed', async () => {
|
||||
await writeTranscripts(2)
|
||||
await sweep()
|
||||
expect(store.indexedSources()).toHaveLength(2)
|
||||
|
||||
store.indexedSources = () => {
|
||||
throw new Error('database is not open')
|
||||
}
|
||||
|
||||
await expect(sweep()).rejects.toThrow('database is not open')
|
||||
})
|
||||
|
||||
@@ -195,6 +195,12 @@ export async function runSessionSearchBackfill(
|
||||
// Only a sweep that finished publishes: an aborted one saw part of the
|
||||
// machine, and its empty findings would clear a live alarm.
|
||||
status.setDegradedRoots(degradedRoots)
|
||||
// A drop says the queue is knowingly missing something. This pass just
|
||||
// re-enumerated every root, so anything still owed is back on the queue
|
||||
// and a drop from before it no longer describes the queue. What this
|
||||
// sweep hands back, and the queue cannot hold, is counted again: the
|
||||
// caller re-queues it after this returns.
|
||||
store.forgetDroppedPending()
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -439,6 +439,40 @@ it('retires a transcript deleted between a sweep and the cycle after it', async
|
||||
expect(sessionsMatching('surviving')).toEqual([OTHER_SESSION_ID])
|
||||
})
|
||||
|
||||
// Round 10, M2. A sweep that throws part way learned nothing, and the flag that
|
||||
// says one is owed was taken on entry. Losing it there leaves nothing armed to
|
||||
// try again, so the machine outside the recency window goes unread until
|
||||
// something else happens to ask for a sweep.
|
||||
it('keeps a sweep due when the one that was running threw', async () => {
|
||||
const older = transcriptPath(OTHER_SESSION_ID)
|
||||
await writeClaudeTranscript(older, ['an older conversation'], OTHER_SESSION_ID)
|
||||
const yesterday = new Date(Date.now() - 86_400_000)
|
||||
await utimes(older, yesterday, yesterday)
|
||||
await writeClaudeTranscript(transcriptPath(), ['the newest conversation'], SESSION_ID)
|
||||
|
||||
// Newest-one per root, so only a sweep can reach the older file. The clock is
|
||||
// read inside the pass, which is where a failure part way through lands.
|
||||
newIndexer({ recentPerAgent: 1 })
|
||||
let thrown = false
|
||||
clock.onNow = () => {
|
||||
if (thrown || indexedSessionCount() === 0) {
|
||||
return
|
||||
}
|
||||
thrown = true
|
||||
throw new Error('the sweep fell over')
|
||||
}
|
||||
await indexer?.start()
|
||||
await indexer?.settled()
|
||||
clock.onNow = null
|
||||
|
||||
expect(errors.map((error) => (error as Error).message)).toEqual(['the sweep fell over'])
|
||||
expect(sessionsMatching('older')).toEqual([])
|
||||
|
||||
// The pass after it is a sweep, not a cycle: a cycle reads one file per root.
|
||||
await nextCycle()
|
||||
expect(sessionsMatching('older')).toEqual([OTHER_SESSION_ID])
|
||||
})
|
||||
|
||||
// Round 10, M1. A transcript the reader cannot open is recorded stale by the
|
||||
// consumer on every attempt, so it was re-read every cycle for ever: pending
|
||||
// stuck at one, a failure count climbing without bound, and a phase that never
|
||||
|
||||
@@ -243,12 +243,6 @@ export class SessionSearchIndexer {
|
||||
signal
|
||||
})
|
||||
this.indexingStatus.sweepFinished(sweep.completed)
|
||||
if (sweep.completed) {
|
||||
// Before the requeue, not after: this sweep re-enumerated every root, so
|
||||
// a drop from before it no longer means anything is missing, while what
|
||||
// this sweep hands back and the queue cannot hold does.
|
||||
this.store.forgetDroppedPending()
|
||||
}
|
||||
this.requeue(sweep.deferred)
|
||||
if (!sweep.completed) {
|
||||
// A sweep stays due until one finishes: an aborted one saw part of the
|
||||
|
||||
Reference in New Issue
Block a user