diff --git a/config/scripts/ci/sharding/ci-shard-timings.json b/config/scripts/ci/sharding/ci-shard-timings.json index 44e7462debc..77c75e7f737 100644 --- a/config/scripts/ci/sharding/ci-shard-timings.json +++ b/config/scripts/ci/sharding/ci-shard-timings.json @@ -2827,7 +2827,6 @@ "src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-failed-create-owner-verdict.test.ts": 2022, "src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-fork-rewind.test.ts": 1797, "src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-held-child-lease-renewal.test.ts": 957, - "src/main/native-chat/agent-session-wire/termination/testing/structured-agent-session-host-teardown.test.ts": 382, "src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-input-order-rewind.test.ts": 1509, "src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-journal-corruption.test.ts": 1860, "src/main/native-chat/agent-session-wire/termination/testing/structured-agent-session-late-proof-tool-ends.test.ts": 754, @@ -2845,7 +2844,6 @@ "src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-queued-refused-card.test.ts": 2478, "src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-queued-repeat-press.test.ts": 2765, "src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-queued-withdrawn-draft.test.ts": 1916, - "src/main/native-chat/agent-session-wire/termination/testing/structured-agent-session-reasoning-sweep.test.ts": 747, "src/main/native-chat/agent-session-wire/journal-ingress/testing/structured-agent-session-received-output.test.ts": 2740, "src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-receiving-cancellation.test.ts": 5834, "src/main/native-chat/agent-session-wire/recovery/testing/structured-agent-session-recovery-read.test.ts": 918, @@ -2868,7 +2866,6 @@ "src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-send-open-stale-turn.test.ts": 2301, "src/main/native-chat/agent-session-wire/delivery/testing/structured-agent-session-send-settlement.test.ts": 197, "src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-stale-turn-open.test.ts": 2210, - "src/main/native-chat/agent-session-wire/termination/testing/structured-agent-session-stale-turn-verdict.test.ts": 305, "src/main/native-chat/agent-session-wire/termination/testing/structured-agent-session-stale-verdict-persistence.test.ts": 972, "src/main/native-chat/agent-session-wire/termination/testing/structured-agent-session-stale-verdict-prompts.test.ts": 665, "src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-startup-attempt-reports.test.ts": 1857, @@ -3020,7 +3017,6 @@ "src/main/orca-profiles/profile-ui-scope.test.ts": 61, "src/main/persistence/migrating-orcad-catalog/orcad-migration-export-holds.test.ts": 231, "src/main/orcad/electron-serve-provider-selection.test.ts": 554, - "src/main/orcad/electron-sidecar-method-routing.test.ts": 287, "src/main/orcad/electron-sidecar-tab-registry.test.ts": 105, "src/main/orcad/external-chromium-browser-session.test.ts": 152, "src/main/orcad/node-pty-prebuilt-slot.test.ts": 185, @@ -3031,7 +3027,6 @@ "src/main/orcad/orcad-artifact-cache-retention.test.ts": 301, "src/main/orcad/orcad-artifact-materializer.test.ts": 1129, "src/main/orcad/orcad-artifact-preflight.test.ts": 344, - "src/main/orcad/orcad-automations.test.ts": 2840, "src/main/orcad/orcad-bind-address.test.ts": 41, "src/main/orcad/orcad-browser-install-park.test.ts": 131, "src/main/orcad/orcad-browser-provider.test.ts": 479, diff --git a/src/main/native-chat/agent-session-wire/termination/testing/structured-agent-session-dead-generation-settlement.test.ts b/src/main/native-chat/agent-session-wire/termination/testing/structured-agent-session-dead-generation-settlement.test.ts index 7658b0c27d3..1ce5ec03781 100644 --- a/src/main/native-chat/agent-session-wire/termination/testing/structured-agent-session-dead-generation-settlement.test.ts +++ b/src/main/native-chat/agent-session-wire/termination/testing/structured-agent-session-dead-generation-settlement.test.ts @@ -6,10 +6,6 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest' import { openAgentSessionJournal } from '../../../agent-session-journal/journal-store-factory' import type { AgentSessionJournal } from '../../../agent-session-journal/journal-store' import { settleObservedExitForTest } from './structured-agent-session-observed-exit.test-fixture' -import { - captureUnfinishedStructuredAgentSessionWork, - unfinishedStructuredAgentSessionWorkWasInterrupted -} from '../structured-agent-session-unfinished-work' import { closeTestJournalHostDatabases, openTestJournalHostDatabase @@ -158,38 +154,3 @@ describe('dead structured-session generation settlement', () => { ]) }) }) - -describe('whether a dead generation interrupted anything', () => { - async function seedIdlePendingApproval(): Promise { - await journal.appendItem( - { provider: 'codex', threadId: THREAD, turnId: 'turn-1', ordinal: 1 }, - { - kind: 'approval', - title: 'Run command?', - detail: null, - options: [{ id: 'yes', label: 'Allow' }], - resolution: { state: 'pending', selectedOptionId: null, resolvedBy: null, resolvedAt: null } - }, - { fence: 7, turnScope: AGENT_JOURNAL_THREAD_SCOPE } - ) - await journal.appendItem( - { provider: 'codex', threadId: THREAD, turnId: 'turn-1', ordinal: 2 }, - { kind: 'turn', turnId: 'turn-1', state: 'completed', startedAt: 900, completedAt: 950 }, - { fence: 7, turnScope: AGENT_JOURNAL_THREAD_SCOPE } - ) - } - - it('says nothing was interrupted when the provider died waiting on an approval', async () => { - await seedIdlePendingApproval() - const before = captureUnfinishedStructuredAgentSessionWork(journal) - - expect(unfinishedStructuredAgentSessionWorkWasInterrupted(before, journal, 1_000)).toBe(false) - }) - - it('still reports an interruption when a turn was running', async () => { - await seedUnfinishedWork() - const before = captureUnfinishedStructuredAgentSessionWork(journal) - - expect(unfinishedStructuredAgentSessionWorkWasInterrupted(before, journal, 1_000)).toBe(true) - }) -}) diff --git a/src/main/native-chat/agent-session-wire/termination/testing/structured-agent-session-eviction.test.ts b/src/main/native-chat/agent-session-wire/termination/testing/structured-agent-session-eviction.test.ts index 3194e3b67c0..6a41f0600db 100644 --- a/src/main/native-chat/agent-session-wire/termination/testing/structured-agent-session-eviction.test.ts +++ b/src/main/native-chat/agent-session-wire/termination/testing/structured-agent-session-eviction.test.ts @@ -1,41 +1,9 @@ -import { describe, expect, it, vi } from 'vitest' -import { - evictStructuredAgentSession, - StructuredAgentSessionEvictionError, - STRUCTURED_AGENT_SESSION_EVICTION_STEPS, - type StructuredAgentSessionEvictionContext -} from '../structured-agent-session-eviction' +import { describe, expect, it } from 'vitest' +import { evictStructuredAgentSession } from '../structured-agent-session-eviction' import { StructuredAgentSessionHostRuntimeState } from '../../host/structured-agent-session-host-runtime-state' import { withJournalQueueMembers } from '../../journal-ingress/testing/structured-agent-session-journal-double-test-support' -import { createStructuredAgentSessionLogger } from '../../host/structured-agent-session-logger' import { recordingStructuredAgentSessionLogger } from '../../host/testing/structured-agent-session-logger-test-support' -function context(closeError?: Error): StructuredAgentSessionEvictionContext & { order: string[] } { - const order: string[] = [] - return { - order, - sessionId: 'session-1', - logger: createStructuredAgentSessionLogger(), - eventSink: { - unbind: vi.fn(() => order.push('unbind')), - drained: vi.fn(async () => { - order.push('drained') - return { ok: true as const } - }), - close: vi.fn(() => { - order.push('close') - if (closeError) { - throw closeError - } - }) - }, - acknowledgeRelease: vi.fn(() => { - order.push('acknowledgeRelease') - }), - discardSink: vi.fn(() => order.push('discardSink')) - } -} - function runtimeState(): StructuredAgentSessionHostRuntimeState { // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: eviction against the sink cache reads only the sinks and the logger; store and adapter are never reached. const deps = { @@ -46,36 +14,6 @@ function runtimeState(): StructuredAgentSessionHostRuntimeState { return new StructuredAgentSessionHostRuntimeState(deps, new Map()) } -describe("the route release after a child's exit", () => { - it('lets the sink go before it acknowledges the release', async () => { - const ctx = context() - await evictStructuredAgentSession(ctx) - expect(ctx.order).toEqual(['unbind', 'close', 'discardSink', 'acknowledgeRelease']) - }) - - it('names every step, so a failure says which one it was', () => { - expect(STRUCTURED_AGENT_SESSION_EVICTION_STEPS.map((step) => step.name)).toEqual([ - 'stop-publishing', - 'close-sink', - 'discard-sink', - 'acknowledge-release' - ]) - }) - - it('reports a failed step with its name and still runs every step after it', async () => { - const recorded = recordingStructuredAgentSessionLogger() - const ctx = { ...context(new Error('sink already gone')), logger: recorded.logger } - - await expect(evictStructuredAgentSession(ctx)).resolves.toBeUndefined() - - expect(ctx.order).toEqual(['unbind', 'close', 'discardSink', 'acknowledgeRelease']) - expect(recorded.entries.map((entry) => entry.fields.error)).toEqual([ - expect.objectContaining({ step: 'close-sink' }) - ]) - expect(recorded.entries[0]?.fields.error).toBeInstanceOf(StructuredAgentSessionEvictionError) - }) -}) - // Cached sinks must be discarded after close so reopened chats can publish through fresh sinks. describe('eviction against the real sink cache', () => { it('lets the session publish again after it is evicted and reattached', async () => { diff --git a/src/main/native-chat/agent-session-wire/termination/testing/structured-agent-session-host-teardown.test.ts b/src/main/native-chat/agent-session-wire/termination/testing/structured-agent-session-host-teardown.test.ts deleted file mode 100644 index eb267a070bb..00000000000 --- a/src/main/native-chat/agent-session-wire/termination/testing/structured-agent-session-host-teardown.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { describe, expect, it, vi } from 'vitest' - -import { structuredAgentSessionHostTeardownPhases } from '../structured-agent-session-host-teardown' - -import { recordingStructuredAgentSessionLogger } from '../../host/testing/structured-agent-session-logger-test-support' - -describe('structured agent-session host teardown', () => { - it('bounds stalled recovery publication without preventing later cleanup', async () => { - vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] }) - const log = recordingStructuredAgentSessionLogger() - const pending = Promise.withResolvers() - const cleaned = vi.fn(async () => {}) - const flush = vi.fn(async () => cleaned()) - const phases = structuredAgentSessionHostTeardownPhases({ - logger: log.logger, - idleSweep: { dispose: cleaned }, - runtimeState: { - stopLeaseRenewal: () => {}, - flushAllEventSinks: flush, - acquireAborts: { abortAll: () => {} } - }, - tasks: { drainAttaches: cleaned }, - evictOwnedSessions: cleaned, - beginResumeMarkers: () => {}, - recordResumeMarkers: () => pending.promise - }) - try { - const teardown = (async () => { - for (const phase of phases) { - await phase.run() - } - })() - await vi.advanceTimersByTimeAsync(2000) - await teardown - expect(cleaned).toHaveBeenCalledTimes(4) - expect(log.scopes()).toEqual(['teardown-recovery-capsule']) - expect(vi.getTimerCount()).toBe(0) - } finally { - pending.resolve() - vi.useRealTimers() - } - }) -}) diff --git a/src/main/native-chat/agent-session-wire/termination/testing/structured-agent-session-reasoning-sweep.test.ts b/src/main/native-chat/agent-session-wire/termination/testing/structured-agent-session-reasoning-sweep.test.ts deleted file mode 100644 index 58f044af7b4..00000000000 --- a/src/main/native-chat/agent-session-wire/termination/testing/structured-agent-session-reasoning-sweep.test.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { mkdtemp, rm } from 'node:fs/promises' -import { tmpdir } from 'node:os' -import { join } from 'node:path' -import { afterEach, beforeEach, describe, expect, it } from 'vitest' -import { - AGENT_JOURNAL_THREAD_SCOPE, - type AgentJournalItemBody -} from '../../../../../shared/agent-session/journal/agent-session-journal-types' -import { codexProviderHandle } from '../../../../../shared/agent-session/lifecycle/agent-session-provider-handle-encoding' -import { openAgentSessionJournal } from '../../../agent-session-journal/journal-store-factory' -import type { AgentSessionJournal } from '../../../agent-session-journal/journal-store' -import { - closeTestJournalHostDatabases, - openTestJournalHostDatabase -} from '../../../agent-session-journal/store-lifecycle/journal-host-database-test-support' -import '../structured-agent-session-dead-generation-settlement' -import { settleStaleStructuredAgentSessionState } from '../structured-agent-session-stale-state-settlement' -import { settleObservedExitForTest } from './structured-agent-session-observed-exit.test-fixture' - -const SESSION = 'session-reasoning-sweep' -const THREAD = 'thread-1' -const REASONING = { provider: 'codex', threadId: THREAD, turnId: 'turn-1', ordinal: 1 } as const -let root: string -let journal: AgentSessionJournal - -beforeEach(async () => { - root = await mkdtemp(join(tmpdir(), 'orca-reasoning-sweep-')) - journal = await openAgentSessionJournal({ - identity: { - sessionId: SESSION, - workspaceId: 'workspace-1', - hostId: 'local', - agent: 'codex', - providerHandle: codexProviderHandle(THREAD) - }, - database: openTestJournalHostDatabase(root), - now: () => 1_000 - }) -}) - -afterEach(async () => { - await journal.close() - await closeTestJournalHostDatabases() - await rm(root, { recursive: true, force: true }) -}) - -async function seedOpenReasoning(turnState: 'running' | 'completed'): Promise { - await journal.appendItem( - REASONING, - { - kind: 'message', - role: 'reasoning', - blocks: [{ type: 'text', text: 'Weighing options' }], - state: 'running' - }, - { fence: 7, turnScope: AGENT_JOURNAL_THREAD_SCOPE } - ) - await journal.appendItem( - { provider: 'codex', threadId: THREAD, turnId: 'turn-1', ordinal: 2 }, - { kind: 'turn', turnId: 'turn-1', state: turnState, startedAt: 900 }, - { fence: 7, turnScope: AGENT_JOURNAL_THREAD_SCOPE } - ) -} - -function reasoningBody(): AgentJournalItemBody | undefined { - return journal - .snapshot() - .items.find((item) => item.body.kind === 'message' && item.body.role === 'reasoning')?.body -} - -describe('an open reasoning row a dead generation left', () => { - it('ends, with no claimed time, when the generation is settled at its exit', async () => { - await seedOpenReasoning('running') - await expect( - settleObservedExitForTest({ - journal, - sessionId: SESSION, - fence: 8, - settlementId: `expected-close:${SESSION}:8`, - pendingSubmissionReason: 'provider_closed_before_acknowledgement', - verdict: { state: 'interrupted', completedAt: 1_500 }, - showUnexpectedExitOutcome: false - }) - ).resolves.toMatchObject({ ok: true }) - expect(reasoningBody()).toEqual({ - kind: 'message', - role: 'reasoning', - blocks: [{ type: 'text', text: 'Weighing options' }], - state: 'completed' - }) - }) - - it('ends when a later acquisition or reopen sweeps the stale generation', async () => { - await seedOpenReasoning('running') - await settleStaleStructuredAgentSessionState({ - journal, - sessionId: SESSION, - fence: 8, - acquisitionGeneration: 'generation-8', - deathEvidence: null - }) - expect(reasoningBody()).toMatchObject({ state: 'completed' }) - expect(reasoningBody()).not.toHaveProperty('completedAt') - }) -}) diff --git a/src/main/native-chat/agent-session-wire/termination/testing/structured-agent-session-stale-turn-verdict.test.ts b/src/main/native-chat/agent-session-wire/termination/testing/structured-agent-session-stale-turn-verdict.test.ts deleted file mode 100644 index 4cf2a1e11aa..00000000000 --- a/src/main/native-chat/agent-session-wire/termination/testing/structured-agent-session-stale-turn-verdict.test.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { expect, it } from 'vitest' -import { agentJournalItemKey } from '../../../../../shared/agent-session/journal/agent-session-journal-item-key' -import { - runningTurnLifecycleRevisions, - turnVerdictFromDeathEvidence -} from '../structured-agent-session-stale-turn-verdict' -import { - RUNNING_IDENTITY, - legacyLifecycleItem, - lifecycleItem, - verdictForTurn -} from './structured-agent-session-stale-verdict-contract-test-fixture' - -it.each(['pid-absent', 'identity-mismatch'] as const)( - 'ends a %s proof at the last proof of life, never after the probe', - (kind) => { - const proof = (lastProvenAliveAt?: number) => ({ - kind, - detail: 'gone', - observedAt: 9_000, - ...(lastProvenAliveAt === undefined ? {} : { lastProvenAliveAt }) - }) - // Last proof of life bounds completion so downtime is never counted as work. - expect(verdictForTurn(proof(8_000))).toEqual({ state: 'interrupted', completedAt: 8_000 }) - expect(verdictForTurn(proof(9_500))).toEqual({ state: 'interrupted', completedAt: 9_000 }) - expect(verdictForTurn(proof())).toEqual({ state: 'interrupted', completedAt: 9_000 }) - } -) - -it('ends a watched exit at the exit even when a renewal is recorded', () => { - expect( - verdictForTurn({ - kind: 'exit-observed', - detail: 'exit', - observedAt: 500, - lastProvenAliveAt: 400 - }) - ).toEqual({ state: 'interrupted', completedAt: 500 }) -}) - -it.each([0, -1, Number.NaN, Number.POSITIVE_INFINITY])( - 'keeps proven exit without an invalid time %s', - (observedAt) => { - expect(verdictForTurn({ kind: 'exit-observed', detail: '', observedAt })).toEqual({ - state: 'interrupted' - }) - } -) - -it('keeps the rule an older build applied to evidence that names no owner', () => { - // Legacy ownerless evidence proves only observed exit, never probe results. - const legacy = { detail: 'gone', observedAt: 9_000, lastProvenAliveAt: 8_000 } - expect(turnVerdictFromDeathEvidence({ ...legacy, kind: 'exit-observed' }, 1)).toEqual({ - state: 'interrupted', - completedAt: 9_000 - }) - expect(turnVerdictFromDeathEvidence({ ...legacy, kind: 'pid-absent' }, 1)).toEqual({ - state: 'unverifiable' - }) -}) - -it('gives a turn no end from evidence about any other owner', () => { - const proof = { - kind: 'pid-absent' as const, - detail: 'gone', - observedAt: 9_000, - lastProvenAliveAt: 8_000 - } - // Another owner’s death or unknown turn provenance cannot prove this owner ended. - for (const [evidence, turnFence] of [ - [{ ...proof, ownerFence: 3 }, 1], - [{ ...proof, kind: 'exit-observed' as const, ownerFence: 3 }, 1], - [{ ...proof, ownerFence: 1 }, undefined] - ] as const) { - expect(turnVerdictFromDeathEvidence(evidence, turnFence)).toEqual({ state: 'unverifiable' }) - } -}) - -it('never ends a turn before it began, when the last proof of life predates it', () => { - const item = lifecycleItem('turn-2', 'running', 2, { startedAt: 500 }) - expect( - runningTurnLifecycleRevisions([item], { state: 'interrupted', completedAt: 300 }) - ).toMatchObject([{ body: { kind: 'turn', state: 'interrupted', completedAt: 500 } }]) -}) - -it('revises a legacy status-form running row from an older host into a typed turn', () => { - expect( - runningTurnLifecycleRevisions([legacyLifecycleItem('turn-2', 30)], { state: 'unverifiable' }) - ).toEqual([ - { - kind: 'item', - itemId: agentJournalItemKey(RUNNING_IDENTITY), - body: { kind: 'turn', turnId: 'turn-2', state: 'unverifiable', startedAt: 30 }, - turnScope: { kind: 'thread' } - } - ]) -}) diff --git a/src/main/native-chat/agent-session-wire/termination/testing/structured-agent-session-stale-verdict-persistence.test.ts b/src/main/native-chat/agent-session-wire/termination/testing/structured-agent-session-stale-verdict-persistence.test.ts index 887bb91b263..37ab608e6f9 100644 --- a/src/main/native-chat/agent-session-wire/termination/testing/structured-agent-session-stale-verdict-persistence.test.ts +++ b/src/main/native-chat/agent-session-wire/termination/testing/structured-agent-session-stale-verdict-persistence.test.ts @@ -2,7 +2,6 @@ import { mkdtemp, rm } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { expect, it } from 'vitest' -import type { AgentSessionDeathEvidence } from '../../../../../shared/agent-session/lifecycle/agent-session-death-evidence' import { readAgentJournalTurn } from '../../../../../shared/agent-session/lifecycle/agent-session-turn-record' import { createTrackedJournalOpener, @@ -19,65 +18,6 @@ import { turnScopeOf } from './structured-agent-session-stale-verdict-contract-test-fixture' -it('revises an unverifiable turn only from a proof naming its own owner, and only once', async () => { - const root = await mkdtemp(join(tmpdir(), 'orca-stale-session-')) - const journals = createTrackedJournalOpener({ legacyInputs: true }) - let now = 100 - try { - const journal = await journals.open({ - identity: { - sessionId: 'session-1', - workspaceId: 'workspace-1', - hostId: 'local', - agent: 'codex', - providerHandle: codexProviderHandle(THREAD) - }, - stateDirectory: root, - now: () => now - }) - await journal.appendItem( - RUNNING_IDENTITY, - { kind: 'turn', turnId: 'turn-2', state: 'running', startedAt: 100 }, - { fence: 1, turnScope: turnScopeOf(RUNNING_IDENTITY) } - ) - now = 400 - const settle = (deathEvidence: AgentSessionDeathEvidence | null) => - settleStaleStructuredAgentSessionState({ - journal, - sessionId: 'session-1', - fence: 2, - acquisitionGeneration: null, - deathEvidence - }) - await settle(null) - now = 9_000 - const turn = () => journal.snapshot().items.map((item) => readAgentJournalTurn(item.body))[0] - expect(turn()).toMatchObject({ state: 'unverifiable' }) - - const unrevised = journal.cursor() - await expect(settle({ ...PROVEN, ownerFence: 2 })).resolves.toBe(0) - const { ownerFence: _ownerFence, ...olderBuildProof } = PROVEN - await expect(settle({ ...olderBuildProof, kind: 'exit-observed' })).resolves.toBe(0) - expect(journal.cursor()).toEqual(unrevised) - expect(turn()).toMatchObject({ state: 'unverifiable' }) - - await expect(settle(PROVEN)).resolves.toBe(2) - expect(turn()).toEqual({ - turnId: 'turn-2', - state: 'interrupted', - startedAt: 100, - completedAt: 100 - }) - const revised = journal.cursor() - await expect(settle(PROVEN)).resolves.toBe(0) - expect(journal.cursor()).toEqual(revised) - expect(journal.snapshot().items.filter((item) => item.body.kind === 'status')).toHaveLength(1) - } finally { - await journals.closeAll() - await rm(root, { recursive: true, force: true }) - } -}) - it('adds one row for a death however many attempts its settle takes to write', async () => { const root = await mkdtemp(join(tmpdir(), 'orca-stale-session-')) const journals = createTrackedJournalOpener({ legacyInputs: true }) diff --git a/src/main/native-chat/native-chat-visuals-sweep.test.ts b/src/main/native-chat/native-chat-visuals-sweep.test.ts index 0cd825679b5..0b7375771d2 100644 --- a/src/main/native-chat/native-chat-visuals-sweep.test.ts +++ b/src/main/native-chat/native-chat-visuals-sweep.test.ts @@ -17,7 +17,6 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import type { StructuredAgentSessionLogger } from './agent-session-wire/host/structured-agent-session-logger' import { nativeChatScratchFolderFor, nativeChatScratchRootFor } from './native-chat-scratch-folder' import { - scheduleNativeChatVisualsSweep, sweepNativeChatVisualsFolders, type NativeChatVisualsSweepDeps } from './native-chat-visuals-sweep' @@ -51,7 +50,7 @@ function age(path: string, daysAgo: number): void { age(join(path, name), daysAgo) } } - const at = new Date(Date.now() - daysAgo * DAY_MS) + const at = new Date(Date.UTC(2020, 0, 1) - daysAgo * DAY_MS) utimesSync(path, at, at) } @@ -69,20 +68,6 @@ function deps( } describe('the visuals folder sweep', () => { - it('removes a folder no chat record maps to and keeps every held chat', async () => { - const state = stateDirectory() - const live = visualsFor(state, 'chat-live') - const unreadable = visualsFor(state, 'chat-unreadable-row') - const orphan = visualsFor(state, 'chat-deleted') - const result = await sweepNativeChatVisualsFolders( - deps(state, ['chat-live', 'chat-unreadable-row']) - ) - expect(result).toEqual({ removed: 1, failed: 0 }) - expect(existsSync(live)).toBe(true) - expect(existsSync(unreadable)).toBe(true) - expect(existsSync(orphan)).toBe(false) - }) - it('keeps a held chat folder however long it went unused, and retains one set aside for startup recovery', async () => { const state = stateDirectory() const idle = visualsFor(state, 'chat-idle') @@ -130,23 +115,6 @@ describe('the visuals folder sweep', () => { expect(existsSync(join(target, 'keep.txt'))).toBe(true) }) - it('runs after startup and again later, and stops for good', async () => { - const state = stateDirectory() - const orphan = visualsFor(state, 'chat-z') - vi.useFakeTimers() - const listHeldSessionIds = vi.fn((): string[] => []) - const stop = scheduleNativeChatVisualsSweep({ ...deps(state, []), listHeldSessionIds }) - await vi.advanceTimersByTimeAsync(60_000) - expect(listHeldSessionIds).not.toHaveBeenCalled() - await vi.advanceTimersByTimeAsync(60_000) - await vi.waitFor(() => expect(existsSync(orphan)).toBe(false)) - expect(listHeldSessionIds).toHaveBeenCalledTimes(1) - await vi.advanceTimersByTimeAsync(6 * 60 * 60_000) - await vi.waitFor(() => expect(listHeldSessionIds).toHaveBeenCalledTimes(2)) - stop() - expect(vi.getTimerCount()).toBe(0) - }) - it('touches nothing under a visuals root that is a symlink', async () => { const state = stateDirectory() const elsewhere = mkdtempSync(join(tmpdir(), 'orca-visuals-elsewhere-')) diff --git a/src/main/opencode/hook-service.test.ts b/src/main/opencode/hook-service.test.ts index 4712b709171..d61edec5bf6 100644 --- a/src/main/opencode/hook-service.test.ts +++ b/src/main/opencode/hook-service.test.ts @@ -16,7 +16,6 @@ import { } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' -import { pathToFileURL } from 'node:url' import { setAppEnvironment } from '../../shared/app-environment' const { getPathMock } = vi.hoisted(() => ({ @@ -46,49 +45,11 @@ beforeEach(() => { afterEach(() => vi.unstubAllEnvs()) -const { isUsableId, toSafeDirName } = _internals - -describe('OpenCode id safety guard', () => { - it('accepts the daemon-path sessionId shape (worktreeId@@uuid with ::/...)', () => { - const daemonSessionId = - '50c010a2-bc8e-4eb1-8847-5812133ad6df::/Users/thebr/ghostx/workspaces/noqa/autoheal@@a1b2c3d4' - expect(isUsableId(daemonSessionId)).toBe(true) - }) - - it('accepts ids at the inclusive upper length bound', () => { - expect(isUsableId('x'.repeat(1024))).toBe(true) - }) - - it('rejects empty or oversized ids', () => { - expect(isUsableId('')).toBe(false) - expect(isUsableId('x'.repeat(1025))).toBe(false) - }) - - it('rejects non-string runtime values even though the type says string', () => { - expect(isUsableId(undefined as unknown as string)).toBe(false) - expect(isUsableId(null as unknown as string)).toBe(false) - expect(isUsableId(42 as unknown as string)).toBe(false) - }) - - it('derives a filesystem-safe directory name independent of the raw id', () => { - const name = toSafeDirName('50c010::/Users/thebr/x/y@@uuid') - expect(name).toMatch(/^[0-9a-f]{32}$/) - }) - - it('is stable across calls for the same id', () => { - const id = 'some-session-id' - expect(toSafeDirName(id)).toBe(toSafeDirName(id)) - }) - - it('produces different names for different ids', () => { - expect(toSafeDirName('a')).not.toBe(toSafeDirName('b')) - }) -}) +const { toSafeDirName } = _internals describe('OpenCodeHookService buildPtyEnv / clearPty round-trip', () => { const daemonSessionId = '50c010a2-bc8e-4eb1-8847-5812133ad6df::/Users/thebr/ghostx/workspaces/noqa/autoheal@@a1b2c3d4' - const plainUuidId = 'c0ffee00-0000-4000-8000-000000000000' let userDataDir: string const originalXdgConfigHome = process.env.XDG_CONFIG_HOME @@ -200,56 +161,6 @@ describe('OpenCodeHookService buildPtyEnv / clearPty round-trip', () => { } }) - it('installs a plugin whose default export satisfies both the v1 and v2 loaders', async () => { - const service = new OpenCodeHookService() - service.buildPtyEnv(daemonSessionId) - - const pluginPath = join(resolveOpenCodeConfigDirectory(), 'plugins', 'orca-opencode-status.js') - // Use .mjs to load installed ESM without a package.json. - const modulePath = join(userDataDir, `installed-opencode-plugin-${Date.now()}.mjs`) - writeFileSync(modulePath, readFileSync(pluginPath, 'utf8')) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the assertions below validate the shape this names. - const module = (await import(pathToFileURL(modulePath).href)) as { - default?: { id?: unknown; server?: unknown; setup?: unknown } - } - - expect(module.default?.id).toBe('orca-opencode-status') - expect(module.default?.server).toBeTypeOf('function') - expect(module.default?.setup).toBeTypeOf('function') - }) - - // Pre-1.4.209 installs left server-only plugins in this retired but still-used directory. - it('refreshes a stale plugin left in the retired shared hooks dir', async () => { - const legacyPluginPath = join( - userDataDir, - 'opencode-hooks', - 'shared', - 'plugins', - 'orca-opencode-status.js' - ) - mkdirSync(join(legacyPluginPath, '..'), { recursive: true }) - writeFileSync( - legacyPluginPath, - 'export default { id: "orca-opencode-status", server: async () => ({}) };\n' - ) - - new OpenCodeHookService().buildPtyEnv(daemonSessionId) - - const modulePath = join(userDataDir, `legacy-opencode-plugin-${Date.now()}.mjs`) - writeFileSync(modulePath, readFileSync(legacyPluginPath, 'utf8')) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the assertions below validate the shape this names. - const module = (await import(pathToFileURL(modulePath).href)) as { - default?: { id?: unknown; server?: unknown; setup?: unknown } - } - expect(module.default?.id).toBe('orca-opencode-status') - expect(module.default?.server).toBeTypeOf('function') - expect(module.default?.setup).toBeTypeOf('function') - // Services stand down only when their TUI copy exists alongside them. - expect( - readFileSync(join(legacyPluginPath, '..', 'orca-opencode-status-tui', 'tui.js'), 'utf8') - ).toContain('tui: setupLegacyOpenCodeTui') - }) - it('repairs late and overwritten legacy plugins atomically on the same service', () => { const service = new OpenCodeHookService() service.refreshLegacySharedPlugin() @@ -308,71 +219,6 @@ describe('OpenCodeHookService buildPtyEnv / clearPty round-trip', () => { new OpenCodeHookService().buildPtyEnv(daemonSessionId) expect(statSync(legacyPluginPath).mtimeMs).toBe(past.getTime()) }) - - // OpenCode 2 disables later plugins with duplicate IDs; verify installed artifacts. - it('installs both family plugins into one config dir under distinct ids', async () => { - expect(new OpenCodeHookService().buildPtyEnv(daemonSessionId)).toEqual({}) - expect(openCode2HookService.buildPtyEnv(daemonSessionId)).toEqual({}) - - const pluginsDir = join(resolveOpenCodeConfigDirectory(), 'plugins') - const ids: string[] = [] - for (const fileName of ['orca-opencode-status.js', 'orca-opencode2-status.js']) { - // Use .mjs to load installed ESM without a package.json. - const modulePath = join(userDataDir, `installed-${fileName}-${Date.now()}.mjs`) - writeFileSync(modulePath, readFileSync(join(pluginsDir, fileName), 'utf8')) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the assertions below validate the shape this names. - const module = (await import(pathToFileURL(modulePath).href)) as { - default?: { id?: unknown } - } - ids.push(String(module.default?.id)) - } - - expect(ids).toEqual(['orca-opencode-status', 'orca-opencode2-status']) - }) - - it('clearPty leaves the shared OpenCode config dir off the teardown hot path', () => { - const service = new OpenCodeHookService() - service.buildPtyEnv(daemonSessionId) - const configDir = resolveOpenCodeConfigDirectory() - expect(existsSync(configDir)).toBe(true) - mkdirSync(join(configDir, 'node_modules', 'opencode-runtime'), { recursive: true }) - writeFileSync(join(configDir, 'node_modules', 'opencode-runtime', 'index.js'), '') - - service.clearPty(daemonSessionId) - expect(existsSync(configDir)).toBe(true) - expect(existsSync(join(configDir, 'node_modules', 'opencode-runtime', 'index.js'))).toBe(true) - }) - - it('buildPtyEnv returns {} for an unusable id and creates nothing on disk', () => { - const service = new OpenCodeHookService() - const hooksRoot = join(userDataDir, 'opencode-hooks') - const overlaysRoot = join(userDataDir, 'opencode-config-overlays') - - expect(service.buildPtyEnv('')).toEqual({}) - expect(existsSync(hooksRoot)).toBe(false) - expect(existsSync(overlaysRoot)).toBe(false) - }) - - it('buildPtyEnv preserves a user-set OPENCODE_CONFIG_DIR when the id is unusable', () => { - const service = new OpenCodeHookService() - const userDir = mkdtempSync(join(tmpdir(), 'orca-opencode-userdir-')) - try { - expect(service.buildPtyEnv('', userDir)).toEqual({ OPENCODE_CONFIG_DIR: userDir }) - } finally { - rmSync(userDir, { recursive: true, force: true }) - } - }) - - it('works end-to-end for a plain UUID id (non-daemon path)', () => { - const service = new OpenCodeHookService() - service.buildPtyEnv(plainUuidId) - - const configDir = resolveOpenCodeConfigDirectory() - expect(existsSync(join(configDir, 'plugins', 'orca-opencode-status.js'))).toBe(true) - - service.clearPty(plainUuidId) - expect(existsSync(configDir)).toBe(true) - }) }) describe('OpenCodeHookService overlay mode (user OPENCODE_CONFIG_DIR set)', () => { @@ -444,22 +290,6 @@ describe('OpenCodeHookService overlay mode (user OPENCODE_CONFIG_DIR set)', () = expectUserConfigIntact() }) - it.skipIf(process.platform === 'win32')( - 'mirrors top-level entries via symlinks so plugins/ is a real directory', - () => { - // Only plugins needs per-entry links; other links preserve live user edits. - const service = new OpenCodeHookService() - const env = service.buildPtyEnv(ptyId, userConfigDir) - - const overlay = env.OPENCODE_CONFIG_DIR! - expect(lstatSync(join(overlay, 'opencode.json')).isSymbolicLink()).toBe(true) - expect(lstatSync(join(overlay, 'auth.json')).isSymbolicLink()).toBe(true) - expect(lstatSync(join(overlay, 'plugins')).isDirectory()).toBe(true) - expect(lstatSync(join(overlay, 'plugins')).isSymbolicLink()).toBe(false) - expect(lstatSync(join(overlay, 'plugins', 'user-plugin.js')).isSymbolicLink()).toBe(true) - } - ) - it("does not overwrite a user plugin file with the same filename as Orca's plugin", () => { // Mirroring this filename would let plugin installation overwrite the user’s file. const userOrcaSentinel = 'USER OWNED ORCA-NAMED PLUGIN — DO NOT CLOBBER' @@ -527,22 +357,6 @@ describe('OpenCodeHookService overlay mode (user OPENCODE_CONFIG_DIR set)', () = } ) - it("preserves the user's OPENCODE_CONFIG_DIR when the path does not exist", () => { - // Preserve typo paths so OpenCode reports them instead of silently hiding user config. - const service = new OpenCodeHookService() - const missingPath = join(tmpdir(), `orca-opencode-nope-${Date.now()}`) - expect(existsSync(missingPath)).toBe(false) - - const env = service.buildPtyEnv(ptyId, missingPath) - expect(env).toEqual({ OPENCODE_CONFIG_DIR: missingPath }) - expect(existsSync(missingPath)).toBe(false) - expect( - existsSync( - join(userDataDir, 'opencode-config-overlays', toSafeDirName(`source:${missingPath}`)) - ) - ).toBe(false) - }) - it("preserves the user's OPENCODE_CONFIG_DIR when the mirror step fails", async () => { const overlayMirror = await import('../pty/scrollback/overlay-mirror') const mirrorSpy = vi.spyOn(overlayMirror, 'mirrorEntry').mockImplementation(() => { @@ -565,53 +379,6 @@ describe('OpenCodeHookService overlay mode (user OPENCODE_CONFIG_DIR set)', () = } }) - it.skipIf(process.platform === 'win32')( - 'clearPty leaves the source overlay off the teardown hot path', - () => { - // Runtime files can make recursive overlay deletion block Electron main. - const service = new OpenCodeHookService() - service.buildPtyEnv(ptyId, userConfigDir) - - const overlayDir = join( - userDataDir, - 'opencode-config-overlays', - toSafeDirName(`source:${userConfigDir}`) - ) - expect(existsSync(overlayDir)).toBe(true) - - service.clearPty(ptyId) - - expect(existsSync(overlayDir)).toBe(true) - expectUserConfigIntact() - expect(readdirSync(join(userConfigDir, 'plugins'))).toEqual(['user-plugin.js']) - } - ) - - it('rebuilding the overlay for the same ptyId does not corrupt the user dir', () => { - const service = new OpenCodeHookService() - service.buildPtyEnv(ptyId, userConfigDir) - service.buildPtyEnv(ptyId, userConfigDir) - const env = service.buildPtyEnv(ptyId, userConfigDir) - - expect( - readFileSync(join(env.OPENCODE_CONFIG_DIR!, 'plugins', 'orca-opencode-status.js'), 'utf8') - ).toContain('OrcaOpenCodeStatusPlugin') - expectUserConfigIntact() - }) - - it('leaves a current overlay plugin file in place across spawns', () => { - const service = new OpenCodeHookService() - const overlayDir = service.buildPtyEnv(ptyId, userConfigDir).OPENCODE_CONFIG_DIR! - const pluginPath = join(overlayDir, 'plugins', 'orca-opencode-status.js') - const past = new Date('2020-01-01T00:00:00Z') - utimesSync(pluginPath, past, past) - - service.buildPtyEnv(ptyId, userConfigDir) - - expect(statSync(pluginPath).mtimeMs).toBe(past.getTime()) - expect(readFileSync(pluginPath, 'utf8')).toBe(_internals.getOpenCodePluginSource()) - }) - it('reconciles stale mirrored entries while preserving OpenCode runtime files', () => { const service = new OpenCodeHookService() const firstEnv = service.buildPtyEnv(ptyId, userConfigDir) diff --git a/src/main/opencode/opencode-owned-text-task.test.ts b/src/main/opencode/opencode-owned-text-task.test.ts index 63d9f008dd9..36d66fa169d 100644 --- a/src/main/opencode/opencode-owned-text-task.test.ts +++ b/src/main/opencode/opencode-owned-text-task.test.ts @@ -6,7 +6,7 @@ import type { SourceControlAgentSpawnInput, SpawnedSourceControlAgentProcess } from '../text-generation/source-control-text-generation-types' -import { OpenCodeTextTaskError, runOwnedOpenCodeTextTask } from './opencode-owned-text-task' +import { runOwnedOpenCodeTextTask } from './opencode-owned-text-task' import { runOpenCodeTextTaskSession } from './opencode-text-task-session' import type * as TextTaskSession from './opencode-text-task-session' @@ -82,72 +82,6 @@ function task(cli: ReturnType, signal = new AbortController } describe('runOwnedOpenCodeTextTask', () => { - it('runs the selected v2 choices on its own stdio server and returns after that server exits', async () => { - const cli = fakeOpenCode('2.0.14') - vi.mocked(runOpenCodeTextTaskSession).mockImplementationOnce(async () => { - expect(cli.children[1].exitCode).toBeNull() - return 'Harbor Lantern' - }) - const base = task(cli) - const input = { ...base, runArgs: [...base.runArgs, '--model', 'opencode/big-pickle#high'] } - - await expect(runOwnedOpenCodeTextTask(input)).resolves.toBe('Harbor Lantern') - - const server = cli.children[1] - expect(server.input.args).toEqual(['--print-logs', 'serve', '--stdio', '--port', '0']) - expect(server.input.cwd).toBe(input.cwd) - expect(server.input.env).toMatchObject({ - OPENCODE_CONFIG_CONTENT: '{"provider":{}}', - OPENCODE_PASSWORD: expect.any(String) - }) - expect(vi.mocked(runOpenCodeTextTaskSession).mock.calls[0][0]).toMatchObject({ - version: 'v2', - url: 'http://127.0.0.1:4096', - directory: input.cwd, - prompt: 'Name this chat', - agent: 'title', - model: { providerID: 'opencode', modelID: 'big-pickle' }, - variant: 'high' - }) - expect(server.exitCode).toBe(0) - expect(cli.stop).not.toHaveBeenCalled() - }) - - it('stops a v1 foreground server on cancel and rejects only once it has exited', async () => { - const cli = fakeOpenCode('1.18.31') - const controller = new AbortController() - vi.mocked(runOpenCodeTextTaskSession).mockImplementationOnce(async ({ signal }) => { - controller.abort(new Error('canceled by user')) - throw signal.reason - }) - - await expect(runOwnedOpenCodeTextTask(task(cli, controller.signal))).rejects.toThrow( - 'canceled by user' - ) - - const server = cli.children[1] - expect(server.input.args).toEqual([ - '--print-logs', - 'serve', - '--hostname', - '127.0.0.1', - '--port', - '0' - ]) - expect(server.input.env).toHaveProperty('OPENCODE_SERVER_PASSWORD') - expect(cli.stop).toHaveBeenCalledWith(server) - expect(server.exitCode).toBe(143) - }) - - it('never starts a server for an executable whose protocol is unknown', async () => { - const cli = fakeOpenCode('opencode development build') - - const error = await runOwnedOpenCodeTextTask(task(cli)).catch((caught: unknown) => caught) - - expect(error).toBeInstanceOf(OpenCodeTextTaskError) - expect(error).toMatchObject({ stage: 'startup' }) - expect(cli.children.map((child) => child.input.args)).toEqual([['--print-logs', '--version']]) - }) it.each([ ['1.2.27', 'conversation-name', true], ['1.3.0-beta.1', 'commit-message', true], @@ -256,90 +190,3 @@ describe('runOwnedOpenCodeTextTask', () => { } ) }) - -it.each(['v1', 'v2'] as const)( - 'uses the selected external naming server’s protocol and URL (%s)', - async (version) => { - const server = createServer((request, response) => { - if (request.url !== (version === 'v1' ? '/prefix/global/health' : '/api/info')) { - response.statusCode = 400 - } - response.setHeader('content-type', 'application/json') - response.end( - JSON.stringify({ - version: version === 'v1' ? '1.2.27' : '2.0.14', - pid: 23, - urls: [], - paths: { tmp: '/tmp' } - }) - ) - }) - await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)) - try { - const address = server.address() - if (!address || typeof address === 'string') { - throw new Error('No endpoint') - } - const url = `http://127.0.0.1:${address.port}` - const cli = fakeOpenCode('1.18.31') - vi.mocked(runOpenCodeTextTaskSession).mockResolvedValueOnce('Harbor Lantern') - const input = { - ...task(cli), - operation: 'conversation-name' as const, - runArgs: [ - version === 'v1' ? '--attach' : '--server', - `${url}/prefix/`, - '--session', - 'saved', - '--model', - 'provider/model', - '--variant', - 'high' - ], - legacyVariantFlag: true - } - await expect(runOwnedOpenCodeTextTask(input)).resolves.toBe('Harbor Lantern') - expect(cli.spawn).not.toHaveBeenCalled() - expect(cli.stop).not.toHaveBeenCalled() - expect(vi.mocked(runOpenCodeTextTaskSession).mock.lastCall?.[0]).toMatchObject({ - version, - url: `${url}/prefix`, - sessionId: 'saved', - model: { providerID: 'provider', modelID: 'model' }, - variant: 'high', - exclusiveServer: false, - v1SessionPolicyFiltersTools: false - }) - if (version === 'v2') { - await expect( - runOwnedOpenCodeTextTask({ ...input, legacyVariantFlag: false }) - ).rejects.toMatchObject({ stage: 'startup' }) - } - await expect( - runOwnedOpenCodeTextTask({ ...input, operation: 'commit-message' }) - ).rejects.toMatchObject({ stage: 'startup' }) - } finally { - server.closeAllConnections() - await new Promise((resolve) => server.close(() => resolve())) - } - } -) - -it('suppresses implicit publication before owned startup while retaining explicit share configuration', async () => { - for (const share of [false, true]) { - const cli = fakeOpenCode('1.18.31') - vi.mocked(runOpenCodeTextTaskSession).mockResolvedValueOnce('Harbor Lantern') - const input = { - ...task(cli), - operation: 'conversation-name' as const, - runArgs: share ? ['--share'] : [], - env: { OPENCODE_DISABLE_SHARE: 'user-value', OPENCODE_CONFIG_CONTENT: '{"share":"auto"}' } - } - await expect(runOwnedOpenCodeTextTask(input)).resolves.toBe('Harbor Lantern') - expect(cli.children[1].input.env).toMatchObject({ - OPENCODE_DISABLE_SHARE: share ? 'user-value' : '1', - OPENCODE_CONFIG_CONTENT: '{"share":"auto"}' - }) - expect(cli.children[1].exitCode).not.toBeNull() - } -}) diff --git a/src/main/opencode/opencode-text-task-input.test.ts b/src/main/opencode/opencode-text-task-input.test.ts deleted file mode 100644 index bf5a8f69653..00000000000 --- a/src/main/opencode/opencode-text-task-input.test.ts +++ /dev/null @@ -1,278 +0,0 @@ -import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises' -import { tmpdir } from 'node:os' -import path from 'node:path' -import { pathToFileURL } from 'node:url' -import { afterEach, expect, it, vi } from 'vitest' -import { planCommitMessageGeneration } from '../../shared/source-control/ai/generation/commit-message-plan' -import { prepareOpenCodeTextTaskInput } from './opencode-text-task-input' - -const roots: string[] = [] -afterEach(async () => { - await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true }))) -}) -async function workspace() { - const root = await mkdtemp(path.join(tmpdir(), 'opencode-text-input-')) - roots.push(root) - await mkdir(path.join(root, 'nested')) - await writeFile(path.join(root, 'context.txt'), 'original context') - return root -} - -it('preserves recipe directory, relative attachment, positional input and global behavior', async () => { - const cwd = await workspace() - const planned = planCommitMessageGeneration( - { - agentId: 'opencode', - model: 'default', - agentCommandOverride: 'opencode --pure', - agentArgs: - '--dir nested --file ../context.txt --agent custom --title=Recipe -- "extra instructions"' - }, - 'generated input' - ) - if (!planned.ok || !planned.plan.openCodeTask) { - throw new Error('No OpenCode plan') - } - const input = await prepareOpenCodeTextTaskInput({ - version: 'v1', - cwd, - prompt: 'generated input', - runArgs: planned.plan.openCodeTask.runArgs, - signal: new AbortController().signal - }) - expect(input).toMatchObject({ - directory: path.join(cwd, 'nested'), - prompt: '"extra instructions"\ngenerated input', - serverArgs: ['--pure'], - agent: 'custom', - title: 'Recipe', - v1Files: [ - { - type: 'file', - filename: 'context.txt', - mime: 'text/plain', - url: pathToFileURL(path.join(cwd, 'context.txt')).href - } - ] - }) -}) - -it('projects v2 text files and binary attachments into the vendor native request shape', async () => { - const cwd = await workspace() - await writeFile(path.join(cwd, 'diagram.png'), Buffer.from([137, 80, 78, 71])) - const input = await prepareOpenCodeTextTaskInput({ - version: 'v2', - cwd, - prompt: 'generated input', - runArgs: [ - '--file', - 'context.txt', - '--file=diagram.png', - '--model=provider/model#low', - '--', - 'extra' - ], - signal: new AbortController().signal - }) - expect(input.prompt).toBe( - 'extra\ngenerated input\n\n\noriginal context\n' - ) - expect(input.v2Files).toEqual([{ name: 'diagram.png', uri: 'data:image/png;base64,iVBORw==' }]) - expect(input.model).toBe('provider/model#low') - await expect( - prepareOpenCodeTextTaskInput({ - version: 'v2', - cwd, - prompt: 'input', - runArgs: ['--dir', 'nested'], - signal: new AbortController().signal - }) - ).rejects.toThrow('unsupported') -}) - -it('retains wrapper operands outside the run parser and keeps command arguments distinct', async () => { - const cwd = await workspace() - await writeFile(path.join(cwd, 'notes.txt'), 'read-only context') - const planned = planCommitMessageGeneration( - { - agentId: 'opencode', - model: 'opencode/gpt-5.4-mini', - agentCommandOverride: - 'npx --yes opencode --model opencode/gpt-5.5 --log-level DEBUG --dir . --file notes.txt', - agentArgs: '--command review --agent build -- extra' - }, - 'generated input' - ) - if (!planned.ok || !planned.plan.openCodeTask) { - throw new Error('No OpenCode plan') - } - expect(planned.plan.openCodeTask.prefixArgs).toEqual(['--yes', 'opencode']) - const input = await prepareOpenCodeTextTaskInput({ - version: 'v1', - cwd, - prompt: 'generated input', - runArgs: planned.plan.openCodeTask.runArgs, - signal: new AbortController().signal - }) - expect(input).toMatchObject({ - command: 'review', - prompt: 'extra\ngenerated input', - agent: 'build', - model: 'opencode/gpt-5.5', - serverArgs: ['--log-level', 'DEBUG'], - v1Files: [ - { - type: 'file', - mime: 'text/plain', - filename: 'notes.txt', - url: expect.stringContaining('/notes.txt') - } - ] - }) -}) - -it('keeps a wrapper run operand attached to the selected executable', async () => { - const planned = planCommitMessageGeneration( - { - agentId: 'opencode', - model: 'default', - agentCommandOverride: 'pnpm run opencode' - }, - 'generated input' - ) - if (!planned.ok || !planned.plan.openCodeTask) { - throw new Error('No OpenCode plan') - } - expect(planned.plan.binary).toBe('pnpm') - expect(planned.plan.openCodeTask.prefixArgs).toEqual(['run', 'opencode']) - const input = await prepareOpenCodeTextTaskInput({ - version: 'v1', - cwd: await workspace(), - prompt: 'generated input', - runArgs: planned.plan.openCodeTask.runArgs, - signal: new AbortController().signal - }) - expect(input.prompt).toBe('generated input') -}) - -it('preserves the original OpenCode v2 variant compatibility without accepting it for other recipes', async () => { - const cwd = await workspace() - for (const model of ['provider/model', 'provider/model#high']) { - const input = await prepareOpenCodeTextTaskInput({ - version: 'v2', - cwd, - prompt: 'input', - legacyVariantFlag: true, - runArgs: ['--model', model, '--variant', 'low'], - signal: new AbortController().signal - }) - expect(input.model).toBe('provider/model#low') - expect(input.variant).toBeUndefined() - expect(input.serverArgs).toEqual([]) - } - await expect( - prepareOpenCodeTextTaskInput({ - version: 'v2', - cwd, - prompt: 'input', - runArgs: ['--model', 'provider/model', '--variant', 'low'], - signal: new AbortController().signal - }) - ).rejects.toThrow('unsupported') - await expect( - prepareOpenCodeTextTaskInput({ - version: 'v2', - cwd, - prompt: 'input', - legacyVariantFlag: true, - runArgs: ['--variant', 'low'], - signal: new AbortController().signal - }) - ).rejects.toThrow('requires a model') -}) - -it('projects npx delimiter arguments without consuming the provider terminator', async () => { - const cwd = await workspace() - await writeFile(path.join(cwd, 'nested', 'notes.txt'), 'selected context') - const planned = planCommitMessageGeneration( - { - agentId: 'opencode', - model: 'default', - agentCommandOverride: 'npx -- opencode --dir nested --file notes.txt' - }, - 'Name this chat' - ) - if (!planned.ok || !planned.plan.openCodeTask) { - throw new Error('Missing native task') - } - expect(planned.plan.openCodeTask.prefixArgs).toEqual(['--', 'opencode']) - const input = await prepareOpenCodeTextTaskInput({ - version: 'v1', - cwd, - prompt: 'Name this chat', - runArgs: planned.plan.openCodeTask.runArgs, - signal: new AbortController().signal - }) - expect(input.directory).toBe(path.join(cwd, 'nested')) - expect(input.v1Files).toEqual([ - { - type: 'file', - mime: 'text/plain', - filename: 'notes.txt', - url: pathToFileURL(path.join(cwd, 'nested', 'notes.txt')).href - } - ]) - const positional = planCommitMessageGeneration( - { agentId: 'opencode', model: 'default', agentCommandOverride: 'npx -- opencode -- literal' }, - 'input' - ) - if (!positional.ok || !positional.plan.openCodeTask) { - throw new Error('Missing native task') - } - expect(positional.plan.openCodeTask.prefixArgs).toEqual(['--', 'opencode']) - expect(positional.plan.openCodeTask.runArgs.slice(0, 2)).toEqual(['--', 'literal']) -}) - -it('does no text decoding or binary classification for known MIME files at N and 10N bytes', async () => { - const cwd = await workspace() - for (const size of [1024 * 1024, 10 * 1024 * 1024]) { - const bytes = Buffer.alloc(size, 65) - await writeFile(path.join(cwd, 'document.pdf'), bytes) - const decode = vi.spyOn(Buffer.prototype, 'toString') - const classify = vi.spyOn(Buffer.prototype, 'includes') - const input = await prepareOpenCodeTextTaskInput({ - version: 'v2', - cwd, - prompt: 'input', - runArgs: ['--file', 'document.pdf'], - signal: new AbortController().signal - }) - const decodedBytes = decode.mock.calls.reduce( - (count, args, index) => count + (args[0] === 'utf8' ? decode.mock.contexts[index].length : 0), - 0 - ) - const classifiedBytes = classify.mock.contexts.reduce( - (count, buffer) => count + buffer.length, - 0 - ) - decode.mockRestore() - classify.mockRestore() - expect(decodedBytes).toBe(0) - expect(classifiedBytes).toBe(0) - expect(input.prompt).toBe('input') - expect(input.v2Files).toEqual([ - { name: 'document.pdf', uri: `data:application/pdf;base64,${bytes.toString('base64')}` } - ]) - } - await writeFile(path.join(cwd, 'unknown.data'), Buffer.from([0, 255, 1])) - const input = await prepareOpenCodeTextTaskInput({ - version: 'v2', - cwd, - prompt: 'input', - runArgs: ['--file', 'unknown.data'], - signal: new AbortController().signal - }) - expect(input.v2Files).toEqual([ - { name: 'unknown.data', uri: 'data:application/octet-stream;base64,AP8B' } - ]) -}) diff --git a/src/main/opencode/opencode-text-task-legacy-policy.test.ts b/src/main/opencode/opencode-text-task-legacy-policy.test.ts index 3ffb1932cb0..ae9888eabd8 100644 --- a/src/main/opencode/opencode-text-task-legacy-policy.test.ts +++ b/src/main/opencode/opencode-text-task-legacy-policy.test.ts @@ -47,67 +47,6 @@ function json(response: ServerResponse, value: unknown) { response.end(JSON.stringify(value)) } -it.each(['commit-message', 'conversation-name'] as const)( - 'keeps selected helpers working best effort without a create policy (%s)', - async (operation) => { - const requests = new Map() - const url = await fixture((path, body, response) => { - requests.set(path, body) - if (path.startsWith('POST /session?')) { - json(response, { id: 'legacy' }) - } else if (path.startsWith('GET /event?')) { - response.writeHead(200, { 'content-type': 'text/event-stream' }) - response.flushHeaders() - } else if (path.startsWith('POST /session/legacy/message?')) { - json(response, { info: {}, parts: [{ type: 'text', text: 'Harbor Lantern' }] }) - } else { - json(response, true) - } - }) - const result = runOpenCodeTextTaskSession({ - version: 'v1', - v1SessionPolicyFiltersTools: true, - url, - operation, - directory: '/workspace', - prompt: 'Name only', - model: { providerID: 'selected', modelID: 'model' }, - variant: 'low', - v1Files: [ - { type: 'file', mime: 'text/plain', filename: 'notes.txt', url: 'file:///notes.txt' } - ], - signal: new AbortController().signal - }) - await expect(result).resolves.toBe('Harbor Lantern') - expect(requests.get('POST /session/legacy/message?directory=%2Fworkspace')).toEqual({ - messageID: expect.stringMatching(/^msg_[0-9a-f]{12}[0-9A-Za-z]{14}$/), - model: { providerID: 'selected', modelID: 'model' }, - variant: 'low', - parts: [ - { type: 'file', mime: 'text/plain', filename: 'notes.txt', url: 'file:///notes.txt' }, - { type: 'text', text: 'Name only' } - ], - tools: { - '*': false, - read: true, - grep: true, - glob: true, - external_directory: true, - bash: false, - edit: false, - write: false, - patch: false, - apply_patch: false, - multiedit: false, - task: false, - todowrite: false - } - }) - expect(requests.has('GET /event?directory=%2Fworkspace')).toBe(true) - expect(requests.has('DELETE /session/legacy?directory=%2Fworkspace')).toBe(true) - } -) - it.each(['permission.updated', 'permission.asked'])( 'keeps a legacy fork command and rejects its owned %s through the native route', async (type) => { diff --git a/src/main/opencode/opencode-text-task-session.test.ts b/src/main/opencode/opencode-text-task-session.test.ts index 82b954a92bf..d9619b0a1a0 100644 --- a/src/main/opencode/opencode-text-task-session.test.ts +++ b/src/main/opencode/opencode-text-task-session.test.ts @@ -48,64 +48,6 @@ function json(response: ServerResponse, value: unknown) { response.end(JSON.stringify(value)) } -it('v1 applies ordered session authority and preserves the selected native prompt', async () => { - const requests = new Map() - const url = await fixture((path, body, response) => { - requests.set(path, body) - if (path.startsWith('GET /agent?')) { - json(response, [{ name: 'configured-agent', mode: 'primary' }]) - } else if (path.startsWith('POST /session?')) { - json(response, { id: 'ses_v1', permission: appliedV1Policy }) - } else if (path.startsWith('POST /session/ses_v1/message?')) { - json(response, { - info: {}, - parts: [ - { type: 'reasoning', text: 'private' }, - { type: 'text', text: 'Harbor Lantern' } - ] - }) - } else { - json(response, true) - } - }) - await expect( - runOpenCodeTextTaskSession({ - version: 'v1', - v1SessionPolicyFiltersTools: true, - url, - password: 'test', - directory: '/workspace with spaces', - prompt: 'Name this chat', - agent: 'configured-agent', - model: { providerID: 'opencode', modelID: 'crouton-alpha-free' }, - variant: 'low', - signal: new AbortController().signal - }) - ).resolves.toBe('Harbor Lantern') - expect(requests.get('POST /session?directory=%2Fworkspace%20with%20spaces')).toEqual({ - title: expect.stringMatching(/^Orca text generation /), - agent: 'configured-agent', - permission: [ - { permission: '*', pattern: '*', action: 'deny' }, - { permission: 'read', pattern: '*', action: 'allow' }, - { permission: 'grep', pattern: '*', action: 'allow' }, - { permission: 'glob', pattern: '*', action: 'allow' }, - { permission: 'external_directory', pattern: '*', action: 'allow' }, - { permission: 'read', pattern: 'mcp:*', action: 'deny' } - ] - }) - expect( - requests.get('POST /session/ses_v1/message?directory=%2Fworkspace%20with%20spaces') - ).toEqual({ - messageID: expect.stringMatching(/^msg_[0-9a-f]{12}[0-9A-Za-z]{14}$/), - model: { providerID: 'opencode', modelID: 'crouton-alpha-free' }, - agent: 'configured-agent', - variant: 'low', - parts: [{ type: 'text', text: 'Name this chat' }] - }) - expect(requests.has('DELETE /session/ses_v1?directory=%2Fworkspace%20with%20spaces')).toBe(true) -}) - it('v2 rejects an unexpected permission and reads final text across fragmented CRLF events', async () => { let events: ServerResponse | undefined const requests = new Map() @@ -207,55 +149,6 @@ it('recovers and deletes an owned session when cancelled during creation without expect(requests).not.toContain('POST /api/session/ses_late/prompt') }) -it('uses the native v1 command endpoint with its arguments and selected model', async () => { - const requests = new Map() - const url = await fixture((path, body, response) => { - requests.set(path, body) - if (path.startsWith('POST /session?')) { - json(response, { id: 'ses_command', permission: appliedV1Policy }) - } else if (path.startsWith('GET /command?')) { - json(response, [{ name: 'review', agent: 'reviewer' }]) - } else if (path.startsWith('GET /agent?')) { - json(response, [ - { name: 'reviewer', mode: 'primary' }, - { name: 'build', mode: 'primary' } - ]) - } else if (path.startsWith('POST /session/ses_command/command?')) { - json(response, { - info: {}, - parts: [{ type: 'text', text: 'Harbor Lantern' }] - }) - } else { - json(response, true) - } - }) - await expect( - runOpenCodeTextTaskSession({ - version: 'v1', - v1SessionPolicyFiltersTools: true, - url, - password: 'test', - directory: '/workspace', - prompt: 'extra\ngenerated', - command: 'review', - agent: 'build', - title: 'Recipe', - model: { providerID: 'opencode', modelID: 'crouton-alpha-free' }, - variant: 'low', - signal: new AbortController().signal - }) - ).resolves.toBe('Harbor Lantern') - expect(requests.get('POST /session/ses_command/command?directory=%2Fworkspace')).toEqual({ - messageID: expect.stringMatching(/^msg_[0-9a-f]{12}[0-9A-Za-z]{14}$/), - command: 'review', - arguments: 'extra\ngenerated', - agent: 'build', - model: 'opencode/crouton-alpha-free', - variant: 'low' - }) - expect(requests.has('POST /session/ses_command/message?directory=%2Fworkspace')).toBe(false) -}) - it('preserves a completed title when deleting its session fails', async () => { const warning = vi.spyOn(console, 'warn').mockImplementation(() => {}) const url = await fixture((path, _body, response) => { @@ -284,126 +177,6 @@ it('preserves a completed title when deleting its session fails', async () => { } }) -it.each(['', 'missing', 'explore'])( - 'v1 preserves default-agent fallback for %j in a normal command', - async (agent) => { - let submitted: unknown - const url = await fixture((path, body, response) => { - if (path.startsWith('GET /agent?')) { - json(response, [ - { name: 'explore', mode: 'subagent' }, - { name: 'build', mode: 'primary' } - ]) - } else if (path.startsWith('GET /command?')) { - json(response, [{ name: 'review' }]) - } else if (path.startsWith('POST /session?')) { - json(response, { id: 'ses_default', permission: appliedV1Policy }) - } else if (path.startsWith('POST /session/ses_default/command?')) { - submitted = body - json(response, { info: {}, parts: [{ type: 'text', text: 'Harbor Lantern' }] }) - } else { - json(response, true) - } - }) - await expect( - runOpenCodeTextTaskSession({ - version: 'v1', - v1SessionPolicyFiltersTools: true, - url, - password: 'test', - directory: '/workspace', - prompt: 'input', - command: 'review', - agent, - signal: new AbortController().signal - }) - ).resolves.toBe('Harbor Lantern') - expect(submitted).toEqual({ - command: 'review', - arguments: 'input', - messageID: expect.stringMatching(/^msg_[0-9a-f]{12}[0-9A-Za-z]{14}$/) - }) - } -) - -it.each([ - { - resume: false, - command: undefined, - parentID: 'msg_compaction_continue', - title: 'Harbor Lantern' - }, - { resume: true, command: undefined, parentID: 'msg_replay', title: 'Harbor Lantern' }, - { - resume: false, - command: 'inspect', - parentID: 'msg_subtask_compaction', - title: 'Harbor Lantern' - }, - { resume: false, command: undefined, parentID: 'msg_other_caller', title: 'Shared run title' } -])( - 'uses the borrowed v1 HTTP response as best effort (%j)', - async ({ resume, command, parentID, title }) => { - const requests: string[] = [] - const url = await fixture((path, body, response) => { - requests.push(path) - path = path.replace('/prefix/', '/') - if (path.startsWith('GET /session/saved?')) { - json(response, { id: 'saved', directory: '/old' }) - } else if (path.startsWith('GET /session?')) { - json(response, [{ id: 'saved', directory: '/old' }]) - } else if (path.startsWith('GET /agent?')) { - json(response, [{ name: 'reader', mode: 'subagent' }]) - } else if (path.startsWith('GET /command?')) { - json(response, [{ name: 'inspect', agent: 'reader', subtask: true }]) - } else if (path.startsWith(`POST /session/saved/${command ? 'command' : 'message'}?`)) { - expect(body).toMatchObject( - command - ? { command: 'inspect', arguments: 'Name only', model: 'configured/model' } - : { - parts: [{ type: 'text', text: 'Name only' }], - model: { providerID: 'configured', modelID: 'model' } - } - ) - json(response, { - info: { id: 'msg_native_reply', parentID, role: 'assistant', time: { completed: 300 } }, - parts: [{ type: 'text', text: title }] - }) - } else { - response.statusCode = 400 - json(response, {}) - } - }) - await expect( - runOpenCodeTextTaskSession({ - version: 'v1', - v1SessionPolicyFiltersTools: true, - url: `${url}/prefix/`, - directory: '/workspace', - externalDirectory: '/requested', - external: { url }, - exclusiveServer: false, - prompt: 'Name only', - model: { providerID: 'configured', modelID: 'model' }, - sessionId: resume ? undefined : 'saved', - continue: resume, - command, - operation: 'conversation-name', - signal: new AbortController().signal - }) - ).resolves.toBe(title) - expect(requests).toEqual([ - resume - ? 'GET /prefix/session?directory=%2Frequested' - : 'GET /prefix/session/saved?directory=%2Frequested', - ...(command - ? ['GET /prefix/agent?directory=%2Frequested', 'GET /prefix/command?directory=%2Frequested'] - : []), - `POST /prefix/session/saved/${command ? 'command' : 'message'}?directory=%2Frequested` - ]) - } -) - it('discards a cancelled borrowed v1 response without aborting or deleting its session', async () => { const controller = new AbortController() const submitted = Promise.withResolvers() @@ -594,78 +367,6 @@ it('retains explicitly shared native history even when publication fails ambiguo ]) }) -it('selects the newest matching root with one filtered request at N and 10N histories', async () => { - for (const population of [200, 2000]) { - let pages = 0 - const history = Array.from({ length: population }, (_, index) => ({ - id: index === population - 2 ? 'selected' : `other_${index}`, - location: { directory: index % 2 === 0 ? '/workspace' : '/elsewhere' }, - parentID: index !== population - 2 && index % 3 === 0 ? 'parent' : null, - updated: index - })) - let events: ServerResponse | undefined - const url = await fixture((path, body, response) => { - const parsed = new URL(path.slice(path.indexOf(' ') + 1), 'http://fixture') - if (path === 'GET /api/event') { - events = response - response.writeHead(200, { 'content-type': 'text/event-stream' }) - response.flushHeaders() - } else if (parsed.pathname === '/api/location') { - json(response, { directory: '/workspace' }) - } else if (path.startsWith('GET /api/session?')) { - pages++ - expect(parsed.searchParams.get('directory')).toBe('/workspace') - expect(parsed.searchParams.get('parentID')).toBe('null') - expect(parsed.searchParams.get('order')).toBe('desc') - const matching = history - .filter( - (item) => - item.location.directory === parsed.searchParams.get('directory') && !item.parentID - ) - .sort((a, b) => b.updated - a.updated) - json(response, { - data: matching.slice(0, Number(parsed.searchParams.get('limit'))), - cursor: {} - }) - } else if (path === 'POST /api/session/selected/prompt') { - const { id } = z.object({ id: z.string() }).parse(body) - const emitted: [string, Record][] = [ - ['session.inbox.delivered', { inboxID: id }], - ['session.step.started', { assistantMessageID: 'msg_selected' }], - [ - 'session.text.ended', - { assistantMessageID: 'msg_selected', ordinal: 0, text: 'Harbor Lantern' } - ], - ['session.execution.succeeded', {}] - ] - for (const [type, data] of emitted) { - events!.write( - `data: ${JSON.stringify({ type, data: { sessionID: 'selected', ...data } })}\n\n` - ) - } - json(response, {}) - } else { - response.statusCode = 400 - json(response, {}) - } - }) - await expect( - runOpenCodeTextTaskSession({ - version: 'v2', - v1SessionPolicyFiltersTools: false, - url, - directory: '/workspace', - continue: true, - exclusiveServer: false, - operation: 'conversation-name', - prompt: 'Name only', - signal: new AbortController().signal - }) - ).resolves.toBe('Harbor Lantern') - expect(pages).toBe(1) - } -}) - it.each([false, true])( 'rejects owned subtask asks with session routing field: %s', async (routing) => { diff --git a/src/main/opencode/opencode-text-task-streams.test.ts b/src/main/opencode/opencode-text-task-streams.test.ts index b3ac21256f6..72950f2ee37 100644 --- a/src/main/opencode/opencode-text-task-streams.test.ts +++ b/src/main/opencode/opencode-text-task-streams.test.ts @@ -10,32 +10,6 @@ afterEach(() => { vi.unstubAllGlobals() }) -function framingWork() { - const original = Buffer.prototype.indexOf - const search = vi.spyOn(Buffer.prototype, 'indexOf') - let searched = 0 - search.mockImplementation(function (this: Buffer, value, offset, encoding) { - if (value === 10) { - searched += this.length - (typeof offset === 'number' ? offset : 0) - } - return original.call(this, value, offset, encoding) - }) - const accounting = vi.spyOn(Buffer, 'byteLength') - const splitting = vi.spyOn(String.prototype, 'split') - return () => ({ - searched: - searched + - splitting.mock.calls.reduce((sum, args, index) => { - const separator: unknown = args[0] - return sum + (separator === '\n' ? String(splitting.mock.contexts[index]).length : 0) - }, 0), - accounted: accounting.mock.calls.reduce( - (sum, args) => sum + (typeof args[0] === 'string' ? args[0].length : args[0].byteLength), - 0 - ) - }) -} - function fakeServer() { const child = new ChildProcess() Object.defineProperties(child, { @@ -89,85 +63,6 @@ function eventStream(bytes: Buffer) { return () => canceled } -it('frames fragmented Unicode/CRLF events with linear search and accounting at N and 10N', async () => { - for (const size of [128 * 1024, 1280 * 1024]) { - const text = `${'a'.repeat(size)}🌊終` - const bytes = Buffer.from( - [ - { - type: 'session.text.ended', - data: { sessionID: 'owned', assistantMessageID: 'msg_output', ordinal: 0, text } - }, - { type: 'session.execution.succeeded', data: { sessionID: 'owned' } } - ] - .map((event) => `data: ${JSON.stringify(event)}\r\n\r\n`) - .join('') - ) - const canceled = eventStream(bytes) - const work = framingWork() - const events = openCodeTextTaskEvents({ - version: 'v2', - url: 'http://fixture', - signal: new AbortController().signal, - lost: new AbortController(), - messageID: 'msg_input', - context: () => ({ - sessionID: 'owned', - owned: true, - ownedTree: new Set(['owned']), - directory: '/workspace', - http: createOpenCodeTextTaskHttp('http://fixture', undefined, '/workspace') - }) - }) - await events.subscribe() - await expect(events.completion).resolves.toBe(text) - await events.close() - // One forward newline scan plus bounded rescans within each 4 KiB transport chunk. - expect(work().searched).toBeLessThanOrEqual(bytes.length + 4 * 4096) - expect(work().accounted).toBeLessThanOrEqual(2 * bytes.length) - expect(canceled()).toBe(true) - vi.restoreAllMocks() - } -}) - -it('reads a fragmented readiness line once at N and 10N and preserves Unicode boundaries', async () => { - for (const size of [128 * 1024, 1280 * 1024]) { - const child = fakeServer() - const bytes = Buffer.from( - `{"log":"${'a'.repeat(size)}🌊終"}\r\n{"url":"http://127.0.0.1:4096"}\r\n` - ) - const work = framingWork() - const ready = readOpenCodeTextTaskServerUrl(child, true, new AbortController().signal) - for (let offset = 0; offset < bytes.length; offset += 4096) { - child.stdout.write(bytes.subarray(offset, offset + 4096)) - } - await expect(ready).resolves.toBe('http://127.0.0.1:4096') - expect(work().searched).toBeLessThanOrEqual(bytes.length + 4096) - expect(work().accounted).toBe(0) - child.stdout.destroy() - child.stderr.destroy() - vi.restoreAllMocks() - } -}) - -it('accepts only native V1 readiness output and rejects malformed native addresses', async () => { - const child = fakeServer() - const ready = readOpenCodeTextTaskServerUrl(child, false, new AbortController().signal) - child.stdout.write('MCP listening on http://localhost:undefined\n') - child.stdout.write('dev server listening on http://localhost:3000\n') - child.stdout.write('opencode server listening on http://127.0.0.1:4096\n') - await expect(ready).resolves.toBe('http://127.0.0.1:4096') - child.stdout.destroy() - child.stderr.destroy() - - const malformed = fakeServer() - const rejected = readOpenCodeTextTaskServerUrl(malformed, false, new AbortController().signal) - expect(() => malformed.stdout.write('opencode server listening on http://[bad\n')).not.toThrow() - await expect(rejected).rejects.toThrow('OpenCode private server returned an unexpected address') - malformed.stdout.destroy() - malformed.stderr.destroy() -}) - it('rejects oversized streams and releases their reader; readiness also honors cancellation and size', async () => { const canceled = eventStream(Buffer.alloc(4 * 1024 * 1024 + 1, 65)) const events = openCodeTextTaskEvents({ diff --git a/src/main/opencode/opencode-wsl-text-task.test.ts b/src/main/opencode/opencode-wsl-text-task.test.ts index fc57d87dadf..2226580949e 100644 --- a/src/main/opencode/opencode-wsl-text-task.test.ts +++ b/src/main/opencode/opencode-wsl-text-task.test.ts @@ -66,37 +66,6 @@ function task(signal = new AbortController().signal) { } describe('runOpenCodeWslTextTask', () => { - it('runs the guest task through the target login shell and returns its framed text', async () => { - const guest = fakeGuest(async () => ({ ok: true, text: 'feat: add lanterns' })) - - await expect( - runOpenCodeWslTextTask(task(), { prepare, spawnAgent: guest.spawnAgent }) - ).resolves.toBe('feat: add lanterns') - - expect(guest.launched[0]).toMatchObject({ - binary: '/usr/bin/env', - args: [ - '-u', - 'NODE_OPTIONS', - '/home/dev/.cache/orca/node-runtime/bin/node', - '/mnt/c/Program Files/Orca/resources/relay/wsl/opencode-text-task-wsl.cjs' - ], - cwd: '\\\\wsl.localhost\\Ubuntu\\home\\dev\\repo', - wslDistro: 'Ubuntu', - stdinMode: 'pipe' - }) - // The guest applies these after OpenCode's own login rc, not to the helper launch. - expect(guest.launched[0].commandEnv).toBeUndefined() - expect(guest.requests[0]).toMatchObject({ - binary: 'opencode', - prefixArgs: ['--print-logs'], - prompt: 'Write a commit message', - runArgs: ['--model', 'anthropic/claude-sonnet', '--file', '/mnt/c/notes/context.txt'], - commandEnv: { OPENCODE_CONFIG: '/home/dev/work.json' } - }) - expect(guest.child().exitCode).toBe(0) - }) - it('cancels by ending the guest stdin and settles only after the guest exits', async () => { const controller = new AbortController() const guest = fakeGuest(async (_request, canceled) => { diff --git a/src/main/orcad/electron-sidecar-method-routing.test.ts b/src/main/orcad/electron-sidecar-method-routing.test.ts deleted file mode 100644 index 84056c5e299..00000000000 --- a/src/main/orcad/electron-sidecar-method-routing.test.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { BROWSER_CORE_METHODS } from '../runtime/rpc/methods/browser/browser-core' -import { BROWSER_EXTRA_METHODS } from '../runtime/rpc/methods/browser/browser-extras' -import { BROWSER_SCREENCAST_METHODS } from '../runtime/rpc/methods/browser/browser-screencast' -import { electronSidecarRuntimeMethodName } from './electron-sidecar-method-routing' - -// Exercise real RPC registration: names that diverge from runtime camel-casing can become unreachable. -const BROWSER_RPC_METHODS = [ - ...BROWSER_CORE_METHODS, - ...BROWSER_SCREENCAST_METHODS, - ...BROWSER_EXTRA_METHODS -] - -// Known Log-suffix mismatches: remove entries when fixed; never add entries to hide new failures. -const UNROUTABLE_RUNTIME_COMMANDS = new Set(['browserConsoleLog', 'browserNetworkLog']) - -const RPC_METHODS_WITHOUT_BROWSER_COMMAND = new Set([ - 'browser.screencast.unsubscribe', - 'browser.screencast.ack' -]) - -async function runtimeCommandFor( - method: (typeof BROWSER_RPC_METHODS)[number] -): Promise { - let recorded: string | null = null - const runtime = new Proxy({} as Record, { - get: (_target, property) => { - if (typeof property === 'string' && recorded === null && property.startsWith('browser')) { - recorded = property - } - return () => ({}) - } - }) - try { - await (method.handler as (...args: unknown[]) => Promise)( - { value: '', input: '', text: '', subscriptionId: 'subscription' }, - { runtime }, - () => undefined - ) - } catch { - // Handlers that guard on host state still record the property access above. - } - return recorded -} - -describe('electronSidecarRuntimeMethodName', () => { - it('round-trips every registered browser RPC method', async () => { - const mismatches: string[] = [] - const unmapped: string[] = [] - for (const method of BROWSER_RPC_METHODS) { - const command = await runtimeCommandFor(method) - if (!command) { - unmapped.push(method.name) - continue - } - if (UNROUTABLE_RUNTIME_COMMANDS.has(command)) { - expect(electronSidecarRuntimeMethodName(command)).not.toBe(method.name) - continue - } - const routed = electronSidecarRuntimeMethodName(command) - if (routed !== method.name) { - mismatches.push(`${command} routes to ${routed}, sidecar registers ${method.name}`) - } - } - expect(mismatches).toEqual([]) - expect(unmapped).toEqual([...RPC_METHODS_WITHOUT_BROWSER_COMMAND]) - }) -}) diff --git a/src/main/orcad/orcad-automations.test.ts b/src/main/orcad/orcad-automations.test.ts deleted file mode 100644 index 763d320c869..00000000000 --- a/src/main/orcad/orcad-automations.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { mkdtempSync, rmSync } from 'node:fs' -import { tmpdir } from 'node:os' -import { join } from 'node:path' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { - closeTestStores, - createStore, - makeRepo, - testState -} from '../persistence/persistence-test-harness' -import type { AutomationService } from '../automations/service' -import { orcadAutomationsKeepHostBusy, startOrcadAutomations } from './orcad-automations' -import { runNowSettled } from '../automations/automation-run-now-settled.test-fixture' - -vi.mock('electron', () => ({ app: { getPath: () => testState.dir } })) - -function headlessRuntime() { - let bound: AutomationService | null = null - const runtime = { - setAutomationService: vi.fn((service: AutomationService) => { - bound = service - }), - notifyAutomationsChanged: vi.fn(), - createManagedWorktree: vi.fn(async () => ({ - worktree: { id: 'r1::/repo/wt', displayName: 'wt' }, - startupTerminal: { handle: 'term-1', tabId: 'tab-1', paneKey: 'tab-1:1', ptyId: 'pty-1' } - })), - waitForTerminal: vi.fn(async () => ({ satisfied: true })), - readTerminal: vi.fn(async () => ({ tail: ['done'] })), - getTerminalHandleForPaneKey: vi.fn(() => null), - getAgentStatusRowsForPane: vi.fn(() => []), - subscribeAgentStatusRowChanges: vi.fn(() => () => {}), - canOpenRunTerminals: () => true - } - return { runtime, service: () => bound } -} - -describe('orcad automations', () => { - beforeEach(() => { - testState.dir = mkdtempSync(join(tmpdir(), 'orcad-automations-')) - }) - afterEach(async () => { - await closeTestStores() - rmSync(testState.dir, { recursive: true, force: true }) - }) - - it('binds a headless service, so a run dispatches on orcad without a renderer', async () => { - const store = createStore() - store.addRepo(makeRepo()) - const automation = store.createAutomation({ - name: 'Nightly', - prompt: 'Run checks', - agentId: 'claude', - projectId: 'r1', - workspaceMode: 'new_per_run', - timezone: 'UTC', - rrule: 'FREQ=DAILY;BYHOUR=9;BYMINUTE=0', - dtstart: Date.parse('2026-05-13T00:00:00Z') - }) - const { runtime, service } = headlessRuntime() - const cleanups: (() => void)[] = [] - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the headless dispatcher reaches only the runtime methods stubbed above. - startOrcadAutomations(runtime as never, store, (cleanup) => cleanups.push(cleanup)) - try { - expect(runtime.setAutomationService).toHaveBeenCalledOnce() - const run = await runNowSettled(service()!, store, automation.id) - expect(runtime.createManagedWorktree).toHaveBeenCalledOnce() - expect(run.status).toBe('dispatched') - expect(orcadAutomationsKeepHostBusy(store)).toBe(true) - } finally { - cleanups.forEach((cleanup) => cleanup()) - } - }) -}) diff --git a/src/main/persistence/applying-settings/persistence-automation-setup-trust.test.ts b/src/main/persistence/applying-settings/persistence-automation-setup-trust.test.ts index 7f9e00af1f7..266ca0427e2 100644 --- a/src/main/persistence/applying-settings/persistence-automation-setup-trust.test.ts +++ b/src/main/persistence/applying-settings/persistence-automation-setup-trust.test.ts @@ -72,22 +72,4 @@ describe('automation setup trust hash', () => { ).toBe('next-hash') expect(store.updateAutomation(id, { setupDecision: 'skip' }).setupTrustHash).toBeUndefined() }) - - it("keeps a run's skipped-setup note through its completion and a reload", async () => { - const { store, id } = await automationApprovedToRunSetup() - const automation = store.listAutomations().find((row) => row.id === id)! - const run = store.createAutomationRun(automation, 1_778_000_000_000, 'manual') - store.updateAutomationRun({ - runId: run.id, - status: 'dispatched', - workspaceId: 'ws-1', - setupSkippedReason: 'setup-changed' - }) - store.updateAutomationRun({ runId: run.id, status: 'completed' }) - - const reloaded = await createStore() - expect( - reloaded.listAutomationRuns(id).find((row) => row.id === run.id)?.setupSkippedReason - ).toBe('setup-changed') - }) }) diff --git a/src/main/persistence/migrating-orcad-catalog/persistence-orcad-migration-lineage.test.ts b/src/main/persistence/migrating-orcad-catalog/persistence-orcad-migration-lineage.test.ts index c81ff45adc8..93315bf1bcc 100644 --- a/src/main/persistence/migrating-orcad-catalog/persistence-orcad-migration-lineage.test.ts +++ b/src/main/persistence/migrating-orcad-catalog/persistence-orcad-migration-lineage.test.ts @@ -126,45 +126,6 @@ describe('orcad migration lineage', () => { } ) - it('keeps an inferred folder parent over a worktree parent with the hydrate stamp', () => { - const store = createStore() - const inferredFolder: WorkspaceLineage = { - ...folderParent, - origin: 'cli', - capture: { source: 'env-workspace', confidence: 'inferred' } - } - - commit(store, withLineage([worktreeParent('inferred')], [inferredFolder])) - - expect(store.getWorktreeLineage(CHILD)).toBeUndefined() - expect(store.getWorkspaceLineage(CHILD_KEY)).toMatchObject({ - parentWorkspaceKey: 'folder:folder-1', - capture: { source: 'env-workspace', confidence: 'inferred' } - }) - }) - - it('refuses two inferred parents without the hydrate stamp, changing nothing', () => { - const store = createStore() - const envCapture = { - origin: 'cli', - capture: { source: 'env-workspace', confidence: 'inferred' } - } as const - - expect(() => - commit( - store, - withLineage( - [{ ...worktreeParent('inferred'), ...envCapture }], - [{ ...folderParent, ...envCapture }] - ) - ) - ).toThrow('orcad_migration_dormant_id_conflict:lineage_parent') - - expect(store.getRepos()).toEqual([]) - expect(store.getWorktreeLineage(CHILD)).toBeUndefined() - expect(store.getWorkspaceLineage(CHILD_KEY)).toBeUndefined() - }) - it('refuses a payload whose worktree has two explicit parents, changing nothing', () => { const store = createStore() @@ -177,21 +138,6 @@ describe('orcad migration lineage', () => { expect(store.getWorkspaceLineage(CHILD_KEY)).toBeUndefined() }) - it('refuses a folder parent for a worktree whose parent is on another machine', () => { - const store = createStore() - store.setCrossStoreWorkspaceLineage(crossMachineParent, 'local') - - expect(() => commit(store, withLineage([], [folderParent]))).toThrow( - 'orcad_migration_dormant_id_conflict:lineage_parent' - ) - - expect(store.getRepos()).toEqual([]) - expect(store.getWorkspaceLineage(CHILD_KEY)).toBeUndefined() - expect(store.getCrossStoreWorkspaceLineage(CHILD_KEY)?.parent.installationId).toBe( - 'other-install' - ) - }) - it('refuses at commit when a parent on another machine appears after staging', () => { const store = createStore() const input = withLineage([], [folderParent])