Files
orca/tests/e2e/structured-chat-tabs-open-close.spec.ts
OrcaWinandOrcaWin 961430903c test(e2e): follow setup review dialog, GitLab work-item listing, chat sign-in probe and onboarding trust prompt (#39983)
- setup-script-import: imported setups are saved from the Review setup
  script dialog since #37937.
- github-url-smart-input-transition: the GitLab source lists through
  gitlab:listWorkItems since #32005, so stub that channel.
- structured-chat-tabs-open-close: the Claude catalog probe also sends an
  initialize request for sign-in discovery since #37191.
- golden-core-flows: onboarding launches the chosen agent in the fresh ~
  workspace (#37898), whose first launch asks the per-repo agent folder
  trust question (#38946); answer it before adding a project.

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-11-08 14:18:24 -08:00

188 lines
6.8 KiB
TypeScript

import { existsSync, readFileSync } from 'node:fs'
import type { Page } from '@stablyai/playwright-test'
import { expect } from './helpers/orca-app'
import { waitForActiveWorktree, waitForSessionReady } from './helpers/store'
import { buildFakeAgentCommandOverride } from './helpers/fake-agent-command-override'
import { SUPPORTED, createChatOnHost, test } from './helpers/structured-claude-stub'
import { resolveCliCommand } from '../../src/shared/node-cli-command-resolution'
import { claudeSessionIdForOrcaSession } from '../../src/main/claude/structured/claude-structured-session-id'
import { buildClaudeModelListArgs } from '../../src/shared/claude-model-list-probe'
type ChatTab = { id: string; entityId: string }
function readChatTabs(page: Page, worktreeId: string): Promise<ChatTab[]> {
return page.evaluate(
(wt) =>
(window.__store?.getState().unifiedTabsByWorktree[wt] ?? [])
.filter((tab) => tab.contentType === 'agent-session')
.map((tab) => ({ id: tab.id, entityId: tab.entityId })),
worktreeId
)
}
async function waitForChatTab(page: Page, worktreeId: string, sessionId: string): Promise<string> {
let tabId: string | undefined
await expect
.poll(
async () => {
tabId = (await readChatTabs(page, worktreeId)).find((tab) => tab.entityId === sessionId)?.id
return tabId
},
{ timeout: 30_000 }
)
.toBeTruthy()
return tabId!
}
function tabInStrip(page: Page, tabId: string) {
return page.locator(`[data-testid="sortable-tab"][data-tab-id="${tabId}"]`)
}
/** Every Claude process Orca started, as the stub recorded it. */
function readStubLaunches(launchLog: string): { pid: number; args: string[] }[] {
if (!existsSync(launchLog)) {
return []
}
return readFileSync(launchLog, 'utf8')
.split('\n')
.filter(Boolean)
.map((line) => {
const entry: unknown = JSON.parse(line)
if (
typeof entry !== 'object' ||
entry === null ||
!('pid' in entry) ||
typeof entry.pid !== 'number' ||
!('args' in entry) ||
!Array.isArray(entry.args)
) {
throw new Error(`unreadable stub launch: ${line}`)
}
return { pid: entry.pid, args: entry.args.map(String) }
})
}
function readStubRequests(launchLog: string, pid: number) {
const file = `${launchLog}.requests.jsonl`
if (!existsSync(file)) {
return []
}
return readFileSync(file, 'utf8')
.split('\n')
.filter(Boolean)
.flatMap((line) => {
const entry: unknown = JSON.parse(line)
if (
typeof entry !== 'object' ||
entry === null ||
!('pid' in entry) ||
!('type' in entry) ||
!('subtype' in entry)
) {
throw new Error(`unreadable stub request: ${line}`)
}
return entry.pid === pid ? [{ type: entry.type, subtype: entry.subtype }] : []
})
}
test.describe('structured chat tabs', () => {
test.skip(!SUPPORTED, 'the Claude stub wrapper is POSIX-only')
test('open from the host and the New tab menu, show in the tab bar, and close', async ({
orcaPage: page,
electronApp,
claudeStub
}) => {
test.setTimeout(120_000)
await waitForSessionReady(page)
const worktreeId = await waitForActiveWorktree(page)
// An absolute Command setting is used as-is or refuses the start; it never falls back.
await page.evaluate(async (command) => {
await window.__store?.getState().updateSettings({
nativeChatProviders: { claude: true, codex: true },
defaultTuiAgent: 'claude',
agentCmdOverrides: { claude: command }
})
}, buildFakeAgentCommandOverride(claudeStub.command))
const first = await createChatOnHost(page, worktreeId)
const firstTab = await waitForChatTab(page, worktreeId, first)
await expect(tabInStrip(page, firstTab)).toBeVisible()
const second = await createChatOnHost(page, worktreeId)
const secondTab = await waitForChatTab(page, worktreeId, second)
await expect(tabInStrip(page, secondTab)).toBeVisible()
await expect(tabInStrip(page, firstTab)).toBeVisible()
// The window closes one chat from its tab; the host closes the other.
await tabInStrip(page, firstTab).locator('[data-tab-close-button="true"]').click()
await expect(tabInStrip(page, firstTab)).toHaveCount(0)
await expect(tabInStrip(page, secondTab)).toBeVisible()
const closed = await page.evaluate(
({ wt, tabId }) =>
window.api.runtime.call({
method: 'session.tabs.close',
params: { worktree: `id:${wt}`, tabId, reason: 'user' }
}),
{ wt: worktreeId, tabId: `agent-session:${second}` }
)
expect(closed, JSON.stringify(closed)).toMatchObject({ ok: true })
await expect(tabInStrip(page, secondTab)).toHaveCount(0)
// A chat the window opens from the New tab menu.
await page.getByRole('button', { name: 'New tab' }).click({ force: true })
const claudeOption = page.getByRole('menuitem', {
name: 'Claude Configure Claude',
exact: true
})
await expect(claudeOption).toBeVisible({ timeout: 15_000 })
await claudeOption.click({ force: true })
let opened: ChatTab | undefined
await expect
.poll(
async () => {
opened = (await readChatTabs(page, worktreeId))[0]
return opened?.entityId
},
{ timeout: 30_000 }
)
.toBeTruthy()
await expect(tabInStrip(page, opened!.id)).toBeVisible()
// Orca's own lookup, on its shell-hydrated PATH and isolated HOME, lands on the stub.
const mainEnv = await electronApp.evaluate(() => ({
pathEnv: process.env.PATH ?? null,
homePath: process.env.HOME
}))
expect(resolveCliCommand('claude', mainEnv)).toBe(claudeStub.command)
// Only this stub's version/catalog probes or one of these exact conversations may start.
const providerIds = [first, second, opened!.entityId].map((id) =>
claudeSessionIdForOrcaSession(id)
)
await expect.poll(() => readStubLaunches(claudeStub.launchLog).length).toBeGreaterThan(0)
for (const { pid, args } of readStubLaunches(claudeStub.launchLog)) {
if (providerIds.some((id) => args.includes(id))) {
continue
}
if (args[0] === '--version') {
expect(args).toEqual(['--version'])
continue
}
expect(args).toEqual([
...buildClaudeModelListArgs(),
'--setting-sources',
'user,project,local'
])
await expect
.poll(() => readStubRequests(claudeStub.launchLog, pid))
.toEqual([
{ type: 'control_request', subtype: 'list_models' },
{ type: 'control_request', subtype: 'get_settings' },
{ type: 'control_request', subtype: 'initialize' }
])
}
await tabInStrip(page, opened!.id).locator('[data-tab-close-button="true"]').click()
await expect(tabInStrip(page, opened!.id)).toHaveCount(0)
})
})