mirror of
https://github.com/stablyai/orca.git
synced 2026-11-13 17:37:40 +00:00
469 lines
17 KiB
TypeScript
469 lines
17 KiB
TypeScript
import { z } from 'zod'
|
|
import { createServer, type ServerResponse } from 'node:http'
|
|
import { once } from 'node:events'
|
|
import { afterEach, expect, it, vi } from 'vitest'
|
|
import { runOpenCodeTextTaskSession } from './opencode-text-task-session'
|
|
|
|
const servers: ReturnType<typeof createServer>[] = []
|
|
const appliedV1Policy = [
|
|
{ permission: '*', pattern: '*', action: 'deny' },
|
|
{ permission: 'read', pattern: '*', action: 'allow' },
|
|
{ permission: 'grep', pattern: '*', action: 'allow' },
|
|
{ permission: 'glob', pattern: '*', action: 'allow' },
|
|
{ permission: 'external_directory', pattern: '*', action: 'allow' },
|
|
{ permission: 'read', pattern: 'mcp:*', action: 'deny' }
|
|
]
|
|
afterEach(async () => {
|
|
await Promise.all(
|
|
servers.splice(0).map(async (server) => {
|
|
server.closeAllConnections()
|
|
await new Promise<void>((resolve) => server.close(() => resolve()))
|
|
})
|
|
)
|
|
})
|
|
|
|
async function fixture(
|
|
handle: (path: string, body: unknown, response: ServerResponse) => void | Promise<void>
|
|
) {
|
|
const server = createServer(async (request, response) => {
|
|
const chunks: Buffer[] = []
|
|
for await (const chunk of request) {
|
|
chunks.push(Buffer.from(chunk))
|
|
}
|
|
const body = Buffer.concat(chunks).toString('utf8')
|
|
await handle(`${request.method} ${request.url}`, body ? JSON.parse(body) : undefined, response)
|
|
})
|
|
servers.push(server)
|
|
server.listen(0, '127.0.0.1')
|
|
await once(server, 'listening')
|
|
const address = server.address()
|
|
if (!address || typeof address === 'string') {
|
|
throw new Error('Missing test address')
|
|
}
|
|
return `http://127.0.0.1:${address.port}`
|
|
}
|
|
|
|
function json(response: ServerResponse, value: unknown) {
|
|
response.setHeader('content-type', 'application/json')
|
|
response.end(JSON.stringify(value))
|
|
}
|
|
|
|
it('v2 rejects an unexpected permission and reads final text across fragmented CRLF events', async () => {
|
|
let events: ServerResponse | undefined
|
|
const requests = new Map<string, unknown>()
|
|
const emit = (
|
|
type: string,
|
|
data: { id?: string; assistantMessageID?: string; ordinal?: number; text?: string }
|
|
) => {
|
|
if (!events) {
|
|
throw new Error('No event subscription')
|
|
}
|
|
const frame = `data: ${JSON.stringify({ type, data: { sessionID: 'ses_v2', ...data } })}\r\n\r\n`
|
|
events.write(frame.slice(0, -3))
|
|
events.write(frame.slice(-3))
|
|
}
|
|
const url = await fixture((path, body, response) => {
|
|
requests.set(path, body)
|
|
if (path === 'GET /api/event') {
|
|
events = response
|
|
response.writeHead(200, { 'content-type': 'text/event-stream' })
|
|
response.flushHeaders()
|
|
} else if (path === 'POST /api/session') {
|
|
json(response, { data: { id: 'ses_v2' } })
|
|
} else if (path === 'POST /api/session/ses_v2/prompt') {
|
|
json(response, {})
|
|
emit('permission.asked', { id: 'per_shell' })
|
|
} else if (path === 'POST /api/session/ses_v2/permission/per_shell/reply') {
|
|
json(response, {})
|
|
emit('session.text.ended', {
|
|
assistantMessageID: 'msg_final',
|
|
ordinal: 0,
|
|
text: 'Harbor Lantern'
|
|
})
|
|
emit('session.execution.succeeded', {})
|
|
} else {
|
|
json(response, {})
|
|
}
|
|
})
|
|
await expect(
|
|
runOpenCodeTextTaskSession({
|
|
version: 'v2',
|
|
v1SessionPolicyFiltersTools: false,
|
|
url,
|
|
password: 'test',
|
|
directory: '/workspace',
|
|
prompt: 'Name this chat',
|
|
model: { providerID: 'opencode', modelID: 'crouton-alpha-free' },
|
|
variant: 'low',
|
|
signal: new AbortController().signal
|
|
})
|
|
).resolves.toBe('Harbor Lantern')
|
|
expect(requests.get('POST /api/session')).toEqual({
|
|
title: expect.stringMatching(/^Orca text generation /),
|
|
location: { directory: '/workspace' },
|
|
model: { providerID: 'opencode', id: 'crouton-alpha-free', variant: 'low' },
|
|
permissions: [
|
|
{ action: '*', resource: '*', effect: 'deny' },
|
|
{ action: 'read', resource: '*', effect: 'allow' },
|
|
{ action: 'grep', resource: '*', effect: 'allow' },
|
|
{ action: 'glob', resource: '*', effect: 'allow' },
|
|
{ action: 'external_directory', resource: '*', effect: 'allow' },
|
|
{ action: 'read', resource: 'mcp:*', effect: 'deny' }
|
|
]
|
|
})
|
|
expect(requests.get('POST /api/session/ses_v2/permission/per_shell/reply')).toEqual({
|
|
decision: 'reject',
|
|
message: 'Tools are unavailable for this text task.'
|
|
})
|
|
expect(requests.has('DELETE /api/session/ses_v2')).toBe(true)
|
|
})
|
|
|
|
it('recovers and deletes an owned session when cancelled during creation without sending a prompt', async () => {
|
|
const controller = new AbortController()
|
|
const requests: string[] = []
|
|
const url = await fixture((path, _body, response) => {
|
|
requests.push(path)
|
|
if (path === 'GET /api/event') {
|
|
response.writeHead(200, { 'content-type': 'text/event-stream' })
|
|
response.flushHeaders()
|
|
} else if (path === 'POST /api/session') {
|
|
controller.abort(new Error('Renamed manually'))
|
|
json(response, { data: { id: 'ses_late' } })
|
|
} else {
|
|
json(response, {})
|
|
}
|
|
})
|
|
await expect(
|
|
runOpenCodeTextTaskSession({
|
|
version: 'v2',
|
|
v1SessionPolicyFiltersTools: false,
|
|
url,
|
|
password: 'test',
|
|
directory: '/workspace',
|
|
prompt: 'Name this chat',
|
|
signal: controller.signal
|
|
})
|
|
).rejects.toThrow('Renamed manually')
|
|
expect(requests).toContain('POST /api/session/ses_late/interrupt')
|
|
expect(requests).toContain('DELETE /api/session/ses_late')
|
|
expect(requests).not.toContain('POST /api/session/ses_late/prompt')
|
|
})
|
|
|
|
it('preserves a completed title when deleting its session fails', async () => {
|
|
const warning = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
const url = await fixture((path, _body, response) => {
|
|
if (path.startsWith('POST /session?')) {
|
|
json(response, { id: 'ses_cleanup', permission: appliedV1Policy })
|
|
} else if (path.startsWith('POST /session/ses_cleanup/message?')) {
|
|
json(response, { info: {}, parts: [{ type: 'text', text: 'Harbor Lantern' }] })
|
|
} else {
|
|
response.writeHead(500).end()
|
|
}
|
|
})
|
|
try {
|
|
await expect(
|
|
runOpenCodeTextTaskSession({
|
|
version: 'v1',
|
|
v1SessionPolicyFiltersTools: true,
|
|
url,
|
|
password: 'test',
|
|
directory: '/workspace',
|
|
prompt: 'Name this chat',
|
|
signal: new AbortController().signal
|
|
})
|
|
).resolves.toBe('Harbor Lantern')
|
|
} finally {
|
|
warning.mockRestore()
|
|
}
|
|
})
|
|
|
|
it('discards a cancelled borrowed v1 response without aborting or deleting its session', async () => {
|
|
const controller = new AbortController()
|
|
const submitted = Promise.withResolvers<void>()
|
|
const requests: string[] = []
|
|
let lateResponse: ServerResponse | undefined
|
|
const url = await fixture((path, _body, response) => {
|
|
requests.push(path)
|
|
if (path.startsWith('GET /session/saved')) {
|
|
json(response, { id: 'saved', directory: '/workspace' })
|
|
} else if (path.startsWith('POST /session/saved/message?')) {
|
|
lateResponse = response
|
|
submitted.resolve()
|
|
} else {
|
|
response.statusCode = 400
|
|
json(response, {})
|
|
}
|
|
})
|
|
const result = runOpenCodeTextTaskSession({
|
|
version: 'v1',
|
|
v1SessionPolicyFiltersTools: true,
|
|
url,
|
|
directory: '/workspace',
|
|
external: { url },
|
|
exclusiveServer: false,
|
|
sessionId: 'saved',
|
|
operation: 'conversation-name',
|
|
prompt: 'Name only',
|
|
signal: controller.signal
|
|
})
|
|
const rejected = expect(result).rejects.toThrow('Renamed manually')
|
|
await submitted.promise
|
|
controller.abort(new Error('Renamed manually'))
|
|
await rejected
|
|
json(lateResponse!, {
|
|
info: { parentID: 'msg_compaction_continue' },
|
|
parts: [{ type: 'text', text: 'Late title' }]
|
|
})
|
|
expect(requests).toEqual([
|
|
'GET /session/saved',
|
|
'POST /session/saved/message?directory=%2Fworkspace'
|
|
])
|
|
})
|
|
|
|
it.each([false, true])(
|
|
'correlates borrowed v2 admission and leaves unrelated work alone (superseded=%s)',
|
|
async (superseded) => {
|
|
const requests: string[] = []
|
|
let events: ServerResponse | undefined
|
|
const emit = (type: string, data: Record<string, unknown>) =>
|
|
events!.write(`data: ${JSON.stringify({ type, data: { sessionID: 'saved', ...data } })}\n\n`)
|
|
const url = await fixture((path, body, response) => {
|
|
requests.push(path)
|
|
if (path === 'GET /api/event') {
|
|
events = response
|
|
response.writeHead(200, { 'content-type': 'text/event-stream' })
|
|
response.flushHeaders()
|
|
} else if (path === 'GET /api/session/saved') {
|
|
json(response, { data: { id: 'saved', location: { directory: '/server' } } })
|
|
} else if (path === 'POST /api/session/saved/prompt') {
|
|
const input = z.object({ id: z.string(), delivery: z.literal('steer') }).parse(body)
|
|
emit('session.text.ended', {
|
|
assistantMessageID: 'msg_earlier',
|
|
ordinal: 0,
|
|
text: 'Wrong title'
|
|
})
|
|
emit('session.execution.succeeded', {})
|
|
emit('session.inbox.delivered', { inboxID: input.id })
|
|
emit('session.compaction.started', { reason: 'auto', recent: 'msg_history' })
|
|
emit('session.compaction.ended', {
|
|
reason: 'auto',
|
|
text: 'This summary is context, not the requested title',
|
|
recent: 'msg_history',
|
|
model: { providerID: 'configured', id: 'model' },
|
|
cost: 0,
|
|
tokens: { input: 100, output: 10, reasoning: 0, cache: { read: 0, write: 0 } }
|
|
})
|
|
emit('permission.asked', { id: 'per_other_owner' })
|
|
emit('session.step.started', { assistantMessageID: 'msg_ours' })
|
|
emit('session.text.ended', {
|
|
assistantMessageID: 'msg_ours',
|
|
ordinal: 0,
|
|
text: 'Harbor Lantern'
|
|
})
|
|
if (superseded) {
|
|
emit('session.inbox.delivered', { inboxID: 'msg_later_input' })
|
|
}
|
|
emit('session.execution.succeeded', {})
|
|
json(response, {})
|
|
} else {
|
|
json(response, {})
|
|
}
|
|
})
|
|
const result = runOpenCodeTextTaskSession({
|
|
version: 'v2',
|
|
v1SessionPolicyFiltersTools: false,
|
|
url,
|
|
directory: '/client',
|
|
external: { url },
|
|
exclusiveServer: false,
|
|
prompt: 'Name only',
|
|
sessionId: 'saved',
|
|
operation: 'conversation-name',
|
|
signal: new AbortController().signal
|
|
})
|
|
await (superseded
|
|
? expect(result).rejects.toThrow('superseded')
|
|
: expect(result).resolves.toBe('Harbor Lantern'))
|
|
expect(requests).toEqual([
|
|
'GET /api/event',
|
|
'GET /api/session/saved',
|
|
'POST /api/session/saved/prompt'
|
|
])
|
|
}
|
|
)
|
|
|
|
it('cancels only its still-queued borrowed v2 input after a late admission response', async () => {
|
|
const controller = new AbortController()
|
|
const requests: string[] = []
|
|
let messageID = ''
|
|
const url = await fixture((path, body, response) => {
|
|
requests.push(path)
|
|
if (path === 'GET /api/event') {
|
|
response.writeHead(200, { 'content-type': 'text/event-stream' })
|
|
response.flushHeaders()
|
|
} else if (path === 'GET /api/session/saved') {
|
|
json(response, { data: { id: 'saved', location: { directory: '/server' } } })
|
|
} else if (path === 'POST /api/session/saved/prompt') {
|
|
messageID = z.object({ id: z.string() }).parse(body).id
|
|
controller.abort(new Error('Renamed manually'))
|
|
json(response, {})
|
|
} else {
|
|
json(response, {})
|
|
}
|
|
})
|
|
await expect(
|
|
runOpenCodeTextTaskSession({
|
|
version: 'v2',
|
|
v1SessionPolicyFiltersTools: false,
|
|
url,
|
|
directory: '/client',
|
|
external: { url },
|
|
exclusiveServer: false,
|
|
prompt: 'Name only',
|
|
sessionId: 'saved',
|
|
operation: 'conversation-name',
|
|
signal: controller.signal
|
|
})
|
|
).rejects.toThrow('Renamed manually')
|
|
expect(requests).toEqual([
|
|
'GET /api/event',
|
|
'GET /api/session/saved',
|
|
'POST /api/session/saved/prompt',
|
|
`DELETE /api/session/saved/inbox/${messageID}`
|
|
])
|
|
})
|
|
|
|
it('retains explicitly shared native history even when publication fails ambiguously', async () => {
|
|
const requests: string[] = []
|
|
const url = await fixture((path, _body, response) => {
|
|
requests.push(path)
|
|
if (path.startsWith('POST /session?')) {
|
|
json(response, { id: 'retained', permission: appliedV1Policy })
|
|
} else if (path.startsWith('POST /session/retained/share?')) {
|
|
response.statusCode = 500
|
|
json(response, {})
|
|
} else if (path.startsWith('POST /session/retained/message?')) {
|
|
json(response, { info: {}, parts: [{ type: 'text', text: 'Harbor Lantern' }] })
|
|
} else {
|
|
json(response, true)
|
|
}
|
|
})
|
|
await expect(
|
|
runOpenCodeTextTaskSession({
|
|
version: 'v1',
|
|
v1SessionPolicyFiltersTools: true,
|
|
url,
|
|
directory: '/workspace',
|
|
prompt: 'Name only',
|
|
share: true,
|
|
operation: 'conversation-name',
|
|
signal: new AbortController().signal
|
|
})
|
|
).resolves.toBe('Harbor Lantern')
|
|
expect(requests).toEqual([
|
|
'POST /session?directory=%2Fworkspace',
|
|
'POST /session/retained/share?directory=%2Fworkspace',
|
|
'POST /session/retained/message?directory=%2Fworkspace'
|
|
])
|
|
})
|
|
|
|
it.each([false, true])(
|
|
'rejects owned subtask asks with session routing field: %s',
|
|
async (routing) => {
|
|
let events: ServerResponse | undefined
|
|
let readAllowed = false
|
|
let deniedChild = false
|
|
let deniedForeign = false
|
|
let commandResponse: ServerResponse | undefined
|
|
const url = await fixture((path, body, response) => {
|
|
if (path.startsWith('GET /agent?')) {
|
|
json(response, [{ name: 'reader', mode: 'subagent' }])
|
|
} else if (path.startsWith('GET /command?')) {
|
|
json(response, [
|
|
{ name: 'inspect', agent: 'reader', subtask: true, model: 'configured/command-model' }
|
|
])
|
|
} else if (path.startsWith('POST /session?')) {
|
|
const rules = z
|
|
.object({
|
|
permission: z.array(
|
|
z.object({ permission: z.string(), pattern: z.string(), action: z.string() })
|
|
)
|
|
})
|
|
.parse(body).permission
|
|
// The native child retains denies and drops ordinary parent read allows.
|
|
const childDenies = rules
|
|
.filter((rule) => rule.action === 'deny')
|
|
.map((rule) => rule.permission)
|
|
expect(childDenies).toEqual(['read', 'bash', 'edit', 'todowrite'])
|
|
readAllowed = !rules.some(
|
|
(rule) =>
|
|
rule.action === 'deny' &&
|
|
rule.pattern === '*' &&
|
|
['*', 'read'].includes(rule.permission)
|
|
)
|
|
json(response, {
|
|
id: 'root',
|
|
permission: [
|
|
{ permission: '*', pattern: '*', action: 'ask' },
|
|
...appliedV1Policy.slice(1),
|
|
{ permission: 'bash', pattern: '*', action: 'deny' },
|
|
{ permission: 'edit', pattern: '*', action: 'deny' },
|
|
{ permission: 'todowrite', pattern: '*', action: 'deny' }
|
|
]
|
|
})
|
|
} else if (path.startsWith('GET /event?')) {
|
|
events = response
|
|
response.writeHead(200, { 'content-type': 'text/event-stream' })
|
|
response.flushHeaders()
|
|
} else if (path.startsWith('POST /session/root/command?')) {
|
|
expect(body).toMatchObject({
|
|
command: 'inspect',
|
|
arguments: 'Name from the recorded request',
|
|
model: 'selected/model'
|
|
})
|
|
events!.write(
|
|
`data: ${JSON.stringify({ type: 'session.created', properties: { info: { id: 'foreign_child', parentID: 'foreign_root' } } })}\n\n`
|
|
)
|
|
events!.write(
|
|
`data: ${JSON.stringify({ type: 'permission.asked', properties: { sessionID: 'foreign_child', id: 'per_foreign' } })}\n\n`
|
|
)
|
|
events!.write(
|
|
`data: ${JSON.stringify({ type: 'session.created', properties: { ...(routing ? { sessionID: 'child' } : {}), info: { id: 'child', parentID: 'root' } } })}\n\n`
|
|
)
|
|
events!.write(
|
|
`data: ${JSON.stringify({ type: 'permission.asked', properties: { sessionID: 'child', id: 'per_child_shell' } })}\n\n`
|
|
)
|
|
commandResponse = response
|
|
} else if (path.startsWith('POST /permission/per_foreign/reply?')) {
|
|
deniedForeign = true
|
|
json(response, true)
|
|
} else if (path.startsWith('POST /permission/per_child_shell/reply?')) {
|
|
expect(body).toMatchObject({ reply: 'reject' })
|
|
deniedChild = true
|
|
json(response, true)
|
|
json(commandResponse!, {
|
|
info: {},
|
|
parts: [{ type: 'text', text: readAllowed ? 'Harbor Lantern' : 'Child read was denied' }]
|
|
})
|
|
} else {
|
|
json(response, true)
|
|
}
|
|
})
|
|
await expect(
|
|
runOpenCodeTextTaskSession({
|
|
version: 'v1',
|
|
v1SessionPolicyFiltersTools: true,
|
|
url,
|
|
directory: '/workspace',
|
|
command: 'inspect',
|
|
model: { providerID: 'selected', modelID: 'model' },
|
|
prompt: 'Name from the recorded request',
|
|
operation: 'conversation-name',
|
|
signal: new AbortController().signal
|
|
})
|
|
).resolves.toBe('Harbor Lantern')
|
|
expect(deniedChild).toBe(true)
|
|
expect(deniedForeign).toBe(false)
|
|
}
|
|
)
|