Merge remote-tracking branch 'origin/master' into xtr/agent-loop-message-machine

# Conflicts:
#	docs/cordis-catalog/services.md
#	docs/core-data-structures/session.i18n.yaml
#	docs/event-producer-consumer.md
#	examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl
This commit is contained in:
_Kerman
2026-07-27 21:51:37 +08:00
110 changed files with 3760 additions and 209 deletions

View File

@@ -57,6 +57,7 @@ function stubAgent(session: Session): Agent {
/** Compose the API over real Session, Agent, Storage, Domain, and Workspace services. */
async function harness(
workspaceRoot = realpathSync(mkdtempSync(join(tmpdir(), 'dsh-apiproxy-workspace-'))),
pickDirectory?: (signal: AbortSignal) => Promise<string | null>,
) {
const ctx = new Context()
await ctx.plugin(SessionStore)
@@ -96,10 +97,33 @@ async function harness(
model: 'test-model',
cwd: workspaceRoot,
workspaceRoot,
...pickDirectory === undefined ? {} : { pickDirectory },
})
return { api, ctx, storageDomain, workspaceRoot }
}
describe('host.pickDirectory', () => {
it('returns a selected path or explicit cancellation from the injected native boundary', async () => {
const selected = await harness(undefined, async () => '/tmp/project')
expect((await selected.api.host.pickDirectory(request({}), new AbortController().signal)).result)
.toEqual({ ok: true, value: { path: '/tmp/project' } })
const cancelled = await harness(undefined, async () => null)
expect((await cancelled.api.host.pickDirectory(request({}), new AbortController().signal)).result)
.toEqual({ ok: true, value: { path: null } })
})
it('propagates abort into the native boundary as a cancelled RPC error', async () => {
const { api } = await harness(undefined, signal => new Promise((_resolve, reject) => {
signal.addEventListener('abort', () => { reject(new Error('aborted')) }, { once: true })
}))
const abort = new AbortController()
const pending = api.host.pickDirectory(request({}), abort.signal)
abort.abort()
expect((await pending).result).toMatchObject({ ok: false, error: { code: 'cancelled' } })
})
})
describe('workspace.create', () => {
it('serializes concurrent names and rejects the duplicate', async () => {
const { api, workspaceRoot } = await harness()
@@ -131,6 +155,13 @@ describe('workspace.create', () => {
expect(first).toMatchObject({ created: true, workspace: { path: existing, title: 'existing' } })
expect(repeated).toMatchObject({ created: false, workspace: { workspaceId: first.workspace.workspaceId } })
expectOk(await api.workspace.rename(request({
workspaceId: first.workspace.workspaceId,
title: 'renamed-existing',
})))
const reopened = expectOk(await api.workspace.create(request({ path: existing })))
expect(reopened.workspace.title).toBe('renamed-existing')
const missing = join(workspaceRoot, 'missing')
const missingResult = await api.workspace.create(request({ path: missing }))
expect(missingResult.result).toMatchObject({ ok: false, error: { code: 'workspace-invalid-path' } })
@@ -141,6 +172,20 @@ describe('workspace.create', () => {
expect(invalid.result).toMatchObject({ ok: false, error: { code: 'workspace-invalid-path' } })
}
})
it('rejects different paths that derive the same Workspace title', async () => {
const { api, workspaceRoot } = await harness()
const first = join(workspaceRoot, 'one', 'project')
const second = join(workspaceRoot, 'two', 'project')
mkdirSync(first, { recursive: true })
mkdirSync(second, { recursive: true })
expectOk(await api.workspace.create(request({ path: first })))
const conflict = await api.workspace.create(request({ path: second }))
expect(conflict.result).toMatchObject({
ok: false,
error: { code: 'workspace-name-conflict', details: { name: 'project' } },
})
})
})
describe('session creation and Workspace membership', () => {

View File

@@ -35,7 +35,11 @@ function scriptedApi(overrides: {
cancel: r => ok(r, { accepted: true as const }),
...overrides.sessions,
},
host: { describe: r => ok(r, { version: '0-test', cwd: '/t', attachedSessions: 0 }), ...overrides.host },
host: {
describe: r => ok(r, { version: '0-test', cwd: '/t', attachedSessions: 0 }),
pickDirectory: r => ok(r, { path: null }),
...overrides.host,
},
workspace: {
list: r => ok(r, { items: [] }),
create: r => ok(r, { workspace: { workspaceId: 'w1' as never, path: '/t', title: 't', sessionIds: [], createdAt: '0', updatedAt: '0' }, created: true }),

View File

@@ -47,6 +47,9 @@ function fakeApi(overrides: Partial<{ muxFrames: MuxFrame[]; hostFrames: HostFra
async describe(request) {
return { rpcId: request.rpcId, result: { ok: true, value: { version: 'v', cwd: '/w', attachedSessions: 0 } } }
},
async pickDirectory(request) {
return { rpcId: request.rpcId, result: { ok: true, value: { path: null } } }
},
},
workspace: {
async list(request) {
@@ -108,8 +111,8 @@ function fakeApi(overrides: Partial<{ muxFrames: MuxFrame[]; hostFrames: HostFra
}
}
function client(api: ApiProxy = fakeApi()): InProcessApiClient {
return new InProcessApiClient(toFetchHandler(api))
function client(api: ApiProxy = fakeApi(), timeoutMs?: number): InProcessApiClient {
return new InProcessApiClient(toFetchHandler(api), timeoutMs)
}
async function collect<F>(stream: AsyncIterable<RpcRequest<F>>): Promise<RpcRequest<F>[]> {
@@ -145,6 +148,16 @@ describe('unary round trip (handler ⇄ client, no network)', () => {
expect((await c.host.describe({})).result.ok).toBe(true)
})
it('round-trips the native picker without the default unary timeout', async () => {
const api = fakeApi()
api.host.pickDirectory = async (request) => {
await new Promise(resolve => setTimeout(resolve, 15))
return { rpcId: request.rpcId, result: { ok: true, value: { path: '/tmp/project' } } }
}
const response = await client(api, 1).host.pickDirectory({})
expect(response.result).toEqual({ ok: true, value: { path: '/tmp/project' } })
})
it('round-trips command.list / command.execute / skill.list through the wire form', async () => {
const c = client()
const list = await c.commands.list({ sessionId: 's' as never })
@@ -170,6 +183,30 @@ describe('unary round trip (handler ⇄ client, no network)', () => {
expect(parsed.rpcId).toBe('r-sig')
expect(parsed.result.error?.code).toBe('cancelled')
})
it('propagates the carrier Request signal into host.pickDirectory', async () => {
const api = fakeApi()
api.host.pickDirectory = async (request, signal) => {
if (!signal.aborted) {
await new Promise<void>((resolve) => {
signal.addEventListener('abort', () => { resolve() }, { once: true })
})
}
return {
rpcId: request.rpcId,
result: { ok: false, error: { code: 'cancelled', message: 'aborted', details: {} } },
}
}
const handler = toFetchHandler(api)
const controller = new AbortController()
const body = JSON.stringify({ type: 'client-request', rpcId: 'r-picker', method: 'host.pickDirectory', payload: {} })
const pending = handler.fetch(new Request('http://x/api/host.pickDirectory', {
method: 'POST', body, signal: controller.signal,
}))
controller.abort()
const parsed = await (await pending).json() as { result: { error?: { code: string } } }
expect(parsed.result.error?.code).toBe('cancelled')
})
})
describe('handler carrier-layer statuses', () => {

View File

@@ -0,0 +1,139 @@
type ExecFileCallback = (
error: (Error & { code?: string | number }) | null,
stdout: string,
stderr: string,
) => void
type ExecFileMock = (
command: string,
args: readonly string[],
options: { encoding: string; signal: AbortSignal; windowsHide: boolean },
callback: ExecFileCallback,
) => void
const { execFileMock } = vi.hoisted(() => ({ execFileMock: vi.fn<ExecFileMock>() }))
vi.mock('node:child_process', () => ({ execFile: execFileMock }))
import { describe, expect, it, vi } from 'vitest'
import { pickNativeDirectory, type DirectoryPickerRunner } from '../src/native-directory-picker.ts'
function failure(code: string | number, stderr = ''): Error {
return Object.assign(new Error(`command failed: ${String(code)}`), { code, stderr })
}
const signal = () => new AbortController().signal
describe('native directory picker', () => {
it('uses the macOS folder chooser and maps user cancellation to null', async () => {
const run = vi.fn<DirectoryPickerRunner>(async () => ({ stdout: '/Users/test/project/\n', stderr: '' }))
await expect(pickNativeDirectory(signal(), { platform: 'darwin', run })).resolves.toBe('/Users/test/project/')
expect(run).toHaveBeenCalledWith('osascript', expect.arrayContaining(['POSIX path of selectedFolder']), expect.any(AbortSignal))
run.mockRejectedValueOnce(failure(1, 'execution error: User canceled. (-128)'))
await expect(pickNativeDirectory(signal(), { platform: 'darwin', run })).resolves.toBeNull()
run.mockRejectedValueOnce(failure(2, 'permission denied'))
await expect(pickNativeDirectory(signal(), { platform: 'darwin', run })).rejects.toThrow('command failed')
})
it.each([
['a primitive error', 'failed'],
['an invalid code type', { code: true }],
['a missing stderr property', { code: 1 }],
['a non-string stderr property', { code: 1, stderr: 42 }],
])('does not mistake %s for macOS cancellation', async (_label, reason) => {
const run = vi.fn<DirectoryPickerRunner>(async () => { throw reason })
await expect(pickNativeDirectory(signal(), { platform: 'darwin', run })).rejects.toBe(reason)
})
it('uses the Windows STA folder dialog and maps empty output to cancellation', async () => {
const run = vi.fn<DirectoryPickerRunner>(async () => ({ stdout: 'C:\\work\\project\r\n', stderr: '' }))
await expect(pickNativeDirectory(signal(), { platform: 'win32', run })).resolves.toBe('C:\\work\\project')
expect(run).toHaveBeenCalledWith(
'powershell.exe',
expect.arrayContaining(['-NoProfile', '-STA', '-Command']),
expect.any(AbortSignal),
)
expect(run.mock.calls[0]?.[1].at(-1)).toContain("$ErrorActionPreference = 'Stop'")
run.mockResolvedValueOnce({ stdout: '', stderr: '' })
await expect(pickNativeDirectory(signal(), { platform: 'win32', run })).resolves.toBeNull()
run.mockRejectedValueOnce(failure(1, 'Add-Type failed'))
await expect(pickNativeDirectory(signal(), { platform: 'win32', run })).rejects.toThrow('command failed')
})
it('runs the default command adapter without a shell and preserves command failures', async () => {
execFileMock.mockImplementationOnce((_command, _args, _options, callback) => {
callback(null, 'C:\\work\\default\r\n', '')
})
await expect(pickNativeDirectory(signal(), { platform: 'win32' })).resolves.toBe('C:\\work\\default')
const [command, args, options] = execFileMock.mock.calls[0]!
expect(command).toBe('powershell.exe')
expect(args).toEqual(expect.arrayContaining(['-NoProfile', '-STA', '-Command']))
expect(options.encoding).toBe('utf8')
expect(options.windowsHide).toBe(true)
expect(options.signal).toBeInstanceOf(AbortSignal)
const commandError = Object.assign(new Error('powershell failed'), { code: 7 })
execFileMock.mockImplementationOnce((_command, _args, _options, callback) => {
callback(commandError, 'partial output', 'failure details')
})
await expect(pickNativeDirectory(signal(), { platform: 'win32' })).rejects.toMatchObject({
message: 'powershell failed', cause: commandError, code: 7,
stdout: 'partial output', stderr: 'failure details',
})
})
it('uses the current process platform when no platform override is supplied', async () => {
const run = vi.fn<DirectoryPickerRunner>(async () => ({ stdout: '/default/platform\n', stderr: '' }))
await expect(pickNativeDirectory(signal(), { run })).resolves.toBe('/default/platform')
})
it('uses Zenity on Linux and falls back to KDialog only when Zenity is missing', async () => {
const run = vi.fn<DirectoryPickerRunner>()
.mockRejectedValueOnce(failure('ENOENT'))
.mockResolvedValueOnce({ stdout: '/home/test/project\n', stderr: '' })
await expect(pickNativeDirectory(signal(), { platform: 'linux', run })).resolves.toBe('/home/test/project')
expect(run.mock.calls.map(call => call[0])).toEqual(['zenity', 'kdialog'])
const zenity = vi.fn<DirectoryPickerRunner>(async () => ({ stdout: '/home/test/direct\n', stderr: '' }))
await expect(pickNativeDirectory(signal(), { platform: 'linux', run: zenity }))
.resolves.toBe('/home/test/direct')
expect(zenity).toHaveBeenCalledOnce()
})
it('maps Linux cancellation to null and reports a missing desktop picker', async () => {
const cancelled = vi.fn<DirectoryPickerRunner>(async () => { throw failure(1) })
await expect(pickNativeDirectory(signal(), { platform: 'linux', run: cancelled })).resolves.toBeNull()
const missing = vi.fn<DirectoryPickerRunner>(async () => { throw failure('ENOENT') })
await expect(pickNativeDirectory(signal(), { platform: 'linux', run: missing }))
.rejects.toThrow('install zenity or kdialog')
const kdialogCancelled = vi.fn<DirectoryPickerRunner>()
.mockRejectedValueOnce(failure('ENOENT'))
.mockRejectedValueOnce(failure(1))
await expect(pickNativeDirectory(signal(), { platform: 'linux', run: kdialogCancelled }))
.resolves.toBeNull()
const zenityFailed = vi.fn<DirectoryPickerRunner>(async () => { throw failure(2) })
await expect(pickNativeDirectory(signal(), { platform: 'linux', run: zenityFailed }))
.rejects.toThrow('command failed')
const kdialogFailed = vi.fn<DirectoryPickerRunner>()
.mockRejectedValueOnce(failure('ENOENT'))
.mockRejectedValueOnce(failure(2))
await expect(pickNativeDirectory(signal(), { platform: 'linux', run: kdialogFailed }))
.rejects.toThrow('command failed')
})
it('does not convert caller aborts into user cancellation', async () => {
const abort = new AbortController()
abort.abort(new Error('closed'))
const run = vi.fn<DirectoryPickerRunner>(async () => { throw failure('ABORT_ERR') })
await expect(pickNativeDirectory(abort.signal, { platform: 'linux', run })).rejects.toThrow('command failed')
})
it('reports unsupported platforms', async () => {
await expect(pickNativeDirectory(signal(), { platform: 'aix' })).rejects.toThrow('unsupported on aix')
})
})