Files
orca/src/main/codex/codex-session-migration-scheduler.test.ts
T
Neil 015f904fca fix(codex): stop re-scanning all Codex session history on every launch (#16251) (#16593)
* fix(codex): stop re-scanning all Codex session history on every launch (#16251)

A launch deleted the backfill completion marker, and a marker could never
be written while a Codex pane was open, so every launch re-derived
"needs full scan" and walked the entire .codex/sessions tree — on Windows
with a large history that read as a hung window.

- v4 marker keeps a durable full-history baseline plus a bounded set of
  pending dates. v3 is read as a baseline, so upgrades pay no full scan.
- A launch now marks dates pending instead of deleting the marker, and a
  full pass certifies the baseline even while a pane is still running;
  the live pane's own date just stays pending.
- Pending dates are persisted, so an abnormal exit or a cross-midnight
  pane recovers a bounded window instead of a full walk.
- A date-limited pass can only extend an existing baseline, never create
  one, so it can no longer certify history it never looked at.
- Marker and index-heal target roots compare through
  normalizeRuntimePathForComparison, so Windows spellings of one
  directory stop invalidating each other.
- Both append-only ledgers stream instead of readFileSync + whole-file
  JSON.parse, keeping the main thread responsive on large histories.

* fix(codex): keep the backfill marker's full-scan demand durable

Review follow-ups on the v4 backfill marker:

- markCodexSessionBackfillMarkerPending no longer erases a persisted
  needsFullScan; the demand survives until a generation-current full walk
  retires it, and the function now reports it so the launch path folds it
  into its own in-memory flag (as @rumoii's #16252 does).
- A full pass settles the whole pending set instead of subtracting the
  empty set, so a date a full walk provably covered stops forcing an extra
  bounded pass on every startup.
- isCodexSessionBackfillDate does a real calendar check, so a corrupted
  marker cannot carry 2026/99/99. No age or future bound: the same guard
  gates rollout publication and a clock-skewed directory holds real
  sessions.
- 'scans only the current date once a baseline exists' now has a second
  date directory, so it fails on a full walk instead of passing either way.
2026-08-26 15:42:54 -07:00

761 lines
27 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { CodexSessionBackfillOptions } from './codex-session-backfill-types'
import { createCodexSessionMigrationScheduler } from './codex-session-migration-scheduler'
describe('createCodexSessionMigrationScheduler', () => {
beforeEach(() => {
vi.useFakeTimers()
})
it('runs after a managed-account startup switches to host system default', async () => {
let eligible = false
const prepareScheduledRun = vi.fn()
const startBackfill = vi.fn().mockResolvedValue({ stopped: false })
const startIndexHeal = vi.fn().mockResolvedValue(null)
const scheduler = createCodexSessionMigrationScheduler({
isEligible: () => eligible,
isQuitting: () => false,
resolveSystemCodexHomePathOverride: () => undefined,
prepareScheduledRun,
startBackfill,
startIndexHeal
})
scheduler.scheduleInitialRun()
await vi.advanceTimersByTimeAsync(15_000)
expect(startBackfill).not.toHaveBeenCalled()
eligible = true
scheduler.requestRun()
await vi.waitFor(() => expect(startIndexHeal).toHaveBeenCalledOnce())
expect(startBackfill).toHaveBeenCalledOnce()
expect(prepareScheduledRun).not.toHaveBeenCalled()
})
it('schedules a delayed rerun after a shared-home launch', async () => {
vi.setSystemTime(new Date(2026, 7, 5, 10, 0, 0))
const prepareScheduledRun = vi.fn()
const finishScheduledRun = vi.fn()
const startBackfill = vi.fn().mockResolvedValue({ stopped: false })
const startIndexHeal = vi.fn().mockResolvedValue(null)
const scheduler = createCodexSessionMigrationScheduler({
isEligible: () => true,
isQuitting: () => false,
resolveSystemCodexHomePathOverride: () => undefined,
prepareScheduledRun,
finishScheduledRun,
startBackfill,
startIndexHeal,
initialDelayMs: 1_000
})
scheduler.scheduleRun()
scheduler.scheduleRun()
await vi.advanceTimersByTimeAsync(999)
expect(startBackfill).not.toHaveBeenCalled()
await vi.advanceTimersByTimeAsync(1)
await vi.waitFor(() => expect(startIndexHeal).toHaveBeenCalledOnce())
expect(startBackfill).toHaveBeenCalledOnce()
expect(startBackfill).toHaveBeenCalledWith(
expect.objectContaining({
scanDates: [['2026', '08', '05']],
ignoreCompletionMarker: true
}),
undefined
)
expect(prepareScheduledRun).toHaveBeenCalledOnce()
await vi.waitFor(() => expect(finishScheduledRun).toHaveBeenCalledOnce())
})
it('covers both launch and run dates when a delayed pass crosses midnight', async () => {
vi.setSystemTime(new Date('2026-08-05T23:59:59.500Z'))
const startBackfill = vi.fn().mockResolvedValue({ stopped: false })
const scheduler = createCodexSessionMigrationScheduler({
isEligible: () => true,
isQuitting: () => false,
resolveSystemCodexHomePathOverride: () => undefined,
startBackfill,
startIndexHeal: vi.fn().mockResolvedValue(null),
initialDelayMs: 1_000
})
scheduler.scheduleRun()
await vi.advanceTimersByTimeAsync(1_000)
expect(startBackfill).toHaveBeenCalledWith(
expect.objectContaining({
scanDates: [
['2026', '08', '05'],
['2026', '08', '06']
]
}),
undefined
)
})
it('publishes the baseline while a Codex pane is still open', async () => {
vi.setSystemTime(new Date('2026-08-05T10:00:00Z'))
const prepareScheduledRun = vi.fn()
const startBackfill = vi.fn().mockResolvedValue({ stopped: false })
const scheduler = createCodexSessionMigrationScheduler({
isEligible: () => true,
isQuitting: () => false,
resolveSystemCodexHomePathOverride: () => undefined,
prepareScheduledRun,
startBackfill,
startIndexHeal: vi.fn().mockResolvedValue(null),
initialDelayMs: 1_000
})
scheduler.beginLaunch('pty-1')
await vi.advanceTimersByTimeAsync(1_000)
await vi.waitFor(() => expect(startBackfill).toHaveBeenCalledOnce())
const options = startBackfill.mock.calls[0]?.[0]
// The open pane only holds its own date pending; publication is not blocked.
expect(options?.retainPendingScanDates).toBe(true)
expect(options?.canWriteCompletionMarker?.()).toBe(true)
// Preparation is handed the dates so it can persist them before the walk.
expect(prepareScheduledRun).toHaveBeenCalledWith([['2026', '08', '05']])
})
it('hands preparation every date a cross-midnight launch spanned', async () => {
vi.setSystemTime(new Date('2026-08-05T23:59:59Z'))
const prepareScheduledRun = vi.fn()
const scheduler = createCodexSessionMigrationScheduler({
isEligible: () => true,
isQuitting: () => false,
resolveSystemCodexHomePathOverride: () => undefined,
prepareScheduledRun,
startBackfill: vi.fn().mockResolvedValue({ stopped: false }),
startIndexHeal: vi.fn().mockResolvedValue(null),
initialDelayMs: 1_000
})
scheduler.beginLaunch('pty-1')
vi.setSystemTime(new Date('2026-08-07T01:00:00Z'))
scheduler.finishLaunch('pty-1')
await vi.advanceTimersByTimeAsync(1_000)
expect(prepareScheduledRun).toHaveBeenLastCalledWith([
['2026', '08', '05'],
['2026', '08', '06'],
['2026', '08', '07']
])
})
it('keeps launch passes full when no completed baseline can cover older failures', async () => {
const startBackfill = vi.fn().mockResolvedValue({ stopped: false })
const scheduler = createCodexSessionMigrationScheduler({
isEligible: () => true,
isQuitting: () => false,
resolveSystemCodexHomePathOverride: () => undefined,
startBackfill,
startIndexHeal: vi.fn().mockResolvedValue(null),
initialDelayMs: 1_000
})
scheduler.scheduleRun(true)
await vi.advanceTimersByTimeAsync(1_000)
expect(startBackfill).toHaveBeenCalledWith(
expect.objectContaining({ scanDates: undefined }),
undefined
)
})
it('upgrades a bounded pass when its target changed before the timer fired', async () => {
const startBackfill = vi.fn().mockResolvedValue({ stopped: false })
const scheduler = createCodexSessionMigrationScheduler({
isEligible: () => true,
isQuitting: () => false,
resolveSystemCodexHomePathOverride: () => '/moved-history',
prepareScheduledRun: () => true,
startBackfill,
startIndexHeal: vi.fn().mockResolvedValue(null),
initialDelayMs: 1_000
})
scheduler.scheduleRun()
await vi.advanceTimersByTimeAsync(1_000)
expect(startBackfill).toHaveBeenCalledWith(
expect.objectContaining({ scanDates: undefined }),
'/moved-history'
)
})
it('delays the startup run from the latest shared-home launch', async () => {
const prepareScheduledRun = vi.fn()
const startBackfill = vi.fn().mockResolvedValue({ stopped: false })
const startIndexHeal = vi.fn().mockResolvedValue(null)
const scheduler = createCodexSessionMigrationScheduler({
isEligible: () => true,
isQuitting: () => false,
resolveSystemCodexHomePathOverride: () => undefined,
prepareScheduledRun,
startBackfill,
startIndexHeal,
initialDelayMs: 1_000
})
scheduler.scheduleInitialRun()
await vi.advanceTimersByTimeAsync(999)
scheduler.scheduleRun()
await vi.advanceTimersByTimeAsync(1)
expect(startBackfill).not.toHaveBeenCalled()
await vi.advanceTimersByTimeAsync(999)
await vi.waitFor(() => expect(startIndexHeal).toHaveBeenCalledOnce())
expect(startBackfill).toHaveBeenCalledOnce()
expect(prepareScheduledRun).toHaveBeenCalledOnce()
})
it('preserves a delayed launch rerun while an earlier migration is active', async () => {
let releaseFirstIndexHeal: (() => void) | undefined
const prepareScheduledRun = vi.fn()
const startBackfill = vi.fn().mockResolvedValue({ stopped: false })
const startIndexHeal = vi
.fn()
.mockImplementationOnce(
() =>
new Promise<void>((resolve) => {
releaseFirstIndexHeal = resolve
})
)
.mockResolvedValueOnce(null)
const scheduler = createCodexSessionMigrationScheduler({
isEligible: () => true,
isQuitting: () => false,
resolveSystemCodexHomePathOverride: () => undefined,
prepareScheduledRun,
startBackfill,
startIndexHeal,
initialDelayMs: 1_000
})
scheduler.requestRun()
await vi.waitFor(() => expect(startIndexHeal).toHaveBeenCalledOnce())
scheduler.scheduleRun()
await vi.advanceTimersByTimeAsync(1_000)
expect(startBackfill).toHaveBeenCalledOnce()
expect(prepareScheduledRun).not.toHaveBeenCalled()
releaseFirstIndexHeal?.()
await vi.waitFor(() => expect(startBackfill).toHaveBeenCalledTimes(2))
await vi.waitFor(() => expect(startIndexHeal).toHaveBeenCalledTimes(2))
expect(prepareScheduledRun).toHaveBeenCalledOnce()
expect(prepareScheduledRun.mock.invocationCallOrder[0]).toBeLessThan(
startBackfill.mock.invocationCallOrder[1]!
)
})
it('prepares a delayed launch pass after an earlier migration settles before the timer', async () => {
let releaseFirstBackfill: (() => void) | undefined
let markerPresent = false
const prepareScheduledRun = vi.fn(() => {
markerPresent = false
})
const startBackfill = vi
.fn()
.mockImplementationOnce(
() =>
new Promise<void>((resolve) => {
releaseFirstBackfill = () => {
markerPresent = true
resolve()
}
})
)
.mockImplementationOnce(async () => {
expect(markerPresent).toBe(false)
})
const startIndexHeal = vi.fn().mockResolvedValue(null)
const scheduler = createCodexSessionMigrationScheduler({
isEligible: () => true,
isQuitting: () => false,
resolveSystemCodexHomePathOverride: () => undefined,
prepareScheduledRun,
startBackfill,
startIndexHeal,
initialDelayMs: 1_000
})
scheduler.requestRun()
scheduler.scheduleRun()
releaseFirstBackfill?.()
await Promise.resolve()
await Promise.resolve()
await Promise.resolve()
expect(markerPresent).toBe(true)
expect(startIndexHeal).toHaveBeenCalledOnce()
await vi.advanceTimersByTimeAsync(1_000)
await vi.waitFor(() => expect(startBackfill).toHaveBeenCalledTimes(2))
expect(prepareScheduledRun).toHaveBeenCalledOnce()
})
it('coalesces concurrent run requests and stops before index heal after opt-out', async () => {
let eligible = true
let releaseBackfill: (() => void) | undefined
const startBackfill = vi.fn(
() =>
new Promise<void>((resolve) => {
releaseBackfill = resolve
})
)
const startIndexHeal = vi.fn().mockResolvedValue(null)
const scheduler = createCodexSessionMigrationScheduler({
isEligible: () => eligible,
isQuitting: () => false,
resolveSystemCodexHomePathOverride: () => '/custom/history',
startBackfill,
startIndexHeal
})
scheduler.requestRun()
scheduler.requestRun()
expect(startBackfill).toHaveBeenCalledOnce()
expect(startBackfill).toHaveBeenCalledWith(expect.any(Object), '/custom/history')
eligible = false
releaseBackfill?.()
await Promise.resolve()
await Promise.resolve()
expect(startIndexHeal).not.toHaveBeenCalled()
})
it('reruns after a stopping migration becomes eligible again', async () => {
let eligible = true
let releaseFirstBackfill: ((result: { stopped: boolean }) => void) | undefined
const startBackfill = vi
.fn()
.mockImplementationOnce(
(_options) =>
new Promise<{ stopped: boolean }>((resolve) => {
releaseFirstBackfill = resolve
})
)
.mockResolvedValueOnce({ stopped: false })
const startIndexHeal = vi.fn().mockResolvedValue(null)
const scheduler = createCodexSessionMigrationScheduler({
isEligible: () => eligible,
isQuitting: () => false,
resolveSystemCodexHomePathOverride: () => undefined,
startBackfill,
startIndexHeal
})
scheduler.requestRun()
const firstRunOptions = startBackfill.mock.calls[0]?.[0]
eligible = false
expect(firstRunOptions?.shouldStop()).toBe(true)
eligible = true
scheduler.requestRun()
releaseFirstBackfill?.({ stopped: true })
await vi.waitFor(() => expect(startBackfill).toHaveBeenCalledTimes(2))
await vi.waitFor(() => expect(startIndexHeal).toHaveBeenCalledOnce())
})
it('preserves scheduled identity and target recovery after a stopped pass', async () => {
let eligible = true
let target = '/old-history'
let releaseFirstBackfill: ((result: { stopped: boolean }) => void) | undefined
const prepareScheduledRun = vi.fn().mockReturnValueOnce(false).mockReturnValueOnce(true)
const finishScheduledRun = vi.fn()
const startBackfill = vi
.fn()
.mockImplementationOnce(
() =>
new Promise<{ stopped: boolean }>((resolve) => {
releaseFirstBackfill = resolve
})
)
.mockResolvedValueOnce({ stopped: false })
const scheduler = createCodexSessionMigrationScheduler({
isEligible: () => eligible,
isQuitting: () => false,
resolveSystemCodexHomePathOverride: () => target,
prepareScheduledRun,
finishScheduledRun,
startBackfill,
startIndexHeal: vi.fn().mockResolvedValue(null),
initialDelayMs: 1_000
})
scheduler.scheduleRun()
await vi.advanceTimersByTimeAsync(1_000)
const firstOptions = startBackfill.mock.calls[0]?.[0]
eligible = false
expect(firstOptions?.shouldStop()).toBe(true)
target = '/new-history'
releaseFirstBackfill?.({ stopped: true })
await vi.waitFor(() => expect(prepareScheduledRun).toHaveBeenCalledOnce())
expect(finishScheduledRun).not.toHaveBeenCalled()
eligible = true
scheduler.requestRun()
await vi.waitFor(() => expect(startBackfill).toHaveBeenCalledTimes(2))
expect(startBackfill).toHaveBeenLastCalledWith(
expect.objectContaining({ ignoreCompletionMarker: true, scanDates: undefined }),
'/new-history'
)
await vi.waitFor(() => expect(finishScheduledRun).toHaveBeenCalledOnce())
})
it('finishes a launch generation only after its PTY exits and every date is rescanned', async () => {
vi.setSystemTime(new Date('2026-08-05T23:59:59Z'))
const finishScheduledRun = vi.fn()
const startBackfill = vi.fn().mockResolvedValue({ stopped: false })
const scheduler = createCodexSessionMigrationScheduler({
isEligible: () => true,
isQuitting: () => false,
resolveSystemCodexHomePathOverride: () => undefined,
prepareScheduledRun: vi.fn(),
finishScheduledRun,
startBackfill,
startIndexHeal: vi.fn().mockResolvedValue(null),
initialDelayMs: 1_000
})
scheduler.beginLaunch('pty-1')
await vi.advanceTimersByTimeAsync(1_000)
await vi.waitFor(() => expect(startBackfill).toHaveBeenCalledOnce())
expect(startBackfill).toHaveBeenLastCalledWith(
expect.objectContaining({ retainPendingScanDates: true }),
undefined
)
expect(finishScheduledRun).not.toHaveBeenCalled()
vi.setSystemTime(new Date('2026-08-07T01:00:00Z'))
scheduler.finishLaunch('pty-1')
await vi.advanceTimersByTimeAsync(1_000)
await vi.waitFor(() => expect(startBackfill).toHaveBeenCalledTimes(2))
expect(startBackfill).toHaveBeenLastCalledWith(
expect.objectContaining({
scanDates: [
['2026', '08', '05'],
['2026', '08', '06'],
['2026', '08', '07']
],
ignoreCompletionMarker: true,
retainPendingScanDates: false,
fullScanRequired: false
}),
undefined
)
await vi.waitFor(() => expect(finishScheduledRun).toHaveBeenCalledOnce())
})
it('keeps a failed full scan required for the final launch pass', async () => {
const finishScheduledRun = vi.fn()
const startBackfill = vi
.fn()
.mockResolvedValueOnce({ stopped: false, failedFiles: 1 })
.mockResolvedValueOnce({ stopped: false, failedFiles: 0 })
const scheduler = createCodexSessionMigrationScheduler({
isEligible: () => true,
isQuitting: () => false,
resolveSystemCodexHomePathOverride: () => undefined,
prepareScheduledRun: () => false,
finishScheduledRun,
startBackfill,
startIndexHeal: vi.fn().mockResolvedValue(null),
initialDelayMs: 1_000
})
scheduler.beginLaunch('pty-1', true)
await vi.advanceTimersByTimeAsync(1_000)
await vi.waitFor(() => expect(startBackfill).toHaveBeenCalledOnce())
expect(startBackfill).toHaveBeenLastCalledWith(
expect.objectContaining({ scanDates: undefined, fullScanRequired: true }),
undefined
)
scheduler.finishLaunch('pty-1')
await vi.advanceTimersByTimeAsync(1_000)
await vi.waitFor(() => expect(startBackfill).toHaveBeenCalledTimes(2))
expect(startBackfill).toHaveBeenLastCalledWith(
expect.objectContaining({ scanDates: undefined, fullScanRequired: true }),
undefined
)
await vi.waitFor(() => expect(finishScheduledRun).toHaveBeenCalledOnce())
})
it('blocks marker publication when a newer launch pass is pending', async () => {
let releaseBackfill: ((result: { stopped: boolean }) => void) | undefined
const startBackfill = vi.fn(
(_options: CodexSessionBackfillOptions) =>
new Promise<{ stopped: boolean }>((resolve) => {
releaseBackfill = resolve
})
)
const startIndexHeal = vi.fn().mockResolvedValue(null)
const scheduler = createCodexSessionMigrationScheduler({
isEligible: () => true,
isQuitting: () => false,
resolveSystemCodexHomePathOverride: () => undefined,
startBackfill,
startIndexHeal,
initialDelayMs: 1_000
})
scheduler.scheduleRun()
await vi.advanceTimersByTimeAsync(1_000)
const firstOptions = startBackfill.mock.calls[0]?.[0]
expect(firstOptions?.canWriteCompletionMarker?.()).toBe(true)
scheduler.beginLaunch('pty-2')
expect(firstOptions?.canWriteCompletionMarker?.()).toBe(false)
scheduler.finishLaunch('pty-2')
expect(firstOptions?.canWriteCompletionMarker?.()).toBe(false)
releaseBackfill?.({ stopped: false })
await vi.waitFor(() => expect(startIndexHeal).toHaveBeenCalledOnce())
})
it('turns an exit-before-begin race into a full recovery pass', async () => {
const finishScheduledRun = vi.fn()
const startBackfill = vi.fn().mockResolvedValue({ stopped: false })
const scheduler = createCodexSessionMigrationScheduler({
isEligible: () => true,
isQuitting: () => false,
resolveSystemCodexHomePathOverride: () => undefined,
finishScheduledRun,
startBackfill,
startIndexHeal: vi.fn().mockResolvedValue(null),
initialDelayMs: 1_000
})
scheduler.finishLaunch('pty-fast-exit', 2)
scheduler.beginLaunch('pty-fast-exit', false, new Date(), 1)
await vi.advanceTimersByTimeAsync(1_000)
expect(startBackfill).toHaveBeenCalledWith(
expect.objectContaining({ scanDates: undefined, fullScanRequired: true }),
undefined
)
await vi.waitFor(() => expect(finishScheduledRun).toHaveBeenCalledOnce())
})
it('keeps an ignored reattach exit from poisoning a same-ID launch already starting', async () => {
const startBackfill = vi.fn().mockResolvedValue({ stopped: false })
const scheduler = createCodexSessionMigrationScheduler({
isEligible: () => true,
isQuitting: () => false,
resolveSystemCodexHomePathOverride: () => undefined,
startBackfill,
startIndexHeal: vi.fn().mockResolvedValue(null),
initialDelayMs: 1_000
})
scheduler.ignoreLaunch('stable-pty', 1)
scheduler.finishLaunch('stable-pty', 3)
scheduler.beginLaunch('stable-pty', false, new Date(), 2)
await vi.advanceTimersByTimeAsync(1_000)
expect(startBackfill).toHaveBeenCalledWith(
expect.objectContaining({
scanDates: expect.any(Array),
retainPendingScanDates: true,
fullScanRequired: false
}),
undefined
)
})
it('matches an ignored reattach exit that arrived before its lifecycle callback', async () => {
const startBackfill = vi.fn().mockResolvedValue({ stopped: false })
const scheduler = createCodexSessionMigrationScheduler({
isEligible: () => true,
isQuitting: () => false,
resolveSystemCodexHomePathOverride: () => undefined,
startBackfill,
startIndexHeal: vi.fn().mockResolvedValue(null),
initialDelayMs: 1_000
})
scheduler.finishLaunch('stable-pty', 2)
scheduler.ignoreLaunch('stable-pty', 1)
scheduler.beginLaunch('stable-pty', false, new Date(), 3)
await vi.advanceTimersByTimeAsync(1_000)
expect(startBackfill).toHaveBeenCalledWith(
expect.objectContaining({
scanDates: expect.any(Array),
retainPendingScanDates: true,
fullScanRequired: false
}),
undefined
)
})
it('keeps a newer same-ID launch active while the ignored incarnation exits', async () => {
const startBackfill = vi.fn().mockResolvedValue({ stopped: false })
const scheduler = createCodexSessionMigrationScheduler({
isEligible: () => true,
isQuitting: () => false,
resolveSystemCodexHomePathOverride: () => undefined,
startBackfill,
startIndexHeal: vi.fn().mockResolvedValue(null),
initialDelayMs: 1_000
})
scheduler.ignoreLaunch('stable-pty', 1)
scheduler.beginLaunch('stable-pty', false, new Date(), 2)
scheduler.finishLaunch('stable-pty', 3)
await vi.advanceTimersByTimeAsync(1_000)
expect(startBackfill).toHaveBeenCalledWith(
expect.objectContaining({ retainPendingScanDates: true }),
undefined
)
scheduler.finishLaunch('stable-pty', 4)
await vi.advanceTimersByTimeAsync(1_000)
expect(startBackfill).toHaveBeenLastCalledWith(
expect.objectContaining({ retainPendingScanDates: false }),
undefined
)
})
it('releases a stranded active launch once the ignored incarnation ages out', async () => {
const startBackfill = vi.fn().mockResolvedValue({ stopped: false })
const scheduler = createCodexSessionMigrationScheduler({
isEligible: () => true,
isQuitting: () => false,
resolveSystemCodexHomePathOverride: () => undefined,
startBackfill,
startIndexHeal: vi.fn().mockResolvedValue(null),
initialDelayMs: 1_000
})
// The ignored incarnation never reports its exit, so only the newer launch ever finishes.
scheduler.ignoreLaunch('stable-pty', 1)
scheduler.beginLaunch('stable-pty', false, new Date(), 2)
await vi.advanceTimersByTimeAsync(61_000)
expect(startBackfill).toHaveBeenLastCalledWith(
expect.objectContaining({ retainPendingScanDates: true }),
undefined
)
scheduler.finishLaunch('stable-pty', 3)
await vi.advanceTimersByTimeAsync(1_000)
expect(startBackfill).toHaveBeenLastCalledWith(
expect.objectContaining({ retainPendingScanDates: false }),
undefined
)
})
it('keeps late duplicate ignored callbacks from poisoning reuse', async () => {
const startBackfill = vi.fn().mockResolvedValue({ stopped: false })
const scheduler = createCodexSessionMigrationScheduler({
isEligible: () => true,
isQuitting: () => false,
resolveSystemCodexHomePathOverride: () => undefined,
startBackfill,
startIndexHeal: vi.fn().mockResolvedValue(null),
initialDelayMs: 1_000
})
scheduler.ignoreLaunch('stable-pty', 1)
scheduler.finishLaunch('stable-pty', 3)
scheduler.ignoreLaunch('stable-pty', 2)
scheduler.ignoreLaunch('stable-pty', 2)
scheduler.beginLaunch('stable-pty', false, new Date(), 4)
await vi.advanceTimersByTimeAsync(1_000)
expect(startBackfill).toHaveBeenCalledTimes(1)
scheduler.finishLaunch('stable-pty', 5)
await vi.advanceTimersByTimeAsync(1_000)
expect(startBackfill).toHaveBeenCalledTimes(2)
expect(startBackfill).toHaveBeenLastCalledWith(
expect.objectContaining({ retainPendingScanDates: false }),
undefined
)
})
it('keeps three exit-before-callback reattaches from stranding a reused ID', async () => {
const startBackfill = vi.fn().mockResolvedValue({ stopped: false })
const scheduler = createCodexSessionMigrationScheduler({
isEligible: () => true,
isQuitting: () => false,
resolveSystemCodexHomePathOverride: () => undefined,
startBackfill,
startIndexHeal: vi.fn().mockResolvedValue(null),
initialDelayMs: 1_000
})
scheduler.finishLaunch('stable-pty', 4)
scheduler.ignoreLaunch('stable-pty', 1)
scheduler.ignoreLaunch('stable-pty', 2)
scheduler.ignoreLaunch('stable-pty', 3)
scheduler.beginLaunch('stable-pty', false, new Date(), 5)
await vi.advanceTimersByTimeAsync(1_000)
expect(startBackfill).toHaveBeenLastCalledWith(
expect.objectContaining({ retainPendingScanDates: true }),
undefined
)
scheduler.finishLaunch('stable-pty', 6)
await vi.advanceTimersByTimeAsync(1_000)
expect(startBackfill).toHaveBeenCalledTimes(2)
expect(startBackfill).toHaveBeenLastCalledWith(
expect.objectContaining({ retainPendingScanDates: false }),
undefined
)
})
it('does not consume an exit from an earlier stable-id incarnation', async () => {
const startBackfill = vi.fn().mockResolvedValue({ stopped: false })
const scheduler = createCodexSessionMigrationScheduler({
isEligible: () => true,
isQuitting: () => false,
resolveSystemCodexHomePathOverride: () => undefined,
startBackfill,
startIndexHeal: vi.fn().mockResolvedValue(null),
initialDelayMs: 1_000
})
scheduler.finishLaunch('stable-pty', 1)
scheduler.beginLaunch('stable-pty', false, new Date(), 2)
await vi.advanceTimersByTimeAsync(1_000)
expect(startBackfill).toHaveBeenCalledWith(
expect.objectContaining({ retainPendingScanDates: true }),
undefined
)
})
it('preserves the pre-spawn UTC date when launch setup crosses midnight', async () => {
vi.setSystemTime(new Date('2026-08-06T00:00:01Z'))
const startBackfill = vi.fn().mockResolvedValue({ stopped: false })
const scheduler = createCodexSessionMigrationScheduler({
isEligible: () => true,
isQuitting: () => false,
resolveSystemCodexHomePathOverride: () => undefined,
startBackfill,
startIndexHeal: vi.fn().mockResolvedValue(null),
initialDelayMs: 1_000
})
scheduler.beginLaunch('pty-midnight', false, new Date('2026-08-05T23:59:59Z'))
scheduler.finishLaunch('pty-midnight')
await vi.advanceTimersByTimeAsync(1_000)
expect(startBackfill).toHaveBeenCalledWith(
expect.objectContaining({
scanDates: [
['2026', '08', '05'],
['2026', '08', '06']
]
}),
undefined
)
})
})