mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
* fix(terminal): retire explicitly closed pending split connections * test(memory): keep pending split proof compatible with formatted source * fix(terminal): confirm pending split retirement before stopping work * fix(terminal): restore the pending split-close gates CI checks Three CI gates were red on this branch and all three were this branch's own. The hook-order parity snapshot did not count the `confirmedCloseRef` this branch adds to `use-terminal-pane-close-actions.ts`. Dumping the flattened order against clean `main` shows exactly one added `useRef` at position 148 and no reordering, so the count moves 211 -> 212 and the digest with it. `pending-split-close-test-fixture.ts` is Vitest support code, but it sits outside the `*.test` / `*.spec` / `tests` globs that already switch `anti-slop/no-module-mocking` off, so the gate failed on all twelve of its `vi.mock` calls. It carries a file-scoped disable with the reason, matching `work-item-search-test-harness.ts`. `fix.patch` still described the pre-confirmation shape of the close hook, so `reproduce.mjs` aborted with `Source changed` and the cited ablation could not run at this head. Regenerated against the committed sources; the harness again reports 10 pass / 14 fail before and 24 pass / 0 fail after. Merges `main` rather than rebasing: #21005 is stacked on this branch. --------- Co-authored-by: m4air <m4air@Mac.localdomain> Co-authored-by: Neil <neil@stably.ai>
133 lines
4.7 KiB
TypeScript
133 lines
4.7 KiB
TypeScript
import { mkdirSync, rmSync, writeFileSync } from 'node:fs'
|
|
import { join } from 'node:path'
|
|
import { expect, it, vi } from 'vitest'
|
|
import { preparePendingSplitClose } from '../../../src/renderer/src/components/terminal-pane/pending-split-close-test-fixture'
|
|
import {
|
|
startDaemonAdapterHarness,
|
|
createMockSubprocess
|
|
} from '../../../src/main/daemon/daemon-pty-adapter-test-harness'
|
|
import { DaemonPtyAdapter } from '../../../src/main/daemon/daemon-pty-adapter'
|
|
import { getHistorySessionDirName } from '../../../src/main/daemon/history-paths'
|
|
import { SessionNotFoundError } from '../../../src/main/daemon/types'
|
|
|
|
async function coldRestoreHarness() {
|
|
const subprocess = createMockSubprocess()
|
|
const h = await startDaemonAdapterHarness(() => subprocess)
|
|
const historyPath = join(h.dir, 'history')
|
|
const sessionDir = join(historyPath, getHistorySessionDirName('pty-restored'))
|
|
mkdirSync(sessionDir, { recursive: true })
|
|
writeFileSync(
|
|
join(sessionDir, 'meta.json'),
|
|
JSON.stringify({
|
|
cwd: h.dir,
|
|
cols: 80,
|
|
rows: 24,
|
|
startedAt: '2026-04-15T10:00:00Z',
|
|
endedAt: null,
|
|
exitCode: null
|
|
})
|
|
)
|
|
writeFileSync(join(sessionDir, 'scrollback.bin'), 'synthetic saved history\r\n')
|
|
const adapter = new DaemonPtyAdapter({
|
|
socketPath: h.socketPath,
|
|
tokenPath: h.tokenPath,
|
|
historyPath
|
|
})
|
|
const requests: Promise<void>[] = []
|
|
const absent: string[] = []
|
|
const bridgeKill = (id: string): Promise<void> => {
|
|
const request = adapter.shutdown(id, { immediate: true }).catch((error) => {
|
|
// The renderer IPC handler treats the provider's already-gone reply as success.
|
|
if (error instanceof SessionNotFoundError) {
|
|
absent.push(id)
|
|
return
|
|
}
|
|
throw error
|
|
})
|
|
requests.push(request)
|
|
return request
|
|
}
|
|
return {
|
|
adapter,
|
|
subprocess,
|
|
requests,
|
|
absent,
|
|
bridgeKill,
|
|
async dispose() {
|
|
adapter.dispose()
|
|
h.adapter.dispose()
|
|
await h.server.shutdown()
|
|
rmSync(h.dir, { recursive: true, force: true })
|
|
}
|
|
}
|
|
}
|
|
|
|
it('explicit split close retires a real daemon cold restore whose reply is pending', async () => {
|
|
const h = await coldRestoreHarness()
|
|
try {
|
|
const p = await preparePendingSplitClose()
|
|
vi.mocked(window.api.pty.kill).mockImplementation(h.bridgeKill)
|
|
const result = await h.adapter.spawn({ cols: 80, rows: 24, sessionId: 'pty-restored' })
|
|
expect(result.isReattach).not.toBe(true)
|
|
expect(result.coldRestore).toBeDefined()
|
|
expect(await h.adapter.probePtyLiveness('pty-restored')).toBe(true)
|
|
p.actions.executeClosePane(1)
|
|
p.spawn.resolve(result)
|
|
await p.connecting
|
|
await Promise.all(h.requests)
|
|
expect(window.api.pty.kill).toHaveBeenCalledTimes(2)
|
|
expect(h.subprocess.forceKill).toHaveBeenCalledOnce()
|
|
expect(await h.adapter.probePtyLiveness('pty-restored')).toBe(false)
|
|
} finally {
|
|
await h.dispose()
|
|
}
|
|
})
|
|
|
|
it('the explicit late reply retries a kill that completed before adapter admission', async () => {
|
|
const h = await coldRestoreHarness()
|
|
try {
|
|
const p = await preparePendingSplitClose()
|
|
vi.mocked(window.api.pty.kill).mockImplementation(h.bridgeKill)
|
|
p.actions.executeClosePane(1)
|
|
// Hold admission outside the adapter; there is no history lock or session yet.
|
|
await Promise.all(h.requests)
|
|
expect(h.absent).toEqual(['pty-restored'])
|
|
const result = await h.adapter.spawn({ cols: 80, rows: 24, sessionId: 'pty-restored' })
|
|
expect(result.coldRestore).toBeDefined()
|
|
expect(await h.adapter.probePtyLiveness('pty-restored')).toBe(true)
|
|
p.spawn.resolve(result)
|
|
await p.connecting
|
|
await Promise.all(h.requests)
|
|
expect(window.api.pty.kill).toHaveBeenCalledTimes(2)
|
|
expect(await h.adapter.probePtyLiveness('pty-restored')).toBe(false)
|
|
} finally {
|
|
await h.dispose()
|
|
}
|
|
})
|
|
|
|
it('a known-ID shutdown already admitted to the adapter waits for its spawn history lock', async () => {
|
|
const h = await coldRestoreHarness()
|
|
const started = Promise.withResolvers<void>()
|
|
const release = Promise.withResolvers<void>()
|
|
const finish = h.adapter['finishSpawn'].bind(h.adapter)
|
|
h.adapter['finishSpawn'] = async (context, result) => {
|
|
started.resolve()
|
|
await release.promise
|
|
return finish(context, result)
|
|
}
|
|
try {
|
|
const spawning = h.adapter.spawn({ cols: 80, rows: 24, sessionId: 'pty-restored' })
|
|
await started.promise
|
|
const stopping = h.adapter.shutdown('pty-restored', { immediate: true })
|
|
expect(h.subprocess.forceKill).not.toHaveBeenCalled()
|
|
release.resolve()
|
|
await spawning
|
|
await stopping
|
|
expect(await h.adapter.probePtyLiveness('pty-restored')).toBe(false)
|
|
expect(h.subprocess.forceKill).toHaveBeenCalledOnce()
|
|
} finally {
|
|
release.resolve()
|
|
await h.dispose()
|
|
}
|
|
})
|