Skill update round trip / roundtrip (false, macos-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, macos-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, ubuntu-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, ubuntu-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, ubuntu-latest, symlink, latest) (push) Canceled after 0s
Skill update round trip / roundtrip (false, windows-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, windows-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, macos-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, macos-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, ubuntu-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, ubuntu-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, windows-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, windows-latest, symlink, 1.5.17) (push) Canceled after 0s
174 lines
5.6 KiB
TypeScript
174 lines
5.6 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import type * as NodeFsPromisesModule from 'node:fs/promises'
|
|
|
|
const UNC_PATH = '\\\\wsl.localhost\\Ubuntu\\home\\ada\\.codex\\sessions\\rollout.jsonl'
|
|
const EMPTY_STATS = { size: 0, mtimeMs: 1, ctimeMs: 1, ino: 1, dev: 1 }
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
bind: vi.fn(),
|
|
dispose: vi.fn(),
|
|
filterRunning: vi.fn(),
|
|
invalidate: vi.fn(),
|
|
observe: vi.fn(),
|
|
observation: undefined as ((running: boolean) => Promise<void> | void) | undefined,
|
|
rebindNeeded: true,
|
|
stat: vi.fn()
|
|
}))
|
|
|
|
vi.mock('../wsl-running-path-filter', () => ({
|
|
filterPathsToRunningWslDistrosAsync: mocks.filterRunning
|
|
}))
|
|
vi.mock('node:fs/promises', async (importOriginal) => ({
|
|
...(await importOriginal<typeof NodeFsPromisesModule>()),
|
|
stat: mocks.stat
|
|
}))
|
|
vi.mock('./transcript-native-watcher', () => ({
|
|
createTranscriptNativeWatcher: () => ({
|
|
bind: mocks.bind,
|
|
dispose: mocks.dispose,
|
|
invalidate: mocks.invalidate,
|
|
needsRebind: () => mocks.rebindNeeded
|
|
})
|
|
}))
|
|
vi.mock('./wsl-transcript-running-observer', () => ({
|
|
observeWslTranscriptRunningState: mocks.observe
|
|
}))
|
|
|
|
import { installTranscriptWatcher } from './transcript-watch-engine'
|
|
|
|
describe('installed WSL transcript watcher lifecycle', () => {
|
|
beforeEach(() => {
|
|
vi.useFakeTimers()
|
|
mocks.rebindNeeded = true
|
|
mocks.bind.mockReset().mockImplementation(() => {
|
|
mocks.rebindNeeded = false
|
|
return true
|
|
})
|
|
mocks.dispose.mockReset()
|
|
mocks.filterRunning.mockReset().mockResolvedValue([UNC_PATH])
|
|
mocks.invalidate.mockReset().mockImplementation(() => {
|
|
mocks.rebindNeeded = true
|
|
})
|
|
mocks.observation = undefined
|
|
mocks.observe.mockReset().mockImplementation((_path, onRunning, onStopped) => {
|
|
mocks.observation = (running) => (running ? onRunning() : onStopped())
|
|
return vi.fn()
|
|
})
|
|
mocks.stat.mockReset().mockResolvedValue(EMPTY_STATS)
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers()
|
|
})
|
|
|
|
it('settles the guarded initial drain when the distro stops after install', async () => {
|
|
const onInitialSnapshot = vi.fn()
|
|
const subscription = await installTranscriptWatcher(UNC_PATH, () => null, {
|
|
agent: 'codex',
|
|
sessionId: 'wsl-session',
|
|
reconciliationIntervalMs: 100,
|
|
onAppend: () => {},
|
|
onInitialSnapshot
|
|
})
|
|
expect(subscription).not.toBeNull()
|
|
|
|
mocks.filterRunning.mockResolvedValue([])
|
|
const statsAfterInstall = mocks.stat.mock.calls.length
|
|
await vi.advanceTimersByTimeAsync(50)
|
|
|
|
expect(mocks.stat).toHaveBeenCalledTimes(statsAfterInstall)
|
|
expect(onInitialSnapshot).toHaveBeenCalledWith([], false, 0, 'Transcript unavailable')
|
|
subscription?.unsubscribe()
|
|
})
|
|
|
|
it('does not settle after unsubscribe wins a delayed running probe', async () => {
|
|
const onInitialSnapshot = vi.fn()
|
|
const subscription = await installTranscriptWatcher(UNC_PATH, () => null, {
|
|
agent: 'codex',
|
|
sessionId: 'wsl-session',
|
|
onAppend: () => {},
|
|
onInitialSnapshot
|
|
})
|
|
let finishProbe: (() => void) | undefined
|
|
mocks.filterRunning.mockImplementationOnce(
|
|
() =>
|
|
new Promise<string[]>((resolve) => {
|
|
finishProbe = () => resolve([])
|
|
})
|
|
)
|
|
|
|
const drain = vi.advanceTimersByTimeAsync(50)
|
|
await vi.waitFor(() => expect(finishProbe).toBeDefined())
|
|
subscription?.unsubscribe()
|
|
finishProbe?.()
|
|
await drain
|
|
|
|
expect(onInitialSnapshot).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('suspends observation while stopped and resumes after an explicit start', async () => {
|
|
const subscription = await installTranscriptWatcher(UNC_PATH, () => null, {
|
|
agent: 'codex',
|
|
sessionId: 'wsl-session',
|
|
reconciliationIntervalMs: 100,
|
|
onAppend: () => {}
|
|
})
|
|
await vi.advanceTimersByTimeAsync(50)
|
|
const statsBeforeStop = mocks.stat.mock.calls.length
|
|
|
|
mocks.observation?.(false)
|
|
await vi.advanceTimersByTimeAsync(100)
|
|
expect(mocks.stat).toHaveBeenCalledTimes(statsBeforeStop)
|
|
|
|
mocks.filterRunning.mockResolvedValue([UNC_PATH])
|
|
mocks.observation?.(true)
|
|
await vi.advanceTimersByTimeAsync(0)
|
|
|
|
expect(mocks.stat.mock.calls.length).toBeGreaterThan(statsBeforeStop)
|
|
expect(mocks.bind).not.toHaveBeenCalled()
|
|
subscription?.unsubscribe()
|
|
})
|
|
|
|
it('returns the running reconciliation promise to the shared observer', async () => {
|
|
const subscription = await installTranscriptWatcher(UNC_PATH, () => null, {
|
|
agent: 'codex',
|
|
sessionId: 'wsl-session',
|
|
onAppend: () => {}
|
|
})
|
|
await vi.advanceTimersByTimeAsync(50)
|
|
let finishStat: (() => void) | undefined
|
|
mocks.stat.mockImplementationOnce(
|
|
() =>
|
|
new Promise((resolve) => {
|
|
finishStat = () => resolve(EMPTY_STATS)
|
|
})
|
|
)
|
|
|
|
const reconciliation = mocks.observation?.(true)
|
|
expect(reconciliation).toBeInstanceOf(Promise)
|
|
await vi.waitFor(() => expect(finishStat).toBeDefined())
|
|
finishStat?.()
|
|
await reconciliation
|
|
subscription?.unsubscribe()
|
|
})
|
|
|
|
it('defers a failed observed UNC probe to the next shared observation', async () => {
|
|
const subscription = await installTranscriptWatcher(UNC_PATH, () => null, {
|
|
agent: 'codex',
|
|
sessionId: 'wsl-session',
|
|
onAppend: () => {}
|
|
})
|
|
await vi.advanceTimersByTimeAsync(50)
|
|
mocks.filterRunning.mockClear().mockResolvedValue([])
|
|
mocks.stat.mockClear().mockRejectedValueOnce(new Error('distro stopped'))
|
|
|
|
await mocks.observation?.(true)
|
|
|
|
expect(mocks.stat).toHaveBeenCalledTimes(1)
|
|
expect(mocks.filterRunning).not.toHaveBeenCalled()
|
|
await mocks.observation?.(false)
|
|
expect(mocks.stat).toHaveBeenCalledTimes(1)
|
|
subscription?.unsubscribe()
|
|
})
|
|
})
|