Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions scripts/run-standard-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,11 @@ function classifySuitePath(token: string): SuiteName | null {
normalizedToken.startsWith('test/server/')
|| normalizedToken.startsWith('test/unit/server/')
|| normalizedToken.startsWith('test/integration/server/')
|| normalizedToken.startsWith('test/integration/real/')
|| normalizedToken.includes('/test/server/')
|| normalizedToken.includes('/test/unit/server/')
|| normalizedToken.includes('/test/integration/server/')
|| normalizedToken.includes('/test/integration/real/')
|| normalizedToken.endsWith('/test/integration/session-repair.test.ts')
|| normalizedToken.endsWith('/test/integration/session-search-e2e.test.ts')
|| normalizedToken.endsWith('/test/integration/extension-system.test.ts')
Expand Down
4 changes: 3 additions & 1 deletion server/terminal-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,9 @@ function resolveCodingCliCommand(
)
}
const effectiveModel = mode === 'opencode'
? resolveOpencodeLaunchModel(providerSettings?.model, { ...process.env, ...commandEnv })
? (resumeSessionId
? undefined
: resolveOpencodeLaunchModel(providerSettings?.model, { ...process.env, ...commandEnv }))
: providerSettings?.model
if (effectiveModel && spec.modelArgs) {
settingsArgs.push(...spec.modelArgs(effectiveModel))
Expand Down
19 changes: 16 additions & 3 deletions src/components/TerminalView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,12 @@ const DEFAULT_MIN_CONTRAST_RATIO = 1
const MAX_LAST_SENT_VIEWPORT_CACHE_ENTRIES = 200
const TRUNCATED_REPLAY_BYTES = 128 * 1024

function viewportHydrateReplayOptions(content?: TerminalPaneContent | null): { maxReplayBytes: number } | undefined {
return content?.mode === 'opencode'
? undefined
: { maxReplayBytes: TRUNCATED_REPLAY_BYTES }
}

type StartupProbeReplayDiscardState = {
remainder: string | null
buffered: string
Expand Down Expand Up @@ -1640,7 +1646,10 @@ function TerminalView({ tabId, paneId, paneContent, hidden }: TerminalViewProps)
}
setIsAttaching(false)
} else {
attachTerminal(tid, 'viewport_hydrate', { clearViewportFirst: true, maxReplayBytes: TRUNCATED_REPLAY_BYTES })
attachTerminal(tid, 'viewport_hydrate', {
clearViewportFirst: true,
...viewportHydrateReplayOptions(currentContent),
})
}

