fix: reset Muse hook state and strengthen regression tests (#33008)

* fix: reset Muse hook state and strengthen boundary tests

* test: require live permission, replay, and recency controls

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
OrcaWin
2026-10-23 19:25:11 -07:00
committed by GitHub
co-authored by OrcaWin
parent 4975e2451e
commit 3b380ddfcf
12 changed files with 306 additions and 93 deletions
@@ -91,19 +91,24 @@ it('removes old generated trees and retains recent, future, malformed and mutabl
it('does not follow cache, label, entry or shared-format symlinks', async () => {
const options = await fixture()
const outside = await stamped(join(options.root, 'outside', `${commit}-format-4`), 4)
const outsideCache = join(options.root, 'outside-cache')
const outsideLabel = join(options.root, 'outside-label')
const outsideEntry = join(options.root, 'outside-entry', `${commit}-format-4`)
const outsideFormat = join(options.root, 'outside-format')
const sentinels = [
await stamped(join(outsideCache, 'v-old', `${commit}-format-4`), 4),
await stamped(join(outsideLabel, `${commit}-format-4`), 4),
await stamped(outsideEntry, 4),
await stamped(join(outsideFormat, commit), CHECKOUT_FORMAT)
]
const second = join(options.root, 'linked-worktree')
await mkdir(join(second, 'tests', 'e2e'), { recursive: true })
await symlink(
join(outside, '..'),
join(second, 'tests', 'e2e', '.cross-version-checkouts'),
'junction'
)
await symlink(outsideCache, join(second, 'tests', 'e2e', '.cross-version-checkouts'), 'junction')
options.targets.worktrees.push(second)
await symlink(join(outside, '..'), join(options.cache, 'linked-label'), 'junction')
await symlink(outsideLabel, join(options.cache, 'linked-label'), 'junction')
await mkdir(join(options.cache, 'linked-entry'))
await symlink(outside, join(options.cache, 'linked-entry', `${commit}-format-4`), 'junction')
await symlink(join(outside, '..'), join(options.shared, 'format-4'), 'junction')
await symlink(outsideEntry, join(options.cache, 'linked-entry', `${commit}-format-4`), 'junction')
await symlink(outsideFormat, join(options.shared, `format-${CHECKOUT_FORMAT}`), 'junction')
const result = await reclaimReleaseTestCaches({
...options,
apply: true,
@@ -111,7 +116,11 @@ it('does not follow cache, label, entry or shared-format symlinks', async () =>
minAgeDays: 0
})
expect(result.entries).toBe(0)
expect(await readFile(join(outside, 'source.ts'), 'utf8')).toContain('historical')
for (const sentinel of sentinels) {
expect(await readFile(join(sentinel, 'source.ts'), 'utf8')).toBe(
'export const historical = true\n'
)
}
})
it('skips publication locks and a shared source copy lease', async () => {
+27 -2
View File
@@ -182,6 +182,14 @@ it('does not sweep while cache initialization holds the maintenance lock', async
it('tracks a real Vite server until shutdown has completed', async () => {
const options = await fixture()
let unblockClose
let enteredClose
const closeBlocked = new Promise((resolve) => {
unblockClose = resolve
})
const closeEntered = new Promise((resolve) => {
enteredClose = resolve
})
const server = await createServer({
configFile: false,
root: options.worktree,
@@ -189,13 +197,30 @@ it('tracks a real Vite server until shutdown has completed', async () => {
logLevel: 'silent',
server: { middlewareMode: true, watch: null, hmr: false },
optimizeDeps: { noDiscovery: true, include: [] },
plugins: [createViteCacheRetentionPlugin(options.cacheRoot)]
plugins: [
createViteCacheRetentionPlugin(options.cacheRoot),
{
name: 'hold-vite-close',
async closeBundle() {
enteredClose()
await closeBlocked
}
}
]
})
try {
await age(options.cacheRoot)
expect(await reclaimViteCaches({ ...options, apply: true })).toMatchObject({ entries: 0 })
} finally {
await server.close()
const closing = server.close()
try {
await closeEntered
await age(options.cacheRoot)
expect(await reclaimViteCaches({ ...options, apply: true })).toMatchObject({ entries: 0 })
} finally {
unblockClose()
await closing
}
}
expect(await readdir(join(options.cacheRoot, VITE_CACHE_USAGE_DIRECTORY))).toEqual([])
expect(await reclaimViteCaches({ ...options, apply: true })).toMatchObject({ entries: 0 })
@@ -56,6 +56,7 @@ it('does not interpret piped input as approval', async () => {
const reclaim = vi.fn().mockResolvedValue({ completed: true })
await runReclaimCommand([], { ...streams, reclaim })
expect(reclaim).toHaveBeenCalledTimes(1)
expect(reclaim.mock.calls[0][0].apply).toBe(false)
expect(streams.text()).not.toContain('[y/N]')
})
@@ -61,6 +61,7 @@ it('leaves historical caches out of automatic invocations', async () => {
excludeWorktree: '/dev-worktree'
}
await reclaimWorktreeSpace(options)
expect(options.execute).toHaveBeenCalledTimes(3)
expect(options.execute.mock.calls.flatMap(([spec]) => spec.args).join(' ')).not.toContain(
'reclaim-release-test-caches'
)
@@ -303,7 +303,7 @@ describe('AgentHookServer listener replay', () => {
})
}
it('re-attaching a retired pane while idle re-opens it for a much later first turn', async () => {
it('re-attaching an idle retired pane accepts completion and a later turn', async () => {
const server = new AgentHookServer()
await server.start({ env: 'production' })
try {
@@ -707,6 +707,13 @@ describe('AgentHookServer closed-tab suppression bound', () => {
internals.markTabClosedForAgentStatus(`closed-tab-${i}`)
}
const refreshedTab = `closed-tab-${total - CLOSED_AGENT_STATUS_TAB_IDS_MAX}`
const nextOldestTab = `closed-tab-${total - CLOSED_AGENT_STATUS_TAB_IDS_MAX + 1}`
internals.markTabClosedForAgentStatus(refreshedTab)
internals.markTabClosedForAgentStatus('closed-tab-next')
expect(internals.closedAgentStatusTabIds.has(refreshedTab)).toBe(true)
expect(internals.closedAgentStatusTabIds.has(nextOldestTab)).toBe(false)
expect(internals.closedAgentStatusTabIds.size).toBe(CLOSED_AGENT_STATUS_TAB_IDS_MAX)
expect(internals.closedAgentStatusTabIds.has('closed-tab-0')).toBe(false)
expect(internals.closedAgentStatusTabIds.has(`closed-tab-${total - 1}`)).toBe(true)
+51 -22
View File
@@ -1,13 +1,25 @@
import { mkdtempSync, readFileSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
import { isAgentHookEndpointFileName, parseAgentHookEndpointFile } from './agent-hook-endpoint-file'
import { parseAgentHookEndpointFile } from './agent-hook-endpoint-file'
import { getEndpointFileName, writeEndpointFile } from './agent-hook-listener/endpoint-publication'
const ENDPOINT_LINES = [
'ORCA_AGENT_HOOK_PORT=12345',
'ORCA_AGENT_HOOK_TOKEN=token-123',
'ORCA_AGENT_HOOK_ENV=production',
'ORCA_AGENT_HOOK_VERSION=1'
]
const REQUIRED_FIELDS = [
'ORCA_AGENT_HOOK_PORT',
'ORCA_AGENT_HOOK_TOKEN',
'ORCA_AGENT_HOOK_ENV',
'ORCA_AGENT_HOOK_VERSION'
]
const ENDPOINT = { port: '12345', token: 'token-123', env: 'production', version: '1' }
describe('agent hook endpoint files', () => {
it('recognizes POSIX and Windows endpoint file names', () => {
expect(isAgentHookEndpointFileName('endpoint.env')).toBe(true)
expect(isAgentHookEndpointFileName('endpoint.cmd')).toBe(true)
expect(isAgentHookEndpointFileName('endpoint.ps1')).toBe(false)
})
it('parses POSIX endpoint.env contents', () => {
expect(
parseAgentHookEndpointFile(
@@ -44,22 +56,39 @@ describe('agent hook endpoint files', () => {
})
})
it('preserves equals signs in endpoint values', () => {
expect(
parseAgentHookEndpointFile(
[
'ORCA_AGENT_HOOK_PORT=12345',
'ORCA_AGENT_HOOK_TOKEN=token=with=equals',
'ORCA_AGENT_HOOK_ENV=production',
'ORCA_AGENT_HOOK_VERSION=1'
].join('\n')
).token
).toBe('token=with=equals')
})
it('throws when required endpoint fields are missing', () => {
expect(() => parseAgentHookEndpointFile('ORCA_AGENT_HOOK_PORT=12345')).toThrow(
it.each(REQUIRED_FIELDS)('rejects an independently missing %s', (field) => {
expect(parseAgentHookEndpointFile(ENDPOINT_LINES.join('\n'))).toEqual(ENDPOINT)
const missing = ENDPOINT_LINES.filter((line) => !line.startsWith(`${field}=`)).join('\n')
expect(() => parseAgentHookEndpointFile(missing)).toThrow(
'Agent hook endpoint file is missing required fields'
)
})
it.each(REQUIRED_FIELDS)('rejects an independently empty %s', (field) => {
expect(parseAgentHookEndpointFile(ENDPOINT_LINES.join('\n'))).toEqual(ENDPOINT)
const empty = ENDPOINT_LINES.map((line) =>
line.startsWith(`${field}=`) ? `${field}=` : line
).join('\n')
expect(() => parseAgentHookEndpointFile(empty)).toThrow(
'Agent hook endpoint file is missing required fields'
)
})
it('parses the endpoint bytes published by the platform writer', () => {
const directory = mkdtempSync(join(tmpdir(), 'orca-hook-endpoint-parser-'))
try {
const path = join(directory, getEndpointFileName())
expect(
writeEndpointFile(directory, path, {
port: 12345,
token: 'token-123',
env: 'production',
version: '1'
})
).toBe(true)
expect(parseAgentHookEndpointFile(readFileSync(path, 'utf8'))).toEqual(ENDPOINT)
} finally {
rmSync(directory, { recursive: true, force: true })
}
})
})
@@ -1,20 +1,30 @@
import { mkdtempSync, readdirSync, rmSync, utimesSync, writeFileSync } from 'node:fs'
import type * as NodeFs from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const fsMocks = vi.hoisted(() => ({
enabled: false,
closeSync: vi.fn(),
readSync: vi.fn(),
statSync: vi.fn(),
unlinkSync: vi.fn()
}))
vi.mock('node:fs', () => ({
opendirSync: () => ({
closeSync: fsMocks.closeSync,
readSync: fsMocks.readSync
}),
statSync: fsMocks.statSync,
unlinkSync: fsMocks.unlinkSync
}))
vi.mock('node:fs', async (importOriginal) => {
const actual = await importOriginal<typeof NodeFs>()
return {
...actual,
opendirSync: (path: string, options: { bufferSize: number }) =>
fsMocks.enabled
? { closeSync: fsMocks.closeSync, readSync: fsMocks.readSync }
: actual.opendirSync(path, options),
statSync: (path: string) => (fsMocks.enabled ? fsMocks.statSync(path) : actual.statSync(path)),
unlinkSync: (path: string) =>
fsMocks.enabled ? fsMocks.unlinkSync(path) : actual.unlinkSync(path)
}
})
import {
AGENT_HOOK_ENDPOINT_SWEEP_MAX_ENTRIES,
@@ -23,11 +33,13 @@ import {
describe('agent hook endpoint temp cleanup', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.resetAllMocks()
fsMocks.enabled = false
fsMocks.statSync.mockReturnValue({ mtimeMs: 0 })
})
it('stops an unbounded directory source at the scan cap', () => {
fsMocks.enabled = true
let entry = 0
fsMocks.readSync.mockImplementation(() => ({
name: `.endpoint-${(entry += 1)}.tmp`
@@ -40,19 +52,47 @@ describe('agent hook endpoint temp cleanup', () => {
expect(fsMocks.closeSync).toHaveBeenCalledOnce()
})
it('only removes stale endpoint temp files', () => {
fsMocks.readSync
.mockReturnValueOnce({ name: '.endpoint-stale.tmp' })
.mockReturnValueOnce({ name: '.endpoint-fresh.tmp' })
.mockReturnValueOnce({ name: 'endpoint.env' })
.mockReturnValueOnce(null)
fsMocks.statSync
.mockReturnValueOnce({ mtimeMs: 0 })
.mockReturnValueOnce({ mtimeMs: 9 * 60 * 1000 })
it('counts skipped names against the scan cap', () => {
fsMocks.enabled = true
let scanned = 0
fsMocks.readSync.mockImplementation(() =>
++scanned <= AGENT_HOOK_ENDPOINT_SWEEP_MAX_ENTRIES + 2
? { name: `unrelated-${scanned}` }
: null
)
sweepStaleAgentHookEndpointTemps('/endpoint', 10 * 60 * 1000)
expect(fsMocks.readSync).toHaveBeenCalledTimes(AGENT_HOOK_ENDPOINT_SWEEP_MAX_ENTRIES)
expect(fsMocks.statSync).not.toHaveBeenCalled()
expect(fsMocks.unlinkSync).not.toHaveBeenCalled()
expect(fsMocks.closeSync).toHaveBeenCalledOnce()
})
expect(fsMocks.unlinkSync).toHaveBeenCalledOnce()
expect(fsMocks.unlinkSync).toHaveBeenCalledWith(expect.stringContaining('.endpoint-stale.tmp'))
it('removes only matching temp files strictly older than five minutes', () => {
const directory = mkdtempSync(join(tmpdir(), 'orca-hook-endpoint-sweep-'))
try {
const now = 1_000_000
for (const [name, age] of [
['.endpoint-stale.tmp', 300_001],
['.endpoint-boundary.tmp', 300_000],
['.endpoint-fresh.tmp', 299_999],
['.endpoint-wrong-extension.txt', 900_000],
['wrong-prefix.tmp', 900_000],
['endpoint.env', 900_000]
] as const) {
const path = join(directory, name)
writeFileSync(path, 'endpoint fixture')
utimesSync(path, (now - age) / 1000, (now - age) / 1000)
}
sweepStaleAgentHookEndpointTemps(directory, now)
expect(readdirSync(directory).sort()).toEqual([
'.endpoint-boundary.tmp',
'.endpoint-fresh.tmp',
'.endpoint-wrong-extension.txt',
'endpoint.env',
'wrong-prefix.tmp'
])
} finally {
rmSync(directory, { recursive: true, force: true })
}
})
})
@@ -1,6 +1,7 @@
import { describe, expect, it, vi } from 'vitest'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { normalizeHookPayload } from './agent-hook-listener'
import type { AgentHookSource } from './agent-hook-relay'
import {
clearAllListenerCaches,
clearPaneCacheState,
@@ -10,54 +11,53 @@ import {
} from './agent-hook-listener/listener-state'
import { warnOnHookEnvOrVersionMismatch } from './agent-hook-listener/listener-limits'
import { resolveHookSource } from './agent-hook-listener/source-routing'
import { bindOpenCodeSession } from './agent-hook-listener/opencode-session-registry'
import { makePaneKey } from './stable-pane-id'
const PANE = makePaneKey('tab-hooks', '11111111-1111-4111-8112-111111111111')
const MOVED_PANE = makePaneKey('tab-hooks', '22222222-2222-4222-8222-222222222222')
const ROUTES = {
'/hook/claude': 'claude',
'/hook/codex': 'codex',
'/hook/gemini': 'gemini',
'/hook/antigravity': 'antigravity',
'/hook/amp': 'amp',
'/hook/opencode': 'opencode',
'/hook/mimo-code': 'mimo-code',
'/hook/cursor': 'cursor',
'/hook/pi': 'pi',
'/hook/omp': 'omp',
'/hook/prime-agent': 'prime-agent',
'/hook/droid': 'droid',
'/hook/command-code': 'command-code',
'/hook/grok': 'grok',
'/hook/copilot': 'copilot',
'/hook/hermes': 'hermes',
'/hook/devin': 'devin',
'/hook/kimi': 'kimi',
'/hook/jcode': 'jcode'
} as const
function normalizeProviderState(
source: (typeof ROUTES)[keyof typeof ROUTES],
source: AgentHookSource,
eventName: string,
payload: Record<string, unknown> = {}
) {
const routedSource = resolveHookSource(`/hook/${source}`)
if (routedSource === null) {
return null
}
return (
normalizeHookPayload(
createHookListenerState(),
source,
routedSource,
{ paneKey: PANE, hook_event_name: eventName, payload },
'production'
)?.payload ?? null
)
}
afterEach(() => vi.restoreAllMocks())
describe('agent hook extraction boundaries', () => {
it('routes exactly the complete provider vocabulary', () => {
for (const [pathname, source] of Object.entries(ROUTES)) {
expect(resolveHookSource(pathname)).toBe(source)
expect(resolveHookSource(`${pathname}/`)).toBeNull()
expect(resolveHookSource(`${pathname}?v=1`)).toBeNull()
expect(resolveHookSource(pathname.toUpperCase())).toBeNull()
}
it.each([
['qoder', 'UserPromptSubmit'],
['qoder-cn', 'UserPromptSubmit'],
['qwen-code', 'UserPromptSubmit'],
['codebuddy', 'UserPromptSubmit'],
['opencode2', 'SessionBusy'],
['muse', 'UserPromptSubmit'],
['zcode', 'UserPromptSubmit'],
['dsh', 'UserPromptSubmit'],
['kiro', 'userPromptSubmit']
] as const)('normalizes %s hook traffic through its routed source', (source, eventName) => {
expect(normalizeProviderState(source, eventName, { prompt: 'routed prompt' })).toMatchObject({
state: 'working',
agentType: source,
prompt: 'routed prompt'
})
})
it('rejects unknown and malformed hook paths', () => {
expect(resolveHookSource('/hook/unknown')).toBeNull()
expect(resolveHookSource('/hook/devin/')).toBeNull()
expect(resolveHookSource('/HOOK/DEVIN')).toBeNull()
})
it('preserves thin provider lifecycle branches and source attribution', () => {
@@ -86,9 +86,17 @@ describe('agent hook extraction boundaries', () => {
}
})
it('warns before tab rejection and caps version and environment warning keys independently', () => {
it('warns before tab rejection and bounds distinct version and environment warnings', () => {
const orderState = createHookListenerState()
const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
expect(
normalizeHookPayload(
createHookListenerState(),
'devin',
{ paneKey: PANE, payload: { hook_event_name: 'UserPromptSubmit' } },
'production'
)?.payload
).toMatchObject({ state: 'working', agentType: 'devin' })
const rejected = normalizeHookPayload(
orderState,
'devin',
@@ -96,13 +104,25 @@ describe('agent hook extraction boundaries', () => {
paneKey: PANE,
tabId: 'wrong-tab',
version: 'outdated',
payload: { hook_event_name: 'UnknownEvent' }
payload: { hook_event_name: 'UserPromptSubmit' }
},
'production'
)
expect(rejected).toBeNull()
expect(warn).toHaveBeenCalledTimes(1)
warn.mockClear()
const repeatedState = createHookListenerState()
for (let attempt = 0; attempt < 2; attempt++) {
warnOnHookEnvOrVersionMismatch(repeatedState, {
version: 'repeated-version',
env: 'repeated-environment',
expectedEnv: 'production'
})
}
expect(warn).toHaveBeenCalledTimes(2)
warn.mockClear()
const cappedState = createHookListenerState()
for (let index = 0; index < 40; index++) {
warnOnHookEnvOrVersionMismatch(cappedState, {
@@ -113,10 +133,10 @@ describe('agent hook extraction boundaries', () => {
}
expect(cappedState.warnedVersions.size).toBe(32)
expect(cappedState.warnedEnvs.size).toBe(32)
warn.mockRestore()
expect(warn).toHaveBeenCalledTimes(64)
})
it('moves every pane-owned collection and preserves NUL suffixes', () => {
it('moves selected pane-owned collections and preserves NUL suffixes', () => {
const state = createHookListenerState()
const scoped = `${PANE}\0child`
const movedScoped = `${MOVED_PANE}\0child`
@@ -169,22 +189,59 @@ describe('agent hook extraction boundaries', () => {
expect(cache.get(movedScoped)).toBe('scoped')
expect(cache.get(sibling)).toBe('sibling')
expect(cache.has(PANE)).toBe(false)
expect(cache.has(scoped)).toBe(false)
}
expect(state.lastStatusByPaneKey.get(MOVED_PANE)?.payload.prompt).toBe('exact')
expect(state.lastStatusByPaneKey.get(movedScoped)?.payload.prompt).toBe('scoped')
expect(state.lastStatusByPaneKey.get(sibling)?.payload.prompt).toBe('sibling')
expect(state.lastStatusByPaneKey.has(PANE)).toBe(false)
expect(state.lastStatusByPaneKey.has(scoped)).toBe(false)
for (const set of paneSets) {
expect(set.has(MOVED_PANE)).toBe(true)
expect(set.has(movedScoped)).toBe(true)
expect(set.has(sibling)).toBe(true)
expect(set.has(PANE)).toBe(false)
expect(set.has(scoped)).toBe(false)
}
movePaneCacheState(state, MOVED_PANE, MOVED_PANE)
expect((state.lastPromptByPaneKey as Map<string, unknown>).get(MOVED_PANE)).toBe('exact')
})
it('preserves bound OpenCode session ownership and launch identity after a pane move', () => {
const state = createHookListenerState()
normalizeHookPayload(
state,
'devin',
{
paneKey: PANE,
launchToken: 'moved-launch',
payload: { hook_event_name: 'UserPromptSubmit' }
},
'production'
)
expect(
bindOpenCodeSession(state, 'moved-session', { paneKey: PANE, boundAt: 1, basis: 'argv' })
).toBe(true)
movePaneCacheState(state, PANE, MOVED_PANE)
const event = normalizeHookPayload(
state,
'opencode',
{
paneKey: PANE,
opencodeSharedServer: 1,
payload: { hook_event_name: 'SessionBusy', sessionID: 'moved-session' }
},
'production'
)
expect(event).toMatchObject({
paneKey: MOVED_PANE,
launchToken: 'moved-launch',
payload: { state: 'working', agentType: 'opencode' }
})
expect(state.lastLaunchTokenByPaneKey.has(PANE)).toBe(false)
})
it('clears exact and NUL-scoped cache keys but not sibling prefixes', () => {
const state = createHookListenerState()
const scoped = `${PANE}\0thread`
@@ -230,6 +287,7 @@ describe('agent hook extraction boundaries', () => {
expect(state.lastStatusByPaneKey.has(scoped)).toBe(false)
expect(state.lastStatusByPaneKey.get(sibling)?.payload.prompt).toBe('sibling')
expect(state.ampCompletedCacheKeys.has(scoped)).toBe(false)
expect(state.ampCompletedCacheKeys.has(PANE)).toBe(false)
expect(state.ampCompletedCacheKeys.has(sibling)).toBe(true)
expect(state.claudeLeadStateByPaneKey.has(PANE)).toBe(false)
expect(state.codexLeadStateByPaneKey.has(PANE)).toBe(false)
@@ -267,6 +325,28 @@ describe('agent hook extraction boundaries', () => {
state.claudeRunningNonAgentTaskPaneKeys.add(PANE)
state.codexLeadStateByPaneKey.set(PANE, { state: 'working', stateStartedAt: 1 })
state.grokActiveTurnByPaneKey.set(PANE, { promptId: 'prompt-1' })
const notification = {
paneKey: PANE,
payload: { hook_event_name: 'Notification', notification_type: 'permission_prompt' }
}
normalizeHookPayload(
state,
'muse',
{
paneKey: PANE,
payload: {
hook_event_name: 'PermissionRequest',
tool_name: 'stale-tool',
tool_input: { command: 'old command' }
}
},
'production'
)
expect(normalizeHookPayload(state, 'muse', notification, 'production')?.payload).toMatchObject({
state: 'waiting',
agentType: 'muse',
toolName: 'stale-tool'
})
clearAllListenerCaches(state)
@@ -276,5 +356,8 @@ describe('agent hook extraction boundaries', () => {
expect(state.claudeRunningNonAgentTaskPaneKeys.size).toBe(0)
expect(state.codexLeadStateByPaneKey.size).toBe(0)
expect(state.grokActiveTurnByPaneKey.size).toBe(0)
const afterReset = normalizeHookPayload(state, 'muse', notification, 'production')
expect(afterReset?.payload).toMatchObject({ state: 'waiting', agentType: 'muse' })
expect(afterReset?.payload.toolName).toBeUndefined()
})
})
@@ -313,6 +313,7 @@ export function clearAllListenerCaches(state: HookListenerState): void {
state.codexLeadStateByPaneKey.clear()
state.grokActiveTurnByPaneKey.clear()
state.grokMainAgentStatusByPaneKey.clear()
state.musePaneStateByPaneKey.clear()
state.opencodeSessionPaneBySessionId.clear()
state.lastLaunchTokenByPaneKey.clear()
}
@@ -248,7 +248,7 @@ describe('annotation viewport bridge live target', () => {
guest.install(true)
guest.flushFrames()
expect(guest.lastReport()?.target).toBeUndefined()
expect(guest.lastReport()).toEqual({ scrollX: 0, scrollY: 0, viewportWidth: 1000 })
})
it('re-reports on resize and when the element changes size', async () => {
@@ -279,7 +279,7 @@ describe('annotation viewport bridge live target', () => {
guest.target.removeAttribute(BROWSER_GRAB_SELECTED_ATTRIBUTE)
guest.window.dispatchEvent(new guest.window.Event('scroll'))
guest.flushFrames()
expect(guest.lastReport()?.target).toBeUndefined()
expect(guest.lastReport()).toEqual({ scrollX: 0, scrollY: 0, viewportWidth: 1000 })
})
})
@@ -590,6 +590,13 @@ describe('publishAgentHookEnvelope redelivery', () => {
publishAgentHookEnvelope(dispatcher, envelope)
}
const refreshed = makeEnvelope({})
refreshed.paneKey = 'tab-1:4f1b0f4e-0000-4000-8000-000000000001'
publishAgentHookEnvelope(dispatcher, refreshed)
const newest = makeEnvelope({})
newest.paneKey = 'tab-1:4f1b0f4e-0000-4000-8000-000000000065'
publishAgentHookEnvelope(dispatcher, newest)
primary.blocked = false
primary.drain()
vi.advanceTimersByTime(1_000)
@@ -598,6 +605,9 @@ describe('publishAgentHookEnvelope redelivery', () => {
expect(redelivered).toHaveLength(64)
expect(redelivered).toContain('tab-1:4f1b0f4e-0000-4000-8000-000000000064')
expect(redelivered).not.toContain('tab-1:4f1b0f4e-0000-4000-8000-000000000000')
expect(redelivered).toContain('tab-1:4f1b0f4e-0000-4000-8000-000000000001')
expect(redelivered).not.toContain('tab-1:4f1b0f4e-0000-4000-8000-000000000002')
expect(redelivered).toContain('tab-1:4f1b0f4e-0000-4000-8000-000000000065')
} finally {
dispatcher.dispose()
}
@@ -150,7 +150,11 @@ describe('Integration: relay hook server → mux → AgentHookServer.ingestRemot
connectionId: 'conn-test',
mainAgent: { state: 'done', outcome: 'cancellation' }
})
const replayOutcomes: (string | undefined)[] = []
orcaServer.setListener((event) => replayOutcomes.push(event.payload.mainAgent?.outcome))
replayOutcomes.length = 0
expect(hookServer.replayCachedPayloadsForPanes()).toBe(1)
await expect.poll(() => replayOutcomes).toEqual(['cancellation'])
await expect
.poll(() => orcaServer.getStatusSnapshotForPane(paneKey)[0]?.mainAgent?.outcome)
.toBe('cancellation')
@@ -341,6 +345,7 @@ describe('Integration: relay hook server → mux → AgentHookServer.ingestRemot
tool_input: { command: 'pnpm test' }
})
).resolves.toMatchObject({ status: 204 })
await expect.poll(() => orcaServer.getStatusSnapshot()[0]?.state).toBe('waiting')
await expect(
postClaude({
hook_event_name: 'PreToolUse',
@@ -394,6 +399,7 @@ describe('Integration: relay hook server → mux → AgentHookServer.ingestRemot
tool_name: 'Bash',
tool_input: { command: 'false' }
})
await expect.poll(() => orcaServer.getStatusSnapshot()[0]?.state).toBe('waiting')
await postClaude({ hook_event_name: 'TeammateIdle', teammate_name: 'reviewer' })
const start = Date.now()
@@ -444,6 +450,7 @@ describe('Integration: relay hook server → mux → AgentHookServer.ingestRemot
tool_input: { command: 'rm -rf /tmp/orca-2824-permission-target' }
})
).resolves.toMatchObject({ status: 204 })
await expect.poll(() => orcaServer.getStatusSnapshot()[0]?.state).toBe('waiting')
await expect(
postClaude({
hook_event_name: 'PostToolUse',