From 2a20d2de94040937fb7598d07fb10dfd75013c53 Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Sat, 24 Oct 2026 01:52:04 -0700 Subject: [PATCH] test: consolidate refusal cases and repair boundary coverage (#33098) Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> --- .../scripts/trim-windows-icon-source.test.mjs | 8 ++++ config/scripts/verify-cli-bin.test.mjs | 16 ++++++- .../verify-linux-glibc-payloads.test.mjs | 4 ++ .../verify-packaged-daemon-entry.test.mjs | 45 ++++++++++++++++++ ...mobile-native-chat-answer-stepping.test.ts | 6 ++- .../session/mobile-native-chat-noise.test.ts | 14 ++++-- .../mobile-native-chat-pair-window.test.ts | 8 ++++ .../mobile-native-chat-pending-echo.test.ts | 5 +- .../agent-hibernation-confirmation.test.ts | 8 ++++ .../lib/agent-hibernation-pane-age.test.ts | 3 ++ .../lib/agent-hibernation-visibility.test.ts | 46 +++++++++++++++---- .../agent-session-context-clear.test.ts | 3 +- ...agent-session-context-usage-schema.test.ts | 26 +++++++++-- .../agent-session-definitive-refusal.test.ts | 13 ++---- 14 files changed, 173 insertions(+), 32 deletions(-) diff --git a/config/scripts/trim-windows-icon-source.test.mjs b/config/scripts/trim-windows-icon-source.test.mjs index 0f4bb849aa3..bf9ad076621 100644 --- a/config/scripts/trim-windows-icon-source.test.mjs +++ b/config/scripts/trim-windows-icon-source.test.mjs @@ -97,6 +97,8 @@ describe('trim pipeline', () => { const image = solidSquareWithInset(100, 60) const cropped = cropImage(image, findOpaqueBounds(image)) expect(cropped.width).toBe(60) + expect(cropped.height).toBe(60) + expect(cropped.data).toEqual(solidSquareWithInset(60, 60).data) const squared = squareWithMargin(cropped, 0.04) // Glyph (60) plus ~4% margin per side keeps it near-full-bleed. expect(glyphFillFraction(squared)).toBeGreaterThanOrEqual(0.9) @@ -108,6 +110,12 @@ describe('trim pipeline', () => { const filled = squareWithMargin(cropImage(image, findOpaqueBounds(image))) const small = resizeImage(filled, 32, 32) expect(small.width).toBe(32) + const edge = { + width: 2, + height: 1, + data: Buffer.from([200, 100, 50, 255, 0, 0, 0, 0]) + } + expect(resizeImage(edge, 1, 1).data).toEqual(Buffer.from([200, 100, 50, 128])) expect(glyphFillFraction(small)).toBeGreaterThanOrEqual(0.9) }) }) diff --git a/config/scripts/verify-cli-bin.test.mjs b/config/scripts/verify-cli-bin.test.mjs index 7d7b3ac3b65..e6e52605e07 100644 --- a/config/scripts/verify-cli-bin.test.mjs +++ b/config/scripts/verify-cli-bin.test.mjs @@ -9,15 +9,23 @@ import { } from 'node:fs' import { tmpdir } from 'node:os' import path from 'node:path' -import { describe, expect, it } from 'vitest' +import { afterEach, describe, expect, it } from 'vitest' import { verifyPackageCliBin } from './verify-cli-bin.mjs' +const roots = [] +afterEach(() => { + for (const root of roots.splice(0)) { + rmSync(root, { recursive: true, force: true }) + } +}) + /** Builds a temporary Orca-style project fixture with a compiled CLI entrypoint. */ function makeProjectWithCli( content, { mode = 0o755, rootPackageType, writeOutPackageJson = true } = {} ) { const projectDir = mkdtempSync(path.join(tmpdir(), 'orca-cli-bin-')) + roots.push(projectDir) const cliPath = path.join(projectDir, 'out', 'cli', 'index.js') const outPackageJsonPath = path.join(projectDir, 'out', 'package.json') mkdirSync(path.dirname(cliPath), { recursive: true }) @@ -39,12 +47,16 @@ function makeProjectWithCli( describe('verifyPackageCliBin', () => { it('accepts a non-empty Node entrypoint and can run help through Node', () => { const { projectDir, cliPath } = makeProjectWithCli( - '#!/usr/bin/env node\nif (process.argv.includes("--help")) process.exit(0)\n' + '#!/usr/bin/env node\nrequire("node:fs").writeFileSync("help-receipt.json", JSON.stringify({ args: process.argv.slice(2), cwd: process.cwd() }))\nif (!process.argv.includes("--help")) process.exit(9)\n' ) expect(verifyPackageCliBin({ projectDir, runHelp: true })).toMatchObject({ binPath: cliPath }) + expect(JSON.parse(readFileSync(path.join(projectDir, 'help-receipt.json'), 'utf8'))).toEqual({ + args: ['--help'], + cwd: projectDir + }) }) it('rejects an empty package bin target', () => { diff --git a/config/scripts/verify-linux-glibc-payloads.test.mjs b/config/scripts/verify-linux-glibc-payloads.test.mjs index 9a68142cf9e..3a8ff4c6705 100644 --- a/config/scripts/verify-linux-glibc-payloads.test.mjs +++ b/config/scripts/verify-linux-glibc-payloads.test.mjs @@ -60,6 +60,10 @@ describe.skipIf(process.platform === 'win32')('static ELF and remote musl payloa it.each([ ['missing program headers', 'Dynamic Section:\n'], + [ + 'version references without a dynamic segment', + `${STATIC_HEADERS}Version References:\n required from libc.so.6:\n 0x0 0x00 02 GLIBC_2.17\n` + ], ['dynamic segment', `${STATIC_HEADERS} DYNAMIC off 0x0000000000001000\n`], ['interpreter segment', `${STATIC_HEADERS} INTERP off 0x0000000000001000\n`], ['dependency without a dynamic segment', `${STATIC_HEADERS} NEEDED libc.so.6\n`] diff --git a/config/scripts/verify-packaged-daemon-entry.test.mjs b/config/scripts/verify-packaged-daemon-entry.test.mjs index a59e0262265..718ffee96d2 100644 --- a/config/scripts/verify-packaged-daemon-entry.test.mjs +++ b/config/scripts/verify-packaged-daemon-entry.test.mjs @@ -126,6 +126,51 @@ describe.skipIf(process.platform === 'win32')('terminal host round trip', () => } } + it('accepts a valid peer round trip and cleans up after observing the boot request', async () => { + const messagesPath = join(root, 'messages.jsonl') + const entry = fakeDaemon(`(socket) => { + let buffer = '' + socket.on('data', (chunk) => { + buffer += chunk + let newline + while ((newline = buffer.indexOf('\\n')) !== -1) { + const message = JSON.parse(buffer.slice(0, newline)) + buffer = buffer.slice(newline + 1) + fs.appendFileSync(${JSON.stringify(messagesPath)}, JSON.stringify(message) + '\\n') + if (message.type === 'hello') { + if (message.role === 'stream') globalThis.stream = socket + socket.write(JSON.stringify({ ok: true }) + '\\n') + } else if (message.type === 'createOrAttach') { + globalThis.stream.write(JSON.stringify({ payload: { data: 'OK 42' } }) + '\\n') + } + } + }) + }`) + const result = await roundTrip(entry, 1_500) + expect(result.error).toBe(null) + expect(result.daemonAlive).toBe(false) + expect(result.scratchDirs).toEqual([]) + const messages = readFileSync(messagesPath, 'utf8').trim().split('\n').map(JSON.parse) + expect(messages.slice(0, 2).map(({ type, role, token }) => ({ type, role, token }))).toEqual([ + { type: 'hello', role: 'control', token: 'tok' }, + { type: 'hello', role: 'stream', token: 'tok' } + ]) + expect(messages[0].clientId).toBe(messages[1].clientId) + expect(messages[0].clientId).toMatch(/^[0-9a-f-]{36}$/) + expect(messages).toHaveLength(3) + expect(messages[2]).toMatchObject({ + id: 'boot-1', + type: 'createOrAttach', + payload: { + sessionId: 'terminal-host-boot', + cols: 80, + rows: 24, + command: 'echo "OK $((6*7))" && exit' + } + }) + expect(messages[2].payload.cwd.startsWith(join(root, 'oth-'))).toBe(true) + }) + it('rejects by the deadline and cleans up when the daemon never answers the hello', async () => { const result = await roundTrip(fakeDaemon('() => {}'), 1_500) expect(result.error?.message).toMatch(/timed out waiting for the control hello reply/) diff --git a/mobile/src/session/mobile-native-chat-answer-stepping.test.ts b/mobile/src/session/mobile-native-chat-answer-stepping.test.ts index e9049b00cc1..f48073bb306 100644 --- a/mobile/src/session/mobile-native-chat-answer-stepping.test.ts +++ b/mobile/src/session/mobile-native-chat-answer-stepping.test.ts @@ -21,7 +21,7 @@ describe('scheduleMobileClaudeAnswer', () => { it('writes each question body then its Enter, paced per step, one Enter per line', () => { const events: string[] = [] const timers = scheduleMobileClaudeAnswer( - ['', 'b', 'c'], // blank middle answer must still get its own body + Enter + ['', 'b', 'c'], // A blank answer still gets its own body + Enter. (line) => events.push(`body:${line}`), () => events.push('enter') ) @@ -29,7 +29,9 @@ describe('scheduleMobileClaudeAnswer', () => { vi.advanceTimersByTime(0) expect(events).toEqual(['body:']) - vi.advanceTimersByTime(MOBILE_NATIVE_CHAT_SUBMIT_DELAY_MS) + vi.advanceTimersByTime(MOBILE_NATIVE_CHAT_SUBMIT_DELAY_MS - 1) + expect(events).toEqual(['body:']) + vi.advanceTimersByTime(1) expect(events).toEqual(['body:', 'enter']) vi.advanceTimersByTime(MOBILE_NATIVE_CHAT_ADVANCE_BUFFER_MS) expect(events).toEqual(['body:', 'enter', 'body:b']) diff --git a/mobile/src/session/mobile-native-chat-noise.test.ts b/mobile/src/session/mobile-native-chat-noise.test.ts index dcda3f3dace..67434917ea2 100644 --- a/mobile/src/session/mobile-native-chat-noise.test.ts +++ b/mobile/src/session/mobile-native-chat-noise.test.ts @@ -42,13 +42,21 @@ describe('isNoiseMessage', () => { expect(isNoiseMessage(msg('user', ' is my band'))).toBe(false) }) - it('keeps assistant and tool turns', () => { + it('keeps assistant, tool, and ordinary system turns', () => { expect(isNoiseMessage(msg('assistant', ' in prose'))).toBe(false) + expect(isNoiseMessage(msg('tool', ' in prose'))).toBe(false) expect(isNoiseMessage(msg('system', 'Conversation interrupted'))).toBe(false) }) - it('keeps a user turn that carries tool results', () => { - expect(isNoiseMessage(msg('user', '', [{ type: 'tool-result', output: 'ok' }]))).toBe(false) + it('keeps tool-bearing user turns even when their text looks like harness noise', () => { + for (const block of [ + { type: 'tool-result', output: 'ok' }, + { type: 'tool-call', name: 'Bash', input: { command: 'ls' } } + ] satisfies NativeChatMessage['blocks']) { + expect( + isNoiseMessage(msg('user', '', [{ type: 'text', text: 'done' }, block])) + ).toBe(false) + } }) it('stripNoiseMessages removes only the noise', () => { diff --git a/mobile/src/session/mobile-native-chat-pair-window.test.ts b/mobile/src/session/mobile-native-chat-pair-window.test.ts index fb2bd15605c..c132b84ab5d 100644 --- a/mobile/src/session/mobile-native-chat-pair-window.test.ts +++ b/mobile/src/session/mobile-native-chat-pair-window.test.ts @@ -34,6 +34,14 @@ describe('mobile complete-pair stream windows', () => { }) expect(result).toEqual({ kind: 'messages', messages: [...base, row('latest')] }) expect(merger.indexById.get('opencode:oldest:reasoning')).toBe(0) + const updated = { ...row('latest'), blocks: [{ type: 'text' as const, text: 'updated' }] } + applyMobileNativeChatStreamFrame({ + merger, + frame: { type: 'appended', messages: [updated] }, + limit: 40, + replaceSnapshot: false + }) + expect(merger.list).toEqual([...base, updated]) } ) diff --git a/mobile/src/session/mobile-native-chat-pending-echo.test.ts b/mobile/src/session/mobile-native-chat-pending-echo.test.ts index 71f1ec9464e..d9adad49988 100644 --- a/mobile/src/session/mobile-native-chat-pending-echo.test.ts +++ b/mobile/src/session/mobile-native-chat-pending-echo.test.ts @@ -47,7 +47,7 @@ describe('appendMobileNativeChatPending ordinals for repeated sends', () => { const MULTILINE = 'first line of the prompt\nsecond line of the prompt' it('gives a repeated multi-line send the next ordinal', () => { - const baseline = [userTurn('m1', 'unrelated')] + const baseline = [userTurn('m1', MULTILINE)] const first = appendMobileNativeChatPending( {}, KEY, @@ -63,7 +63,8 @@ describe('appendMobileNativeChatPending ordinals for repeated sends', () => { MULTILINE ) - expect(both[KEY]?.map((item) => item.expectedOccurrence)).toEqual([1, 2]) + expect(sendOrigin(MULTILINE, baseline).baselineOccurrences).toBe(1) + expect(both[KEY]?.map((item) => item.expectedOccurrence)).toEqual([2, 3]) }) it('retires only the first echo when the first of two identical rows lands', () => { diff --git a/src/renderer/src/lib/agent-hibernation-confirmation.test.ts b/src/renderer/src/lib/agent-hibernation-confirmation.test.ts index f24c8dec78b..770828f29c0 100644 --- a/src/renderer/src/lib/agent-hibernation-confirmation.test.ts +++ b/src/renderer/src/lib/agent-hibernation-confirmation.test.ts @@ -26,6 +26,14 @@ describe('agent sleep confirmation', () => { confirmAgentHibernationCandidates(first.confirmationState, [firstCandidate]).candidates ).toEqual([firstCandidate]) + const absent = confirmAgentHibernationCandidates(first.confirmationState, []) + expect(absent.confirmationState).toEqual({}) + const reappeared = confirmAgentHibernationCandidates(absent.confirmationState, [firstCandidate]) + expect(reappeared.candidates).toEqual([]) + expect( + confirmAgentHibernationCandidates(reappeared.confirmationState, [firstCandidate]).candidates + ).toEqual([firstCandidate]) + const changed = candidate({ signature: 'changed-signature' }) expect( confirmAgentHibernationCandidates(first.confirmationState, [changed]).candidates diff --git a/src/renderer/src/lib/agent-hibernation-pane-age.test.ts b/src/renderer/src/lib/agent-hibernation-pane-age.test.ts index a23d6a730dc..7feac3f80a4 100644 --- a/src/renderer/src/lib/agent-hibernation-pane-age.test.ts +++ b/src/renderer/src/lib/agent-hibernation-pane-age.test.ts @@ -77,6 +77,9 @@ describe('hibernation PTY binding age', () => { it('expires an unseen binding once it is older than the idle window', () => { observe('pty-1', 1_000) recordHibernationBoundaryResolved(PANE, 1_000) + observe(null, 1_000 + IDLE_MS) + expect(getHibernationPtyBindingFirstSeenAtByPaneKey()[PANE]).toBe(1_000) + expect(getHibernationBoundaryResolvedAtByPaneKey()[PANE]).toBe(1_000) observe(null, 1_000 + IDLE_MS + 1) expect(getHibernationPtyBindingFirstSeenAtByPaneKey()[PANE]).toBeUndefined() expect(getHibernationBoundaryResolvedAtByPaneKey()[PANE]).toBeUndefined() diff --git a/src/renderer/src/lib/agent-hibernation-visibility.test.ts b/src/renderer/src/lib/agent-hibernation-visibility.test.ts index 2f6642e2c39..ed648283bad 100644 --- a/src/renderer/src/lib/agent-hibernation-visibility.test.ts +++ b/src/renderer/src/lib/agent-hibernation-visibility.test.ts @@ -1,7 +1,6 @@ // @vitest-environment happy-dom import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { - resetAgentHibernationCoordinatorForTests, startAgentHibernationCoordinator, stopAgentHibernationCoordinator } from './agent-hibernation-coordinator' @@ -17,21 +16,50 @@ describe('agent hibernation coordinator visibility wiring', () => { setVisibility('visible') resetStaleDocumentVisibilityForTesting() }) - afterEach(() => { - resetAgentHibernationCoordinatorForTests() + afterEach(async () => { + const { resetAgentHibernationCoordinatorFixture } = + await import('./agent-hibernation-coordinator-test-fixture') + resetAgentHibernationCoordinatorFixture() resetStaleDocumentVisibilityForTesting() setVisibility('visible') + vi.unstubAllGlobals() vi.restoreAllMocks() }) - it('subscribes to the becoming-visible pass on start and unsubscribes on stop', () => { - const add = vi.spyOn(document, 'addEventListener') - const remove = vi.spyOn(document, 'removeEventListener') + it('subscribes to the becoming-visible pass on start and unsubscribes on stop', async () => { + // Keep happy-dom's event target when the state fixture stubs window. + const browserWindow = window + const { installEligibleState, NOW, LEAF } = + await import('./agent-hibernation-coordinator-test-fixture') + vi.stubGlobal('window', browserWindow) + vi.useFakeTimers() + const shutdown = installEligibleState(vi.fn().mockResolvedValue(undefined)) - startAgentHibernationCoordinator({ intervalMs: 60_000, now: () => 0 }) - expect(add.mock.calls.some(([type]) => type === 'visibilitychange')).toBe(true) + startAgentHibernationCoordinator({ intervalMs: 1_000, now: () => NOW }) + await vi.advanceTimersByTimeAsync(1_000) + expect(shutdown).not.toHaveBeenCalled() + setVisibility('hidden') + await vi.advanceTimersByTimeAsync(3_000) + expect(shutdown).not.toHaveBeenCalled() + + setVisibility('visible') + await vi.advanceTimersByTimeAsync(0) + expect(shutdown).toHaveBeenCalledExactlyOnceWith('wt-bg', { + paneKey: `tab-1:${LEAF}`, + tabId: 'tab-1', + leafId: LEAF, + ptyId: 'pty-1' + }) + + shutdown.mockClear() stopAgentHibernationCoordinator() - expect(remove.mock.calls.some(([type]) => type === 'visibilitychange')).toBe(true) + setVisibility('hidden') + setVisibility('visible') + await vi.advanceTimersByTimeAsync(0) + setVisibility('hidden') + setVisibility('visible') + await vi.advanceTimersByTimeAsync(3_000) + expect(shutdown).not.toHaveBeenCalled() }) }) diff --git a/src/shared/agent-session-context-clear.test.ts b/src/shared/agent-session-context-clear.test.ts index 8c084db069f..23d81984db6 100644 --- a/src/shared/agent-session-context-clear.test.ts +++ b/src/shared/agent-session-context-clear.test.ts @@ -147,8 +147,9 @@ describe('completed context clear projections', () => { it('includes new sends but excludes older unknown sends even at the current fence', () => { const currentSend = { ...oldSend, clientMessageId: 'new-send', acceptedSequence: 6 } + const boundarySend = { ...oldSend, clientMessageId: 'boundary-send', acceptedSequence: 5 } const rows = [user, clear, row(7, { kind: 'turn', turnId: 'new-turn', state: 'running' })] - expect(agentSessionCurrentContextRows(rows, [oldSend, currentSend])).toEqual({ + expect(agentSessionCurrentContextRows(rows, [oldSend, boundarySend, currentSend])).toEqual({ items: [rows[2]], submissions: [currentSend] }) diff --git a/src/shared/agent-session-context-usage-schema.test.ts b/src/shared/agent-session-context-usage-schema.test.ts index 0afe704df00..ae627f3e4de 100644 --- a/src/shared/agent-session-context-usage-schema.test.ts +++ b/src/shared/agent-session-context-usage-schema.test.ts @@ -1,5 +1,9 @@ import { describe, expect, it } from 'vitest' -import { MAX_CONTEXT_CATEGORIES, MAX_CONTEXT_MODEL_ID_CHARS } from './agent-session-context-usage' +import { + MAX_CONTEXT_CATEGORIES, + MAX_CONTEXT_CATEGORY_NAME_CHARS, + MAX_CONTEXT_MODEL_ID_CHARS +} from './agent-session-context-usage' import { AgentSessionContextUsageSchema, isAdmissibleAgentSessionContextUsage @@ -32,8 +36,18 @@ describe('context usage admission', () => { capturedAt: 1 } }, - { used: report, future: { retained: true } } - ])('accepts canonical facts and leaves additive fields untouched (%j)', (value) => { + { used: report, future: { retained: true } }, + { + used: { + ...report, + model: 'x'.repeat(MAX_CONTEXT_MODEL_ID_CHARS), + categories: Array.from({ length: MAX_CONTEXT_CATEGORIES }, () => ({ + name: 'x'.repeat(MAX_CONTEXT_CATEGORY_NAME_CHARS), + tokens: 0 + })) + } + } + ])('accepts canonical facts and leaves inputs untouched (%j)', (value) => { const original = structuredClone(value) expect(isAdmissibleAgentSessionContextUsage(value)).toBe(true) expect(AgentSessionContextUsageSchema.safeParse(value).success).toBe(true) @@ -51,6 +65,12 @@ describe('context usage admission', () => { { used: { ...report, categories: Array(MAX_CONTEXT_CATEGORIES + 1).fill(report.categories[0]) } }, + { + used: { + ...report, + categories: [{ name: 'x'.repeat(MAX_CONTEXT_CATEGORY_NAME_CHARS + 1), tokens: 0 }] + } + }, { used: { ...report, categories: [{ name: 'messages', tokens: null }] } } ])('rejects malformed facts and bounds just like parsing (%j)', (value) => { expect(isAdmissibleAgentSessionContextUsage(value)).toBe(false) diff --git a/src/shared/agent-session-definitive-refusal.test.ts b/src/shared/agent-session-definitive-refusal.test.ts index ed3141dbb02..b59450ccbfd 100644 --- a/src/shared/agent-session-definitive-refusal.test.ts +++ b/src/shared/agent-session-definitive-refusal.test.ts @@ -9,16 +9,6 @@ describe('definitive agent-session create refusals', () => { expect(isDefinitiveAgentSessionCreateRefusal('agent_session_ownership_unknown')).toBe(false) }) - it('leaves transport failures, timeouts and a missing code unknown', () => { - expect(isDefinitiveAgentSessionCreateRefusal('runtime_error')).toBe(false) - expect(isDefinitiveAgentSessionCreateRefusal('remote_runtime_unavailable')).toBe(false) - expect(isDefinitiveAgentSessionCreateRefusal('timeout')).toBe(false) - expect(isDefinitiveAgentSessionCreateRefusal('runtime_timeout')).toBe(false) - expect(isDefinitiveAgentSessionCreateRefusal(undefined)).toBe(false) - expect(isDefinitiveAgentSessionCreateRefusal(null)).toBe(false) - expect(isDefinitiveAgentSessionCreateRefusal('')).toBe(false) - }) - it('counts a method an old host never registered as definitive', () => { expect(isDefinitiveAgentSessionCreateRefusal('method_not_found')).toBe(true) }) @@ -41,6 +31,9 @@ describe('definitive agent-session create refusals', () => { // Pins the RPC half (round-2 S5): transport and client-minted codes never allow a fallback. it.each([ + 'runtime_error', + 'timeout', + '', 'runtime_unavailable', 'remote_runtime_unavailable', 'runtime_timeout',