dispatch(consumePaneRefreshRequest({ tabId, paneId, requestId: request.requestId }))
Expand Down Expand Up @@ -1689,7 +1698,9 @@ function TerminalView({ tabId, paneId, paneContent, hidden }: TerminalViewProps)
clearViewportFirst: deferred.pendingIntent === 'viewport_hydrate',
suppressNextMatchingResize: true,
skipPreAttachFit: true,
...(deferred.pendingIntent === 'viewport_hydrate' ? { maxReplayBytes: TRUNCATED_REPLAY_BYTES } : {}),
...(deferred.pendingIntent === 'viewport_hydrate'
? viewportHydrateReplayOptions(contentRef.current)
: undefined),
})
return
}
Expand Down Expand Up @@ -2342,7 +2353,9 @@ function TerminalView({ tabId, paneId, paneContent, hidden }: TerminalViewProps)
const intent: AttachIntent = deferredAttachStateRef.current.mode === 'live'
? 'keepalive_delta'
: 'viewport_hydrate'
attachTerminal(currentTerminalId, intent, intent === 'viewport_hydrate' ? { maxReplayBytes: TRUNCATED_REPLAY_BYTES } : undefined)
attachTerminal(currentTerminalId, intent, intent === 'viewport_hydrate'
? viewportHydrateReplayOptions(contentRef.current)
: undefined)
}
} else {
deferredAttachStateRef.current = {
Expand Down
2 changes: 1 addition & 1 deletion test/e2e/sidebar-click-opens-pane.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ vi.mock('@/lib/api', async () => {
const sessionId = (label: string) => {
const chars = Array.from(label).map((ch, idx) => ((ch.charCodeAt(0) + idx) % 16).toString(16))
const hex = chars.join('').padEnd(32, '0').slice(0, 32)
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-4${hex.slice(13, 16)}-8${hex.slice(17, 20)}-${hex.slice(20, 32)}`
}

function createStore(options: {
Expand Down
18 changes: 16 additions & 2 deletions test/helpers/coding-cli/real-session-contract-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ async function listFilesRecursive(rootDir: string): Promise<string[]> {
async function waitForHttpJson(url: string, timeoutMs = 30_000): Promise<unknown> {
return waitFor(`HTTP JSON at ${url}`, async () => {
try {
const response = await fetch(url)
const response = await fetchWithTimeout(url)
if (!response.ok) {
return undefined
}
Expand All @@ -224,6 +224,16 @@ async function waitForHttpJson(url: string, timeoutMs = 30_000): Promise<unknown
}, timeoutMs, 200)
}

async function fetchWithTimeout(url: string, timeoutMs = 2_000): Promise<Response> {
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), timeoutMs)
try {
return await fetch(url, { signal: controller.signal })
} finally {
clearTimeout(timeout)
}
}

function parseJsonLines(text: string): unknown[] {
return text
.split(/\r?\n/)
Expand Down Expand Up @@ -1268,13 +1278,17 @@ export async function waitForFileSizeIncrease(filePath: string, previousSize: nu
}

export async function fetchJson(url: string): Promise<any> {
const response = await fetch(url)
const response = await fetchWithTimeout(url)
if (!response.ok) {
throw new Error(`Expected a successful response from ${url}, received ${response.status}.`)
}
return response.json()
}

export async function waitForJsonResponse(url: string): Promise<any> {
return waitForHttpJson(url)
}

export async function waitForHttpBusyStatus(url: string, sessionId: string): Promise<Record<string, { type: string }>> {
return waitFor(`OpenCode busy status for ${sessionId}`, async () => {
const payload = await fetchJson(url).catch(() => undefined)
Expand Down
4 changes: 2 additions & 2 deletions test/integration/real/coding-cli-session-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
captureCodexBootstrapEvents,
captureCodexResumeBootstrapEvents,
extractCodexResumeId,
fetchJson,
findClaudeTranscript,
findCodexSessionArtifacts,
loadCodingCliSessionContractNote,
Expand All @@ -27,6 +26,7 @@ import {
waitForFileSizeIncrease,
waitForAnyHttpBusyStatus,
waitForHttpHealthy,
waitForJsonResponse,
waitForJsonLine,
waitForOpencodeDbSession,
} from '../../helpers/coding-cli/real-session-contract-harness.js'
Expand Down Expand Up @@ -451,7 +451,7 @@ describe.sequential('coding cli real provider session contract', () => {
healthy: true,
version: note.providers.opencode.version,
})
expect(await fetchJson(statusUrl)).toEqual({})
expect(await waitForJsonResponse(statusUrl)).toEqual({})

const attachedRun = await workspace.spawnProcess(
opencodePath,
Expand Down
29 changes: 26 additions & 3 deletions test/unit/client/components/TerminalView.lifecycle.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3091,6 +3091,7 @@ describe('TerminalView lifecycle updates', () => {
async function renderTerminalHarness(opts?: {
status?: 'creating' | 'running'
terminalId?: string
mode?: TerminalPaneContent['mode']
hidden?: boolean
clearSends?: boolean
requestId?: string
Expand All @@ -3102,12 +3103,13 @@ describe('TerminalView lifecycle updates', () => {
const requestId = opts?.requestId ?? 'req-v2-stream'
const initialStatus = opts?.status ?? 'running'
const terminalId = opts?.terminalId
const mode = opts?.mode ?? 'shell'

const paneContent: TerminalPaneContent = {
kind: 'terminal',
createRequestId: requestId,
status: initialStatus,
mode: 'shell',
mode,
shell: 'system',
...(terminalId ? { terminalId } : {}),
}
Expand All @@ -3126,9 +3128,9 @@ describe('TerminalView lifecycle updates', () => {
tabs: {
tabs: [{
id: tabId,
mode: 'shell',
mode,
status: initialStatus,
title: 'Shell',
title: mode === 'opencode' ? 'OpenCode' : 'Shell',
titleSetByUser: false,
createRequestId: requestId,
...(terminalId ? { terminalId } : {}),
Expand Down Expand Up @@ -3977,6 +3979,27 @@ describe('TerminalView lifecycle updates', () => {
}))
})

it('does not cap OpenCode viewport hydration replay for restored running terminals', async () => {
const { terminalId } = await renderTerminalHarness({
status: 'running',
terminalId: 'term-opencode-restored',
mode: 'opencode',
clearSends: false,
})

const attach = wsMocks.send.mock.calls
.map(([msg]) => msg)
.find((msg) => msg?.type === 'terminal.attach' && msg?.terminalId === terminalId)

expect(attach).toMatchObject({
type: 'terminal.attach',
terminalId,
intent: 'viewport_hydrate',
sinceSeq: 0,
})
expect(attach).not.toHaveProperty('maxReplayBytes')
})

it('revealing a hidden running pane sends a viewport attach with sinceSeq=0', async () => {
const { store, tabId, paneId, terminalId, rerender } = await renderTerminalHarness({
status: 'running',
Expand Down
15 changes: 15 additions & 0 deletions test/unit/server/run-standard-tests.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,21 @@ describe('run-standard-tests', () => {
})
})

it('routes real provider integration paths to the server suite only', () => {
expect(createStandardTestPlan({
availableParallelism: 32,
ci: false,
forwardedArgs: ['test/integration/real/coding-cli-session-contract.test.ts'],
})).toEqual({
mode: 'desktop',
stages: [
[
{ name: 'server', configPath: 'vitest.server.config.ts', maxWorkers: '3', priority: 'background' },
],
],
})
})

it('routes electron-targeted paths to the electron suite only', () => {
expect(createStandardTestPlan({
availableParallelism: 32,
Expand Down
32 changes: 32 additions & 0 deletions test/unit/server/terminal-registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -990,6 +990,38 @@ describe('buildSpawnSpec Unix paths', () => {
expect(spec.args).toContain('openai/gpt-5-mini')
})

it('does not pass a default OpenCode model when resuming a session', () => {
delete process.env.OPENCODE_CMD

const spec = buildSpawnSpec('opencode', '/Users/john/project', 'system', 'ses_existing', {
model: 'abc',
opencodeServer: TEST_OPENCODE_SERVER,
})

expect(spec.args).toContain('--session')
expect(spec.args).toContain('ses_existing')
expect(spec.args).not.toContain('--model')
expect(spec.args).not.toContain('abc')
})

it('does not pass an inferred OpenCode model when resuming a session', () => {
delete process.env.OPENCODE_CMD
delete process.env.GOOGLE_GENERATIVE_AI_API_KEY
delete process.env.GOOGLE_API_KEY
delete process.env.OPENAI_API_KEY
delete process.env.ANTHROPIC_API_KEY
process.env.GEMINI_API_KEY = 'gemini-key'

const spec = buildSpawnSpec('opencode', '/Users/john/project', 'system', 'ses_existing', {
opencodeServer: TEST_OPENCODE_SERVER,
})

expect(spec.args).toContain('--session')
expect(spec.args).toContain('ses_existing')
expect(spec.args).not.toContain('--model')
expect(spec.args).not.toContain('google/gemini-3-pro-preview')
})

it('defaults OpenCode to a usable Google model and alias env when only GEMINI_API_KEY is set', () => {
delete process.env.OPENCODE_CMD
delete process.env.GOOGLE_GENERATIVE_AI_API_KEY
Expand Down
8 changes: 4 additions & 4 deletions test/unit/server/ws-sdk-session-history-cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,7 @@ describe('WsHandler agent history source DI', () => {
messages: [],
model: 'claude-sonnet-4-20250514',
cwd: '/tmp',
resumeSessionId: '01234567-89ab-cdef-0123-456789abcdef',
resumeSessionId: '01234567-89ab-4def-8123-456789abcdef',
streamingActive: false,
streamingText: '',
pendingPermissions: new Map(),
Expand All @@ -394,7 +394,7 @@ describe('WsHandler agent history source DI', () => {
messages: [],
model: 'claude-sonnet-4-20250514',
cwd: '/tmp',
resumeSessionId: '01234567-89ab-cdef-0123-456789abcdef',
resumeSessionId: '01234567-89ab-4def-8123-456789abcdef',
streamingActive: false,
streamingText: '',
})),
Expand Down Expand Up @@ -423,12 +423,12 @@ describe('WsHandler agent history source DI', () => {
type: 'sdk.create',
requestId: 'req-module',
cwd: '/tmp',
resumeSessionId: '01234567-89ab-cdef-0123-456789abcdef',
resumeSessionId: '01234567-89ab-4def-8123-456789abcdef',
}))

await waitForMessage(ws, (d) => d.type === 'sdk.session.snapshot')

expect(moduleLoadSessionHistoryMock).toHaveBeenCalledWith('01234567-89ab-cdef-0123-456789abcdef')
expect(moduleLoadSessionHistoryMock).toHaveBeenCalledWith('01234567-89ab-4def-8123-456789abcdef')

ws.close()
})
Expand Down
12 changes: 10 additions & 2 deletions test/unit/vite-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,11 +150,19 @@ describe('vitest config', () => {
}
})

it('does not exclude real-provider integration contracts from the default suite', async () => {
it('excludes real-provider integration contracts from the default jsdom suite', async () => {
const configModule = await import('../../vitest.config.ts')
const config = configModule.default
const excluded = config.test?.exclude ?? []

expect(excluded).not.toContain('test/integration/real/**')
expect(excluded).toContain('test/integration/real/**')
})

it('runs real-provider integration contracts in the node server suite', async () => {
const configModule = await import('../../vitest.server.config.ts')
const config = configModule.default
const included = config.test?.include ?? []

expect(included).toContain('test/integration/real/**/*.test.ts')
})
})
1 change: 1 addition & 0 deletions vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export default defineConfig({
'test/integration/session-repair.test.ts',
'test/integration/session-search-e2e.test.ts',
'test/e2e-browser/**',
'test/integration/real/**',
// Electron tests run under vitest.electron.config.ts (node environment)
'test/unit/electron/**',
// Electron E2E tests run under Playwright, not Vitest
Expand Down
1 change: 1 addition & 0 deletions vitest.server.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export default defineConfig({
'test/unit/server/**/*.test.ts',
'test/unit/visible-first/**/*.test.ts',
'test/integration/server/**/*.test.ts',
'test/integration/real/**/*.test.ts',
'test/integration/session-repair.test.ts',
'test/integration/session-search-e2e.test.ts',
'test/integration/extension-system.test.ts',
Expand Down
Loading