feat(remote-mock): expose typed native mocks and controlled streams

This commit is contained in:
imccyu committed 2026-09-10 16:34:27 +08:00
1 parent 0718711886
commit 3d6fa12a6d
17 files changed
+1633

No files matched your search

+1
View File
@@ -169,6 +169,7 @@ External packages **directly declared** for development, tests, types, or toolin
| [`@types/ws`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT |
| [`@vitejs/plugin-react`](https://github.com/vitejs/vite-plugin-react) | MIT |
| [`@vitest/coverage-v8`](https://github.com/vitest-dev/vitest) | MIT |
| [`@vitest/spy`](https://github.com/vitest-dev/vitest) | MIT |
| [`@yao-pkg/pkg`](https://github.com/yao-pkg/pkg) | MIT |
| [`@yarnpkg/cli-dist`](https://github.com/yarnpkg/berry) | BSD-2-Clause |
| [`app-builder-lib`](https://github.com/electron-userland/electron-builder) | MIT |
@@ -0,0 +1,42 @@
{
"name": "@deepseek-ai/dsh-remote-mock",
"description": "Endpoint-named mock for Typert Remote traffic: unary answers and stream scripts per <namespace>/<method>, live stream control, a log, and the Connection carrier face whole-client specs install",
"version": "0.1.5-rc.1",
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "packages/test-support/remote-mock"
},
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
"exports": {
".": {
"types": "./lib/types/index.d.ts",
"default": "./lib/index.js"
},
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"license": "MIT",
"dependencies": {
"@vitest/spy": "^4.1.8"
},
"peerDependencies": {
"@deepseek-ai/cordis": "workspace:^"
},
"devDependencies": {
"@deepseek-ai/cordis": "workspace:^",
"@deepseek-ai/dsh-api-settings-controller": "workspace:^",
"@deepseek-ai/dsh-client-connection": "workspace:^",
"@deepseek-ai/dsh-typert-protocol": "workspace:^",
"typescript": "^6.0.3"
},
"files": [
"lib/index.js",
"lib/types/**/*.d.ts"
]
}
@@ -0,0 +1,16 @@
/**
* Endpoint-named mock for Typert Remote traffic: a table of unary answers and
* stream scripts keyed by `<namespace>/<method>`, scripted-stream control, a
* carrier log, and the Connection carrier face the `connection` plugin
* accepts through `__DSH_TRANSPORT__.rpc`. Values are whatever the test
* registers; the only declaration is whether an endpoint is unary or a
* stream. Browser-safe: no DOM, React, or Node imports, no runtime import
* from another harness package.
* @module @deepseek-ai/dsh-remote-mock
*/
export { RemoteMock, ok } from './remote-mock.ts'
export type { OpenStreams, RemoteMockOptions, RemoteTable, StreamFilter, UnaryRuleFn } from './remote-mock.ts'
export { frames, openStream } from './streams.ts'
export type { StreamHandle, StreamScript } from './streams.ts'
export type { LoggedCall, LoggedMiss, LoggedStream, MockLog } from './log.ts'
export type { MockedRemote } from './remote-proxy.ts'
@@ -0,0 +1,131 @@
/** What a `RemoteMock` saw: answered calls, opened streams, and requests without a rule. */
/** One unary call, logged when its rule is selected and settled when the rule does. */
export interface LoggedCall {
readonly endpoint: string
/** Positional args as the caller passed them. */
readonly args: readonly unknown[]
/** `pending` until the rule settles, then `answered` or `failed`. */
readonly state: 'pending' | 'answered' | 'failed'
/** The answer once `answered`, the thrown error once `failed`. */
readonly result: unknown
/** Global order across calls and stream opens. */
readonly seq: number
}
/** One stream open and its current state. */
export interface LoggedStream {
readonly endpoint: string
/** Positional args the stream was opened with. */
readonly args: readonly unknown[]
readonly state: 'open' | 'ended' | 'failed' | 'cancelled'
/** Items delivered to the consumer so far. */
readonly pushed: number
/** Global order across calls and stream opens. */
readonly seq: number
}
/** One request that found no rule. */
export interface LoggedMiss {
readonly endpoint: string
readonly mode: 'unary' | 'stream'
}
/** Read side of the log. */
export interface MockLog {
/**
* Unary calls, oldest first, with their live state.
* @param endpoint - restrict to one endpoint.
* @returns matching entries.
*/
calls(endpoint?: string): readonly LoggedCall[]
/**
* Stream opens, oldest first, with their live state.
* @param endpoint - restrict to one endpoint.
* @returns matching entries.
*/
streams(endpoint?: string): readonly LoggedStream[]
/**
* The first positional arg of every unary call and stream open, oldest first: the request object of the Gateway's
* one-parameter endpoints. Without `endpoint`, every endpoint the client asked the Host for, except the Gateway's
* own `$`-prefixed ones (`$events`).
* @param endpoint - restrict to one endpoint.
* @returns the requests.
*/
requests(endpoint?: string): readonly unknown[]
/** Requests that found no rule, oldest first. */
unmatched(): readonly LoggedMiss[]
}
/** Mutable call entry the dispatcher settles. */
export interface CallRecord extends LoggedCall {
state: LoggedCall['state']
result: unknown
}
/** Mutable stream entry shared with the live stream. */
export interface StreamRecord extends LoggedStream {
state: LoggedStream['state']
pushed: number
}
/** The mock's log store. */
export class MockLogStore implements MockLog {
private readonly callEntries: CallRecord[] = []
private readonly streamEntries: StreamRecord[] = []
private readonly missEntries: LoggedMiss[] = []
private seq = 0
calls(endpoint?: string): readonly LoggedCall[] {
return this.callEntries.filter(entry => endpoint === undefined || entry.endpoint === endpoint)
}
streams(endpoint?: string): readonly LoggedStream[] {
return this.streamEntries.filter(entry => endpoint === undefined || entry.endpoint === endpoint)
}
requests(endpoint?: string): readonly unknown[] {
const entries: (LoggedCall | LoggedStream)[] = [...this.calls(endpoint), ...this.streams(endpoint)]
return entries
.filter(entry => endpoint !== undefined || !entry.endpoint.startsWith('$'))
.sort((a, b) => a.seq - b.seq)
.map(entry => entry.args[0])
}
unmatched(): readonly LoggedMiss[] {
return [...this.missEntries]
}
/**
* Record one call whose rule was selected.
* @param endpoint - endpoint.
* @param args - positional args.
* @returns the mutable entry the dispatcher settles.
*/
call(endpoint: string, args: readonly unknown[]): CallRecord {
const record: CallRecord = { endpoint, args, state: 'pending', result: undefined, seq: ++this.seq }
this.callEntries.push(record)
return record
}
/**
* Record one stream open.
* @param endpoint - endpoint.
* @param args - positional args.
* @returns the mutable entry the stream updates.
*/
stream(endpoint: string, args: readonly unknown[]): StreamRecord {
const record: StreamRecord = { endpoint, args, state: 'open', pushed: 0, seq: ++this.seq }
this.streamEntries.push(record)
return record
}
/**
* Record one request without a rule.
* @param endpoint - endpoint.
* @param mode - how it was requested.
*/
miss(endpoint: string, mode: 'unary' | 'stream'): void {
this.missEntries.push({ endpoint, mode })
}
}
@@ -0,0 +1,385 @@
/** `RemoteMock`: an endpoint table (unary answers or stream scripts), live stream control, a log, and the Connection carrier face. */
import type { ClientConnectionRpc, ConnectionRpcResult } from '@deepseek-ai/dsh-client-connection/client'
import { fn, type Mock } from '@vitest/spy'
import { MockLogStore, type MockLog } from './log.ts'
import { MockStream, toError, type StreamScript } from './streams.ts'
import { createRemoteProxy, type MockedRemote } from './remote-proxy.ts'
/** A unary handler receives the caller's positional arguments, without its trailing `AbortSignal`. */
export type UnaryRuleFn<Args extends readonly unknown[] = readonly unknown[], Result = unknown> = (...args: Args) => Result
/** Endpoint defaults: unary values or positional handlers, stream scripts, and stream declarations. */
export interface RemoteTable {
readonly unary?: Readonly<Record<string, unknown>>
readonly stream?: Readonly<Record<string, StreamScript>>
/** Endpoints declared as streams without a script, so an open finds a stream miss rather than a unary one. */
readonly streams?: readonly string[]
}
/** Construction options. */
export interface RemoteMockOptions {
/** Host facts the built-in `$events` ready frame carries; default `{ home: '/home/mock' }`. */
readonly host?: { readonly home: string }
}
/** Filter over the args a stream was opened with. */
export type StreamFilter = (args: readonly unknown[]) => boolean
/** Control and synchronization for streams opened by registered scripts; native overrides are excluded. */
export interface OpenStreams {
/**
* Push one item into every open stream on `endpoint`, optionally filtered by its open args.
* @param endpoint - endpoint.
* @param item - item to deliver.
* @param where - filter over open args.
* @returns how many streams received it.
*/
push(endpoint: string, item: unknown, where?: StreamFilter): number
/**
* End every matching open stream.
* @param endpoint - endpoint.
* @param where - filter over open args.
* @returns how many streams ended.
*/
end(endpoint: string, where?: StreamFilter): number
/**
* Fail every matching open stream with `error`.
* @param endpoint - endpoint.
* @param error - error the consumer's read rejects with.
* @param where - filter over open args.
* @returns how many streams failed.
*/
fail(endpoint: string, error: Error, where?: StreamFilter): number
/**
* Resolve once registered scripts have opened `endpoint` at least `count` times in total.
* @param endpoint - endpoint.
* @param count - opens to wait for.
*/
opened(endpoint: string, count: number): Promise<void>
/**
* Resolve once the consumer of every matching stream has pulled everything pushed so far and, for a stream still
* open, waits for more; a settled stream counts once its queue is empty, and a stream nobody reads never drains.
* Pulled means taken from the queue: a consumer that processes items asynchronously after pulling them may still
* be working on the last one.
* @param endpoint - endpoint.
* @param where - filter over open args.
*/
drained(endpoint: string, where?: StreamFilter): Promise<void>
}
const EVENTS_ENDPOINT = '$events'
/**
* The success envelope the Client's Remote callers read: `{ ok: true, value }`.
* @param value - success value.
* @returns the envelope.
*/
export function ok<T>(value: T): { readonly ok: true; readonly value: T } {
return { ok: true, value }
}
interface OpenWaiter {
readonly endpoint: string
readonly count: number
resolve(): void
}
/** A missing default answer is an unmatched request, not a selected rule that failed. */
class MissingUnaryRule extends Error {}
/**
* Endpoint-named Remote mock. `dispatch` / `open` are the core; `rpc` is the
* same core as the Connection carrier the `connection` plugin accepts through
* `__DSH_TRANSPORT__.rpc`.
*/
export class RemoteMock {
/**
* Create a mock whose `$events` stream answers the Gateway client's opening
* with one ready frame and then stays open, so the assembled client connects.
* @param options - host facts for the ready frame.
* @returns the mock.
*/
static create(options: RemoteMockOptions = {}): RemoteMock {
const mock = new RemoteMock()
const host = options.host ?? { home: '/home/mock' }
let generation = 0
return mock.stream(EVENTS_ENDPOINT, (_args, stream) => {
generation += 1
stream.push({ type: 'ready', clientId: `mock-client-${String(generation)}`, host })
})
}
private readonly unaryDefaults = new Map<string, unknown>()
private readonly scripts = new Map<string, StreamScript | undefined>()
private live: MockStream[] = []
private openWaiters: OpenWaiter[] = []
private readonly logStore = new MockLogStore()
private readonly unaryMocks = new Map<string, Mock<UnaryRuleFn>>()
private readonly streamMocks = new Map<string, Mock<UnaryRuleFn>>()
private readonly proxy = createRemoteProxy(endpoint => this.modeOf(endpoint) === 'stream'
? this.streamMock(endpoint)
: this.unaryMock(endpoint))
/**
* Native mock functions for every namespace: configure answers and inspect calls without declaring method names.
* Explicit stream registrations select stream mocks; all other methods use unary mocks.
* Native overrides belong to this instance; `mockReset()` restores the implementation reading current defaults.
* Generated declarations provide exact signatures; absent declarations weaken only this test-owned proxy.
*/
// oxlint-disable-next-line typescript/no-unsafe-assignment -- Only this test proxy becomes any without generated namespaces.
readonly remote: MockedRemote = this.proxy as MockedRemote
/** Everything observed so far. */
readonly log: MockLog = this.logStore
/** Control over streams opened by registered scripts. */
readonly streams: OpenStreams = {
push: (endpoint, item, where) => this.forEachOpen(endpoint, where, (stream) => { stream.push(item) }),
end: (endpoint, where) => this.forEachOpen(endpoint, where, (stream) => { stream.end() }),
fail: (endpoint, error, where) => this.forEachOpen(endpoint, where, (stream) => { stream.fail(error) }),
opened: (endpoint, count) => {
if (this.logStore.streams(endpoint).length >= count) return Promise.resolve()
return new Promise<void>((resolve) => { this.openWaiters.push({ endpoint, count, resolve }) })
},
drained: (endpoint, where) => Promise.all(this.matching(endpoint, where).map(stream => stream.drained())).then(() => undefined),
}
/**
* The Connection carrier face: `call` dispatches, `open` opens; payloads carry
* `{ args }` as the whole-client proxies (an array) or the Gateway's own
* endpoints (one object) send them, and a call aborted by its signal rejects.
*/
readonly rpc: ClientConnectionRpc = {
call: async (_channel, endpoint, payload, signal) => {
const pending = this.dispatch(endpoint, argsOf(endpoint, payload))
const value = await (signal === undefined ? pending : settleOrAbort(pending, signal))
// The carrier contract names the result envelope; the registered value is taken as that envelope unchecked.
return value as ConnectionRpcResult<unknown>
},
open: (_channel, endpoint, payload, signal) => this.open(endpoint, argsOf(endpoint, payload), signal),
}
private constructor() {}
/**
* Set the endpoint's default positional-argument handler without changing its native mock overrides.
* @param endpoint - `<namespace>/<method>`.
* @param rule - handler computing the answer.
* @returns this.
*/
unary<Args extends readonly unknown[]>(endpoint: string, rule: UnaryRuleFn<Args>): this
/**
* Set the endpoint's default answer, including explicit `undefined`, without changing its native mock overrides.
* @param endpoint - `<namespace>/<method>`.
* @param rule - default answer.
* @returns this.
*/
unary(endpoint: string, rule: unknown): this
unary(endpoint: string, rule: unknown): this {
this.unaryDefaults.set(endpoint, rule)
return this
}
/**
* Declare `endpoint` as a stream and replace its default script when supplied. A declaration without a script
* preserves any existing script; an endpoint declared without one fails an open as a stream miss.
* @param endpoint - `<namespace>/<method>`.
* @param script - script (`frames` / `openStream` build the common ones).
* @returns this.
*/
stream(endpoint: string, script?: StreamScript): this {
if (script !== undefined || !this.scripts.has(endpoint)) this.scripts.set(endpoint, script)
return this
}
/**
* Register endpoint defaults without changing native mock overrides.
* @param table - unary answers and stream scripts.
* @returns this.
*/
load(table: RemoteTable): this {
for (const [endpoint, rule] of Object.entries(table.unary ?? {})) this.unary(endpoint, rule)
for (const endpoint of table.streams ?? []) this.stream(endpoint)
for (const [endpoint, script] of Object.entries(table.stream ?? {})) this.stream(endpoint, script)
return this
}
/**
* Whether `endpoint` is declared a stream (with or without a script) or has a unary rule — the one declaration
* the whole-client proxies need; an endpoint neither declared nor ruled is dispatched as a unary call.
* @param endpoint - endpoint.
* @returns the mode, or undefined when nothing is registered.
*/
modeOf(endpoint: string): 'unary' | 'stream' | undefined {
if (this.scripts.has(endpoint)) return 'stream'
if (this.unaryDefaults.has(endpoint)) return 'unary'
return undefined
}
/**
* Registered endpoints and accessed proxy methods, sorted.
* @returns endpoint names.
*/
endpoints(): readonly string[] {
const names = new Set<string>(this.scripts.keys())
for (const endpoint of this.unaryDefaults.keys()) names.add(endpoint)
for (const [namespace, methods] of Object.entries(this.proxy)) {
for (const method of Object.keys(methods)) names.add(`${namespace}/${method}`)
}
return [...names].sort()
}
/**
* Answer one unary call with the registered rule's value, verbatim. The call
* is logged as soon as its rule is selected and settles with the rule: a
* rule that throws or rejects fails the call with that error.
* @param endpoint - endpoint.
* @param args - positional args.
* @returns the answer.
* @throws {Error} when no rule is registered (logged as unmatched).
*/
async dispatch(endpoint: string, args: readonly unknown[]): Promise<unknown> {
let answer: unknown
try {
answer = this.unaryMock(endpoint)(...args)
} catch (error) {
if (error instanceof MissingUnaryRule) throw error
answer = Promise.reject(toError(error))
}
const record = this.logStore.call(endpoint, args)
try {
record.result = await answer
} catch (error) {
record.state = 'failed'
record.result = error
throw error
}
record.state = 'answered'
return record.result
}
private unaryMock(endpoint: string): Mock<UnaryRuleFn> {
let mock = this.unaryMocks.get(endpoint)
if (mock === undefined) {
mock = fn((...args: readonly unknown[]) => Promise.resolve(this.unaryAnswer(endpoint, args)))
this.unaryMocks.set(endpoint, mock)
}
return mock
}
private unaryAnswer(endpoint: string, args: readonly unknown[]): unknown {
if (!this.unaryDefaults.has(endpoint)) {
this.logStore.miss(endpoint, 'unary')
throw new MissingUnaryRule(this.noRuleMessage(endpoint))
}
const rule = this.unaryDefaults.get(endpoint)
return isRuleFn(rule) ? answerOf(rule, args) : rule
}
/**
* Open through the endpoint's native mock; its default runs the registered script as a controlled stream.
* A native override returns its own iterable: the caller owns consumption and cancellation, outside `OpenStreams`.
* @param endpoint - endpoint.
* @param args - positional args.
* @param signal - consumer cancellation.
* @returns the controlled script stream or the native override's caller-owned iterable.
* @throws {Error} when the default runs without a registered script (logged as unmatched).
*/
open(endpoint: string, args: readonly unknown[], signal: AbortSignal): AsyncIterable<unknown> {
return this.streamMock(endpoint)(...args, signal) as AsyncIterable<unknown>
}
private streamMock(endpoint: string): Mock<UnaryRuleFn> {
let mock = this.streamMocks.get(endpoint)
if (mock === undefined) {
mock = fn((...values: readonly unknown[]) => {
const args = [...values]
const signal = args.at(-1) instanceof AbortSignal ? args.pop() as AbortSignal : new AbortController().signal
return this.openScript(endpoint, args, signal)
})
this.streamMocks.set(endpoint, mock)
}
return mock
}
private openScript(endpoint: string, args: readonly unknown[], signal: AbortSignal): AsyncIterable<unknown> {
const script = this.scripts.get(endpoint)
if (script === undefined) {
this.logStore.miss(endpoint, 'stream')
throw new Error(this.noRuleMessage(endpoint))
}
const stream = new MockStream(this.logStore.stream(endpoint, args), signal)
this.live.push(stream)
this.wakeOpened(endpoint)
stream.run(script, args)
return stream
}
/** Throw when any request found no rule, naming the endpoints and the registered ones. */
assertNoUnmatched(): void {
const unmatched = this.logStore.unmatched()
if (unmatched.length === 0) return
const lines = unmatched.map(entry => ` ${entry.endpoint} (${entry.mode})`)
throw new Error(`remote-mock: ${String(unmatched.length)} unmatched request(s):\n${lines.join('\n')}\nregistered: ${this.endpoints().join(', ')}`)
}
private noRuleMessage(endpoint: string): string {
return `remote-mock: no rule for ${endpoint}; registered: ${this.endpoints().join(', ')}`
}
/** Streams on `endpoint` still open or still holding items their consumer has not pulled; the rest are forgotten. */
private matching(endpoint: string, where: StreamFilter | undefined): MockStream[] {
this.live = this.live.filter(stream => stream.record.state === 'open' || stream.queued > 0)
return this.live.filter(stream => stream.record.endpoint === endpoint && (where === undefined || where(stream.record.args)))
}
private forEachOpen(endpoint: string, where: StreamFilter | undefined, action: (stream: MockStream) => void): number {
const targets = this.matching(endpoint, where).filter(stream => stream.record.state === 'open')
for (const stream of targets) action(stream)
return targets.length
}
private wakeOpened(endpoint: string): void {
const opened = this.logStore.streams(endpoint).length
const ready = this.openWaiters.filter(waiter => waiter.endpoint === endpoint && opened >= waiter.count)
this.openWaiters = this.openWaiters.filter(waiter => !ready.includes(waiter))
for (const waiter of ready) waiter.resolve()
}
}
function isRuleFn(rule: unknown): rule is UnaryRuleFn {
return typeof rule === 'function'
}
/** A rule's synchronous throw becomes a rejection so the call settles through one path. */
function answerOf(rule: UnaryRuleFn, args: readonly unknown[]): unknown {
try {
return rule(...args)
} catch (error) {
return Promise.reject(toError(error))
}
}
/** Positional args from a carrier payload: the array the whole-client proxies send, or the one object the Gateway's own endpoints send. */
function argsOf(endpoint: string, payload: unknown): readonly unknown[] {
if (typeof payload === 'object' && payload !== null && 'args' in payload) {
const { args } = payload
if (Array.isArray(args)) return args
if (typeof args === 'object' && args !== null) return [args]
}
throw new TypeError(`remote-mock: payload of ${endpoint} must be { args: unknown[] | object }`)
}
/**
* Settle with the call, or reject with the abort reason first; the call itself
* always keeps a handler so its own outcome is never an unhandled rejection.
*/
function settleOrAbort<T>(pending: Promise<T>, signal: AbortSignal): Promise<T> {
return new Promise<T>((resolve, reject) => {
const abort = (): void => { reject(signal.reason instanceof Error ? signal.reason : new Error('remote-mock: call aborted', { cause: signal.reason })) }
signal.addEventListener('abort', abort, { once: true })
pending.then(resolve, reject).finally(() => { signal.removeEventListener('abort', abort) })
if (signal.aborted) abort()
})
}
@@ -0,0 +1,29 @@
/** Lazy namespace access for native Remote mocks; production Remote declarations stay unchanged. */
import type { MaybeMockedDeep } from '@vitest/spy'
import type { TypertRemoteNamespaceMap } from '@deepseek-ai/dsh-typert-protocol'
/**
* Native mocks for every generated Remote namespace, or an unrestricted local proxy without generated types.
* @template Api - generated namespace map available in the caller's TypeScript program.
*/
// oxlint-disable-next-line typescript/no-explicit-any -- An unbuilt namespace map has neither method names nor signatures.
export type MockedRemote<Api = TypertRemoteNamespaceMap> = keyof Api extends never ? any : MaybeMockedDeep<Api>
/**
* Cache accessed namespaces and expose the current native mock for each method.
* @param method - selects a stable endpoint mock for its configured invocation mode.
* @returns enumerable accessed namespaces and methods; symbols and thenable probes remain inert.
*/
export function createRemoteProxy(method: (endpoint: string) => unknown): Record<string, object> {
return new Proxy(Object.create(null) as Record<string, object>, {
get(namespaces, namespace) {
if (typeof namespace !== 'string' || namespace === 'then') return undefined
return namespaces[namespace] ??= new Proxy(Object.create(null) as Record<string, unknown>, {
get(methods, name) {
if (typeof name !== 'string' || name === 'then') return undefined
return methods[name] = method(`${namespace}/${name}`)
},
})
},
})
}
@@ -0,0 +1,192 @@
/** Stream scripts and the pushable, abort-aware stream a script drives. */
import type { StreamRecord } from './log.ts'
/** Test-side controls over one open stream. */
export interface StreamHandle {
/** Queue one item for the consumer. */
push(item: unknown): void
/** End the stream after the queued items drain. */
end(): void
/** Fail the consumer's next read with `error` after the queued items drain. */
fail(error: Error): void
/** Consumer cancellation. */
readonly signal: AbortSignal
}
/**
* How a stream endpoint answers an open: receives the open args and the
* handle; the stream stays open after the script returns until `end()` or
* `fail()`. A script that throws or rejects fails the stream with that error.
*/
export type StreamScript = (args: readonly unknown[], stream: StreamHandle) => void | Promise<void>
/**
* Script yielding `items`, then ending.
* @param items - items in order.
* @returns the script.
*/
export function frames(items: readonly unknown[]): StreamScript {
return (_args, stream) => {
for (const item of items) stream.push(item)
stream.end()
}
}
/**
* Script yielding `initial`, then staying open for `streams.push`.
* @param initial - items yielded on open.
* @returns the script.
*/
export function openStream(initial: readonly unknown[] = []): StreamScript {
return (_args, stream) => {
for (const item of initial) stream.push(item)
}
}
type Settled = { readonly kind: 'end' } | { readonly kind: 'fail'; readonly error: Error }
/** One open stream: a queue the script pushes into and a single consumer reads from; the log entry tracks its state. */
export class MockStream implements StreamHandle, AsyncIterable<unknown> {
private readonly queue: unknown[] = []
private settled: Settled | undefined
private waiting: { readonly resolve: (result: IteratorResult<unknown>) => void; readonly reject: (error: unknown) => void } | undefined
private drainWaiters: (() => void)[] = []
/** Consumer cancellation listener; attached while the stream is open, removed when it settles or cancels. */
private readonly onAbort = (): void => { this.cancel() }
/** Items pushed but not yet pulled by the consumer. */
get queued(): number {
return this.queue.length
}
/**
* @param record - log entry this stream updates.
* @param signal - consumer cancellation.
*/
constructor(readonly record: StreamRecord, readonly signal: AbortSignal) {
if (signal.aborted) this.cancel()
else signal.addEventListener('abort', this.onAbort, { once: true })
}
push(item: unknown): void {
if (this.record.state !== 'open') return
this.record.pushed += 1
if (this.waiting !== undefined) {
const { resolve } = this.waiting
this.waiting = undefined
resolve({ value: item, done: false })
return
}
this.queue.push(item)
}
end(): void {
this.settle({ kind: 'end' }, 'ended')
}
fail(error: Error): void {
this.settle({ kind: 'fail', error }, 'failed')
}
/**
* Start `script` on this stream.
* @param script - the registered script.
* @param args - open args.
*/
run(script: StreamScript, args: readonly unknown[]): void {
let outcome: void | Promise<void>
try {
outcome = script(args, this)
} catch (error) {
this.fail(toError(error))
return
}
if (outcome instanceof Promise) void outcome.catch((error: unknown) => { this.fail(toError(error)) })
}
/**
* Resolve once the consumer has pulled every queued item and waits for the next one, or the stream is no longer
* open and holds nothing the consumer could still pull (a consumer that returns discards what it left).
* @returns settles when drained.
*/
drained(): Promise<void> {
if (this.isDrained()) return Promise.resolve()
return new Promise<void>((resolve) => { this.drainWaiters.push(resolve) })
}
[Symbol.asyncIterator](): AsyncIterator<unknown> {
return {
next: () => this.next(),
return: () => {
this.cancel()
return Promise.resolve({ value: undefined, done: true })
},
}
}
private next(): Promise<IteratorResult<unknown>> {
const result = this.pull()
this.wakeDrained()
return result
}
private pull(): Promise<IteratorResult<unknown>> {
if (this.queue.length > 0) return Promise.resolve({ value: this.queue.shift(), done: false })
if (this.settled !== undefined) return this.finish(this.settled)
if (this.record.state === 'cancelled') return Promise.resolve({ value: undefined, done: true })
if (this.waiting !== undefined) return Promise.reject(new Error(`remote-mock: ${this.record.endpoint} stream has one consumer`))
return new Promise((resolve, reject) => { this.waiting = { resolve, reject } })
}
private finish(settled: Settled): Promise<IteratorResult<unknown>> {
return settled.kind === 'end' ? Promise.resolve({ value: undefined, done: true }) : Promise.reject(settled.error)
}
private settle(settled: Settled, state: 'ended' | 'failed'): void {
if (this.record.state !== 'open') return
this.record.state = state
this.settled = settled
this.signal.removeEventListener('abort', this.onAbort)
this.wakeDrained()
// A pending read exists only while the queue is empty: push() resolves it directly instead of queueing.
const waiting = this.waiting
if (waiting === undefined) return
this.waiting = undefined
void this.finish(settled).then(waiting.resolve, waiting.reject)
}
private cancel(): void {
if (this.record.state === 'open') {
this.record.state = 'cancelled'
this.signal.removeEventListener('abort', this.onAbort)
const waiting = this.waiting
this.waiting = undefined
waiting?.resolve({ value: undefined, done: true })
}
// Whether cancelled or left after the producer settled, the consumer pulls nothing more: drop what it left.
this.queue.length = 0
this.wakeDrained()
}
private isDrained(): boolean {
return this.queue.length === 0 && (this.waiting !== undefined || this.record.state !== 'open')
}
private wakeDrained(): void {
if (!this.isDrained()) return
const waiters = this.drainWaiters
this.drainWaiters = []
for (const resolve of waiters) resolve()
}
}
/**
* The `Error` a thrown value stands for: itself, or a new Error carrying its string form.
* @param reason - thrown value.
* @returns the error.
*/
export function toError(reason: unknown): Error {
return reason instanceof Error ? reason : new Error(String(reason))
}
@@ -0,0 +1,184 @@
/** Mock-local unbuilt typing and native deep mocks over generated Remote declarations. */
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import ts from 'typescript'
import type { MaybeMockedDeep } from '@vitest/spy'
import { describe, expect, expectTypeOf, it } from 'vitest'
import type { TypertRemoteNamespace, TypertRemoteNamespaceMap } from '@deepseek-ai/dsh-typert-protocol'
import type {} from '@deepseek-ai/dsh-api-settings-controller/remote'
import type { RemoteMock } from '../src/index.ts'
const root = resolve(import.meta.dirname, '../../../..')
const probePath = resolve(import.meta.dirname, '__remote_proxy_probe.ts')
const artifactPath = resolve(import.meta.dirname, '__remote_proxy_artifact.d.ts')
const protocolPath = resolve(root, 'packages/typert/protocol/src/types.ts')
const proxyPath = resolve(import.meta.dirname, '../src/remote-proxy.ts')
const protocolEntry = resolve(import.meta.dirname, '__remote_protocol.d.ts')
const cordisEntry = resolve(import.meta.dirname, '__remote_cordis.d.ts')
function modulePath(path: string): string {
return JSON.stringify(path.replaceAll('\\', '/'))
}
function virtualKey(path: string): string {
const normalized = path.replaceAll('\\', '/')
return ts.sys.useCaseSensitiveFileNames ? normalized : normalized.toLowerCase()
}
function compile(source: string, artifact?: string) {
const config = ts.readConfigFile(resolve(root, 'tsconfig.base.client.json'), filename => ts.sys.readFile(filename))
if (config.error !== undefined) throw new Error(ts.flattenDiagnosticMessageText(config.error.messageText, '\n'))
const parsed = ts.parseJsonConfigFileContent(config.config, ts.sys, root)
const options: ts.CompilerOptions = {
...parsed.options,
composite: false,
incremental: false,
noEmit: true,
declaration: false,
declarationMap: false,
rewriteRelativeImportExtensions: false,
types: ['node'],
paths: {
'@deepseek-ai/cordis': [cordisEntry],
'@deepseek-ai/dsh-typert-protocol': [protocolEntry],
'@deepseek-ai/fixture/remote': [artifactPath],
},
}
// The real protocol and Mock alias are compiled; only unrelated Cordis/runtime assembly is excluded.
const virtual = new Map([
[virtualKey(probePath), source],
[virtualKey(protocolEntry), `export * from ${modulePath(protocolPath)}\nexport { RemoteError } from ${modulePath(resolve(root, 'packages/typert/protocol/src/remote-error.ts'))}`],
[virtualKey(cordisEntry), 'export interface Context {}\nexport interface Events {}'],
])
if (artifact !== undefined) virtual.set(virtualKey(artifactPath), artifact)
const host = ts.createCompilerHost(options, true)
const read = host.readFile.bind(host)
const exists = host.fileExists.bind(host)
const hidden = (path: string): boolean => path.replaceAll('\\', '/').includes('/typert.remote-client.')
host.fileExists = path => virtual.has(virtualKey(path)) || !hidden(path) && exists(path)
host.readFile = path => virtual.get(virtualKey(path)) ?? (hidden(path) ? undefined : read(path))
host.getSourceFile = (path, languageVersion) => {
const text = host.readFile(path)
return text === undefined ? undefined : ts.createSourceFile(path, text, languageVersion, true)
}
const program = ts.createProgram([probePath], options, host)
expect(program.getSourceFile(protocolPath)).toBeDefined()
expect(program.getSourceFile(proxyPath)).toBeDefined()
expect(program.getSourceFiles().some(file => hidden(file.fileName))).toBe(false)
expect(program.getSourceFile(artifactPath) !== undefined).toBe(artifact !== undefined)
const diagnostics = ts.getPreEmitDiagnostics(program)
const formatted = ts.formatDiagnosticsWithColorAndContext(diagnostics, {
getCanonicalFileName: path => path,
getCurrentDirectory: () => root,
getNewLine: () => '\n',
})
expect(diagnostics.length, formatted).toBe(0)
}
describe('RemoteMock proxy types', { timeout: 60_000 }, () => {
it('keeps production closed while an empty generated map permits Mock calls and overrides', () => {
compile(`
import type { TypertRemoteNamespace, TypertClientRemote } from '@deepseek-ai/dsh-typert-protocol'
import type { MockedRemote } from '../src/remote-proxy.ts'
type IsAny<T> = 0 extends (1 & T) ? true : false
declare const local: MockedRemote
const loose: IsAny<MockedRemote> = true
local.fixture.echo(123, 'unbuilt')
local.fixture.echo.mockResolvedValueOnce({ arbitrary: 'answer' })
local.fixture.echo.mockImplementation((value: unknown, extra: unknown) => [value, extra])
declare const production: TypertClientRemote
declare const strict: TypertRemoteNamespace<'fixture'>
// @ts-expect-error -- Importing the Mock alias cannot add a production namespace.
production.fixture.echo('unbuilt')
// @ts-expect-error -- Production methods stay unavailable without generated declarations.
strict.echo('unbuilt')
void loose
`)
})
it('preserves generated arguments, results and native spy overrides', () => {
compile(`
import type { MockedRemote } from '../src/remote-proxy.ts'
import type {} from '@deepseek-ai/fixture/remote'
type IsAny<T> = 0 extends (1 & T) ? true : false
declare const local: MockedRemote
const spy = local.fixture.echo
spy.mockImplementation(value => value.length)
const argumentIsTyped: IsAny<Parameters<typeof spy>[0]> = false
const resultIsTyped: IsAny<ReturnType<typeof spy>> = false
const result: number = spy('typed')
spy.mockReturnValueOnce(2)
// @ts-expect-error -- Generated arguments remain checked through the native spy.
spy(123)
// @ts-expect-error -- Generated return values remain checked through native overrides.
spy.mockReturnValueOnce('wrong')
// @ts-expect-error -- Known namespaces do not gain arbitrary methods.
local.fixture.missing()
// @ts-expect-error -- A non-empty map does not gain arbitrary namespaces.
local.missing.echo('wrong')
void [argumentIsTyped, resultIsTyped, result]
`, fixtureDeclaration)
})
it('keeps partial and empty namespaces closed inside a non-empty map', () => {
compile(`
import type { TypertClientRemote, TypertRemoteNamespace } from '@deepseek-ai/dsh-typert-protocol'
import type { MockedRemote } from '../src/remote-proxy.ts'
import type {} from '@deepseek-ai/fixture/remote'
type PartialMap = { fixture: Pick<TypertRemoteNamespace<'fixture'>, 'echo'>; empty: {} }
declare const local: MockedRemote<PartialMap>
declare const production: TypertClientRemote
const known: number = local.fixture.echo('known')
// @ts-expect-error -- A partial namespace remains partial.
local.fixture.watch()
// @ts-expect-error -- Only an empty whole map enables the Mock fallback.
local.empty.echo()
// @ts-expect-error -- Local Mock types cannot open production namespace lookup.
production.notANamespace.echo()
void known
`, fixtureDeclaration)
})
// Ordinary built Client tsc checks these generated business types; Vitest itself remains build-free.
it('derives real settings mocks from the generated namespace map and native deep mapping', () => {
type Settings = RemoteMock['remote']['settings']
type Generated = TypertRemoteNamespace<'settings'>
expectTypeOf<RemoteMock['remote']>().toEqualTypeOf<MaybeMockedDeep<TypertRemoteNamespaceMap>>()
expectTypeOf<ReturnType<Settings['describe']>>().not.toBeAny()
expectTypeOf<ReturnType<Settings['update']>>().not.toBeAny()
expectTypeOf<Parameters<Settings['update']>>().toEqualTypeOf<Parameters<Generated['update']>>()
expectTypeOf<Parameters<Settings['update']>[1]>().not.toBeAny()
expectTypeOf<Parameters<Settings['mutate']>>().toEqualTypeOf<Parameters<Generated['mutate']>>()
expectTypeOf<[number, boolean, string]>().not.toExtend<Parameters<Settings['update']>>()
expectTypeOf<{ ok: true; value: number }>().not.toExtend<Awaited<ReturnType<Settings['update']>>>()
expectTypeOf<Settings>().not.toHaveProperty('missing')
})
it('retains the native/generated type dependencies only in the proxy declaration', () => {
const source = readFileSync(proxyPath, 'utf8')
const declaration = ts.transpileDeclaration(source, { fileName: proxyPath })
expect(declaration.diagnostics ?? []).toHaveLength(0)
expect(declaration.outputText).toContain('MaybeMockedDeep')
expect(declaration.outputText).toContain('TypertRemoteNamespaceMap')
const runtime = ts.transpileModule(source, {
fileName: proxyPath,
compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2024 },
})
expect(runtime.outputText).not.toContain('@deepseek-ai/dsh-typert-protocol')
expect(runtime.outputText).not.toContain('@vitest/spy')
})
})
/** Synthetic Remote methods exercise the mapping without copying business signatures. */
const fixtureDeclaration = `
import type { TypertRemoteNamespace } from '@deepseek-ai/dsh-typert-protocol'
declare module '@deepseek-ai/dsh-typert-protocol' {
interface TypertRemoteMap {
'fixture/echo': (value: string) => number
'fixture/watch': (after: number, signal?: AbortSignal) => AsyncIterable<number>
}
interface TypertRemoteNamespaceMap {
fixture: TypertRemoteNamespace<'fixture'>
}
}
`
@@ -0,0 +1,269 @@
/** Native namespace mocks shared by local callers and the Connection carrier. */
import { describe, expect, it, onTestFinished, vi } from 'vitest'
import { RemoteMock, frames, ok, openStream, type StreamHandle } from '../src/index.ts'
const describeValue = (hasDocument: boolean) => ok({ writable: true, hasDocument, namespaces: [] })
const baseline = { type: 'baseline' as const, value: { queues: {}, jobs: {}, projections: {} } }
async function drain(source: AsyncIterable<unknown>): Promise<unknown[]> {
const values: unknown[] = []
for await (const value of source) values.push(value)
return values
}
describe('RemoteMock.remote unary proxies', () => {
it('caches native namespace/method identities and leaves thenable/symbol probes inert', async () => {
const mock = RemoteMock.create()
const settings = mock.remote.settings
const describe = settings.describe
expect(mock.remote.settings).toBe(settings)
expect(settings.describe).toBe(describe)
expect(vi.isMockFunction(describe)).toBe(true)
expect(RemoteMock.create().remote.settings.describe).not.toBe(describe)
for (const value of [mock.remote, settings]) {
expect(Reflect.get(value, 'then')).toBeUndefined()
expect(Reflect.get(value, Symbol.iterator)).toBeUndefined()
expect(Reflect.get(value, Symbol.toStringTag)).toBeUndefined()
expect(await Promise.resolve(value)).toBe(value)
}
expect(describe).not.toHaveBeenCalled()
expect(mock.endpoints()).toEqual(['$events', 'settings/describe'])
expect(mock.log.calls()).toEqual([])
expect(mock.log.streams()).toEqual([])
expect(mock.log.unmatched()).toEqual([])
})
it('consumes native one-shot answers in FIFO order across direct and carrier calls, then uses the default handler', async () => {
const fallback = describeValue(false)
const local = describeValue(true)
const dispatched = { ...describeValue(false), value: { writable: false, hasDocument: false, namespaces: [] } }
const carrier = describeValue(true)
const handler = vi.fn(() => fallback)
const mock = RemoteMock.create().unary('settings/describe', handler)
const describe = mock.remote.settings.describe
describe.mockReturnValueOnce(Promise.resolve(local)).mockResolvedValueOnce(dispatched).mockResolvedValueOnce(carrier)
await expect(describe()).resolves.toBe(local)
await expect(mock.dispatch('settings/describe', [])).resolves.toBe(dispatched)
await expect(mock.rpc.call('/api', 'settings/describe', { args: [] })).resolves.toBe(carrier)
await expect(describe()).resolves.toBe(fallback)
expect(handler).toHaveBeenCalledOnce()
expect(describe.mock.calls).toEqual([[], [], [], []])
expect(mock.log.calls('settings/describe').map(call => [call.args, call.state, call.result]))
.toEqual([[[], 'answered', dispatched], [[], 'answered', carrier]])
mock.assertNoUnmatched()
})
it('records every business argument while the carrier keeps its signal out of positional args', async () => {
const mock = RemoteMock.create()
const mutate = mock.remote.settings.mutate
const result = ok({ ns: 'locale', schema: {}, value: { preference: 'en' }, applies: 'live' as const, secrets: [], revision: 8 })
const ops = [{ op: 'set' as const, path: ['preference'], value: 'en' }]
mutate.mockResolvedValue(result)
await expect(mutate('locale', ops, 7)).resolves.toBe(result)
await expect(mock.rpc.call('/api', 'settings/mutate', { args: ['locale', ops, 7] }, new AbortController().signal))
.resolves.toBe(result)
expect(mutate.mock.calls).toEqual([['locale', ops, 7], ['locale', ops, 7]])
expect(mutate.mock.calls[1]?.[1]).toBe(ops)
expect(mock.log.calls('settings/mutate').map(call => call.args)).toEqual([['locale', ops, 7]])
})
it('keeps native one-shot answers local to each mock when they load the same default table', async () => {
const fallback = describeValue(false)
const table = { unary: { 'settings/describe': fallback } }
const a = RemoteMock.create().load(table)
const b = RemoteMock.create().load(table)
const first = describeValue(true)
const second = { ...describeValue(true), value: { writable: false, hasDocument: true, namespaces: [] } }
a.remote.settings.describe.mockResolvedValueOnce(first)
b.remote.settings.describe.mockResolvedValueOnce(second)
await expect(a.dispatch('settings/describe', [])).resolves.toBe(first)
await expect(a.dispatch('settings/describe', [])).resolves.toBe(fallback)
expect(b.remote.settings.describe).not.toHaveBeenCalled()
await expect(b.rpc.call('/api', 'settings/describe', { args: [] })).resolves.toBe(second)
await expect(b.remote.settings.describe()).resolves.toBe(fallback)
})
it('keeps queued overrides on mockClear and restores the live default rule on mockReset', async () => {
const original = describeValue(false)
const updated = describeValue(true)
const override = { ...describeValue(false), value: { writable: false, hasDocument: false, namespaces: [] } }
const mock = RemoteMock.create().unary('settings/describe', original)
const describe = mock.remote.settings.describe
await describe()
describe.mockResolvedValueOnce(override)
describe.mockClear()
await expect(mock.rpc.call('/api', 'settings/describe', { args: [] })).resolves.toBe(override)
expect(describe.mock.calls).toEqual([[]])
describe.mockResolvedValue(override)
mock.load({ unary: { 'settings/describe': updated } })
await expect(mock.dispatch('settings/describe', [])).resolves.toBe(override)
describe.mockReset()
await expect(mock.dispatch('settings/describe', [])).resolves.toBe(updated)
expect(mock.remote.settings.describe).toBe(describe)
expect(describe.mock.calls).toEqual([[]])
expect(mock.log.calls('settings/describe').map(call => call.result)).toEqual([override, override, updated])
})
it('reports missing defaults after native one-shot answers are exhausted', async () => {
const mock = RemoteMock.create()
const describe = mock.remote.settings.describe
expect(mock.modeOf('settings/describe')).toBeUndefined()
const answer = describeValue(false)
describe.mockResolvedValueOnce(answer)
await expect(mock.rpc.call('/api', 'settings/describe', { args: [] })).resolves.toBe(answer)
expect(() => describe()).toThrow('no rule for settings/describe')
await expect(mock.dispatch('settings/describe', [])).rejects.toThrow('no rule for settings/describe')
await expect(mock.rpc.call('/api', 'settings/describe', { args: [] })).rejects.toThrow('no rule for settings/describe')
expect(describe).toHaveBeenCalledTimes(4)
expect(mock.log.calls('settings/describe').map(call => [call.state, call.result])).toEqual([['answered', answer]])
expect(mock.log.unmatched()).toEqual(Array.from({ length: 3 }, () => ({ endpoint: 'settings/describe', mode: 'unary' })))
expect(() => { mock.assertNoUnmatched() }).toThrow('3 unmatched request(s)')
})
it('records native rejected answers as failed carrier calls without unmatched entries', async () => {
const mock = RemoteMock.create()
const failure = new Error('mock service unavailable')
const describe = mock.remote.settings.describe.mockRejectedValueOnce(failure)
await expect(mock.rpc.call('/api', 'settings/describe', { args: [] })).rejects.toBe(failure)
expect(describe.mock.calls).toEqual([[]])
expect(mock.log.calls('settings/describe').map(call => [call.state, call.result])).toEqual([['failed', failure]])
mock.assertNoUnmatched()
})
it('preserves synchronous native override failures in the async carrier log', async () => {
const mock = RemoteMock.create()
const failure = new Error('handler threw')
const describe = mock.remote.settings.describe.mockImplementation(() => { throw failure })
await expect(mock.dispatch('settings/describe', [])).rejects.toBe(failure)
expect(describe).toHaveBeenCalledOnce()
expect(mock.log.calls('settings/describe').map(call => [call.state, call.result])).toEqual([['failed', failure]])
mock.assertNoUnmatched()
})
})
describe('RemoteMock.remote stream proxies', () => {
it('selects the cached spy for the current mode and preserves its identity across script replacement', async () => {
const mock = RemoteMock.create()
const session = mock.remote.session
const unary = session.control
expect(() => unary()).toThrow('no rule for session/control')
expect(mock.log.unmatched()).toEqual([{ endpoint: 'session/control', mode: 'unary' }])
mock.stream('session/control', frames([baseline])).stream('session/control')
const stream = session.control
expect(mock.remote.session).toBe(session)
expect(stream).not.toBe(unary)
expect(vi.isMockFunction(stream)).toBe(true)
expect(session.control).toBe(stream)
const opened = stream()
mock.stream('session/control', frames([]))
expect(session.control).toBe(stream)
await expect(drain(opened)).resolves.toEqual([baseline])
await expect(drain(stream())).resolves.toEqual([])
expect(unary).toHaveBeenCalledOnce()
expect(stream).toHaveBeenCalledTimes(2)
expect(mock.log.streams('session/control').map(entry => entry.state)).toEqual(['ended', 'ended'])
})
it('shares stream spies with the carrier, preserves its args, and forwards each caller signal', async () => {
const signals: AbortSignal[] = []
const script = vi.fn((_args: readonly unknown[], stream: StreamHandle) => {
signals.push(stream.signal)
stream.push(baseline)
})
const mock = RemoteMock.create().stream('session/control', script)
const localController = new AbortController()
const wireController = new AbortController()
onTestFinished(async () => {
localController.abort()
wireController.abort()
await mock.streams.drained('session/control')
})
const control = mock.remote.session.control
const request = { after: 7 }
const local = control(localController.signal)[Symbol.asyncIterator]()
const wire = mock.rpc.open!('/api', 'session/control', { args: [request] }, wireController.signal)[Symbol.asyncIterator]()
await expect(local.next()).resolves.toEqual({ value: baseline, done: false })
await expect(wire.next()).resolves.toEqual({ value: baseline, done: false })
expect(control.mock.calls).toEqual([[localController.signal], [request, wireController.signal]])
expect(script.mock.calls.map(([args]) => args)).toEqual([[], [request]])
expect(signals).toEqual([localController.signal, wireController.signal])
expect(mock.log.streams('session/control').map(entry => entry.args)).toEqual([[], [request]])
const waiting = local.next()
localController.abort()
await expect(waiting).resolves.toEqual({ value: undefined, done: true })
await wire.return!()
await mock.streams.drained('session/control')
expect(mock.streams.push('session/control', baseline)).toBe(0)
expect(mock.log.streams('session/control').map(entry => entry.state)).toEqual(['cancelled', 'cancelled'])
})
it('provides a cancellable default signal when a local stream call omits one', async () => {
let openedSignal: AbortSignal | undefined
const mock = RemoteMock.create().stream('session/control', (_args, stream) => { openedSignal = stream.signal })
const control = mock.remote.session.control
const reader = control()[Symbol.asyncIterator]()
onTestFinished(async () => { await reader.return!() })
expect(openedSignal).toBeInstanceOf(AbortSignal)
expect(openedSignal?.aborted).toBe(false)
const waiting = reader.next()
await reader.return!()
await expect(waiting).resolves.toEqual({ value: undefined, done: true })
expect(control.mock.calls).toEqual([[]])
expect(mock.log.streams('session/control')[0]?.state).toBe('cancelled')
})
it('excludes unread native stream overrides from controlled streams and restores the script on reset', async () => {
const mock = RemoteMock.create().stream('session/control', frames([baseline]))
const control = mock.remote.session.control
const signal = new AbortController().signal
let consumed = false
async function* response() {
consumed = true
yield baseline
}
const native = response()
onTestFinished(async () => { await native.return(undefined) })
control.mockReturnValueOnce(native)
const overridden = mock.open('session/control', [], signal)
expect(overridden).toBe(native)
await expect(mock.streams.drained('session/control')).resolves.toBeUndefined()
expect(consumed).toBe(false)
expect(control.mock.calls).toEqual([[signal]])
expect(mock.log.streams('session/control')).toHaveLength(0)
await expect(drain(overridden)).resolves.toEqual([baseline])
expect(consumed).toBe(true)
control.mockReset()
await expect(drain(mock.rpc.open!('/api', 'session/control', { args: [] }, signal))).resolves.toEqual([baseline])
await expect(mock.streams.opened('session/control', 1)).resolves.toBeUndefined()
await expect(mock.streams.drained('session/control')).resolves.toBeUndefined()
expect(mock.remote.session.control).toBe(control)
expect(control.mock.calls).toEqual([[signal]])
expect(mock.log.streams('session/control').map(entry => [entry.state, entry.pushed])).toEqual([['ended', 1]])
mock.assertNoUnmatched()
})
it('requires an explicit stream script and keeps live failures observable by the consumer', async () => {
const mock = RemoteMock.create().stream('session/control')
expect(() => mock.remote.session.control()).toThrow('no rule for session/control')
expect(mock.log.unmatched()).toEqual([{ endpoint: 'session/control', mode: 'stream' }])
mock.stream('session/control', openStream())
const controller = new AbortController()
onTestFinished(() => { controller.abort() })
const reader = mock.remote.session.control(controller.signal)[Symbol.asyncIterator]()
const failure = new Error('stream disconnected')
const pending = expect(reader.next()).rejects.toBe(failure)
expect(mock.streams.fail('session/control', failure)).toBe(1)
await pending
await mock.streams.drained('session/control')
expect(mock.log.streams('session/control')[0]?.state).toBe('failed')
expect(mock.streams.push('session/control', baseline)).toBe(0)
})
})
@@ -0,0 +1,43 @@
/** The Connection carrier face: payload forms, streams, unmatched rejections, and abort. */
import { describe, expect, it } from 'vitest'
import { RemoteMock, ok, openStream } from '../src/index.ts'
const idle = (): AbortSignal => new AbortController().signal
describe('RemoteMock.rpc', () => {
it('calls and opens by endpoint, taking the proxies\' positional args or the Gateway\'s single object', async () => {
const mock = RemoteMock.create()
.unary('session/list', ok({ items: [] }))
.unary('$events/result', (...args) => ok(args))
.stream('session/control', openStream([{ type: 'baseline' }]))
await expect(mock.rpc.call('/api', 'session/list', { args: [{}] })).resolves.toEqual({ ok: true, value: { items: [] } })
await expect(mock.rpc.call('/api', '$events/result', { args: { clientId: 'c' } }, idle())).resolves.toEqual({ ok: true, value: [{ clientId: 'c' }] })
const stream = mock.rpc.open!('/api', 'session/control', { args: [{ since: 1 }] }, idle())[Symbol.asyncIterator]()
await expect(stream.next()).resolves.toEqual({ value: { type: 'baseline' }, done: false })
expect(mock.log.calls().map(call => [call.endpoint, call.args])).toEqual([['session/list', [{}]], ['$events/result', [{ clientId: 'c' }]]])
expect(mock.log.streams('session/control').map(open => open.args)).toEqual([[{ since: 1 }]])
})
it('rejects malformed payloads and unmatched endpoints, logging the miss', async () => {
const mock = RemoteMock.create()
await expect(mock.rpc.call('/api', 'a/b', 'bare')).rejects.toThrow('remote-mock: payload of a/b must be { args: unknown[] | object }')
await expect(mock.rpc.call('/api', 'a/b', { args: 'x' })).rejects.toThrow('must be { args: unknown[] | object }')
expect(() => mock.rpc.open!('/api', 'a/b', null, idle())).toThrow('payload of a/b must be { args: unknown[] | object }')
await expect(mock.rpc.call('/api', 'a/b', { args: [] })).rejects.toThrow('remote-mock: no rule for a/b')
expect(mock.log.unmatched()).toEqual([{ endpoint: 'a/b', mode: 'unary' }])
})
it('rejects an aborted call: immediately when already aborted, with the reason while the rule is pending', async () => {
const mock = RemoteMock.create().unary('slow/call', () => new Promise(() => {}))
const pending = new AbortController()
const request = mock.rpc.call('/api', 'slow/call', { args: [] }, pending.signal)
pending.abort(new Error('caller left'))
await expect(request).rejects.toThrow('caller left')
const already = new AbortController()
already.abort('not an error')
await expect(mock.rpc.call('/api', 'slow/call', { args: [] }, already.signal)).rejects.toThrow('remote-mock: call aborted')
await expect(mock.rpc.call('/api', 'session/list', { args: [] }, idle())).rejects.toThrow('no rule for session/list')
// The abort rejects the caller; the never-settling rules stay pending in the log, and the miss is not a call.
expect(mock.log.calls().map(call => [call.endpoint, call.state])).toEqual([['slow/call', 'pending'], ['slow/call', 'pending']])
})
})
@@ -0,0 +1,183 @@
/** Stream scripts, live stream control, cancellation, and the built-in `$events` opening. */
import { describe, expect, it } from 'vitest'
import { RemoteMock, frames, openStream } from '../src/index.ts'
const idle = (): AbortSignal => new AbortController().signal
async function drain(source: AsyncIterable<unknown>): Promise<unknown[]> {
const items: unknown[] = []
for await (const item of source) items.push(item)
return items
}
async function take(source: AsyncIterable<unknown>, count: number): Promise<unknown[]> {
const items: unknown[] = []
for await (const item of source) {
items.push(item)
if (items.length === count) break
}
return items
}
describe('RemoteMock streams', () => {
it('yields frames() then ends, logging the open, and throws on an unmatched endpoint', async () => {
const mock = RemoteMock.create().stream('s/f', frames([{ n: 1 }, { n: 2 }]))
await expect(drain(mock.open('s/f', [{ id: 'a' }], idle()))).resolves.toEqual([{ n: 1 }, { n: 2 }])
expect(mock.log.streams('s/f')).toEqual([{ endpoint: 's/f', args: [{ id: 'a' }], state: 'ended', pushed: 2, seq: 1 }])
expect(mock.log.streams('other')).toEqual([])
expect(() => mock.open('s/g', [], idle())).toThrow('remote-mock: no rule for s/g; registered: $events, s/f')
expect(mock.log.unmatched()).toEqual([{ endpoint: 's/g', mode: 'stream' }])
})
it('keeps openStream() open for pushes, filters by open args, and reports delivery counts', async () => {
const mock = RemoteMock.create().stream('s/f', openStream(['hello']))
const sessionOf = ([request]: readonly unknown[]): string => (request as { sessionId: string }).sessionId
const a = mock.open('s/f', [{ sessionId: 'a' }], idle())
const b = mock.open('s/f', [{ sessionId: 'b' }], idle())
expect(mock.streams.push('s/f', 'only-b', open => sessionOf(open) === 'b')).toBe(1)
expect(mock.streams.push('s/f', 'both')).toBe(2)
expect(mock.streams.fail('s/f', new Error('gone'), open => sessionOf(open) === 'a')).toBe(1)
expect(mock.streams.end('s/f')).toBe(1)
await expect(drain(b)).resolves.toEqual(['hello', 'only-b', 'both'])
await expect(drain(a)).rejects.toThrow('gone')
expect(mock.log.streams().map(entry => [entry.state, entry.pushed])).toEqual([['failed', 2], ['ended', 3]])
expect(mock.streams.push('s/f', 'late')).toBe(0)
})
it('resolves a pending read on push, end, or fail; a second concurrent read is a bug; settling twice is a no-op', async () => {
const mock = RemoteMock.create().stream('s/f', openStream())
const first = mock.open('s/f', [], idle())[Symbol.asyncIterator]()
const pending = first.next()
await expect(first.next()).rejects.toThrow('remote-mock: s/f stream has one consumer')
mock.streams.push('s/f', 'x')
await expect(pending).resolves.toEqual({ value: 'x', done: false })
const ending = first.next()
mock.streams.end('s/f')
await expect(ending).resolves.toEqual({ value: undefined, done: true })
const second = mock.open('s/f', [], idle())[Symbol.asyncIterator]()
const failing = second.next()
mock.streams.fail('s/f', new Error('boom'))
await expect(failing).rejects.toThrow('boom')
await expect(second.next()).rejects.toThrow('boom')
expect(mock.streams.end('s/f')).toBe(0)
const settled = new AbortController()
const ended = mock.open('s/f', [], settled.signal)
mock.streams.end('s/f')
settled.abort()
await expect(drain(ended)).resolves.toEqual([])
expect(mock.log.streams().map(entry => entry.state)).toEqual(['ended', 'failed', 'ended'])
})
it('lets a consumer that returns after the producer ended drop the rest, so drained() settles', async () => {
const mock = RemoteMock.create().stream('s/f', frames(['a', 'b']))
const reader = mock.open('s/f', [], idle())[Symbol.asyncIterator]()
await expect(reader.next()).resolves.toEqual({ value: 'a', done: false })
const pending = mock.streams.drained('s/f')
await expect(reader.return!()).resolves.toEqual({ value: undefined, done: true })
await expect(pending).resolves.toBeUndefined()
expect(mock.log.streams('s/f')).toEqual([{ endpoint: 's/f', args: [], state: 'ended', pushed: 2, seq: 1 }])
expect(mock.streams.push('s/f', 'late')).toBe(0)
})
it('treats consumer abort or return() as cancellation and drops later pushes', async () => {
const mock = RemoteMock.create().stream('s/f', openStream(['queued']))
const controller = new AbortController()
const cancelled = mock.open('s/f', [], controller.signal)[Symbol.asyncIterator]()
await expect(cancelled.next()).resolves.toEqual({ value: 'queued', done: false })
const waiting = cancelled.next()
controller.abort()
await expect(waiting).resolves.toEqual({ value: undefined, done: true })
expect(mock.streams.push('s/f', 'after')).toBe(0)
const aborted = new AbortController()
aborted.abort()
await expect(drain(mock.open('s/f', [], aborted.signal))).resolves.toEqual([])
await expect(take(mock.open('s/f', [], idle()), 1)).resolves.toEqual(['queued'])
expect(mock.log.streams().map(entry => entry.state)).toEqual(['cancelled', 'cancelled', 'cancelled'])
})
it('runs script functions with the open args and fails the stream when they throw or reject', async () => {
const mock = RemoteMock.create()
.stream('s/echo', (args, stream) => {
stream.push(args)
stream.end()
stream.end()
stream.fail(new Error('too late'))
})
.stream('s/async', async (_args, stream) => {
await Promise.resolve()
stream.push('later')
stream.end()
})
.stream('s/throws', () => { throw new Error('sync boom') })
.stream('s/rejects', () => Promise.reject(new Error('async boom')))
.stream('s/odd', () => { throw 'string reason' })
await expect(drain(mock.open('s/echo', [{ a: 1 }], idle()))).resolves.toEqual([[{ a: 1 }]])
await expect(drain(mock.open('s/async', [], idle()))).resolves.toEqual(['later'])
await expect(drain(mock.open('s/throws', [], idle()))).rejects.toThrow('sync boom')
await expect(drain(mock.open('s/rejects', [], idle()))).rejects.toThrow('async boom')
await expect(drain(mock.open('s/odd', [], idle()))).rejects.toThrow('string reason')
})
it('drained() settles once the consumer has pulled every push and waits again, or the stream closed', async () => {
const mock = RemoteMock.create().stream('s/f', openStream(['first']))
await expect(mock.streams.drained('s/f')).resolves.toBeUndefined() // nothing open: nothing to drain
const reader = mock.open('s/f', [], idle())[Symbol.asyncIterator]()
const unread = mock.streams.drained('s/f')
let settled = false
void unread.then(() => { settled = true })
await Promise.resolve()
expect(settled).toBe(false) // 'first' is queued and nobody has pulled it
await expect(reader.next()).resolves.toEqual({ value: 'first', done: false })
await Promise.resolve()
expect(settled).toBe(false) // pulled, but the consumer is not waiting for more yet
const waiting = reader.next()
await expect(unread).resolves.toBeUndefined()
mock.streams.push('s/f', 'second')
await expect(waiting).resolves.toEqual({ value: 'second', done: false })
mock.streams.push('s/f', 'third')
mock.streams.end('s/f')
const ended = mock.streams.drained('s/f')
let endedSettled = false
void ended.then(() => { endedSettled = true })
await Promise.resolve()
expect(endedSettled).toBe(false) // ended, but 'third' is still queued
await expect(reader.next()).resolves.toEqual({ value: 'third', done: false })
await expect(ended).resolves.toBeUndefined() // queue empty: a settled stream counts as drained
await expect(reader.next()).resolves.toEqual({ value: undefined, done: true })
await expect(reader.return!()).resolves.toEqual({ value: undefined, done: true }) // returning a settled stream changes nothing
const second = mock.open('s/f', [], idle())[Symbol.asyncIterator]()
await expect(second.next()).resolves.toEqual({ value: 'first', done: false })
const parked = second.next()
await expect(mock.streams.drained('s/f')).resolves.toBeUndefined() // its consumer is waiting
const controller = new AbortController()
const third = mock.open('s/f', [], controller.signal)
const cancelling = mock.streams.drained('s/f', () => true) // third holds 'first' that nobody has read
controller.abort()
await expect(cancelling).resolves.toBeUndefined() // cancellation discards the queue: closed counts as drained
await expect(drain(third)).resolves.toEqual([])
mock.streams.end('s/f')
await expect(parked).resolves.toEqual({ value: undefined, done: true })
})
it('declares a stream without a script: modeOf answers stream and an open is a stream miss', () => {
const mock = RemoteMock.create().load({ streams: ['s/declared'] })
expect(mock.modeOf('s/declared')).toBe('stream')
expect(mock.endpoints()).toEqual(['$events', 's/declared'])
expect(() => mock.open('s/declared', [], idle())).toThrow('remote-mock: no rule for s/declared; registered: $events, s/declared')
expect(mock.log.unmatched()).toEqual([{ endpoint: 's/declared', mode: 'stream' }])
mock.stream('s/declared', frames(['now']))
expect(mock.modeOf('s/declared')).toBe('stream')
})
it('waits for opens with opened(), and answers $events with one ready frame per generation', async () => {
const mock = RemoteMock.create({ host: { home: '/home/me' } })
const second = mock.streams.opened('$events', 2)
const first = mock.open('$events', [{}], idle())
await expect(take(first, 1)).resolves.toEqual([{ type: 'ready', clientId: 'mock-client-1', host: { home: '/home/me' } }])
const again = mock.open('$events', [{}], idle())
await expect(second).resolves.toBeUndefined()
await expect(mock.streams.opened('$events', 1)).resolves.toBeUndefined()
await expect(take(again, 1)).resolves.toEqual([{ type: 'ready', clientId: 'mock-client-2', host: { home: '/home/me' } }])
expect(RemoteMock.create().modeOf('$events')).toBe('stream')
})
})
@@ -0,0 +1,107 @@
/** Registered unary defaults and the carrier's request and outcome log. */
import { describe, expect, expectTypeOf, it, vi } from 'vitest'
import { RemoteMock, ok, openStream, type RemoteTable, type UnaryRuleFn } from '../src/index.ts'
describe('RemoteMock unary answers', () => {
it('answers a fixed value verbatim and logs args and result with a global seq', async () => {
const mock = RemoteMock.create().unary('session/list', ok({ items: [] }))
await expect(mock.dispatch('session/list', [{}])).resolves.toEqual({ ok: true, value: { items: [] } })
expect(mock.log.calls()).toEqual([{ endpoint: 'session/list', args: [{}], state: 'answered', result: { ok: true, value: { items: [] } }, seq: 1 }])
expect(mock.log.calls('other/x')).toEqual([])
expect(mock.modeOf('session/list')).toBe('unary')
expect(mock.modeOf('other/x')).toBeUndefined()
expect(mock.endpoints()).toEqual(['$events', 'session/list'])
})
it('replaces defaults on registration and table reload, including an explicit undefined answer', async () => {
const table: RemoteTable = { unary: { 'a/b': 'table' } }
const mock = RemoteMock.create().load(table)
await expect(mock.dispatch('a/b', [])).resolves.toBe('table')
mock.unary('a/b', 'replacement')
await expect(mock.dispatch('a/b', [])).resolves.toBe('replacement')
mock.load(table)
await expect(mock.dispatch('a/b', [])).resolves.toBe('table')
mock.load({ unary: { 'a/b': undefined } })
await expect(mock.dispatch('a/b', [])).resolves.toBeUndefined()
expect(mock.modeOf('a/b')).toBe('unary')
expect(mock.endpoints()).toEqual(['$events', 'a/b'])
mock.assertNoUnmatched()
})
it('passes positional arguments to native spies and leaves scenario state with the handler', async () => {
let n = 0
const handler = vi.fn((request: { id: number }, extra?: string) => {
expectTypeOf(request).toEqualTypeOf<{ id: number }>()
expectTypeOf(extra).toEqualTypeOf<string | undefined>()
return ok(++n)
})
const mock = RemoteMock.create()
.unary('a/b', handler)
.unary('a/c', () => Promise.resolve(n))
await expect(mock.dispatch('a/b', [{ id: 1 }])).resolves.toEqual(ok(1))
await expect(mock.dispatch('a/b', [{ id: 2 }, 'more'])).resolves.toEqual(ok(2))
await expect(mock.dispatch('a/c', [])).resolves.toBe(2)
expect(handler).toHaveBeenCalledTimes(2)
expect(handler).toHaveBeenNthCalledWith(1, { id: 1 })
expect(handler).toHaveBeenNthCalledWith(2, { id: 2 }, 'more')
})
it('keeps raw answers and typed handlers in tables without wrapping their arguments', async () => {
const handler: UnaryRuleFn<[namespace: string, value: number]> = (namespace, value) => ok({ namespace, value })
const mock = RemoteMock.create().load({ unary: { 'a/read': handler, 'a/void': undefined, 'a/null': null } })
await expect(mock.dispatch('a/read', ['settings', 2])).resolves.toEqual(ok({ namespace: 'settings', value: 2 }))
await expect(mock.dispatch('a/void', [])).resolves.toBeUndefined()
await expect(mock.dispatch('a/null', [])).resolves.toBeNull()
mock.unary('a/inferred', (value) => {
expectTypeOf(value).toEqualTypeOf<unknown>()
return value
})
await expect(mock.dispatch('a/inferred', [3])).resolves.toBe(3)
})
it('logs a call when its rule is selected and settles it with the rule: pending, answered, or failed', async () => {
const gate = Promise.withResolvers<unknown>()
const mock = RemoteMock.create()
.unary('a/slow', () => gate.promise)
.unary('a/throws', () => { throw new Error('sync boom') })
.unary('a/rejects', () => Promise.reject(new Error('async boom')))
const slow = mock.dispatch('a/slow', [1])
expect(mock.log.calls('a/slow')).toEqual([{ endpoint: 'a/slow', args: [1], state: 'pending', result: undefined, seq: 1 }])
gate.resolve('late')
await expect(slow).resolves.toBe('late')
expect(mock.log.calls('a/slow')[0]).toMatchObject({ state: 'answered', result: 'late' })
await expect(mock.dispatch('a/throws', [])).rejects.toThrow('sync boom')
await expect(mock.dispatch('a/rejects', [])).rejects.toThrow('async boom')
expect(mock.log.calls().map(call => [call.endpoint, call.state, call.result instanceof Error ? call.result.message : call.result]))
.toEqual([['a/slow', 'answered', 'late'], ['a/throws', 'failed', 'sync boom'], ['a/rejects', 'failed', 'async boom']])
expect(mock.log.unmatched()).toEqual([])
})
it('requests() lists the first arg of calls and opens in order, per endpoint or across all but the Gateway\'s own', async () => {
const mock = RemoteMock.create()
.unary('session/page', ok(null))
.stream('session/follow', openStream([]))
const signal = new AbortController().signal
await mock.dispatch('session/page', [{ throughSeq: 5 }])
mock.open('session/follow', [{ address: 'a' }], signal)
mock.open('$events', [{}], signal)
await mock.dispatch('session/page', [{ throughSeq: 9 }, 'extra'])
expect((mock.log.requests('session/page') as { throughSeq: number }[]).map(request => request.throughSeq)).toEqual([5, 9])
expect(mock.log.requests('session/follow')).toEqual([{ address: 'a' }])
expect(mock.log.requests('$events')).toEqual([{}])
expect(mock.log.requests()).toEqual([{ throughSeq: 5 }, { address: 'a' }, { throughSeq: 9 }])
expect(RemoteMock.create().log.requests()).toEqual([])
})
it('loads a whole table', () => {
const mock = RemoteMock.create().load({
unary: { 'session/list': ok({ items: [] }), 'a/b': 1 },
stream: { 'session/control': openStream([{ type: 'baseline' }]) },
})
expect(mock.modeOf('session/list')).toBe('unary')
expect(mock.modeOf('a/b')).toBe('unary')
expect(mock.modeOf('session/control')).toBe('stream')
expect(() => RemoteMock.create().load({})).not.toThrow()
RemoteMock.create().assertNoUnmatched()
})
})
@@ -0,0 +1,21 @@
{
"extends": "../../../tsconfig.base.client.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib/types"
},
"include": [
"src"
],
"references": [
{
"path": "../../typert/protocol"
},
{
"path": "../../../vendor/cordis"
},
{
"path": "../../client/connection/tsconfig.client.json"
}
]
}
@@ -0,0 +1,6 @@
import { clientLibrary } from '../../client/tsdown.client.ts'
export default clientLibrary(
'@deepseek-ai/dsh-remote-mock',
['lib/types/index.js'],
)
+22
View File
@@ -10113,6 +10113,28 @@ importers:
specifier: workspace:^
version: link:../../core/session
packages/test-support/remote-mock:
dependencies:
'@vitest/spy':
specifier: ^4.1.8
version: 4.1.8
devDependencies:
'@deepseek-ai/cordis':
specifier: workspace:^
version: link:../../../vendor/cordis
'@deepseek-ai/dsh-api-settings-controller':
specifier: workspace:^
version: link:../../api/settings-controller
'@deepseek-ai/dsh-client-connection':
specifier: workspace:^
version: link:../../client/connection
'@deepseek-ai/dsh-typert-protocol':
specifier: workspace:^
version: link:../../typert/protocol
typescript:
specifier: ^6.0.3
version: 6.0.3
packages/test-support/session-snapshot:
dependencies:
'@agentclientprotocol/sdk':
+1
View File
@@ -336,6 +336,7 @@
"@deepseek-ai/dsh-plan-mode/invariant": ["./packages/plan/plan-mode/src/invariant.ts"],
"@deepseek-ai/dsh-plugin-package-inventory-deepseek": ["./packages/llm/plugin-package-inventory-deepseek/src"],
"@deepseek-ai/dsh-pwsh-sandbox": ["./packages/shell/pwsh-sandbox/src"],
"@deepseek-ai/dsh-remote-mock": ["./packages/test-support/remote-mock/src"],
"@deepseek-ai/dsh-repeat-tool-reminder": ["./packages/guard/repeat-tool-reminder/src"],
"@deepseek-ai/dsh-sandbox": ["./packages/sandbox/sandbox/src"],
"@deepseek-ai/dsh-sandbox-local": ["./packages/sandbox/sandbox-local/src"],
+1
View File
@@ -68,6 +68,7 @@
{ "path": "./packages/extensions/cordis-client-runner" },
{ "path": "./packages/extensions/ui-cordis" },
{ "path": "./packages/test-support/client-runtime" },
{ "path": "./packages/test-support/remote-mock" },
{ "path": "./packages/client/ui-layout" },
{ "path": "./packages/client/ui-sidebar" },
{ "path": "./packages/client/ui-brand-official" },