mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
test: strengthen hook, navigation, and release coverage (#33028)
* test: strengthen hook identity and transport assertions * test: strengthen mobile handoff and release policy coverage --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
@@ -260,20 +260,26 @@ describe('source derivation agreement', () => {
|
||||
).toThrow(/disagree on a source file/)
|
||||
})
|
||||
|
||||
it('diffs the checkout with pnpm formatting so the two halves stay comparable', () => {
|
||||
expect(CHECKOUT_DIFF_FLAGS.filter((flag) => flag !== '--relative')).toEqual(
|
||||
PNPM_DIFF_FLAGS.filter((flag) => flag !== '--no-index')
|
||||
)
|
||||
expect(CHECKOUT_DIFF_FLAGS).toContain('--full-index')
|
||||
expect(CHECKOUT_DIFF_FLAGS).not.toContain('--no-index')
|
||||
})
|
||||
it('diffs a nested checkout with package-relative paths and full object IDs', async () => {
|
||||
const root = await createDirectory()
|
||||
const checkout = path.join(root, 'checkout')
|
||||
const packageRoot = path.join(checkout, 'addons', 'widget')
|
||||
const pristine = path.join(root, 'pristine')
|
||||
await writeTree(packageRoot, PRISTINE)
|
||||
await writeTree(pristine, PRISTINE)
|
||||
execFileSync('git', ['init', '--quiet', checkout], { env: pnpmDiffEnvironment() })
|
||||
execFileSync('git', ['add', '.'], { cwd: checkout, env: pnpmDiffEnvironment() })
|
||||
await writeTree(packageRoot, PATCHED)
|
||||
|
||||
it('passes --relative as a flag, not as a pathspec', () => {
|
||||
// After `--` git reads it as a path, silently leaving addon diffs rooted at the
|
||||
// repo instead of the package, which drops every source hunk from the patch.
|
||||
expect(CHECKOUT_DIFF_FLAGS.indexOf('--relative')).toBeLessThan(
|
||||
CHECKOUT_DIFF_FLAGS.indexOf('--')
|
||||
)
|
||||
const patch = execFileSync('git', [...CHECKOUT_DIFF_FLAGS, 'src/'], {
|
||||
cwd: packageRoot,
|
||||
encoding: 'utf8',
|
||||
env: pnpmDiffEnvironment()
|
||||
})
|
||||
|
||||
expect(patch).toBe(sourceHunks(diffFolders(pristine, packageRoot)))
|
||||
expect(patch).toContain('diff --git a/src/Widget.ts b/src/Widget.ts')
|
||||
expect(patch).toMatch(/^index [0-9a-f]{40}\.\.[0-9a-f]{40} 100644$/m)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -455,7 +461,7 @@ describe('committed xterm patch artifacts', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('pins a full upstream commit and a buildable package entry', async () => {
|
||||
it('requires a full upstream commit, a package, and allowed build steps', async () => {
|
||||
const manifest = JSON.parse(await readFile(MANIFEST_PATH, 'utf8'))
|
||||
expect(manifest.upstream.commit).toMatch(/^[0-9a-f]{40}$/)
|
||||
expect(manifest.packages.length).toBeGreaterThan(0)
|
||||
|
||||
@@ -47,6 +47,9 @@ describe.each(refusalTargets)(
|
||||
({ jobSteps, index, laterStep }) => {
|
||||
it('checks the ref before it builds or tags anything', () => {
|
||||
expect(index).toBeGreaterThan(0)
|
||||
expect(jobSteps[index].if).toBeUndefined()
|
||||
expect(jobSteps[index]['continue-on-error'] ?? false).toBe(false)
|
||||
expect(jobSteps[index].shell).toBe('bash')
|
||||
expect(jobSteps[index - 1].uses).toMatch(/^actions\/checkout@/)
|
||||
const sparse = jobSteps[index - 1].with['sparse-checkout']
|
||||
expect(sparse?.split('\n').includes('/config/tsconfig.relay.json') ?? true).toBe(true)
|
||||
|
||||
@@ -140,7 +140,9 @@ function normalizePermissions(permissions) {
|
||||
)
|
||||
}
|
||||
|
||||
function intersectPermissions(granted, requested) {
|
||||
const permissionEscalations = []
|
||||
|
||||
function intersectPermissions(granted, requested, identity) {
|
||||
const rank = { none: 0, read: 1, write: 2 }
|
||||
const scopes = new Set([...Object.keys(granted), ...Object.keys(requested)])
|
||||
return Object.fromEntries(
|
||||
@@ -148,6 +150,14 @@ function intersectPermissions(granted, requested) {
|
||||
.map((scope) => {
|
||||
const grantedLevel = granted[scope] ?? 'none'
|
||||
const requestedLevel = requested[scope] ?? 'none'
|
||||
if (rank[requestedLevel] > rank[grantedLevel]) {
|
||||
permissionEscalations.push({
|
||||
identity,
|
||||
scope,
|
||||
granted: grantedLevel,
|
||||
requested: requestedLevel
|
||||
})
|
||||
}
|
||||
return [scope, rank[grantedLevel] < rank[requestedLevel] ? grantedLevel : requestedLevel]
|
||||
})
|
||||
.filter(([, level]) => level !== 'none')
|
||||
@@ -165,12 +175,12 @@ function resolveWorkflowMatrix(workflow, workflowPath, inheritedPermissions, pre
|
||||
if (!requested) {
|
||||
throw new Error(`${workflowPath}#${jobName} has no resolvable permissions`)
|
||||
}
|
||||
const effective = inheritedPermissions
|
||||
? intersectPermissions(inheritedPermissions, requested)
|
||||
: requested
|
||||
const identity = prefix
|
||||
? `${prefix} -> ${workflowPath}#${jobName}`
|
||||
: `${workflowPath}#${jobName}`
|
||||
const effective = inheritedPermissions
|
||||
? intersectPermissions(inheritedPermissions, requested, identity)
|
||||
: requested
|
||||
matrix[identity] = effective
|
||||
|
||||
if (job.uses?.startsWith('./.github/workflows/')) {
|
||||
@@ -249,6 +259,7 @@ describe('release-cut token permissions', () => {
|
||||
)
|
||||
|
||||
it('keeps the complete effective job and reusable-workflow matrix least-privileged', () => {
|
||||
expect(permissionEscalations).toEqual([])
|
||||
expect(matrix).toEqual(EXPECTED_MATRIX)
|
||||
})
|
||||
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
* the session screen inside the same document, decided by the page's real handoff rule against the
|
||||
* `init` the shell's real policy builds.
|
||||
*/
|
||||
import { act, create } from 'react-test-renderer'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createShellPageClient } from '../mobile-web-shell/bridge/page-bootstrap'
|
||||
import { createBridgeInitFrame } from '../mobile-web-shell/bridge/bridge-init-frame'
|
||||
import { routeViewOf, type MobileWebPageRoute } from '../mobile-web-shell/page-route-policy'
|
||||
import { BRIDGE_PROTOCOL_VERSION } from '../mobile-web-shell/bridge/bridge-envelope'
|
||||
import type { BridgeRpcClient } from '../mobile-web-shell/bridge/bridge-rpc-client'
|
||||
import type { RouteHandoff } from './route-handoff'
|
||||
|
||||
const router = vi.hoisted(() => ({
|
||||
@@ -65,6 +67,9 @@ const ROUTES: MobileWebPageRoute[] = [
|
||||
}
|
||||
]
|
||||
|
||||
const trees: ReactTestRenderer[] = []
|
||||
const clients: BridgeRpcClient[] = []
|
||||
|
||||
const held: { handoff: RouteHandoff | null } = { handoff: null }
|
||||
|
||||
function Screen(): null {
|
||||
@@ -76,7 +81,7 @@ function Screen(): null {
|
||||
function mountPage(
|
||||
pathname: string,
|
||||
wide: boolean
|
||||
): { handoff: RouteHandoff; navigations: () => number } {
|
||||
): { handoff: RouteHandoff; navigations: () => unknown[] } {
|
||||
const posted: string[] = []
|
||||
const channel: {
|
||||
postMessage: (json: string) => void
|
||||
@@ -87,6 +92,7 @@ function mountPage(
|
||||
if (client === null) {
|
||||
throw new Error('no channel installed')
|
||||
}
|
||||
clients.push(client)
|
||||
const view = routeViewOf(ROUTES, pathname, wide)
|
||||
const init = createBridgeInitFrame({
|
||||
sessionId: 'session-a',
|
||||
@@ -108,10 +114,12 @@ function mountPage(
|
||||
})
|
||||
channel.onmessage?.({ data: JSON.stringify(init) })
|
||||
act(() => {
|
||||
create(
|
||||
<RpcClientProvider client={client}>
|
||||
<Screen />
|
||||
</RpcClientProvider>
|
||||
trees.push(
|
||||
create(
|
||||
<RpcClientProvider client={client}>
|
||||
<Screen />
|
||||
</RpcClientProvider>
|
||||
)
|
||||
)
|
||||
})
|
||||
if (held.handoff === null) {
|
||||
@@ -121,7 +129,7 @@ function mountPage(
|
||||
return {
|
||||
handoff,
|
||||
navigations: () =>
|
||||
posted.map((json) => JSON.parse(json)).filter((frame) => frame.name === 'navigate').length
|
||||
posted.map((json) => JSON.parse(json)).filter((frame) => frame.name === 'navigate')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,26 +138,45 @@ beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
act(() => {
|
||||
for (const tree of trees.splice(0)) {
|
||||
tree.unmount()
|
||||
}
|
||||
})
|
||||
for (const client of clients.splice(0)) {
|
||||
client.close()
|
||||
}
|
||||
Reflect.deleteProperty(globalThis, 'orcaBridge')
|
||||
})
|
||||
|
||||
describe('a worktree row in the host-area page', () => {
|
||||
it('opens the session inside this document, without a native push', () => {
|
||||
const page = mountPage('/h/host-1', true)
|
||||
page.handoff.push('/h/host-1/session/wt-1?name=main')
|
||||
expect(router.push).toHaveBeenCalledWith('/h/host-1/session/wt-1?name=main', undefined)
|
||||
expect(page.navigations()).toBe(0)
|
||||
expect(page.navigations()).toEqual([])
|
||||
})
|
||||
|
||||
it('switches workspaces in place too, which the embedded sidebar does with a replace', () => {
|
||||
const page = mountPage('/h/host-1', true)
|
||||
page.handoff.replace('/h/host-1/session/wt-2')
|
||||
expect(router.replace).toHaveBeenCalledWith('/h/host-1/session/wt-2', undefined)
|
||||
expect(page.navigations()).toBe(0)
|
||||
expect(page.navigations()).toEqual([])
|
||||
})
|
||||
|
||||
it('is still handed to the shell from a phone host page, whose grants do not cover it', () => {
|
||||
const page = mountPage('/h/host-1', false)
|
||||
page.handoff.push('/h/host-1/session/wt-1')
|
||||
expect(router.push).not.toHaveBeenCalled()
|
||||
expect(page.navigations()).toBe(1)
|
||||
expect(page.navigations()).toEqual([
|
||||
{
|
||||
v: BRIDGE_PROTOCOL_VERSION,
|
||||
type: 'notify',
|
||||
name: 'navigate',
|
||||
href: '/h/host-1/session/wt-1'
|
||||
}
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -161,7 +188,9 @@ describe('a session page opened on its own, leaving for the host route', () => {
|
||||
const page = mountPage('/h/host-1/session/wt-1', true)
|
||||
page.handoff.replace(hop)
|
||||
expect(router.replace).not.toHaveBeenCalled()
|
||||
expect(page.navigations()).toBe(1)
|
||||
expect(page.navigations()).toEqual([
|
||||
{ v: BRIDGE_PROTOCOL_VERSION, type: 'notify', name: 'navigate', href: hop }
|
||||
])
|
||||
expect(routeViewOf(ROUTES, '/h/host-1', true).ownsHostArea).toBe(true)
|
||||
})
|
||||
|
||||
@@ -169,6 +198,6 @@ describe('a session page opened on its own, leaving for the host route', () => {
|
||||
const page = mountPage('/h/host-1/session/wt-1', false)
|
||||
page.handoff.replace(hop)
|
||||
expect(router.replace).toHaveBeenCalledWith(hop, undefined)
|
||||
expect(page.navigations()).toBe(0)
|
||||
expect(page.navigations()).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ReactElement } from 'react'
|
||||
import { act, create } from 'react-test-renderer'
|
||||
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { BRIDGE_MAX_ROUTE_HREF_CHARS } from '../mobile-web-shell/bridge/bridge-caps'
|
||||
import {
|
||||
@@ -36,7 +36,7 @@ vi.mock('../transport/host-client-hooks', () => ({
|
||||
}))
|
||||
|
||||
import { RpcClientProvider } from '../transport/client-context.web'
|
||||
import { useRouteHandoff, WRAPPED_HREF_MEMBERS } from './route-handoff.web'
|
||||
import { useRouteHandoff } from './route-handoff.web'
|
||||
|
||||
const INIT = {
|
||||
v: BRIDGE_PROTOCOL_VERSION,
|
||||
@@ -55,6 +55,9 @@ const INIT = {
|
||||
pageRoutes: ['/h/[hostId]']
|
||||
}
|
||||
|
||||
const trees: ReactTestRenderer[] = []
|
||||
const clients: BridgeRpcClient[] = []
|
||||
|
||||
const held: { handoff: RouteHandoff | null } = { handoff: null }
|
||||
|
||||
function Screen(): null {
|
||||
@@ -62,13 +65,21 @@ function Screen(): null {
|
||||
return null
|
||||
}
|
||||
|
||||
function installChannel(): { posted: string[]; deliver: (frame: unknown) => void } {
|
||||
function installChannel(): {
|
||||
posted: string[]
|
||||
deliver: (frame: unknown) => void
|
||||
failPosts: () => void
|
||||
} {
|
||||
let failed = false
|
||||
const posted: string[] = []
|
||||
const channel: {
|
||||
postMessage: (json: string) => void
|
||||
onmessage: ((event: { data: string }) => void) | null
|
||||
} = {
|
||||
postMessage: (json) => {
|
||||
if (failed) {
|
||||
throw new Error('page port unavailable')
|
||||
}
|
||||
posted.push(json)
|
||||
},
|
||||
onmessage: null
|
||||
@@ -76,6 +87,9 @@ function installChannel(): { posted: string[]; deliver: (frame: unknown) => void
|
||||
Object.defineProperty(globalThis, 'orcaBridge', { value: channel, configurable: true })
|
||||
return {
|
||||
posted,
|
||||
failPosts: () => {
|
||||
failed = true
|
||||
},
|
||||
deliver: (frame) => {
|
||||
channel.onmessage?.({ data: JSON.stringify(frame) })
|
||||
}
|
||||
@@ -83,21 +97,22 @@ function installChannel(): { posted: string[]; deliver: (frame: unknown) => void
|
||||
}
|
||||
|
||||
/** The page as the entry leaves it: one client, already holding a session. */
|
||||
function mount(init: unknown): { posted: string[]; handoff: RouteHandoff } {
|
||||
function mount(init: unknown): { posted: string[]; handoff: RouteHandoff; failPosts: () => void } {
|
||||
const channel = installChannel()
|
||||
const client = createShellPageClient()
|
||||
if (client === null) {
|
||||
throw new Error('no channel installed')
|
||||
}
|
||||
clients.push(client)
|
||||
channel.deliver(init)
|
||||
act(() => {
|
||||
create(render(client))
|
||||
trees.push(create(render(client)))
|
||||
})
|
||||
const handoff = held.handoff
|
||||
if (handoff === null) {
|
||||
throw new Error('no screen mounted')
|
||||
}
|
||||
return { posted: channel.posted, handoff }
|
||||
return { posted: channel.posted, handoff, failPosts: channel.failPosts }
|
||||
}
|
||||
|
||||
function render(client: BridgeRpcClient): ReactElement {
|
||||
@@ -147,6 +162,14 @@ beforeEach(() => {
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
act(() => {
|
||||
for (const tree of trees.splice(0)) {
|
||||
tree.unmount()
|
||||
}
|
||||
})
|
||||
for (const client of clients.splice(0)) {
|
||||
client.close()
|
||||
}
|
||||
restoreWarn?.()
|
||||
restoreWarn = null
|
||||
vi.useRealTimers()
|
||||
@@ -346,8 +369,12 @@ describe('a back this document cannot serve', () => {
|
||||
})
|
||||
|
||||
it('does not throw out of a tap handler when there is nowhere to post', () => {
|
||||
const { handoff } = mount({ ...INIT, grants: { ...INIT.grants, native: [] } })
|
||||
const { posted, handoff, failPosts } = mount(INIT)
|
||||
failPosts()
|
||||
expect(() => handoff.back()).not.toThrow()
|
||||
expect(backs(posted)).toEqual([])
|
||||
expect(router.back).toHaveBeenCalledTimes(1)
|
||||
expect(JSON.stringify(warned)).toContain('send-failed')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -360,10 +387,25 @@ describe('a back this document cannot serve', () => {
|
||||
*/
|
||||
describe('every member that takes a target', () => {
|
||||
it('is replaced rather than handed through, all of them', () => {
|
||||
const { handoff } = mount(INIT)
|
||||
for (const member of WRAPPED_HREF_MEMBERS) {
|
||||
expect(typeof handoff[member], member).toBe('function')
|
||||
expect(handoff[member], member).not.toBe(router[member])
|
||||
const options = { withAnchor: false }
|
||||
for (const member of ['push', 'replace', 'navigate', 'dismissTo'] as const) {
|
||||
router[member].mockClear()
|
||||
const { posted, handoff } = mount(INIT)
|
||||
handoff[member]('/h/host-b', options)
|
||||
expect(router[member], member).toHaveBeenCalledExactlyOnceWith('/h/host-b', options)
|
||||
router[member].mockClear()
|
||||
handoff[member]('/h/host-a/session/wt-1')
|
||||
expect(navigations(posted), member).toEqual([
|
||||
{
|
||||
v: BRIDGE_PROTOCOL_VERSION,
|
||||
type: 'notify',
|
||||
name: 'navigate',
|
||||
href: '/h/host-a/session/wt-1'
|
||||
}
|
||||
])
|
||||
handoff[member]('/h/host-a/tasks#top')
|
||||
expect(navigations(posted), member).toHaveLength(1)
|
||||
expect(router[member], member).not.toHaveBeenCalled()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createElement } from 'react'
|
||||
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
|
||||
import { beforeEach, describe, expect, it, vi, type Mock } from 'vitest'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi, type Mock } from 'vitest'
|
||||
import {
|
||||
createPageBackConsumers,
|
||||
type PageBackConsumers
|
||||
@@ -38,11 +38,14 @@ function Screen({ claim }: { claim: (() => boolean) | null }): null {
|
||||
return null
|
||||
}
|
||||
|
||||
const trees: ReactTestRenderer[] = []
|
||||
|
||||
function render(claim: (() => boolean) | null): ReactTestRenderer {
|
||||
let tree!: ReactTestRenderer
|
||||
act(() => {
|
||||
tree = create(createElement(Screen, { claim }))
|
||||
})
|
||||
trees.push(tree)
|
||||
return tree
|
||||
}
|
||||
|
||||
@@ -55,6 +58,15 @@ beforeEach(() => {
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
act(() => {
|
||||
for (const tree of trees.splice(0)) {
|
||||
tree.unmount()
|
||||
}
|
||||
})
|
||||
page.consumers = null
|
||||
})
|
||||
|
||||
/**
|
||||
* The page half of the seam every sheet takes. There is no hardware key inside a WebView:
|
||||
* react-native-web answers `BackHandler.addEventListener` with a console warning and an inert
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { createElement } from 'react'
|
||||
import { act, create } from 'react-test-renderer'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const route = vi.hoisted(() => {
|
||||
const opened: unknown[] = []
|
||||
const router = {
|
||||
push: (href: unknown) => opened.push(['push', href]),
|
||||
replace: (href: unknown) => opened.push(['replace', href]),
|
||||
navigate: (href: unknown) => opened.push(['navigate', href]),
|
||||
push: (href: unknown, options?: unknown) => opened.push(['push', href, options]),
|
||||
replace: (href: unknown, options?: unknown) => opened.push(['replace', href, options]),
|
||||
navigate: (href: unknown, options?: unknown) => opened.push(['navigate', href, options]),
|
||||
back: () => opened.push(['back'])
|
||||
}
|
||||
const state: { opened: unknown[]; router: typeof router } = {
|
||||
@@ -23,6 +23,16 @@ import { useWorkspaceRouteHandoff } from './workspace-route-handoff'
|
||||
import { normalizeExecutionHostId } from '../../../src/shared/execution-host'
|
||||
import { WorkspaceExecutionHostContext } from './workspace-execution-host'
|
||||
|
||||
const trees: ReactTestRenderer[] = []
|
||||
|
||||
afterEach(() => {
|
||||
act(() => {
|
||||
for (const tree of trees.splice(0)) {
|
||||
tree.unmount()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
async function workspaceRouterFor(
|
||||
params: Record<string, string>
|
||||
): Promise<ReturnType<typeof useWorkspaceRouteHandoff>> {
|
||||
@@ -33,11 +43,13 @@ async function workspaceRouterFor(
|
||||
return null
|
||||
}
|
||||
await act(async () => {
|
||||
create(
|
||||
createElement(
|
||||
WorkspaceExecutionHostContext.Provider,
|
||||
{ value: normalizeExecutionHostId(params.executionHost) ?? undefined },
|
||||
createElement(Probe)
|
||||
trees.push(
|
||||
create(
|
||||
createElement(
|
||||
WorkspaceExecutionHostContext.Provider,
|
||||
{ value: normalizeExecutionHostId(params.executionHost) ?? undefined },
|
||||
createElement(Probe)
|
||||
)
|
||||
)
|
||||
)
|
||||
})
|
||||
@@ -50,19 +62,24 @@ async function workspaceRouterFor(
|
||||
describe('a workspace screen’s router', () => {
|
||||
it('keeps every screen it opens on the workspace’s server', async () => {
|
||||
const router = await workspaceRouterFor({ executionHost: 'runtime:vm' })
|
||||
router.push('/h/host-1/review/wt?name=api')
|
||||
router.replace('/h/host-1/session/wt')
|
||||
router.navigate({ pathname: '/h/[hostId]/files/[worktreeId]', params: { hostId: 'host-1' } })
|
||||
const options = { withAnchor: false }
|
||||
router.push('/h/host-1/review/wt?name=api', options)
|
||||
router.replace('/h/host-1/session/wt', options)
|
||||
router.navigate(
|
||||
{ pathname: '/h/[hostId]/files/[worktreeId]', params: { hostId: 'host-1' } },
|
||||
options
|
||||
)
|
||||
router.back()
|
||||
expect(route.opened).toEqual([
|
||||
['push', '/h/host-1/review/wt?name=api&executionHost=runtime%3Avm'],
|
||||
['replace', '/h/host-1/session/wt?executionHost=runtime%3Avm'],
|
||||
['push', '/h/host-1/review/wt?name=api&executionHost=runtime%3Avm', options],
|
||||
['replace', '/h/host-1/session/wt?executionHost=runtime%3Avm', options],
|
||||
[
|
||||
'navigate',
|
||||
{
|
||||
pathname: '/h/[hostId]/files/[worktreeId]',
|
||||
params: { hostId: 'host-1', executionHost: 'runtime:vm' }
|
||||
}
|
||||
},
|
||||
options
|
||||
],
|
||||
['back']
|
||||
])
|
||||
|
||||
@@ -2,7 +2,7 @@ import { readdirSync } from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { createElement, type ReactElement } from 'react'
|
||||
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const route = vi.hoisted(() => {
|
||||
const state: {
|
||||
@@ -155,6 +155,22 @@ const WORKSPACE_ROUTE_FILES = readdirSync(HOST_ROUTES, { recursive: true, encodi
|
||||
.filter((entry) => /(^|\/)\[worktreeId\](\.web)?\.tsx$/.test(entry))
|
||||
.sort()
|
||||
|
||||
const trees: ReactTestRenderer[] = []
|
||||
const pairs: ReturnType<typeof createBridgePortPair>[] = []
|
||||
|
||||
afterEach(async () => {
|
||||
await act(async () => {
|
||||
for (const tree of trees.splice(0)) {
|
||||
tree.unmount()
|
||||
}
|
||||
})
|
||||
for (const pair of pairs.splice(0)) {
|
||||
pair.client.close()
|
||||
await pair.flush()
|
||||
pair.host.dispose()
|
||||
}
|
||||
})
|
||||
|
||||
async function render(element: ReactElement, gates: HostStatusGates): Promise<ReactTestRenderer> {
|
||||
route.seen.length = 0
|
||||
route.shellRoutes.length = 0
|
||||
@@ -170,6 +186,7 @@ async function render(element: ReactElement, gates: HostStatusGates): Promise<Re
|
||||
if (!renderer) {
|
||||
throw new Error('did not render')
|
||||
}
|
||||
trees.push(renderer)
|
||||
return renderer
|
||||
}
|
||||
|
||||
@@ -181,7 +198,7 @@ function texts(renderer: ReactTestRenderer): string[] {
|
||||
|
||||
describe('every workspace route', () => {
|
||||
it('is found on disk', () => {
|
||||
expect(WORKSPACE_ROUTE_FILES.length).toBeGreaterThanOrEqual(14)
|
||||
expect(WORKSPACE_ROUTE_FILES.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it.each(WORKSPACE_ROUTE_FILES)('keeps %s on the server its route names', async (file) => {
|
||||
@@ -225,9 +242,10 @@ describe('a server workspace', () => {
|
||||
|
||||
it('gates its features on that server’s own status, not the desktop’s', async () => {
|
||||
route.params = { hostId: 'host-1', executionHost: 'runtime:vm' }
|
||||
const session = desktop([])
|
||||
const serverCapability = 'server-feature.v1'
|
||||
const session = desktop([serverCapability])
|
||||
await render(screen, desktopGates([...RELAYS, VAULT]))
|
||||
await vi.waitFor(() => expect(below.capabilities).toEqual([]))
|
||||
await vi.waitFor(() => expect(below.capabilities).toEqual([serverCapability]))
|
||||
expect(session.sendRequest).toHaveBeenCalledWith('status.get', undefined, {
|
||||
envelope: { executionHost: 'runtime:vm' }
|
||||
})
|
||||
@@ -273,8 +291,10 @@ describe('a server workspace', () => {
|
||||
desktop()
|
||||
serverReport.current = FAILED
|
||||
await act(async () => {
|
||||
create(
|
||||
createElement(HostStatusGatesContext.Provider, { value: desktopGates(RELAYS) }, screen)
|
||||
trees.push(
|
||||
create(
|
||||
createElement(HostStatusGatesContext.Provider, { value: desktopGates(RELAYS) }, screen)
|
||||
)
|
||||
)
|
||||
})
|
||||
expect(below.state).toBe('disconnected')
|
||||
@@ -299,6 +319,7 @@ describe('a server workspace', () => {
|
||||
rpc: shell,
|
||||
rewriteToPage: (json) => json.replace(',"rpcEnvelope"', '')
|
||||
})
|
||||
pairs.push(pair)
|
||||
await pair.flush()
|
||||
expect(pair.client.carriesRpcEnvelope?.()).toBe(false)
|
||||
route.client = pair.client
|
||||
|
||||
@@ -237,7 +237,7 @@ describe('Grok completion observations', () => {
|
||||
).toBe('done')
|
||||
})
|
||||
|
||||
it('keeps a starting finite task working until a later terminal observation', () => {
|
||||
it('keeps a starting finite task working', () => {
|
||||
expect(
|
||||
normalize({
|
||||
hookEventName: 'Stop',
|
||||
|
||||
@@ -289,9 +289,8 @@ describe('shared agent-hook-listener', () => {
|
||||
expect(event?.payload.prompt).toBe('Find recent PR')
|
||||
})
|
||||
|
||||
it('strips newline-heavy Grok user_query wrappers without regex matching', () => {
|
||||
const matchSpy = vi.spyOn(String.prototype, 'match')
|
||||
const promptText = 'Find recent PR\n'.repeat(300)
|
||||
it('preserves both ends of newline-heavy Grok user_query prompts', () => {
|
||||
const promptText = `Find recent PR${'\n'.repeat(300)}Keep the final requirement`
|
||||
const event = normalizeHookPayload(
|
||||
state,
|
||||
'grok',
|
||||
@@ -305,16 +304,18 @@ describe('shared agent-hook-listener', () => {
|
||||
'production'
|
||||
)
|
||||
|
||||
expect(event?.payload.prompt).toContain('Find recent PR')
|
||||
expect(event?.payload.prompt).not.toContain('<user_query>')
|
||||
expect(event?.payload.prompt).not.toContain('</user_query>')
|
||||
const usedGrokWrapperMatch = matchSpy.mock.calls.some(
|
||||
([pattern]) =>
|
||||
pattern instanceof RegExp &&
|
||||
pattern.source.startsWith('^<user_query>') &&
|
||||
pattern.source.includes('[\\s\\S]')
|
||||
expect(event?.payload.prompt).toBe('Find recent PR Keep the final requirement')
|
||||
|
||||
const tool = normalizeHookPayload(
|
||||
state,
|
||||
'grok',
|
||||
{
|
||||
paneKey: PANE_KEY,
|
||||
payload: { hookEventName: 'pre_tool_use', toolName: 'web_search' }
|
||||
},
|
||||
'production'
|
||||
)
|
||||
expect(usedGrokWrapperMatch).toBe(false)
|
||||
expect(tool?.payload.prompt).toBe('Find recent PR Keep the final requirement')
|
||||
})
|
||||
|
||||
it('maps Grok feedback notifications to waiting without overwriting the prompt', () => {
|
||||
|
||||
@@ -138,6 +138,13 @@ describe('correlateOpenCodeSessionOwners', () => {
|
||||
knownOwners: new Map()
|
||||
})
|
||||
expect(results).toEqual([])
|
||||
const expired = correlateOpenCodeSessionOwners({
|
||||
sessions: [session('ses_1', NOW - 60_000)],
|
||||
panes: [{ paneKey: 'pane-a', directory: DIR }],
|
||||
clients: [client('pane-a', NOW - 120_000, NOW - 90_000)],
|
||||
knownOwners: new Map()
|
||||
})
|
||||
expect(expired).toEqual([])
|
||||
})
|
||||
|
||||
it('breaks a same-directory tie by client evidence', () => {
|
||||
|
||||
@@ -74,7 +74,7 @@ describe('legacy structural TUI identity at the execution-host boundary', () =>
|
||||
bindOpenCodeSession(state, 'ses_b', { paneKey: PANE_A, boundAt: 1, basis: 'argv' })
|
||||
trackOpenCodePaneLaunchToken(state, PANE_A, 'creator-live')
|
||||
const tokens = [...state.lastLaunchTokenByPaneKey]
|
||||
const owner = lookupOpenCodeSessionPane(state, 'ses_b')
|
||||
const owner = { ...lookupOpenCodeSessionPane(state, 'ses_b') }
|
||||
const admitOpenCodeTui = vi.fn(() => false)
|
||||
expect(normalizeHookPayload(state, 'opencode', body(), 'test', { admitOpenCodeTui })).toBeNull()
|
||||
expect(admitOpenCodeTui).toHaveBeenCalledExactlyOnceWith({
|
||||
@@ -84,7 +84,7 @@ describe('legacy structural TUI identity at the execution-host boundary', () =>
|
||||
hasExplicitPrompt: false
|
||||
})
|
||||
expect([...state.lastLaunchTokenByPaneKey]).toEqual(tokens)
|
||||
expect(lookupOpenCodeSessionPane(state, 'ses_b')).toBe(owner)
|
||||
expect(lookupOpenCodeSessionPane(state, 'ses_b')).toEqual(owner)
|
||||
expect(state.lastStatusByPaneKey.size).toBe(0)
|
||||
expect(state.lastPromptByPaneKey.size).toBe(0)
|
||||
})
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
AGENT_HOOK_INSTALL_PLUGINS_METHOD,
|
||||
AGENT_HOOK_NOTIFICATION_METHOD,
|
||||
AGENT_HOOK_REQUEST_REPLAY_METHOD,
|
||||
AGENT_HOOK_SHED_FIELDS_KEY,
|
||||
ORCA_FEATURE_REMOTE_AGENT_HOOKS_ENV,
|
||||
createShedSubagentsField,
|
||||
isAgentHookSource,
|
||||
@@ -13,12 +9,6 @@ import {
|
||||
import type { ParsedAgentStatusPayload } from './agent-status-types'
|
||||
|
||||
describe('agent-hook-relay wire shape', () => {
|
||||
it('exposes stable JSON-RPC method names', () => {
|
||||
expect(AGENT_HOOK_NOTIFICATION_METHOD).toBe('agent.hook')
|
||||
expect(AGENT_HOOK_REQUEST_REPLAY_METHOD).toBe('agent_hook.requestReplay')
|
||||
expect(AGENT_HOOK_INSTALL_PLUGINS_METHOD).toBe('agent_hook.installPlugins')
|
||||
})
|
||||
|
||||
it('validates hook sources crossing persisted and relay trust boundaries', () => {
|
||||
expect(isAgentHookSource('claude')).toBe(true)
|
||||
expect(isAgentHookSource('kimi')).toBe(true)
|
||||
@@ -69,7 +59,6 @@ describe('restoreShedStatusFields', () => {
|
||||
})
|
||||
|
||||
it('does not restore a shed roster across a prompt identity change', () => {
|
||||
expect(AGENT_HOOK_SHED_FIELDS_KEY).toBe('shedFields')
|
||||
const nextTurn = { ...shed, prompt: 'next prompt' }
|
||||
const restored = restoreShedStatusFields(
|
||||
nextTurn,
|
||||
@@ -110,11 +99,13 @@ describe('restoreShedStatusFields', () => {
|
||||
expect(restoreShedStatusFields(shed, [{ toString: () => 'subagents' }], cached)).toBe(shed)
|
||||
})
|
||||
|
||||
it('does not overwrite a value the envelope still carries', () => {
|
||||
const fresh: ParsedAgentStatusPayload = { ...shed, lastAssistantMessage: 'fresh' }
|
||||
expect(
|
||||
restoreShedStatusFields(fresh, ['lastAssistantMessage'], cached).lastAssistantMessage
|
||||
).toBe('fresh')
|
||||
it('does not overwrite a roster the envelope still carries', () => {
|
||||
const freshRoster = [
|
||||
{ id: 'child-fresh', agentType: 'reviewer', state: 'working' as const, startedAt: 2 }
|
||||
]
|
||||
const fresh: ParsedAgentStatusPayload = { ...shed, subagents: freshRoster }
|
||||
const restored = restoreShedStatusFields(fresh, [createShedSubagentsField(roster)], cached)
|
||||
expect(restored.subagents).toEqual(freshRoster)
|
||||
})
|
||||
|
||||
it('is a no-op with no cached payload for the pane', () => {
|
||||
|
||||
@@ -30,9 +30,9 @@ describe('classifyTruncatedHookRequest', () => {
|
||||
})
|
||||
|
||||
it('reads the first value when the header arrives duplicated', () => {
|
||||
expect(classifyTruncatedHookRequest(['4096', '4096'], 12)).toBeInstanceOf(
|
||||
HookRequestTruncatedError
|
||||
)
|
||||
const error = classifyTruncatedHookRequest(['4096', '8'], 12)
|
||||
expect(error).toBeInstanceOf(HookRequestTruncatedError)
|
||||
expect(error).toMatchObject({ bytesRead: 12, contentLength: 4096 })
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -2,14 +2,18 @@ import { describe, expect, it } from 'vitest'
|
||||
import { buildAgentNotificationId } from './agent-notification-id'
|
||||
|
||||
describe('buildAgentNotificationId', () => {
|
||||
it('builds a stable id for the same agent event metadata', () => {
|
||||
it('builds a stable escaped id for the same agent event metadata', () => {
|
||||
const args = {
|
||||
worktreeId: 'repo::/Users/me/orca/workspaces/feature',
|
||||
paneKey: 'tab-1:11111111-1111-4111-8111-111111111111',
|
||||
stateStartedAt: 1780000000123
|
||||
}
|
||||
|
||||
expect(buildAgentNotificationId(args)).toBe(buildAgentNotificationId(args))
|
||||
const id = buildAgentNotificationId(args)
|
||||
expect(id).toBe(
|
||||
'agent:repo%3A%3A%2FUsers%2Fme%2Forca%2Fworkspaces%2Ffeature:tab-1%3A11111111-1111-4111-8111-111111111111:1780000000123'
|
||||
)
|
||||
expect(buildAgentNotificationId(args)).toBe(id)
|
||||
})
|
||||
|
||||
it('changes when the agent state start time changes', () => {
|
||||
|
||||
Reference in New Issue
Block a user