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:
507 files changed
+3130
-2238
No files matched your search
+6
-5
@@ -14,7 +14,6 @@ import type { SlashSource } from '@deepseek-ai/dsh-client-ui-slash/client'
|
||||
import type { CommandServiceContract } from '../src/client/contract.ts'
|
||||
import type { PopupSelectInjected } from '../src/client/PopupSelectView.tsx'
|
||||
import { LocaleService } from '@deepseek-ai/dsh-client-locale/client'
|
||||
import { TestRemote } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { apply, CommandService, inject } from '../src/client/index.ts'
|
||||
|
||||
const sid = (k: string): SessionId => k as SessionId
|
||||
@@ -33,14 +32,16 @@ async function bench() {
|
||||
scope: (id: SessionId) => scopes.get(id),
|
||||
scopeOf: (c: Context) => scopeOf(c),
|
||||
})
|
||||
ctx.provide('connection', { api: { commands: { list: () => Promise.resolve({ result: { ok: true, value: { commands: [] } } }) } } })
|
||||
const commandsRemote = { list: () => Promise.resolve([]) }
|
||||
// The service subscribes its cache-invalidation events on construction, so
|
||||
// the Remote face needs `$on` even where this spec dispatches none.
|
||||
ctx.provide('remote', { commands: commandsRemote, $on: () => () => {} })
|
||||
ctx.provide('remote.commands', commandsRemote)
|
||||
await ctx.plugin(SlotsService).await()
|
||||
ctx.slots.register({
|
||||
name: 'root', children: { 'conversation.input.overlay': { kind: 'list', scope: 'session' } },
|
||||
} as never, (() => null) as never)
|
||||
ctx.provide('locale', new LocaleService(ctx))
|
||||
// CommandService injects `remote` for the forwarded directory invalidation.
|
||||
new TestRemote(ctx)
|
||||
const fiber = ctx.plugin({ inject: [...inject], apply })
|
||||
await fiber.await()
|
||||
const mint = (key: string) => {
|
||||
@@ -53,7 +54,7 @@ async function bench() {
|
||||
|
||||
describe('apply', () => {
|
||||
it('declares the services it binds', () => {
|
||||
expect(inject).toEqual(['slash', 'sessions', 'connection', 'locale', 'remote'])
|
||||
expect(inject).toEqual(['slash', 'sessions', 'remote', 'remote.commands', 'locale'])
|
||||
})
|
||||
|
||||
it('mounts ctx.command, registers the source and the overlay entry, and folds up on disposal', async () => {
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
* gate, and the per-key ensureReady strong-wait policy.
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import type { CommandDescriptor } from '../src/client/directory.ts'
|
||||
import { CommandDirectory } from '../src/client/directory.ts'
|
||||
|
||||
File renamed without changes.
File renamed without changes.
+63
-19
@@ -10,7 +10,6 @@
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { createScope, scopeOf } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { TestRemote } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { ClientSessionContext, ConsumeTokenRequest, SlashPick, SlashSource } from '@deepseek-ai/dsh-client-ui-slash/client'
|
||||
import type { CommandContribution, CommandDecoration, CommandUiSpec, SelectOption } from '../src/client/contract.ts'
|
||||
@@ -32,7 +31,7 @@ const S2_CMDS: CommandDescriptor[] = [
|
||||
{ name: 'attach', description: 'scoped shadow', input: { hint: 'path' } },
|
||||
]
|
||||
|
||||
type ExecuteValue = { matched: boolean }
|
||||
type ExecuteValue = { matched: boolean; commandId?: string }
|
||||
|
||||
interface BenchOptions {
|
||||
/** Scripted catalog per list payload; default serves the fixed catalogs by session. */
|
||||
@@ -41,25 +40,54 @@ interface BenchOptions {
|
||||
addressed?: SessionId
|
||||
}
|
||||
|
||||
/**
|
||||
* Fold one programmed answer into the generated Remote face's outcome: a
|
||||
* resolved value is the ok branch, a rejection is the transport failure the
|
||||
* carrier reports in the error branch instead of throwing at the caller.
|
||||
* @param produce - the scripted answer for one Remote method.
|
||||
* @returns the carried result the service reads.
|
||||
*/
|
||||
async function carried<T>(produce: () => Promise<T>) {
|
||||
try {
|
||||
return { ok: true as const, value: await produce() }
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false as const,
|
||||
error: {
|
||||
code: 'internal',
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
details: {},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function bench(opts: BenchOptions = {}) {
|
||||
const ctx = new Context()
|
||||
const registered = new Map<string, SlashSource>()
|
||||
const listCalls: Array<{ sessionId: SessionId }> = []
|
||||
const executeCalls: Array<{ sessionId: SessionId; line: string }> = []
|
||||
const api = {
|
||||
commands: {
|
||||
list: async (payload: { sessionId: SessionId }) => {
|
||||
listCalls.push(payload)
|
||||
// The service reads the generated commands Remote, which delivers the
|
||||
// carrier's outcome, so a programmed failure answers the error branch.
|
||||
const commandsRemote = {
|
||||
list: async (sessionId: SessionId) => {
|
||||
listCalls.push({ sessionId })
|
||||
return await carried(async () => {
|
||||
const value = await (opts.commands ?? (p => Promise.resolve({
|
||||
commands: p.sessionId === sid('s2') ? S2_CMDS : S1_CMDS,
|
||||
})))(payload)
|
||||
return { result: { ok: true as const, value } }
|
||||
},
|
||||
execute: async (payload: { sessionId: SessionId; line: string }) => {
|
||||
executeCalls.push(payload)
|
||||
const value = await (opts.execute ?? (() => Promise.resolve({ matched: true })))(payload)
|
||||
return { result: { ok: true as const, value } }
|
||||
},
|
||||
})))({ sessionId })
|
||||
return value.commands
|
||||
})
|
||||
},
|
||||
execute: async (sessionId: SessionId, line: string) => {
|
||||
executeCalls.push({ sessionId, line })
|
||||
return await carried(async () => {
|
||||
const fallback = (): Promise<ExecuteValue> => Promise.resolve({ matched: true })
|
||||
const value = await (opts.execute ?? fallback)({ sessionId, line })
|
||||
return value.matched
|
||||
? { commandId: value.commandId ?? 'fake-command', result: { kind: 'success' as const } }
|
||||
: undefined
|
||||
})
|
||||
},
|
||||
}
|
||||
ctx.provide('slash', {
|
||||
@@ -78,10 +106,20 @@ async function bench(opts: BenchOptions = {}) {
|
||||
? { parentSessionId: sid('parent'), childSessionId: id, mode: 'continuable' as const }
|
||||
: undefined,
|
||||
})
|
||||
ctx.provide('connection', { api })
|
||||
// CommandService injects `remote`; the directory invalidation arrives on the
|
||||
// same `$dispatch` handoff the connection sink makes.
|
||||
new TestRemote(ctx)
|
||||
const forwarded = new Map<string, Array<(...args: never[]) => void>>()
|
||||
ctx.provide('remote', {
|
||||
commands: commandsRemote,
|
||||
$on: (event: string, listener: (...args: never[]) => void) => {
|
||||
const listeners = forwarded.get(event) ?? []
|
||||
listeners.push(listener)
|
||||
forwarded.set(event, listeners)
|
||||
return () => { forwarded.set(event, listeners.filter(entry => entry !== listener)) }
|
||||
},
|
||||
$dispatch: (event: string, args: readonly unknown[]) => {
|
||||
for (const listener of forwarded.get(event) ?? []) listener(...args as never[])
|
||||
},
|
||||
})
|
||||
ctx.provide('remote.commands', commandsRemote)
|
||||
/** Notices the fake conversation face collected (runDetached routing). */
|
||||
const notices: Array<{ scope: SessionId | undefined; level: 'info' | 'error'; text: string }> = []
|
||||
ctx.provide('conversation', {
|
||||
@@ -515,7 +553,13 @@ describe('detached admission notices', () => {
|
||||
mode = 'reject'
|
||||
menuPick(source, 'plan', proj('s1'))
|
||||
await flush()
|
||||
expect(notices).toEqual([{ scope: sid('s1'), level: 'error', text: 'network down' }])
|
||||
// A dead Remote call and a rejected one now read alike: both arrive as a
|
||||
// failed result, so the notice names the endpoint either way.
|
||||
expect(notices).toEqual([{
|
||||
scope: sid('s1'),
|
||||
level: 'error',
|
||||
text: 'command.execute failed: internal: network down',
|
||||
}])
|
||||
})
|
||||
|
||||
it('a torn-down scope drops the failure notice', async () => {
|
||||
Reference in New Issue
Block a user