) {
+ const facts = new TerminalRunFactsRegister()
+ const stops = new TerminalIntentionalStops()
+ stops.mark(PTY_ID, 'reversible', null)(true)
+ const runtime = {
+ terminalRunFacts: facts,
+ intentionalPtyStops: stops,
+ // Why the real method: the case under test is what the runtime does with each commit.
+ noteTerminalSpawnCommit: OrcaRuntimeWithRuntimeId.prototype.noteTerminalSpawnCommit,
+ registerPreAllocatedHandleForPty: vi.fn(),
+ registerPty: vi.fn(),
+ cancelPendingPtyRegistration: vi.fn(),
+ reflowHeadlessTerminalToPtyGrid: vi.fn(),
+ seedHeadlessTerminal: vi.fn(),
+ noteTerminalSpawnCommand: vi.fn()
+ }
+ const ports = {
+ runtime,
+ store: { persistPtyBinding },
+ options: {},
+ sendPtySpawnedToRenderer: vi.fn()
+ }
+ // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the commit reads only the ports above, and the store only for its binding save.
+ const deps = ports as unknown as PtySpawnIpcDeps
+ const ctx = createPtyIpcSpawnState(deps, {
+ worktreeId: 'wt-1',
+ tabId: 'tab-1',
+ leafId: 'leaf-1',
+ cols: 120,
+ rows: 40
+ })
+ ctx.validatedLeafId = 'leaf-1'
+ ctx.provider = { ...ctx.provider, shutdown: vi.fn().mockResolvedValue(undefined) }
+ ctx.result = { id: PTY_ID, incarnationId: INCARNATION_ID }
+ const outcome = await commitPtyIpcSpawn(ctx).then(
+ () => 'committed',
+ () => 'discarded'
+ )
+ return { outcome, facts, stops }
+}
+
+describe('renderer spawn commit: run facts', () => {
+ afterEach(() => {
+ ptySizes.delete(PTY_ID)
+ ptyOwnership.delete(PTY_ID)
+ ptyIncarnationById.delete(PTY_ID)
+ })
+
+ it('records a committed spawn and lets it supersede a landed stop no exit pinned', async () => {
+ const { outcome, facts, stops } = await commit(vi.fn().mockResolvedValue(true))
+
+ expect(outcome).toBe('committed')
+ expect(facts.read(PTY_ID, INCARNATION_ID).freshSpawn).toBe(true)
+ expect(stops.claimExit(PTY_ID, INCARNATION_ID)).toEqual([])
+ })
+
+ it('records nothing for a spawn discarded because its binding save failed', async () => {
+ const persistPtyBinding = vi.fn().mockRejectedValue(new Error('disk full'))
+ const { outcome, facts, stops } = await commit(persistPtyBinding)
+
+ expect(outcome).toBe('discarded')
+ expect(persistPtyBinding).toHaveBeenCalledOnce()
+ expect(facts.read(PTY_ID, INCARNATION_ID).freshSpawn).toBe(false)
+ expect(stops.claimExit(PTY_ID, INCARNATION_ID)).toEqual(['reversible'])
+ })
+})
diff --git a/src/main/ipc/pty/ipc/write-input-chunk-yield.test.ts b/src/main/ipc/pty/ipc/write-input-chunk-yield.test.ts
index ceeb5127b7d..4afd2b008f1 100644
--- a/src/main/ipc/pty/ipc/write-input-chunk-yield.test.ts
+++ b/src/main/ipc/pty/ipc/write-input-chunk-yield.test.ts
@@ -68,7 +68,7 @@ describe('chunked pty write yield', () => {
}) as typeof setImmediate)
const outcome = await Promise.race([
- createWriteInput()({ id: PTY_ID, data: THREE_CHUNK_INPUT }),
+ createWriteInput()({ inputKind: 'driving', id: PTY_ID, data: THREE_CHUNK_INPUT }),
afterImmediateTurns(50)
])
@@ -88,6 +88,7 @@ describe('chunked pty write yield', () => {
const immediate = vi.spyOn(globalThis, 'setImmediate')
const outcome = createWriteInput()({
+ inputKind: 'driving',
id: PTY_ID,
data: 'x'.repeat(TERMINAL_INPUT_CHUNK_MAX_BYTES)
})
diff --git a/src/main/ipc/pty/ipc/write-input-user-input.test.ts b/src/main/ipc/pty/ipc/write-input-user-input.test.ts
new file mode 100644
index 00000000000..d4f20f00a68
--- /dev/null
+++ b/src/main/ipc/pty/ipc/write-input-user-input.test.ts
@@ -0,0 +1,63 @@
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+import { TerminalRunFactsRegister } from '../../../runtime/terminal-run-facts'
+import { ptyOwnership } from '../provider/ownership-state'
+import { createPtyWriteInput } from './write-input'
+
+const PTY_ID = 'pty-user-input'
+
+const { provider } = vi.hoisted(() => ({
+ provider: { write: vi.fn(), hasPty: vi.fn(() => true) }
+}))
+
+vi.mock('../provider/registry', () => ({
+ tryGetProviderForPty: (id: string) => (id === PTY_ID ? provider : undefined)
+}))
+
+function createWriteInput(facts: TerminalRunFactsRegister) {
+ const runtime = { getDriver: () => ({ kind: 'desktop' }), terminalRunFacts: facts }
+ const mainWindow = { isDestroyed: () => false, webContents: { send: vi.fn() } }
+ // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: write input reads only getDriver and terminalRunFacts from the runtime, and isDestroyed/webContents from the window.
+ return createPtyWriteInput({ mainWindow: mainWindow as never, runtime: runtime as never })
+}
+
+beforeEach(() => {
+ ptyOwnership.set(PTY_ID, null)
+ provider.write.mockReset()
+})
+
+afterEach(() => {
+ ptyOwnership.delete(PTY_ID)
+})
+
+describe('renderer PTY writes: input kind', () => {
+ it.each(['writePtyInput', 'writePtyInputAccepted'] as const)(
+ '%s records driving input before the provider write',
+ async (writer) => {
+ const facts = new TerminalRunFactsRegister()
+ facts.recordSpawnCommit({ id: PTY_ID, incarnationId: 'inc-1' })
+ const recordedAtWrite: (number | null)[] = []
+ provider.write.mockImplementation(() => {
+ recordedAtWrite.push(facts.read(PTY_ID, 'inc-1').firstUserInputAt)
+ })
+
+ await createWriteInput(facts)[writer]({ id: PTY_ID, data: 'exit\r', inputKind: 'driving' })
+
+ expect(recordedAtWrite).toEqual([expect.any(Number)])
+ }
+ )
+
+ it.each([
+ ['a launch write', 'launch', 'echo startup\r'],
+ ['a query reply', 'query-reply', '\x1b[3;4R'],
+ ['a driving write that is only a reply', 'driving', '\x1b[3;4R'],
+ ['a driving write that is only focus reports', 'driving', '\x1b[I\x1b[O']
+ ] as const)('records nothing for %s', async (_label, inputKind, data) => {
+ const facts = new TerminalRunFactsRegister()
+ facts.recordSpawnCommit({ id: PTY_ID, incarnationId: 'inc-1' })
+
+ await createWriteInput(facts).writePtyInput({ id: PTY_ID, data, inputKind })
+
+ expect(provider.write).toHaveBeenCalledOnce()
+ expect(facts.read(PTY_ID, 'inc-1').firstUserInputAt).toBeNull()
+ })
+})
diff --git a/src/main/ipc/pty/ipc/write-input.ts b/src/main/ipc/pty/ipc/write-input.ts
index 77b27ef16f0..2f8ea72cd9b 100644
--- a/src/main/ipc/pty/ipc/write-input.ts
+++ b/src/main/ipc/pty/ipc/write-input.ts
@@ -9,6 +9,7 @@ import {
} from '../../../../shared/terminal-input'
import { ptyOwnership } from '../provider/ownership-state'
import { tryGetProviderForPty } from '../provider/registry'
+import type { TerminalInputKind } from '../../../../shared/terminal-input-kind'
import { interactiveOutputCharsByPty, lastInputAtByPty } from '../delivery/visibility-state'
export function isMainWindowPtyIpcEvent(
@@ -25,7 +26,7 @@ export function isMainWindowPtyIpcEvent(
)
}
-export type PtyWritePayload = { id: string; data: string }
+export type PtyWritePayload = { id: string; data: string; inputKind: TerminalInputKind }
export type PtyViewportClaimPayload = { id: string; cols: number; rows: number }
export function createPtyWriteInput(deps: {
@@ -148,6 +149,12 @@ export function createPtyWriteInput(deps: {
const isPtyWriteEventFromMainWindow = (event: IpcMainEvent | IpcMainInvokeEvent): boolean =>
isMainWindowPtyIpcEvent(event, mainWindow)
+ const noteRendererPtyInput = (args: PtyWritePayload): void => {
+ lastInputAtByPty.set(args.id, performance.now())
+ interactiveOutputCharsByPty.set(args.id, 0)
+ runtime?.terminalRunFacts?.recordInput(args.id, args.inputKind, args.data)
+ }
+
const writePtyInput = (args: PtyWritePayload): boolean | Promise${escape(failure.status)}: ${escape(failure.file)} — ${escape(failure.title)}
${escape(failure.message)}${escape(error.message ?? error.value)}`),
+ '',
+ 'Failures retain their original verdict. Repeated failures need a tracked owner, reproduction and review date; do not treat a red baseline as passing.',
+ ''
+ ].join('\n')
+}
+
+if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
+ try {
+ const summary = e2eFailureSummary(JSON.parse(readFileSync(process.argv[2], 'utf8')))
+ const records = JSON.parse(
+ readFileSync(new URL('../e2e-failure-tracking.json', import.meta.url), 'utf8')
+ )
+ appendFileSync(process.env.GITHUB_STEP_SUMMARY, renderE2eFailures(summary, records))
+ } catch (error) {
+ appendFileSync(
+ process.env.GITHUB_STEP_SUMMARY,
+ `E2E report unavailable (${error.code ?? 'invalid report'}); inspect the failing step and traces.\n`
+ )
+ }
+}
diff --git a/config/scripts/ci-e2e-failure-summary.test.mjs b/config/scripts/ci-e2e-failure-summary.test.mjs
new file mode 100644
index 00000000000..5594e4faa56
--- /dev/null
+++ b/config/scripts/ci-e2e-failure-summary.test.mjs
@@ -0,0 +1,79 @@
+import { expect, it } from 'vitest'
+import { e2eFailureSummary, renderE2eFailures } from './ci-e2e-failure-summary.mjs'
+import { trackE2eFailures } from './ci-e2e-failure-tracking.mjs'
+
+it('requires exact failures with an owner, issue and unexpired review date', () => {
+ const failure = {
+ file: 'test.spec.ts',
+ title: 'case',
+ project: 'electron',
+ message: 'expected focus failed'
+ }
+ const record = {
+ ...failure,
+ message: 'expected focus',
+ owner: '@owner',
+ issue: 'https://github.com/stablyai/orca/issues/123',
+ expires: '2026-10-01'
+ }
+ const now = new Date('2026-09-28')
+ expect(trackE2eFailures([failure], [null, record], now)).toMatchObject({
+ known: [{ ...failure, tracking: record }],
+ invalid: [null]
+ })
+ expect(trackE2eFailures([failure], null, now).untracked).toEqual([failure])
+ for (const change of [
+ { expires: '2026-09-27' },
+ { expires: '2026-09-31' },
+ { owner: '' },
+ { issue: '' },
+ { title: 'different' },
+ { message: 'different' },
+ { project: 'other' }
+ ]) {
+ expect(trackE2eFailures([failure], [{ ...record, ...change }], now).untracked).toEqual([
+ failure
+ ])
+ }
+})
+
+it('keeps failure, flaky, skipped and startup-error evidence separate', () => {
+ const report = {
+ errors: [{ message: 'startup failed' }],
+ suites: [
+ {
+ title: 'file',
+ file: 'tests/e2e/test.spec.ts',
+ suites: [
+ {
+ title: 'feature',
+ specs: [
+ {
+ title: 'case',
+ tests: [
+ { status: 'expected' },
+ { status: 'skipped' },
+ {
+ status: 'unexpected',
+ projectName: 'electron-headless',
+ results: [{ errors: [{ message: '