Merge remote-tracking branch 'origin/master' into feat/web-message-feedback-ui
Adapt to two contract changes master introduced: - The generated Remote face now wraps every business result in RemoteResult, folding carrier failures into an ok:false branch instead of rejecting. The controller reads that envelope at its three call sites and maps a carrier failure onto the same settled shape the controls already render; three specs cover the new branch. - Client packages split their tsconfig into host and client halves, and the host aggregate now compiles any test not named *.client.spec.*. Rename this package's specs to the client convention and drop the ../connection project reference, which pointed at a solution file that no longer carries the client sources. Keep master's mount loop with its rollback-on-failure in api-remotes and add messageFeedbackRemote to it.
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
import { CommandId } from '@deepseek-ai/dsh-commands/brand'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { ApiProxy, HostFrame, MuxFrame } from '../src/api/index.ts'
|
||||
import type { ClientResponse, RpcMessage, RpcReceipt, RpcRequest } from '../src/api/rpc.ts'
|
||||
@@ -190,25 +189,6 @@ function fakeApi(overrides: Partial<{ muxFrames: MuxFrame[]; hostFrames: HostFra
|
||||
return { rpcId: request.rpcId, result: { ok: true, value: { archivedSessionIds: [request.payload.sessionId] } } }
|
||||
},
|
||||
},
|
||||
commands: {
|
||||
async list(request) {
|
||||
return { rpcId: request.rpcId, result: { ok: true, value: { commands: [{ name: 'plan', description: 'Toggle plan mode', input: { hint: 'on|off' } }] } } }
|
||||
},
|
||||
async execute(request, signal) {
|
||||
if (request.payload.line === '/hang') {
|
||||
// Cooperative hang: settles only through the carrier signal (sticky
|
||||
// abort checked first — listeners never fire retroactively).
|
||||
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: {} } } }
|
||||
}
|
||||
if (request.payload.line.startsWith('/plan')) {
|
||||
return { rpcId: request.rpcId, result: { ok: true, value: { matched: true, commandId: CommandId('cmd-x') } } }
|
||||
}
|
||||
return { rpcId: request.rpcId, result: { ok: true, value: { matched: false } } }
|
||||
},
|
||||
},
|
||||
agentPresets: {
|
||||
list(request: RpcRequest<{}>) {
|
||||
return Promise.resolve({
|
||||
@@ -441,19 +421,13 @@ describe('unary round trip (handler ⇄ client, no network)', () => {
|
||||
expect(response.result).toEqual({ ok: true, value: { opened: true } })
|
||||
})
|
||||
|
||||
it('round-trips command.list / command.execute / skill.list through the wire form', async () => {
|
||||
it('round-trips skill.list through the wire form', async () => {
|
||||
const c = client()
|
||||
const list = await c.commands.list({ sessionId: 's' as never })
|
||||
expect(list.result).toEqual({ ok: true, value: { commands: [{ name: 'plan', description: 'Toggle plan mode', input: { hint: 'on|off' } }] } })
|
||||
const hit = await c.commands.execute({ sessionId: 's' as never, line: '/plan off' })
|
||||
expect(hit.result).toEqual({ ok: true, value: { matched: true, commandId: 'cmd-x' } })
|
||||
const miss = await c.commands.execute({ sessionId: 's' as never, line: '/nope' })
|
||||
expect(miss.result).toEqual({ ok: true, value: { matched: false } })
|
||||
const skills = await c.skills.list({ sessionId: 's' as never })
|
||||
expect(skills.result).toEqual({ ok: true, value: { skills: [{ name: 'commit-helper', description: 'Git commits', modelInvocable: true }] } })
|
||||
})
|
||||
|
||||
it('lets command.execute finish after the 30-second default unary deadline', async () => {
|
||||
it('lets host.pickDirectory finish after the 30-second default unary deadline', async () => {
|
||||
vi.useFakeTimers()
|
||||
const timeoutSpy = vi.spyOn(AbortSignal, 'timeout').mockImplementation((milliseconds) => {
|
||||
const controller = new AbortController()
|
||||
@@ -464,16 +438,13 @@ describe('unary round trip (handler ⇄ client, no network)', () => {
|
||||
})
|
||||
try {
|
||||
const api = fakeApi()
|
||||
api.commands.execute = async (request) => {
|
||||
api.host.pickDirectory = async (request) => {
|
||||
await new Promise(resolve => setTimeout(resolve, 30_001))
|
||||
return {
|
||||
rpcId: request.rpcId,
|
||||
result: { ok: true, value: { matched: true, commandId: CommandId('cmd-slow') } },
|
||||
}
|
||||
return { rpcId: request.rpcId, result: { ok: true, value: { path: '/tmp/slow' } } }
|
||||
}
|
||||
const execution = client(api).commands.execute({ sessionId: 's' as never, line: '/slow' })
|
||||
const execution = client(api).host.pickDirectory({})
|
||||
const assertion = expect(execution).resolves.toMatchObject({
|
||||
result: { ok: true, value: { matched: true, commandId: 'cmd-slow' } },
|
||||
result: { ok: true, value: { path: '/tmp/slow' } },
|
||||
})
|
||||
|
||||
await Promise.all([
|
||||
@@ -509,10 +480,10 @@ describe('unary round trip (handler ⇄ client, no network)', () => {
|
||||
})).result).toEqual({ ok: true, value: { accepted: true } })
|
||||
})
|
||||
|
||||
it('keeps caller and connection aborts on command.execute', async () => {
|
||||
it('keeps caller and connection aborts on a deadline-exempt unary', async () => {
|
||||
const api = fakeApi()
|
||||
const started = Promise.withResolvers<AbortSignal>()
|
||||
api.commands.execute = async (request, signal) => {
|
||||
api.host.pickDirectory = async (request, signal) => {
|
||||
started.resolve(signal)
|
||||
if (!signal.aborted) {
|
||||
await new Promise<void>((resolve) => {
|
||||
@@ -525,10 +496,7 @@ describe('unary round trip (handler ⇄ client, no network)', () => {
|
||||
}
|
||||
}
|
||||
const controller = new AbortController()
|
||||
const execution = client(api).commands.execute(
|
||||
{ sessionId: 's' as never, line: '/hang' },
|
||||
controller.signal,
|
||||
)
|
||||
const execution = client(api).host.pickDirectory({}, controller.signal)
|
||||
const handlerSignal = await started.promise
|
||||
|
||||
controller.abort(new Error('connection closed'))
|
||||
@@ -537,20 +505,6 @@ describe('unary round trip (handler ⇄ client, no network)', () => {
|
||||
expect(handlerSignal.aborted).toBe(true)
|
||||
})
|
||||
|
||||
it('propagates the carrier Request signal into command.execute', async () => {
|
||||
const handler = toFetchHandler(fakeApi())
|
||||
const controller = new AbortController()
|
||||
const body = JSON.stringify({ type: 'client-request', rpcId: 'r-sig', method: 'command.execute', payload: { sessionId: 's', line: '/hang' } })
|
||||
// The fake's /hang settles only when the invoke-level signal aborts: a
|
||||
// completed response with the cancelled error proves req.signal reached it.
|
||||
const pending = handler.fetch(new Request('http://x/api/command.execute', { method: 'POST', headers: { 'content-type': 'application/json' }, body, signal: controller.signal }))
|
||||
controller.abort()
|
||||
const response = await pending
|
||||
const parsed = await response.json() as { rpcId: string; result: { ok: boolean; error?: { code: string } } }
|
||||
expect(parsed.rpcId).toBe('r-sig')
|
||||
expect(parsed.result.error?.code).toBe('cancelled')
|
||||
})
|
||||
|
||||
it('propagates the carrier Request signal into session.search', async () => {
|
||||
const handler = toFetchHandler(fakeApi())
|
||||
const controller = new AbortController()
|
||||
|
||||
Reference in New Issue
Block a user