mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
test: strengthen hook, release, and mobile transport coverage (#33031)
* test: exercise hook and decorative title limits * test: reject non-string launch preferences * test: preserve live builds on release rejection * test: verify restored child and expiry session state * test: strengthen mobile transport lifecycle coverage --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import { mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join } from 'node:path'
|
||||
@@ -97,7 +98,9 @@ describe('release JavaScript configuration', () => {
|
||||
const { root, env } = configurationFixture(version)
|
||||
const result = releaseJavascriptConfiguration(root, { ...env, ORCA_BUILD_IDENTITY: identity })
|
||||
expect(result.identity).toBe(identity)
|
||||
expect(result.telemetryKeySha256).toMatch(/^[a-f0-9]{64}$/)
|
||||
expect(result.telemetryKeySha256).toBe(
|
||||
createHash('sha256').update(env.ORCA_POSTHOG_WRITE_KEY).digest('hex')
|
||||
)
|
||||
expect(result.sourceSha).toMatch(/^[a-f0-9]{40}$/)
|
||||
})
|
||||
|
||||
@@ -200,15 +203,16 @@ describe('release JavaScript transfer', () => {
|
||||
])('rejects a different %s before replacing the current build', (field) => {
|
||||
const options = fixture()
|
||||
packReleaseJavascript(options)
|
||||
writeFileSync(join(options.root, 'out', 'main', 'index.js'), 'current build')
|
||||
writeFileSync(join(options.root, 'out', 'current-only.js'), 'current only')
|
||||
const current = javascriptInventory(join(options.root, 'out'))
|
||||
expect(() =>
|
||||
restoreReleaseJavascript({
|
||||
...options,
|
||||
configuration: { ...configuration, [field]: 'different' }
|
||||
})
|
||||
).toThrow('JavaScript build configuration differs')
|
||||
expect(readFileSync(join(options.root, 'out', 'main', 'index.js'), 'utf8')).toBe(
|
||||
'main/index.js'
|
||||
)
|
||||
expect(javascriptInventory(join(options.root, 'out'))).toEqual(current)
|
||||
})
|
||||
|
||||
it('rejects a damaged archive before replacing the current build', () => {
|
||||
@@ -224,9 +228,13 @@ describe('release JavaScript transfer', () => {
|
||||
it('rejects a file inventory that no longer describes the archive', () => {
|
||||
const options = fixture()
|
||||
const manifest = packReleaseJavascript(options)
|
||||
writeFileSync(join(options.root, 'out', 'main', 'index.js'), 'current build')
|
||||
writeFileSync(join(options.root, 'out', 'current-only.js'), 'current only')
|
||||
const current = javascriptInventory(join(options.root, 'out'))
|
||||
manifest.files[0].sha256 = 'c'.repeat(64)
|
||||
writeFileSync(join(options.artifactDir, 'manifest.json'), JSON.stringify(manifest))
|
||||
expect(() => restoreReleaseJavascript(options)).toThrow('JavaScript file inventory differs')
|
||||
expect(javascriptInventory(join(options.root, 'out'))).toEqual(current)
|
||||
})
|
||||
|
||||
it.each([
|
||||
@@ -248,10 +256,9 @@ describe('release JavaScript transfer', () => {
|
||||
'rejects symlinks instead of following files outside the build',
|
||||
() => {
|
||||
const options = fixture()
|
||||
symlinkSync(
|
||||
join(options.root, 'out', 'main', 'index.js'),
|
||||
join(options.root, 'out', 'linked.js')
|
||||
)
|
||||
const outside = join(options.root, 'outside.js')
|
||||
writeFileSync(outside, 'outside the build')
|
||||
symlinkSync(outside, join(options.root, 'out', 'linked.js'))
|
||||
expect(() => packReleaseJavascript(options)).toThrow('contains a symlink')
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import {
|
||||
dispatchReleaseMacBuildWorkflow,
|
||||
executeReleaseMacBuildMode,
|
||||
expectedReleaseMacBuildRunTitle,
|
||||
readReleaseMacBuildWorkflowOptions,
|
||||
@@ -22,13 +21,15 @@ const baseOptions = {
|
||||
|
||||
describe('release mac build workflow dispatch', () => {
|
||||
it('passes the shared JavaScript commit only when the parent produced a bundle', async () => {
|
||||
const request = vi.fn(async () => ({}))
|
||||
const api = { owner: 'stablyai', repo: 'orca', request }
|
||||
await dispatchReleaseMacBuildWorkflow(api, {
|
||||
...baseOptions,
|
||||
javascriptSourceSha: 'a'.repeat(40)
|
||||
})
|
||||
expect(request.mock.calls[0][2].inputs).toEqual({
|
||||
const { fetch, requests } = createGitHubFetch([
|
||||
jsonResponse(200, { workflow_run_id: 123 }),
|
||||
jsonResponse(200, { id: 123, status: 'completed', conclusion: 'success' })
|
||||
])
|
||||
await runReleaseMacBuildWorkflow(
|
||||
{ ...baseOptions, javascriptSourceSha: 'a'.repeat(40) },
|
||||
{ fetch, now: () => Date.parse('2026-06-30T12:00:00Z'), sleep: vi.fn() }
|
||||
)
|
||||
expect(requests[0].body.inputs).toEqual({
|
||||
release_run_id: '777',
|
||||
tag: 'v1.2.3-rc.4',
|
||||
javascript_source_sha: 'a'.repeat(40)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { createElement, type ReactElement } from 'react'
|
||||
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
|
||||
import { createElement } from 'react'
|
||||
import type { ReactElement } from 'react'
|
||||
import { act, create } from 'react-test-renderer'
|
||||
import type { ReactTestRenderer } from 'react-test-renderer'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { ConnectionState } from './types'
|
||||
import type { RpcClient } from './rpc-client'
|
||||
@@ -180,6 +182,7 @@ describe('useHostClient', () => {
|
||||
connectMock.mockReturnValueOnce(client1).mockReturnValueOnce(client2)
|
||||
loadHostsMock.mockResolvedValue([HOST, host2])
|
||||
|
||||
const selections: { hostId: string; value: ReturnType<typeof useHostClient> }[] = []
|
||||
let selectedHostId = HOST.id
|
||||
let selectedClient: RpcClient | null = null
|
||||
let selectedClientId: string | null = null
|
||||
@@ -187,6 +190,7 @@ describe('useHostClient', () => {
|
||||
let renderer: ReactTestRenderer | null = null
|
||||
function Probe(): null {
|
||||
const selected = useHostClient(selectedHostId)
|
||||
selections.push({ hostId: selectedHostId, value: selected })
|
||||
selectedClient = selected.client
|
||||
selectedClientId = selected.clientId
|
||||
selectedState = selected.state
|
||||
@@ -213,6 +217,15 @@ describe('useHostClient', () => {
|
||||
expect(selectedClient).toBe(client2)
|
||||
expect(selectedClientId).toBe(host2.deviceToken)
|
||||
expect(selectedState).toBe('disconnected')
|
||||
for (const { value } of selections.filter(({ hostId }) => hostId === host2.id)) {
|
||||
expect(value.state).toBe('disconnected')
|
||||
if (value.client === null) {
|
||||
expect(value.clientId).toBeNull()
|
||||
} else {
|
||||
expect(value.client).toBe(client2)
|
||||
expect(value.clientId).toBe(host2.deviceToken)
|
||||
}
|
||||
}
|
||||
expect(connectMock).toHaveBeenCalledTimes(2)
|
||||
} finally {
|
||||
act(() => renderer?.unmount())
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { ReactElement } from 'react'
|
||||
import { act, create } from 'react-test-renderer'
|
||||
import type { ReactTestRenderer } from 'react-test-renderer'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { BRIDGE_PROTOCOL_VERSION } from '../mobile-web-shell/bridge/bridge-envelope'
|
||||
import { BRIDGE_PAGE_CLIENT_ID } from '../mobile-web-shell/bridge/bridge-page-client-identity'
|
||||
@@ -47,6 +48,15 @@ function Screen(): null {
|
||||
return null
|
||||
}
|
||||
|
||||
const trees: ReactTestRenderer[] = []
|
||||
const clients: BridgeRpcClient[] = []
|
||||
|
||||
function mount(client: BridgeRpcClient): ReactTestRenderer {
|
||||
const tree = create(render(client))
|
||||
trees.push(tree)
|
||||
return tree
|
||||
}
|
||||
|
||||
function render(client: BridgeRpcClient): ReactElement {
|
||||
return (
|
||||
<RpcClientProvider client={client}>
|
||||
@@ -73,12 +83,16 @@ function installChannel(): { deliver: (frame: unknown) => void } {
|
||||
}
|
||||
|
||||
/** What the entry hands the provider: one client, already holding a session. */
|
||||
function createReadyClient(deliver: (frame: unknown) => void): BridgeRpcClient {
|
||||
function createReadyClient(
|
||||
deliver: (frame: unknown) => void,
|
||||
init: unknown = INIT
|
||||
): BridgeRpcClient {
|
||||
const client = createShellPageClient()
|
||||
if (client === null) {
|
||||
throw new Error('no channel installed')
|
||||
}
|
||||
deliver(INIT)
|
||||
clients.push(client)
|
||||
deliver(init)
|
||||
return client
|
||||
}
|
||||
|
||||
@@ -97,6 +111,15 @@ beforeEach(() => {
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
act(() => {
|
||||
for (const tree of trees.splice(0)) {
|
||||
tree.unmount()
|
||||
}
|
||||
})
|
||||
for (const client of clients.splice(0)) {
|
||||
client.close()
|
||||
}
|
||||
vi.restoreAllMocks()
|
||||
vi.useRealTimers()
|
||||
Reflect.deleteProperty(globalThis, 'orcaBridge')
|
||||
})
|
||||
@@ -106,7 +129,7 @@ describe('the page provider', () => {
|
||||
const channel = installChannel()
|
||||
const client = createReadyClient(channel.deliver)
|
||||
act(() => {
|
||||
create(render(client))
|
||||
mount(client)
|
||||
})
|
||||
|
||||
expect(screen.mounts).toBe(1)
|
||||
@@ -124,7 +147,7 @@ describe('the page provider', () => {
|
||||
const channel = installChannel()
|
||||
const client = createReadyClient(channel.deliver)
|
||||
act(() => {
|
||||
create(render(client))
|
||||
mount(client)
|
||||
})
|
||||
|
||||
const context = readContext()
|
||||
@@ -138,7 +161,7 @@ describe('the page provider', () => {
|
||||
const channel = installChannel()
|
||||
const client = createReadyClient(channel.deliver)
|
||||
act(() => {
|
||||
create(render(client))
|
||||
mount(client)
|
||||
})
|
||||
|
||||
// What `terminal.subscribe` carries as `client.id` and what the send gate reads. Null was not a
|
||||
@@ -152,7 +175,7 @@ describe('the page provider', () => {
|
||||
const channel = installChannel()
|
||||
const client = createReadyClient(channel.deliver)
|
||||
act(() => {
|
||||
create(render(client))
|
||||
mount(client)
|
||||
})
|
||||
const first = readContext().getClientId('host-a')
|
||||
|
||||
@@ -160,8 +183,14 @@ describe('the page provider', () => {
|
||||
// The composer's send journal refuses a retained operation whose caller fingerprint moved, and
|
||||
// it has no expiry, so that would make "send it again" a permanent refusal for that message.
|
||||
act(() => {
|
||||
channel.deliver({ ...INIT, sessionId: 'session-b' })
|
||||
create(render(client))
|
||||
for (const tree of trees.splice(0)) {
|
||||
tree.unmount()
|
||||
}
|
||||
})
|
||||
client.close()
|
||||
const nextChannel = installChannel()
|
||||
act(() => {
|
||||
mount(createReadyClient(nextChannel.deliver, { ...INIT, sessionId: 'session-b' }))
|
||||
})
|
||||
|
||||
expect(readContext().getClientId('host-a')).toBe(first)
|
||||
@@ -172,7 +201,7 @@ describe('the page provider', () => {
|
||||
const channel = installChannel()
|
||||
const client = createReadyClient(channel.deliver)
|
||||
act(() => {
|
||||
create(render(client))
|
||||
mount(client)
|
||||
})
|
||||
const listener = vi.fn()
|
||||
readContext().subscribeHostState('host-a', listener)
|
||||
@@ -193,10 +222,10 @@ describe('the page provider', () => {
|
||||
const channel = installChannel()
|
||||
const client = createReadyClient(channel.deliver)
|
||||
act(() => {
|
||||
create(render(client))
|
||||
mount(client)
|
||||
})
|
||||
const listener = vi.fn()
|
||||
readContext().subscribeAllHosts(listener)
|
||||
const unsubscribe = readContext().subscribeAllHosts(listener)
|
||||
|
||||
act(() => {
|
||||
channel.deliver({
|
||||
@@ -207,13 +236,22 @@ describe('the page provider', () => {
|
||||
})
|
||||
|
||||
expect(listener).toHaveBeenCalledTimes(1)
|
||||
unsubscribe()
|
||||
act(() => {
|
||||
channel.deliver({
|
||||
v: BRIDGE_PROTOCOL_VERSION,
|
||||
type: 'state',
|
||||
connection: { ...INIT.connection, state: 'disconnected' }
|
||||
})
|
||||
})
|
||||
expect(listener).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('stops listening when the screen that asked goes away', () => {
|
||||
const channel = installChannel()
|
||||
const client = createReadyClient(channel.deliver)
|
||||
act(() => {
|
||||
create(render(client))
|
||||
mount(client)
|
||||
})
|
||||
const listener = vi.fn()
|
||||
const unsubscribe = readContext().subscribeHostState('host-a', listener)
|
||||
@@ -235,10 +273,11 @@ describe('the page provider', () => {
|
||||
const client = createReadyClient(channel.deliver)
|
||||
const close = vi.spyOn(client, 'close')
|
||||
act(() => {
|
||||
create(render(client))
|
||||
mount(client)
|
||||
})
|
||||
|
||||
const context = readContext()
|
||||
expect(context.acquire('host-a', {})).toBe(client)
|
||||
context.release('host-a', {})
|
||||
context.releaseAndCloseIfUnused('host-a', {})
|
||||
context.closeIfUnused('host-a')
|
||||
@@ -255,7 +294,7 @@ describe('the page provider', () => {
|
||||
// renders, and an inert one painted controls whose only effect on the page was nothing.
|
||||
const channel = installChannel()
|
||||
act(() => {
|
||||
create(render(createReadyClient(channel.deliver)))
|
||||
mount(createReadyClient(channel.deliver))
|
||||
})
|
||||
expect(readContext().forceReconnect).toBeNull()
|
||||
})
|
||||
|
||||
@@ -35,10 +35,13 @@ describe('executionHost on the envelope', () => {
|
||||
streams.subscribe('terminal.subscribe', params, () => {})
|
||||
await vi.waitFor(() => expect(sent).toHaveLength(2))
|
||||
disposeServer()
|
||||
expect(sent.at(-1)).toMatchObject({
|
||||
expect(sent.at(-1)).toEqual({
|
||||
id: 'relay-3',
|
||||
method: 'terminal.unsubscribe',
|
||||
params: { subscriptionId: 't1:phone', client: { id: 'phone' }, requestId: 'relay-1' },
|
||||
envelope: { executionHost: SERVER }
|
||||
})
|
||||
streams.clear()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -59,13 +62,21 @@ describe('a relay stream slot', () => {
|
||||
envelope: { executionHost: SERVER, trace: 't' }
|
||||
})
|
||||
await vi.waitFor(() => expect(sent).toHaveLength(1))
|
||||
streams.subscribe('terminal.subscribe', params, () => {}, {
|
||||
const disposeNewer = streams.subscribe('terminal.subscribe', params, () => {}, {
|
||||
envelope: { trace: 't', executionHost: SERVER }
|
||||
})
|
||||
await vi.waitFor(() => expect(sent).toHaveLength(2))
|
||||
// The newer sibling already holds the slot on the server, so the older one sends no unsubscribe.
|
||||
disposeOlder()
|
||||
expect(sent.map((frame) => frame.method)).toEqual(['terminal.subscribe', 'terminal.subscribe'])
|
||||
disposeNewer()
|
||||
expect(sent.at(-1)).toEqual({
|
||||
id: 'relay-3',
|
||||
method: 'terminal.unsubscribe',
|
||||
params: { subscriptionId: 't1:phone', client: { id: 'phone' }, requestId: 'relay-2' },
|
||||
envelope: { trace: 't', executionHost: SERVER }
|
||||
})
|
||||
streams.clear()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -81,18 +92,28 @@ describe('a server workspace call on the direct wire', () => {
|
||||
streams.subscribe(method, params, onData, options)
|
||||
}
|
||||
const server = scopeRpcClientToExecutionHost(direct, SERVER)
|
||||
void server.sendRequest('worktree.ps', { limit: 1 })
|
||||
await vi.waitFor(() => expect(socket.sent).toHaveLength(1))
|
||||
server.subscribe('terminal.subscribe', { terminal: 't1' }, () => {})
|
||||
// What #26501 wrote with its own `executionHost` option; the desktop parses JSON, so key order is not read.
|
||||
expect(socket.frames()).toStrictEqual([
|
||||
JSON.parse(
|
||||
'{"id":"rpc-1","deviceToken":"device-token","method":"worktree.ps","params":{"limit":1},"executionHost":"runtime:env-1"}'
|
||||
),
|
||||
JSON.parse(
|
||||
'{"id":"rpc-2","deviceToken":"device-token","method":"terminal.subscribe","params":{"terminal":"t1"},"executionHost":"runtime:env-1"}'
|
||||
)
|
||||
])
|
||||
const request = server.sendRequest('worktree.ps', { limit: 1 })
|
||||
let dispose: (() => void) | undefined
|
||||
try {
|
||||
await vi.waitFor(() => expect(socket.sent).toHaveLength(1))
|
||||
dispose = server.subscribe('terminal.subscribe', { terminal: 't1' }, () => {})
|
||||
// What #26501 wrote with its own `executionHost` option; the desktop parses JSON, so key order is not read.
|
||||
expect(socket.frames()).toStrictEqual([
|
||||
JSON.parse(
|
||||
'{"id":"rpc-1","deviceToken":"device-token","method":"worktree.ps","params":{"limit":1},"executionHost":"runtime:env-1"}'
|
||||
),
|
||||
JSON.parse(
|
||||
'{"id":"rpc-2","deviceToken":"device-token","method":"terminal.subscribe","params":{"terminal":"t1"},"executionHost":"runtime:env-1"}'
|
||||
)
|
||||
])
|
||||
const reply: RpcResponse = { id: 'rpc-1', ok: true, result: { worktrees: [] } }
|
||||
expect(requests.resolve(reply)).toBe(true)
|
||||
await expect(request).resolves.toBe(reply)
|
||||
} finally {
|
||||
dispose?.()
|
||||
requests.rejectAll('test cleanup')
|
||||
await request.catch(() => {})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -124,7 +145,11 @@ describe('scopeRpcClientToExecutionHost', () => {
|
||||
const client = fakeClient()
|
||||
const view = scopeRpcClientToExecutionHost(client, SERVER)
|
||||
void view.sendRequest('worktree.ps', { limit: 1 }, { timeoutMs: 5, envelope: { trace: 't' } })
|
||||
view.subscribe('terminal.subscribe', { terminal: 't1' }, () => {})
|
||||
const onBinaryFrame = vi.fn()
|
||||
view.subscribe('terminal.subscribe', { terminal: 't1' }, () => {}, {
|
||||
onBinaryFrame,
|
||||
envelope: { trace: 'stream', executionHost: 'runtime:wrong' }
|
||||
})
|
||||
view.close()
|
||||
expect(client.sendRequest).toHaveBeenCalledWith(
|
||||
'worktree.ps',
|
||||
@@ -135,7 +160,7 @@ describe('scopeRpcClientToExecutionHost', () => {
|
||||
'terminal.subscribe',
|
||||
{ terminal: 't1' },
|
||||
expect.any(Function),
|
||||
{ envelope: { executionHost: SERVER } }
|
||||
{ onBinaryFrame, envelope: { trace: 'stream', executionHost: SERVER } }
|
||||
)
|
||||
expect(client.close).not.toHaveBeenCalled()
|
||||
expect(scopeRpcClientToExecutionHost(client, SERVER)).toBe(view)
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
DECORATIVE_AGENT_TITLE_SIGNATURE_SOURCE_SCAN_LIMIT,
|
||||
getDecorativeAgentTitleSignature,
|
||||
isDecorativeAgentTitleFrameChange
|
||||
} from './agent-decorative-title-signature'
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
const BRAILLE_SPINNER_FRAME_A = String.fromCharCode(0x280b)
|
||||
const BRAILLE_SPINNER_FRAME_B = String.fromCharCode(0x2819)
|
||||
|
||||
@@ -26,27 +22,20 @@ describe('agent decorative title signatures', () => {
|
||||
expect(getDecorativeAgentTitleSignature('vim src/index.ts')).toBeNull()
|
||||
})
|
||||
|
||||
it('folds title whitespace without full whitespace replacement', () => {
|
||||
const replace = vi.spyOn(String.prototype, 'replace')
|
||||
it('folds title whitespace in the decorative signature', () => {
|
||||
const title = ` \t${BRAILLE_SPINNER_FRAME_A}\t Codex is\nthinking ${' pasted text '.repeat(40)}`
|
||||
|
||||
expect(getDecorativeAgentTitleSignature(title)).toBe(
|
||||
`working:Codex is thinking ${'pasted text '.repeat(40).trimEnd()}`
|
||||
)
|
||||
expect(
|
||||
replace.mock.calls.filter(
|
||||
([pattern]) => pattern instanceof RegExp && pattern.source === '\\s+'
|
||||
)
|
||||
).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('does not normalize oversized title text on the hot path', () => {
|
||||
const replace = vi.spyOn(String.prototype, 'replace')
|
||||
const title = `${BRAILLE_SPINNER_FRAME_A} Codex is thinking ${'x'.repeat(
|
||||
DECORATIVE_AGENT_TITLE_SIGNATURE_SOURCE_SCAN_LIMIT + 1
|
||||
)}`
|
||||
it('accepts the scan limit and returns null for longer decorative titles', () => {
|
||||
const prefix = `${BRAILLE_SPINNER_FRAME_A} Codex is thinking `
|
||||
const tail = 'x'.repeat(DECORATIVE_AGENT_TITLE_SIGNATURE_SOURCE_SCAN_LIMIT - prefix.length)
|
||||
const title = `${prefix}${tail}`
|
||||
|
||||
expect(getDecorativeAgentTitleSignature(title)).toBeNull()
|
||||
expect(replace).not.toHaveBeenCalled()
|
||||
expect(getDecorativeAgentTitleSignature(title)).toBe(`working:Codex is thinking ${tail}`)
|
||||
expect(getDecorativeAgentTitleSignature(`${title}x`)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -598,10 +598,7 @@ describe('shared agent-hook-listener', () => {
|
||||
})
|
||||
|
||||
it('does not resurrect persisted idle child rows after a restart', () => {
|
||||
// Why: the roster tracks only working children now. A persisted idle
|
||||
// snapshot (from a build that kept idle rows) is a finished child, so
|
||||
// hydration must drop it — otherwise restart would re-pile the exact
|
||||
// squatting rows this fix removes.
|
||||
// Idle snapshots have no live confirmation after restart.
|
||||
seedClaudeSubagentRosterFromSnapshots(state, PANE_KEY, [
|
||||
{
|
||||
id: 'aprobe2-6d3cb5b52120b7bf',
|
||||
@@ -610,7 +607,8 @@ describe('shared agent-hook-listener', () => {
|
||||
agentType: 'security-reviewer'
|
||||
}
|
||||
])
|
||||
claudeEvent({ hook_event_name: 'UserPromptSubmit', prompt: 'after restart' })
|
||||
const resumed = claudeEvent({ hook_event_name: 'UserPromptSubmit', prompt: 'after restart' })
|
||||
expect(resumed?.payload.subagents).toBeUndefined()
|
||||
const stop = claudeEvent({
|
||||
hook_event_name: 'Stop',
|
||||
background_tasks: [
|
||||
|
||||
@@ -408,7 +408,7 @@ describe('Claude owed task notifications that never arrive', () => {
|
||||
wallClock.mockRestore()
|
||||
})
|
||||
|
||||
it("restates under the stored row's launch token and owner, as a row with no hook event", () => {
|
||||
it("restates under the stored row's launch token and session, as a row with no hook event", () => {
|
||||
const { post, restated } = host()
|
||||
const tokened = (payload: Record<string, unknown>) =>
|
||||
post(payload, PANE_KEY, { launchToken: 'launch-1' })
|
||||
@@ -421,7 +421,12 @@ describe('Claude owed task notifications that never arrive', () => {
|
||||
|
||||
vi.advanceTimersByTime(LEASE)
|
||||
|
||||
expect(restated[0]).toMatchObject({ launchToken: 'launch-1', payload: { state: 'done' } })
|
||||
expect(restated[0]).toMatchObject({
|
||||
launchToken: 'launch-1',
|
||||
source: 'claude',
|
||||
providerSession: { key: 'session_id', id: SESSION },
|
||||
payload: { state: 'done' }
|
||||
})
|
||||
expect(restated[0].hookEventName).toBeUndefined()
|
||||
})
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
normalizeAndAccept,
|
||||
PANE_KEY
|
||||
} from './agent-hook-listener-test-harness'
|
||||
import { makePaneKey } from './stable-pane-id'
|
||||
|
||||
describe('shared agent-hook-listener', () => {
|
||||
let state: HookListenerState
|
||||
@@ -154,7 +155,7 @@ describe('shared agent-hook-listener', () => {
|
||||
state,
|
||||
'claude',
|
||||
{
|
||||
paneKey: 'x'.repeat(300),
|
||||
paneKey: makePaneKey('x'.repeat(300), '11111111-1111-4111-8111-111111111111'),
|
||||
payload: { hook_event_name: 'UserPromptSubmit', prompt: 'hi' }
|
||||
},
|
||||
'production'
|
||||
|
||||
@@ -39,10 +39,6 @@ describe('agentKindToTuiAgent', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it("reverses Claude's product id back to the TuiAgent id", () => {
|
||||
expect(agentKindToTuiAgent('claude-code')).toBe('claude')
|
||||
})
|
||||
|
||||
it('returns null for the catch-all and missing kinds', () => {
|
||||
expect(agentKindToTuiAgent('other')).toBeNull()
|
||||
expect(agentKindToTuiAgent(null)).toBeNull()
|
||||
|
||||
@@ -14,7 +14,9 @@ describe('toAgentLaunchPreferences', () => {
|
||||
})
|
||||
|
||||
it('answers nothing when no preference survives', () => {
|
||||
expect(toAgentLaunchPreferences({ model: ' ', fastMode: true })).toBeUndefined()
|
||||
expect(
|
||||
toAgentLaunchPreferences({ model: ' ', effort: 123, mode: false, fastMode: true })
|
||||
).toBeUndefined()
|
||||
expect(toAgentLaunchPreferences(undefined)).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user