|
|
@@ -141,9 +141,6 @@ async function seedRecord(
|
|
|
await writeFile(path, JSON.stringify({ version: projectionCacheDomainSpec.version, record: { identity, rows } }))
|
|
|
}
|
|
|
|
|
|
-/** Wait until queued fail-soft writes (event-listener fire-and-forget over real fs I/O) drain. */
|
|
|
-const settle = () => new Promise(resolve => setTimeout(resolve, 40))
|
|
|
-
|
|
|
afterEach(async () => {
|
|
|
vi.useRealTimers()
|
|
|
await Promise.all(contexts.splice(0).map(ctx => ctx.fiber.dispose()))
|
|
|
@@ -157,12 +154,14 @@ describe('SessionProjectionCache write policy', () => {
|
|
|
mark(session, ['a'])
|
|
|
// Creation already wrote the init cut; the mark is throttled, so the
|
|
|
// stored row is still the creation-time cut (no marks folded).
|
|
|
- await settle()
|
|
|
- expect((await storedRows(root, session.id))?.['cache-test/marks']?.seq).toBe(-1)
|
|
|
+ await vi.waitFor(async () => {
|
|
|
+ expect((await storedRows(root, session.id))?.['cache-test/marks']?.seq).toBe(-1)
|
|
|
+ }, { timeout: 5_000 })
|
|
|
const end = endTurn(session)
|
|
|
- await settle()
|
|
|
- const rows = await storedRows(root, session.id)
|
|
|
- expect(rows?.['cache-test/marks']).toEqual({ ver: 1, seq: end.seq, val: { marks: ['a'] } })
|
|
|
+ await vi.waitFor(async () => {
|
|
|
+ expect((await storedRows(root, session.id))?.['cache-test/marks'])
|
|
|
+ .toEqual({ ver: 1, seq: end.seq, val: { marks: ['a'] } })
|
|
|
+ }, { timeout: 5_000 })
|
|
|
})
|
|
|
|
|
|
it('writes a checkpoint at session creation, capturing the seed-derived cut', async () => {
|
|
|
@@ -173,9 +172,10 @@ describe('SessionProjectionCache write policy', () => {
|
|
|
const session = ctx.sessions.create(SessionId('seeded'), {
|
|
|
seed: [{ type: 'cache-test/mark', seq: 0, time: 1, data: { marks: ['seed'] } }] as SessionEvent[],
|
|
|
})
|
|
|
- await settle()
|
|
|
- expect((await storedRows(root, session.id))?.['cache-test/marks']?.val)
|
|
|
- .toEqual({ marks: ['seed'] })
|
|
|
+ await vi.waitFor(async () => {
|
|
|
+ expect((await storedRows(root, session.id))?.['cache-test/marks']?.val)
|
|
|
+ .toEqual({ marks: ['seed'] })
|
|
|
+ }, { timeout: 5_000 })
|
|
|
})
|
|
|
|
|
|
it('writes at session disposal (detach, the live-to-cold moment)', async () => {
|
|
|
@@ -188,8 +188,10 @@ describe('SessionProjectionCache write policy', () => {
|
|
|
if (session === undefined) throw new Error('session was not created')
|
|
|
mark(session, ['live'])
|
|
|
await owner.dispose()
|
|
|
- await settle()
|
|
|
- expect((await storedRows(root, session.id))?.['cache-test/marks']?.val).toEqual({ marks: ['live'] })
|
|
|
+ const detached = session
|
|
|
+ await vi.waitFor(async () => {
|
|
|
+ expect((await storedRows(root, detached.id))?.['cache-test/marks']?.val).toEqual({ marks: ['live'] })
|
|
|
+ }, { timeout: 5_000 })
|
|
|
})
|
|
|
|
|
|
it('flushes when the in-turn event count reaches the configured threshold', async () => {
|
|
|
@@ -197,11 +199,13 @@ describe('SessionProjectionCache write policy', () => {
|
|
|
const session = ctx.sessions.create(SessionId('count'))
|
|
|
mark(session, ['1'])
|
|
|
mark(session, ['2'])
|
|
|
- await settle()
|
|
|
- expect((await storedRows(root, session.id))?.['cache-test/marks']?.seq).toBe(-1) // still the creation cut
|
|
|
+ await vi.waitFor(async () => {
|
|
|
+ expect((await storedRows(root, session.id))?.['cache-test/marks']?.seq).toBe(-1) // still the creation cut
|
|
|
+ }, { timeout: 5_000 })
|
|
|
mark(session, ['3'])
|
|
|
- await settle()
|
|
|
- expect((await storedRows(root, session.id))?.['cache-test/marks']?.val).toEqual({ marks: ['3'] })
|
|
|
+ await vi.waitFor(async () => {
|
|
|
+ expect((await storedRows(root, session.id))?.['cache-test/marks']?.val).toEqual({ marks: ['3'] })
|
|
|
+ }, { timeout: 5_000 })
|
|
|
})
|
|
|
|
|
|
it('flushes on the configured interval when the count threshold is not reached', async () => {
|
|
|
@@ -270,15 +274,22 @@ describe('SessionProjectionCache write policy', () => {
|
|
|
const session = ctx.sessions.create(SessionId('fail-soft'))
|
|
|
mark(session, ['x'])
|
|
|
endTurn(session)
|
|
|
- await settle()
|
|
|
- expect(await storedRows(root, session.id)).toBeUndefined()
|
|
|
- expect(warn).toHaveBeenCalledWith(expect.stringContaining('turn/end write for "fail-soft" failed'))
|
|
|
+ // The failed creation/turn-end writes are fire-and-forget: wait for the
|
|
|
+ // warn (the write actually failed), then assert no row landed — the
|
|
|
+ // property under test is that a failed write leaves no partial row.
|
|
|
+ await vi.waitFor(() => {
|
|
|
+ expect(warn).toHaveBeenCalledWith(expect.stringContaining('turn/end write for "fail-soft" failed'))
|
|
|
+ }, { timeout: 5_000 })
|
|
|
+ await vi.waitFor(async () => {
|
|
|
+ expect(await storedRows(root, session.id)).toBeUndefined()
|
|
|
+ }, { timeout: 5_000 })
|
|
|
// Self-heal: once the blocker clears, the next mandatory point writes.
|
|
|
await rm(recordPath(root, session.id), { recursive: true })
|
|
|
mark(session, ['y'])
|
|
|
endTurn(session)
|
|
|
- await settle()
|
|
|
- expect((await storedRows(root, session.id))?.['cache-test/marks']?.val).toEqual({ marks: ['y'] })
|
|
|
+ await vi.waitFor(async () => {
|
|
|
+ expect((await storedRows(root, session.id))?.['cache-test/marks']?.val).toEqual({ marks: ['y'] })
|
|
|
+ }, { timeout: 5_000 })
|
|
|
})
|
|
|
})
|
|
|
|
|
|
@@ -473,7 +484,10 @@ describe('SessionProjectionCache cold-read seeding', () => {
|
|
|
const meta = headerOf(SessionId('cold-fail'))
|
|
|
await mkdir(recordPath(root, meta.id), { recursive: true })
|
|
|
expect(ctx.sessionProjectionCache.coldSnapshot(meta, [])).toBeDefined()
|
|
|
- await settle()
|
|
|
- expect(warn).toHaveBeenCalledWith(expect.stringContaining('cold-read write-back for "cold-fail" failed'))
|
|
|
+ // The failed write-back is fire-and-forget: poll for the warn instead of
|
|
|
+ // assuming a fixed settle window (slow runners exceed it).
|
|
|
+ await vi.waitFor(() => {
|
|
|
+ expect(warn).toHaveBeenCalledWith(expect.stringContaining('cold-read write-back for "cold-fail" failed'))
|
|
|
+ }, { timeout: 5_000 })
|
|
|
})
|
|
|
})
|