This commit is contained in:
imccyu
2026-08-13 01:33:31 +08:00
parent 978e573605
commit a7d4cd8e1b
31 changed files with 493 additions and 1243 deletions
@@ -16,8 +16,8 @@ const icons = Object.fromEntries(
const iconNames = Object.keys(icons)
describe('ic_ds_ icon set', () => {
it('exports the full icon set (46 deepsuite + 20 figma extracts + three product glyphs outside those sets)', () => {
expect(iconNames.length).toBe(69)
it('exports the full icon set (46 deepsuite + 20 figma extracts + four product glyphs outside those sets)', () => {
expect(iconNames.length).toBe(70)
})
it.each(iconNames)('%s renders an svg with currentColor fills and no hardcoded palette', (name) => {
@@ -3,6 +3,7 @@
exports[`sidebar shell snapshots > renders the collapsed rail after the crossfade settles, in place 1`] = `
<div
data-slot="sidebar"
style="display: contents;"
>
<div
class="root collapsed railIn quietBars"
@@ -52,10 +53,32 @@ exports[`sidebar shell snapshots > renders the collapsed rail after the crossfad
</button>
<div
class="regionArea"
/>
>
<div
data-slot="sidebar.workspaces"
style="display: contents;"
/>
</div>
<div
class="footArea"
/>
>
<div
class="footerActions"
>
<div
data-slot="sidebar.footer.action"
style="display: contents;"
/>
</div>
<div
class="settingsArea"
>
<div
data-slot="sidebar.settings"
style="display: contents;"
/>
</div>
</div>
</div>
</div>
`;
@@ -63,6 +86,7 @@ exports[`sidebar shell snapshots > renders the collapsed rail after the crossfad
exports[`sidebar shell snapshots > renders the expanded column (wordmark, capsule, empty holes) 1`] = `
<div
data-slot="sidebar"
style="display: contents;"
>
<div
class="root quietBars"
@@ -122,10 +146,32 @@ exports[`sidebar shell snapshots > renders the expanded column (wordmark, capsul
</button>
<div
class="regionArea"
/>
>
<div
data-slot="sidebar.workspaces"
style="display: contents;"
/>
</div>
<div
class="footArea"
/>
>
<div
class="footerActions"
>
<div
data-slot="sidebar.footer.action"
style="display: contents;"
/>
</div>
<div
class="settingsArea"
>
<div
data-slot="sidebar.settings"
style="display: contents;"
/>
</div>
</div>
</div>
</div>
`;
@@ -133,6 +179,7 @@ exports[`sidebar shell snapshots > renders the expanded column (wordmark, capsul
exports[`sidebar shell snapshots > renders the expanded column in the default locale (zh, no setLocale) 1`] = `
<div
data-slot="sidebar"
style="display: contents;"
>
<div
class="root quietBars"
@@ -192,10 +239,32 @@ exports[`sidebar shell snapshots > renders the expanded column in the default lo
</button>
<div
class="regionArea"
/>
>
<div
data-slot="sidebar.workspaces"
style="display: contents;"
/>
</div>
<div
class="footArea"
/>
>
<div
class="footerActions"
>
<div
data-slot="sidebar.footer.action"
style="display: contents;"
/>
</div>
<div
class="settingsArea"
>
<div
data-slot="sidebar.settings"
style="display: contents;"
/>
</div>
</div>
</div>
</div>
`;
@@ -2,7 +2,11 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import { stubSettingsScope, type StubSettingsScope } from '@deepseek-ai/dsh-client-test-runtime'
import type { ThemeSettings, ThemeSnapshot } from '@deepseek-ai/dsh-client-ui-theme/client'
import type {
ThemeSettings,
ThemeSnapshot,
ThemeTokenOverrides,
} from '@deepseek-ai/dsh-client-ui-theme/client'
import { ThemeRuntime } from '@deepseek-ai/dsh-client-ui-theme/client'
const make = (host = stubSettingsScope<ThemeSettings>()): {
@@ -103,6 +107,91 @@ describe('ThemeRuntime', () => {
expect(events.map(e => e.revision)).toEqual([1, 2, 3, 4])
})
it('stacks reversible token overrides in call order and selects the active palette value', () => {
const { theme } = make()
const firstTokens: ThemeTokenOverrides = {
'--shared': { light: 'first-light', dark: 'first-dark' },
'--first': { light: 'first-only-light', dark: 'first-only-dark' },
}
const disposeFirst = theme.overrideTokens('first', firstTokens)
firstTokens['--shared']!.light = 'mutated-after-call'
const disposeSecond = theme.overrideTokens('second', {
'--shared': { light: 'second-light', dark: 'second-dark' },
})
expect(theme.getTheme().active.tokens).toMatchObject({
'--first': 'first-only-light',
'--shared': 'second-light',
})
theme.setTheme('dark')
expect(theme.getTheme().active.tokens).toMatchObject({
'--first': 'first-only-dark',
'--shared': 'second-dark',
})
disposeSecond()
expect(theme.getTheme().active.tokens['--shared']).toBe('first-dark')
disposeFirst()
expect(theme.getTheme().active.tokens['--shared']).toBeUndefined()
})
it('replacing one source leaves its stale disposer harmless', () => {
const { theme, events } = make()
const stale = theme.overrideTokens('package', {
'--old': { light: 'old-light', dark: 'old-dark' },
})
const current = theme.overrideTokens('package', {
'--new': { light: 'new-light', dark: 'new-dark' },
})
stale()
expect(theme.getTheme().active.tokens).toEqual({ '--new': 'new-light' })
current()
current()
expect(theme.getTheme().active.tokens).toEqual({})
expect(events).toHaveLength(3)
})
it('exports sorted built-in, registered, and override-only token descriptions as copies', () => {
const { theme } = make()
theme.register({
id: 'custom',
colorScheme: 'light',
tokens: {
'--dsw-alias-bg-base': 'duplicate-built-in',
'--registered': 'registered',
},
})
theme.overrideTokens('package', {
'--registered': { light: 'duplicate-registered', dark: 'duplicate-registered' },
semanticAccent: { light: 'pink', dark: 'red' },
})
const tokens = theme.exportInspectTokens()
expect(tokens.map(token => token.name)).toEqual([...tokens.map(token => token.name)].sort())
expect(tokens.find(token => token.name === '--registered')).toMatchObject({
valueType: 'CSS value',
cssVariable: '--registered',
})
const semantic = tokens.find(token => token.name === 'semanticAccent')
expect(semantic).toMatchObject({ valueType: 'CSS value' })
expect(semantic).not.toHaveProperty('cssVariable')
expect(tokens.filter(token => token.name === '--dsw-alias-bg-base')).toHaveLength(1)
tokens[0]!.description = 'caller mutation'
expect(theme.exportInspectTokens()[0]!.description).not.toBe('caller mutation')
})
it('rejects every malformed token override value with a teaching error', () => {
const { theme } = make()
const override = (value: unknown): void => {
theme.overrideTokens('package', { '--bad': value } as unknown as ThemeTokenOverrides)
}
expect(() => { override('red') }).toThrow(/bare string.*light.*dark/)
for (const value of [1, null, {}, { light: 1, dark: 'dark' }, { light: 'light' }]) {
expect(() => { override(value) }).toThrow(/must map to a \{ light, dark \} pair/)
}
})
it('context dispose releases the scope subscription', async () => {
const { ctx, host } = make()
expect(host.listenerCount()).toBe(1)
@@ -25,7 +25,7 @@ describe('gen-tool-catalog collectToolCatalog', () => {
it('boots every shipped tool package and harvests its model-facing schemas', async () => {
const catalog = await collectToolCatalog()
const names = catalog.flatMap(entry => entry.schemas.map(s => s.name)).sort()
expect(names).toEqual(['ask_user_question', 'bash', 'bash', 'cordis_define', 'cordis_package_inspect', 'cordis_run', 'cordis_runtime_inspect', 'cordis_stop', 'cordis_undefine', 'create_goal', 'edit', 'exit_plan_mode', 'get_goal', 'glob', 'grep', 'interrupt_agent', 'list_agents', 'lsp', 'pwsh', 'ralph', 'read', 'read_image', 'report', 'run_code', 'schedule_create', 'schedule_delete', 'schedule_list', 'send_message', 'session_event_read', 'session_event_search', 'session_event_trace', 'session_search', 'session_trace', 'skill', 'str_replace_editor', 'subagent', 'task_kill', 'task_list', 'task_output', 'terminal_close', 'terminal_list', 'terminal_open', 'terminal_read', 'terminal_send', 'terminal_signal', 'todo_write', 'update_goal', 'web_fetch', 'web_search', 'workflow', 'write'])
expect(names).toEqual(['ask_user_question', 'bash', 'bash', 'cordis_define', 'cordis_inspect_list', 'cordis_inspect_query', 'cordis_inspect_self', 'cordis_run', 'cordis_stop', 'cordis_undefine', 'create_goal', 'edit', 'exit_plan_mode', 'get_goal', 'glob', 'grep', 'interrupt_agent', 'list_agents', 'lsp', 'pwsh', 'ralph', 'read', 'read_image', 'report', 'run_code', 'schedule_create', 'schedule_delete', 'schedule_list', 'send_message', 'session_event_read', 'session_event_search', 'session_event_trace', 'session_search', 'session_trace', 'skill', 'str_replace_editor', 'subagent', 'task_kill', 'task_list', 'task_output', 'terminal_close', 'terminal_list', 'terminal_open', 'terminal_read', 'terminal_send', 'terminal_signal', 'todo_write', 'update_goal', 'web_fetch', 'web_search', 'workflow', 'write'])
// Every tool carries a JSON-Schema `parameters` object (what the model sees).
for (const entry of catalog) {
for (const schema of entry.schemas) {
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/extensions/README.md
README.md: 1a09693e76291eee1b34c152dbd106226a02f468
README.zh.md: ad485e78fef89ba3b2f3f7db84f1ba526dffc358
README.md: 245a2c59515591bcd9e6b87cb94ea8ddeed6727d
README.zh.md: f30acac479397e0e00b294dfe860c37ee227426a
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/extensions/cordis-client-runner/README.md
README.md: 530f7d225ea74e34651b54fe94c4211730c6e271
README.zh.md: f78de8e38c37b43d758aec5c285a04c34a7eff22
README.md: ba60e3256ca6c80792645daa26c6872cfc846940
README.zh.md: 2d8712de6b0444847eb731d84140fe2c1cad5b63
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/extensions/cordis-host-runner/README.md
README.md: af84270e549ebc71cb2176f5d1d61531603060f9
README.zh.md: d608426b4b0d6fe11e20cea0cb119740bfcfe100
README.md: f09e7506fe24676ea95b3ae490b55ae120bfad5f
README.zh.md: f60d3a64ecb1e90427b966c820a857e62d65b375
@@ -44,10 +44,7 @@
},
"devDependencies": {
"@deepseek-ai/cordis-plugin-loader": "workspace:^",
"@deepseek-ai/cordis-plugin-timer": "workspace:^",
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-agent-loop": "workspace:^",
"@deepseek-ai/dsh-agent-loop-testkit": "workspace:^",
"@deepseek-ai/dsh-cordis-host-runner": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
@@ -1,168 +0,0 @@
import { Context } from '@deepseek-ai/cordis'
import Timer from '@deepseek-ai/cordis-plugin-timer'
import { CallId } from '@deepseek-ai/dsh-llm'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import type { ToolExecutionResult } from '@deepseek-ai/dsh-tools'
import CordisHostRunner from '@deepseek-ai/dsh-cordis-host-runner'
import type { Config as RunnerConfig } from '@deepseek-ai/dsh-cordis-host-runner'
import type { Agent } from '@deepseek-ai/dsh-agent'
import type { SessionId } from '@deepseek-ai/dsh-session'
import * as tool from '../src/index.ts'
const testToolSignal = new AbortController().signal
/**
* Shared spec helpers: a real `SystemPrompt` + `ToolRegistry` + timer + the
* dynamic runner + this toolset (only the model and the browser are absent — the
* code strings below stand in for what the model would write, and no gateway is
* composed, so a browser half has nowhere to go).
*
* Every dynamic-package tool is session-scoped, so calls carry a stand-in agent.
*/
/** The session every spec call runs as. */
export const AGENT = { id: 'S-spec' as SessionId } as Agent
/** Mount the toolset on a fresh context with a real ToolRegistry, the timer service, and the runner. */
export async function setup(config?: RunnerConfig): Promise<Context> {
const ctx = new Context()
await ctx.plugin(Timer)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(CordisHostRunner, config)
await ctx.plugin(tool)
return ctx
}
/**
* The same composition plus a stand-in browser: an `apiProxy` whose broadcast
* answers a run request by walking the runner's own verbs, exactly as the real
* client half does. Without it a package with a browser half can only ever be
* refused, so the tool's success reporting for that shape stays untested.
* @param waitingFor - services the answering page reports its half parked on.
* @returns the mounted context.
*/
export async function setupWithBrowser(waitingFor?: readonly string[]): Promise<Context> {
const ctx = await setup()
const runner = ctx.dynamicCordisRunner
// The fake browser subscribes the way a real page does — to the forwarded Host
// event, not to a transport frame — and answers by walking the same verbs.
ctx.on('cordis/request-run', (request) => {
const { requestId, pluginId, packageId, mode } = request
queueMicrotask(() => {
void (async (): Promise<void> => {
const half = await runner.runHostHalf(AGENT, pluginId, packageId, mode, requestId, false)
if (!half.ok) return
const source = runner.getClientCode(AGENT, pluginId, half.pluginRunId)
await runner.resolveRequestRun(requestId, {
ok: true,
pluginRunId: source.pluginRunId,
...waitingFor === undefined ? {} : { waitingFor },
})
})()
})
})
return ctx
}
let callCounter = 0
/** Execute a registered tool through the real registry pipeline, as the spec agent. */
export function call(ctx: Context, name: string, args: unknown): Promise<ToolExecutionResult> {
return ctx.tools.execute({
signal: testToolSignal,
callId: CallId(`call-${++callCounter}`),
name,
arguments: args,
agent: AGENT,
})
}
/** Concatenated text blocks of one tool result. */
export function text(result: ToolExecutionResult): string {
return result.content.filter(block => block.type === 'text').map(block => block.text).join('')
}
/** Define one host-half package and run it, returning its minted id. */
export async function defineAndRun(ctx: Context, code: string, name = 'spec-package'): Promise<string> {
const defined = await call(ctx, 'cordis_define', {
plugin: { kind: 'new', idPrefix: 'spec' },
name,
purpose: 'spec fixture',
code: { host: code },
})
if (defined.isError) throw new Error(`define failed: ${text(defined)}`)
const { pluginId, packageId } = defined.value as { pluginId: string; packageId: string }
const ran = await call(ctx, 'cordis_run', { pluginId, packageId, mode: 'run' })
if (ran.isError) throw new Error(`run failed: ${text(ran)}`)
return pluginId
}
/** Host-half code for a listener plugin: logs on every `tools/change`. */
export const LISTENER_CODE = `
return {
name: 'change-logger',
apply(ctx) {
ctx.on('tools/change', () => console.log('tools changed'))
},
}
`
/** Host-half code for a self-made tool: registers `reverse_text` via the sandbox's harness helpers. */
export const REVERSE_TOOL_CODE = `
return {
name: 'reverse-text',
inject: ['tools'],
apply(ctx) {
harness.registerTool(ctx, harness.defineTool({
name: 'reverse_text',
description: 'Reverse a string.',
parameters: { text: { type: 'string', required: true } },
output: {
schema: { type: 'string' },
render(_args, value) {
return [{ type: 'text', text: value }]
},
},
async execute(args) {
return args.text.split('').reverse().join('')
},
}))
},
}
`
/** Host-half code providing a `greeter` service other packages can inject. */
export const PROVIDER_CODE = `
return {
name: 'greeter-provider',
apply(ctx) {
ctx.provide('greeter', { greet: (name) => 'hi ' + name })
},
}
`
/** Host-half code consuming the `greeter` service through inject, exposing it as a tool. */
export const CONSUMER_CODE = `
return {
name: 'greeter-consumer',
inject: ['greeter', 'tools'],
apply(ctx) {
harness.registerTool(ctx, harness.defineTool({
name: 'greet',
description: 'Greet someone via the greeter service.',
parameters: { name: { type: 'string', required: true } },
output: {
schema: { type: 'string' },
render(_args, value) {
return [{ type: 'text', text: value }]
},
},
async execute(args) {
return ctx.greeter.greet(args.name)
},
}))
},
}
`
@@ -1,397 +0,0 @@
import { describe, expect, it } from 'vitest'
import type { Context, Fiber } from '@deepseek-ai/cordis'
import type { Agent } from '@deepseek-ai/dsh-agent'
import { FiberState } from '../src/fiber-state.ts'
import {
describeApi, describeClient, describeDynamic, describeEvents, describePlugins, describeServices,
} from '../src/inspect.ts'
import type { ClientSlotEntry } from '../src/client-catalog.ts'
import { call, defineAndRun, LISTENER_CODE, setup, text } from './helpers.ts'
/** A single seat the shipped composition already occupies. */
const SEAT: ClientSlotEntry = {
key: 'demo.seat',
kind: 'single',
scope: 'root',
summary: 'A seat.',
doc: 'A seat.',
registerOptions: [],
ownerProps: [],
ownerPropsReferences: [],
standardProps: ['useSessions: Hook'],
keyDomain: '',
hookContext: '',
slotInject: '',
declaredBy: 'the runtime itself (built in; always present)',
occupants: ['client-demo DemoSeat'],
replaceRisk: 'shadows-shipped-ui',
example: 'return {}',
// A hypothetical package: naming a real one would tie this fixture to a
// surface it does not describe, and the real catalog carries the pointer.
source: 'a demo client package, slots.ts:1',
}
/** An empty list seat: the additive-with-no-occupant wording and the detail block. */
const LIST_SEAT: ClientSlotEntry = {
...SEAT,
key: 'demo.list',
kind: 'list',
summary: 'A list.',
doc: 'A list.',
registerOptions: [{ name: 'id', requirement: 'required', type: 'string', doc: 'Your cell key.' }],
declaredBy: "an entry in 'demo.parent' (client-demo), so it exists while that entry is mounted",
occupants: [],
replaceRisk: 'none',
example: "ctx.slots.register({ name: 'demo.list', id: 'mine' }, C)",
}
/** An occupied list seat: additive, but the report still names who is already there. */
const LIST_SEAT_OCCUPIED: ClientSlotEntry = {
...LIST_SEAT,
key: 'demo.list.busy',
occupants: ["client-demo DemoRow id 'shipped'"],
}
/** A keyed seat carrying every optional field, so each one's presence branch renders. */
const KEYED_SEAT: ClientSlotEntry = {
...SEAT,
key: 'demo.keyed',
kind: 'keyed',
summary: 'A keyed seat.',
doc: 'A keyed seat.',
registerOptions: [{ name: 'key', requirement: 'required', type: 'string', doc: 'Your cell key.' }],
ownerProps: ['export interface KeyedOwnerProps {\n block: ToolCallBlock\n}'],
ownerPropsReferences: ['ToolCallBlock'],
keyDomain: 'open: any string the owner dispatches, already taken: bash',
hookContext: 'ChatNodeContext',
slotInject: 'ChatNodeInjected',
occupants: ["client-demo DemoView key 'bash'"],
replaceRisk: 'shadows-shipped-ui',
}
/**
* The `cordis_runtime_inspect` sections: rendered against the real runtime through the
* tool, plus direct renderer calls for the states a minimal harness cannot
* reach (empty service store, same-named sibling fibers, a fully-live catalog).
*/
describe('cordis_runtime_inspect', () => {
it('reports all seven sections by default', async () => {
const ctx = await setup()
const result = await call(ctx, 'cordis_runtime_inspect', {})
expect(result.isError).toBe(false)
const report = text(result)
if (result.isError) throw new Error('expected cordis_runtime_inspect success')
expect(result.value).toBe(report)
for (const heading of ['services', 'plugins', 'tools', 'Dynamic Packages', 'api', 'events', 'client']) {
expect(report).toContain(`## ${heading}`)
}
// The services section sees the real providers; the plugins list shows
// this plugin and its dynamic group flat; the tools section lists the
// cordis tools.
expect(report).toContain('- tools (provided by ToolRegistry)')
expect(report).toContain('- tool-cordis [active]')
expect(report).toContain('- cordis_define')
expect(report).toContain('No dynamic packages are defined in this session.')
})
it('limits the report to one section via `what`', async () => {
const ctx = await setup()
const result = await call(ctx, 'cordis_runtime_inspect', { what: 'tools' })
const report = text(result)
expect(report).toContain('## tools')
expect(report).not.toContain('## services')
expect(report).not.toContain('## plugins')
})
it('shows a running dynamic package in its exact section and in the flat plugins list', async () => {
const ctx = await setup()
await defineAndRun(ctx, LISTENER_CODE, 'logger')
const report = text(await call(ctx, 'cordis_runtime_inspect', {}))
expect(report).toContain('## Dynamic Packages')
expect(report).toContain('- dyn-1: logger [running, rev 1] (host) — spec fixture; provides: none; waiting for: none')
// The group fiber and the package's own plugin are both live in the flat list.
expect(report).toContain('- cordis-dynamic [active]')
expect(report).toContain('- change-logger [active]')
})
it('shows a defined-but-not-running package, and the invoke methods a running one registered', async () => {
const ctx = await setup()
await call(ctx, 'cordis_define', { name: 'idle', purpose: 'waits to be started', code: 'return () => {}' })
await defineAndRun(ctx, 'harness.handle(\'ping\', async () => \'pong\')\nreturn () => {}', 'handler')
const report = text(await call(ctx, 'cordis_runtime_inspect', { what: 'temporary' }))
expect(report).toContain('- dyn-1: idle [defined, not running] (host) — waits to be started')
expect(report).toContain('host methods: ping')
})
it('renders the api section from the generated catalog intersected with the LIVE runtime', async () => {
const ctx = await setup()
const report = text(await call(ctx, 'cordis_runtime_inspect', { what: 'api' }))
// Live catalogued services render summary + signatures.
expect(report).toContain('- tools — Tool registry and execution pipeline.')
expect(report).toContain('register(definition: ToolDefinition)')
// The projection carries public METHODS only: state and symbol-keyed seams
// between plugins are not calls a package can make.
expect(report).not.toContain('store: Map<string, ToolDefinition>')
expect(report).not.toContain('TOOL_REGISTRY_SCHEDULER')
// Catalogued services with no live provider are listed tersely.
expect(report).toMatch(/not running \(loadable services with no live provider\): .*bash/)
// The type shapes the LIVE signatures reference follow, so a consumer can see
// field types rather than only names.
expect(report).toContain('type shapes (referenced by the signatures above')
expect(report).toContain('export interface ToolDefinition')
// A type only reachable through a NOT-live service (e.g. bash) is scoped out.
expect(report).not.toContain('export interface BashRunResult')
// The inherited ctx API closes the section.
expect(report).toContain('inherited ctx API:')
expect(report).toContain('- ctx.effect — ')
// The broad report stays compact; exact-name lookup owns full JSDoc.
expect(report).not.toContain('/**')
expect(report).not.toContain('@param definition')
})
it('adds original method JSDoc only for an exact live api name', async () => {
const ctx = await setup()
const report = text(await call(ctx, 'cordis_runtime_inspect', { what: 'api', name: 'tools' }))
expect(report).toContain('## api')
expect(report).toContain('- tools — Tool registry and execution pipeline.')
expect(report).toContain('/**')
expect(report).toContain('Register globally or in the calling agent scope.')
expect(report).toContain('@param definition - tool schema, execution, and optional')
expect(report).toContain('@returns the exact disposer that unregisters the tool.')
expect(report).toContain('register(definition: ToolDefinition)')
expect(report).toContain('type shapes (referenced by the signatures above')
expect(report).not.toContain('not running (loadable services')
expect(report).not.toContain('inherited ctx API:')
})
it('renders the events section with mode badges, signatures, and the waterfall caution', async () => {
const ctx = await setup()
const report = text(await call(ctx, 'cordis_runtime_inspect', { what: 'events' }))
expect(report).toContain('- tools/change [emit]')
expect(report).toContain('- tools/pre-execute [waterfall]')
expect(report).toMatch(/'tools\/change'\(/)
expect(report).toContain('returning without next() short-circuits the chain')
expect(report).not.toContain('/**')
expect(report).not.toContain('@mode waterfall')
})
it('adds original event JSDoc only for an exact event name', async () => {
const ctx = await setup()
const report = text(await call(ctx, 'cordis_runtime_inspect', { what: 'events', name: 'tools/pre-execute' }))
expect(report).toContain('## events')
expect(report).toContain('- tools/pre-execute [waterfall]')
expect(report).toContain('/**')
expect(report).toContain('Allow, deny, or ask before dispatch.')
expect(report).toContain('@param exec - the pending call')
expect(report).toContain('@mode waterfall')
expect(report).not.toContain('- tools/change [emit]')
})
it('fails loud for incompatible, unknown, and non-running names', async () => {
const ctx = await setup()
const incompatible = await call(ctx, 'cordis_runtime_inspect', { what: 'tools', name: 'tools' })
expect(incompatible.isError).toBe(true)
expect(text(incompatible)).toContain('name is valid only with what:"api", what:"events", or what:"client"')
const unknownService = await call(ctx, 'cordis_runtime_inspect', { what: 'api', name: 'not-a-service' })
expect(unknownService.isError).toBe(true)
expect(text(unknownService)).toContain('no catalogued service named "not-a-service"')
const nonRunning = await call(ctx, 'cordis_runtime_inspect', { what: 'api', name: 'bash' })
expect(nonRunning.isError).toBe(true)
expect(text(nonRunning)).toContain('catalogued service "bash" is not running')
const unknownEvent = await call(ctx, 'cordis_runtime_inspect', { what: 'events', name: 'not/an-event' })
expect(unknownEvent.isError).toBe(true)
expect(text(unknownEvent)).toContain('no catalogued event named "not/an-event"')
})
})
describe('inspect renderers (direct)', () => {
it('describeServices reports an empty store as such, and labels a non-active provider', () => {
const empty = { reflect: { store: {} } } as unknown as Context
expect(describeServices(empty, [])).toEqual(['(no services provided)'])
const pendingFiber = { state: FiberState.PENDING, name: 'half-loaded' } as unknown as Fiber
const store: Record<symbol, unknown> = {}
store[Symbol('impl')] = { name: 'thing', fiber: pendingFiber }
const ctx = { reflect: { store } } as unknown as Context
const lines = describeServices(ctx, [])
// A service the catalog does not cover still appears, with its owner and the
// non-active lifecycle label; only the summary is missing.
expect(lines).toEqual(['- thing (provided by half-loaded, pending)'])
})
it('describePlugins lists every fiber flat, sorted by name, one line per instance', () => {
const fiber = (name: string): Fiber => ({ name, state: FiberState.ACTIVE }) as unknown as Fiber
const ctx = {
registry: { values: () => [{ fibers: [fiber('beta'), fiber('alpha')] }, { fibers: [fiber('alpha')] }] },
} as unknown as Context
expect(describePlugins(ctx)).toEqual([
'- alpha [active]',
'- alpha [active]',
'- beta [active]',
])
})
it('describeApi omits the not-running line and type shapes when nothing applies', async () => {
const ctx = await setup()
const lines = describeApi(ctx, [{
key: 'tools',
summary: 'The registry.',
description: 'The registry.',
methods: [{
signature: 'register(x): void',
description: 'Register x.',
parameters: [{ name: 'x', description: 'Value to register.' }],
}],
}], undefined, [], [])
expect(lines[0]).toBe('- tools — The registry.')
expect(lines[1]).toBe(' register(x): void')
expect(lines.join('\n')).not.toContain('not running')
expect(lines.join('\n')).not.toContain('type shapes')
})
it('expands the shapes a service names transitively, listing each one once', async () => {
const ctx = await setup()
const lines = describeApi(ctx, [{
key: 'tools',
summary: 'The registry.',
description: 'The registry.',
methods: [{
signature: 'register(definition: ToolDefinition): void',
description: '',
parameters: [],
}],
}], 'tools', [], [
{ name: 'ToolDefinition', declaration: 'export interface ToolDefinition {\n schema: ToolSchema\n}' },
{ name: 'ToolSchema', declaration: 'export interface ToolSchema {\n owner: ToolDefinition\n}' },
]).join('\n')
// The signature names one shape, that shape names the second, and the two
// reference each other back: a reader gets both, each exactly once.
expect(lines.match(/export interface ToolDefinition/g)).toHaveLength(1)
expect(lines.match(/export interface ToolSchema/g)).toHaveLength(1)
})
it('describeEvents renders an empty catalog as just the waterfall caution', () => {
expect(describeEvents([])).toEqual([
'waterfall listeners receive a trailing next() and MUST call it to delegate — returning without next() short-circuits the chain.',
])
})
it('describeApi reports a live service with no catalogued signature as still injectable', async () => {
const ctx = await setup()
const lines = describeApi(ctx, []).join('\n')
// The framework tier is exactly this case: reachable through inject, but
// with no projected signature — the report must not read as "absent".
expect(lines).toContain('running, but this catalog has no signature for it')
expect(lines).toContain('still reaches it')
})
it('describeClient lists every seat with what registering there costs', () => {
const lines = describeClient([SEAT, LIST_SEAT, LIST_SEAT_OCCUPIED], ['one rule']).join('\n')
expect(lines).toContain('- demo.seat [single, root] — A seat.')
expect(lines).toContain('OCCUPIED — registering here REPLACES: client-demo DemoSeat')
expect(lines).toContain('- demo.list [list, root]')
expect(lines).toContain('additive (no shipped entries)')
// Additive does not mean empty: an id already in use is still a takeover.
expect(lines).toContain("additive (beside: client-demo DemoRow id 'shipped')")
expect(lines).toContain('- one rule')
// The compact listing must not spend context on per-seat detail.
expect(lines).not.toContain('register options besides name:')
expect(lines).not.toContain('framework props for this scope:')
})
it('describeClient expands the optional contract fields only where a seat has them', () => {
const keyed = describeClient([KEYED_SEAT], [], 'demo.keyed').join('\n')
expect(keyed).toContain('key domain: open: any string the owner dispatches, already taken: bash')
expect(keyed).toContain('owner props (the shapes the owner passes down):')
// Owner props expand one level; referenced shapes are named, not inlined.
expect(keyed).toContain('shapes those fields reference, not expanded here: ToolCallBlock')
expect(keyed).toContain('slot-level inject face every entry receives: ChatNodeInjected')
expect(keyed).toContain('per-render-site hook context: ChatNodeContext')
expect(keyed).toContain('key (required, string)')
// A seat without them says nothing about them.
const plain = describeClient([SEAT], [], 'demo.seat').join('\n')
expect(plain).toContain('owner props: none')
expect(plain).toContain('register options besides name: none')
expect(plain).not.toContain('key domain:')
expect(plain).not.toContain('slot-level inject face')
expect(plain).not.toContain('per-render-site hook context')
expect(plain).not.toContain('shapes those fields reference')
})
it('describeClient expands one seat into its full register contract', () => {
const lines = describeClient([SEAT, LIST_SEAT], ['one rule'], 'demo.list').join('\n')
expect(lines).toContain('exists: an entry in \'demo.parent\'')
expect(lines).toContain('id (required, string) — Your cell key.')
expect(lines).toContain('owner props: none')
expect(lines).toContain('useSessions: Hook')
expect(lines).toContain('minimal browser half:')
expect(lines).toContain('ctx.slots.register(')
// A narrowed report is one seat only, and carries no cross-cutting rules.
expect(lines).not.toContain('demo.seat')
expect(lines).not.toContain('one rule')
})
it('describeDynamic tells the model whether a failed browser half is still on the page', () => {
const row = (abdicated: boolean): unknown => ({
id: 'dyn-1',
name: 'panel',
purpose: 'ui',
hasHostHalf: false,
hasClientHalf: true,
run: { rev: 1, handlers: [] },
renderFailure: { slot: 'settings.section', message: 'useX is not a function', abdicated },
})
const ctxFor = (abdicated: boolean): Context => ({
dynamicCordisRunner: { snapshot: () => [row(abdicated)] },
reflect: { store: {} },
get: () => undefined,
} as unknown as Context)
const gone = describeDynamic(ctxFor(true), {} as unknown as Agent).join('\n')
expect(gone).toContain('BROWSER HALF FAILED TO RENDER at slot settings.section: useX is not a function')
// The two states differ in the one fact the author needs: is my UI there?
expect(gone).toContain('that seat was handed back to the shipped UI')
const kept = describeDynamic(ctxFor(false), {} as unknown as Agent).join('\n')
expect(kept).toContain('that seat is still yours, so what the page shows may be incomplete')
})
it('describeClient names the shipped neighbours when an occupied list seat is expanded', () => {
const lines = describeClient([LIST_SEAT_OCCUPIED], [], 'demo.list.busy').join('\n')
expect(lines).toContain("additive (beside: client-demo DemoRow id 'shipped')")
})
it('describeDynamic renders a browser-only row, a half-loaded host half, and a fiberless run', () => {
// Rows a host-only harness cannot produce: the runner's own shapes are the
// contract this renderer reads, so they are supplied directly.
const pending = { state: FiberState.PENDING, name: 'half-loaded', inject: {} } as unknown as Fiber
const rows = [
{ id: 'dyn-1', name: 'browser only', purpose: 'ui', hasHostHalf: false, hasClientHalf: true },
{ id: 'dyn-2', name: 'no fiber', purpose: 'client half only', hasHostHalf: false, hasClientHalf: true, run: { rev: 1, handlers: [] } },
{ id: 'dyn-3', name: 'waiting', purpose: 'both halves', hasHostHalf: true, hasClientHalf: true, run: { rev: 2, fiber: pending, handlers: ['ping'] } },
]
const ctx = {
dynamicCordisRunner: { snapshot: () => rows },
reflect: { store: {} },
get: () => undefined,
} as unknown as Context
// No agent means no definition space to report, not an empty registry.
expect(describeDynamic(ctx)).toEqual([
'No dynamic packages are defined in this session. Definitions live only in this process\'s memory, so a DSH restart clears them.',
])
const lines = describeDynamic(ctx, {} as unknown as Agent)
expect(lines[0]).toBe('- dyn-1: browser only [defined, not running] (browser) — ui')
expect(lines[1]).toBe('- dyn-2: no fiber [running, rev 1] (browser) — client half only; provides: none; waiting for: none')
expect(lines[2]).toBe('- dyn-3: waiting [pending, rev 2] (host+browser) — both halves; provides: none; waiting for: none; host methods: ping')
})
it('describeClient refuses an unknown slot key instead of answering emptily', () => {
expect(() => describeClient([SEAT], [], 'nope.seat')).toThrow('no catalogued client slot named "nope.seat"')
})
})
@@ -1,110 +0,0 @@
import { describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import { createUserMessage, CallId } from '@deepseek-ai/dsh-llm'
import { SessionId } from '@deepseek-ai/dsh-session'
import type { Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
import CordisHostRunner from '@deepseek-ai/dsh-cordis-host-runner'
import * as ToolCordis from '../src/index.ts'
import { MockAdapter, textResponse, toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
import { call, REVERSE_TOOL_CODE, setup, text } from './helpers.ts'
/**
* Full-loop integration: a scripted mock model defines and runs a package that
* registers a NEW tool, calls that tool on the very next step (tool schemas are
* reassembled per step — the real loop proves the self-extension contract), and
* undefines it again. Only the model is mocked; the sandbox, the fiber tree, and
* the session log are real — including the presentation metadata the card needs.
*/
async function harness(adapter: MockAdapter): Promise<Context> {
const ctx = new Context()
await mountAgentLoopTestDependencies(ctx)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(CordisHostRunner)
await ctx.plugin(ToolCordis)
ctx.llm.registerAdapter(['mock'], adapter)
return ctx
}
function waitForIdle(ctx: Context, agent: Agent): Promise<void> {
return new Promise((resolve) => {
const dispose = ctx.on('agent/status', ({ agent: subject, status }) => {
if (subject === agent && status === 'idle') {
dispose()
resolve()
}
})
})
}
describe('cordis tools through the agent loop', () => {
it('defines, runs, calls, and undefines a self-made tool — all as real tool/call events', async () => {
const adapter = new MockAdapter([
toolCallResponse('call-1', 'cordis_define', { name: 'reverser', purpose: 'reverses text', code: REVERSE_TOOL_CODE }, 'Extending myself.'),
toolCallResponse('call-2', 'cordis_run', { id: 'dyn-1' }),
toolCallResponse('call-3', 'reverse_text', { text: 'harness' }),
toolCallResponse('call-4', 'cordis_undefine', { id: 'dyn-1' }),
textResponse('Done.'),
])
const ctx = await harness(adapter)
const agent = ctx.agentLoop.create(SessionId('it-cordis'), { provider: 'mock', model: 'mock' })
agent.followup(createUserMessage({ content: [{ type: 'text', text: 'give yourself reverse_text, use it, clean up' }], source: { kind: 'user' } }))
await waitForIdle(ctx, agent)
const log = agent.session.events
const calls = log.filter(event => event.type === 'tool/call').map(event => event.data.name)
expect(calls).toEqual(['cordis_define', 'cordis_run', 'reverse_text', 'cordis_undefine'])
const results = log.filter(event => event.type === 'tool/result')
expect(results.map(event => event.data.message.content[0].isError)).toEqual([false, false, false, false])
// The define result's durable metadata carries the minted id — this is what
// a card reads to address run/stop, and replay reproduces it verbatim.
expect(results[0]!.data.meta).toEqual({ id: 'dyn-1' })
const reversed = results[2]!.data.message.content[0].content
.filter(block => block.type === 'text')
.map(block => block.text)
.join('')
expect(reversed).toBe('ssenrah')
// After the undefine the self-made tool is gone from the registry.
expect(ctx.tools.get('reverse_text')).toBeUndefined()
})
it('keeps a running package across turns, undefines it, and does not restore it in a new runtime', async () => {
const adapter = new MockAdapter([
toolCallResponse('define-1', 'cordis_define', { name: 'marker', purpose: 'marks the turn', code: 'return { name: \'turn-marker\', apply() {} }' }),
toolCallResponse('run-1', 'cordis_run', { id: 'dyn-1' }),
toolCallResponse('inspect-1', 'cordis_runtime_inspect', { what: 'temporary' }),
textResponse('Turn one complete.'),
toolCallResponse('inspect-2', 'cordis_runtime_inspect', { what: 'temporary' }),
toolCallResponse('undefine-1', 'cordis_undefine', { id: 'dyn-1' }),
toolCallResponse('inspect-3', 'cordis_runtime_inspect', { what: 'temporary' }),
textResponse('Turn two complete.'),
])
const ctx = await harness(adapter)
const agent = ctx.agentLoop.create(SessionId('it-cordis-turn-lifetime'), { provider: 'mock', model: 'mock' })
agent.followup(createUserMessage({ content: [{ type: 'text', text: 'Define and run the marker, then inspect it.' }], source: { kind: 'user' } }))
await waitForIdle(ctx, agent)
agent.followup(createUserMessage({ content: [{ type: 'text', text: 'On this later turn, inspect the marker, undefine it, then inspect again.' }], source: { kind: 'user' } }))
await waitForIdle(ctx, agent)
const resultText = new Map(
agent.session.events
.filter(event => event.type === 'tool/result')
.map(event => [event.data.message.source.callId, event.data.message.content[0].content.filter(block => block.type === 'text').map(block => block.text).join('')]),
)
expect(resultText.get(CallId('inspect-1'))).toContain('- dyn-1: marker [running, rev 1] (host) — marks the turn')
expect(resultText.get(CallId('inspect-2'))).toContain('- dyn-1: marker [running, rev 1] (host) — marks the turn')
expect(resultText.get(CallId('undefine-1'))).toBe('Dynamic package dyn-1 is stopped and undefined; its id is now invalid.')
expect(resultText.get(CallId('inspect-3'))).toContain('No dynamic packages are defined in this session.')
// A fresh runtime restores nothing: definitions never left this process.
const restarted = await setup()
expect(text(await call(restarted, 'cordis_runtime_inspect', { what: 'temporary' })))
.toContain('No dynamic packages are defined in this session.')
})
})
@@ -1,56 +0,0 @@
import { describe, expect, it } from 'vitest'
import {
presentDefineCall, presentPackageInspectCall, presentRunCall, presentRuntimeInspectCall,
presentStopCall, presentUndefineCall,
} from '../src/present.ts'
import { setup } from './helpers.ts'
describe('Cordis tool presenters', () => {
it('renders runtime and Package inspection as read calls', () => {
expect(presentRuntimeInspectCall({ what: 'api', name: 'tools' })).toEqual({
card: 'generic',
kind: 'read',
title: 'Inspect cordis runtime: api: tools',
})
expect(presentPackageInspectCall({ pluginId: 'clock-1', packageId: 'pkg-2' })).toEqual({
card: 'generic',
kind: 'read',
title: 'Inspect Cordis package clock-1/pkg-2',
})
})
it('renders versioned define and lifecycle calls', () => {
expect(presentDefineCall({
plugin: { kind: 'existing', pluginId: 'clock-1' },
name: 'Clock v2',
purpose: 'show seconds',
code: { host: 'HOST', client: 'CLIENT' },
})).toEqual({
card: 'generic',
kind: 'execute',
title: 'Define clock-1 package "Clock v2": show seconds',
rawInput: { host: 'HOST', client: 'CLIENT' },
})
expect(presentRunCall({ pluginId: 'clock-1', packageId: 'pkg-2', mode: 'update' })).toEqual({
card: 'generic', kind: 'execute', title: 'Update clock-1 with pkg-2',
})
expect(presentStopCall({ pluginId: 'clock-1' })).toEqual({
card: 'generic', kind: 'execute', title: 'Stop dynamic plugin clock-1',
})
expect(presentUndefineCall({ pluginId: 'clock-1' })).toEqual({
card: 'generic', kind: 'delete', title: 'Remove dynamic plugin clock-1',
})
})
it('wires the split inspection presenters onto their tools', async () => {
const ctx = await setup()
expect(ctx.tools.get('cordis_runtime_inspect')!.presentCall!({ what: 'tools' })).toMatchObject({
kind: 'read', title: 'Inspect cordis runtime: tools',
})
expect(ctx.tools.get('cordis_package_inspect')!.presentCall!({
pluginId: 'clock-1', packageId: 'pkg-1',
})).toMatchObject({
kind: 'read', title: 'Inspect Cordis package clock-1/pkg-1',
})
})
})
@@ -1,50 +0,0 @@
import { describe, expect, it } from 'vitest'
import Loader from '@deepseek-ai/cordis-plugin-loader'
import * as tool from '../src/index.ts'
import { setup } from './helpers.ts'
/**
* Export shape and registration API: the namespace-plugin contract the
* real Loader path depends on, the registered tool set, and the Config
* validator's defaults and rejections.
*/
describe('export shape', () => {
it('has no default export, and survives the real Loader unwrapExports', () => {
// A stray `export default` would make `unwrapExports` (`exports.default ??
// exports`) collapse the module to the bare function and DROP `inject`,
// crashing at real load (docs/postmortem/0001). Assert directly AND through
// the real unwrap so adding `export default apply` fails here.
expect('default' in tool).toBe(false)
const loader = Object.create(Loader.prototype) as Loader
const unwrapped = loader.unwrapExports(tool) as Record<string, unknown>
expect(unwrapped).toBe(tool)
expect(unwrapped.name).toBe('tool-cordis')
expect(unwrapped.inject).toEqual(['tools', 'dynamicCordisRunner'])
expect(typeof unwrapped.apply).toBe('function')
// The vm bound moved to the runner service with the sandbox it bounds, so
// this toolset has no config of its own.
expect('Config' in tool).toBe(false)
})
})
describe('tool registration', () => {
it('registers the six cordis tools with split inspection schemas', async () => {
const ctx = await setup()
const names = ctx.tools.schemas().map(schema => schema.name)
expect(names).toEqual(expect.arrayContaining([
'cordis_runtime_inspect', 'cordis_package_inspect', 'cordis_define',
'cordis_run', 'cordis_stop', 'cordis_undefine',
]))
// The one-shot mount pair retired with the two-step verbs.
expect(names).not.toEqual(expect.arrayContaining(['cordis_mount']))
expect(names).not.toEqual(expect.arrayContaining(['cordis_unmount']))
const inspect = ctx.tools.schemas().find(schema => schema.name === 'cordis_runtime_inspect')!
const props = (inspect.parameters as { properties: Record<string, { enum?: string[]; type?: string }> }).properties
expect(props.what?.enum).toEqual(['services', 'plugins', 'tools', 'temporary', 'api', 'events', 'client'])
expect(props.name?.type).toBe('string')
const packageInspect = ctx.tools.schemas().find(schema => schema.name === 'cordis_package_inspect')!
const packageProps = (packageInspect.parameters as { properties: Record<string, { type?: string }> }).properties
expect(packageProps).toMatchObject({ pluginId: { type: 'string' }, packageId: { type: 'string' } })
})
})
@@ -1,286 +0,0 @@
import { describe, expect, it, vi } from 'vitest'
import { AGENT, CONSUMER_CODE, LISTENER_CODE, PROVIDER_CODE, REVERSE_TOOL_CODE, call, defineAndRun, setup, setupWithBrowser, text } from './helpers.ts'
/**
* The five model-facing tools driven through the real registry pipeline: define
* records and mints, run starts and reports, stop and undefine unwind, and every
* refusal reaches the model as a tool error carrying the runner's teaching text.
* The runner's own semantics are covered by its package; here the subject is the
* model-facing contract (arguments, canonical values, rendered text, metadata).
*/
describe('cordis_define', () => {
it('records a definition and carries the minted id in its value and its presentation metadata', async () => {
const ctx = await setup()
const result = await call(ctx, 'cordis_define', {
name: 'greeter',
purpose: 'greets by name',
code: PROVIDER_CODE,
})
expect(result.isError).toBe(false)
if (result.isError) throw new Error('expected cordis_define success')
expect(result.value).toEqual({
id: 'dyn-1',
name: 'greeter',
purpose: 'greets by name',
hasHostHalf: true,
hasClientHalf: false,
})
// The card addresses run/stop by this id, and only the durable metadata
// carries it (the model never wrote it).
expect(result.meta).toEqual({ id: 'dyn-1' })
expect(text(result)).toBe(
'Dynamic package dyn-1 ("greeter") is defined with a host half and is NOT running yet. '
+ 'Run it with cordis_run id:"dyn-1", or let the user press start on its card.',
)
// Nothing ran: the provided service is absent until cordis_run.
expect(ctx.get('greeter')).toBeUndefined()
})
it('names both halves in the rendered summary', async () => {
const ctx = await setup()
const result = await call(ctx, 'cordis_define', {
name: 'dual',
purpose: 'both halves',
code: PROVIDER_CODE,
client: 'return () => {}',
})
expect(text(result)).toContain('is defined with a host + browser half')
})
it('reports a parse failure as a tool error and records nothing', async () => {
const ctx = await setup()
const result = await call(ctx, 'cordis_define', {
name: 'broken',
purpose: 'p',
code: 'return { name: \'ts\' as const }',
})
expect(result.isError).toBe(true)
expect(text(result)).toContain('plain JavaScript, not TypeScript')
expect(text(await call(ctx, 'cordis_runtime_inspect', { what: 'temporary' })))
.toContain('No dynamic packages are defined in this session')
})
it('refuses a definition with neither half', async () => {
const ctx = await setup()
const result = await call(ctx, 'cordis_define', { name: 'empty', purpose: 'p' })
expect(result.isError).toBe(true)
expect(text(result)).toContain('needs `code` (host half), `client` (browser half), or both')
})
})
describe('cordis_run', () => {
it('starts the host half and reports what it provides', async () => {
const ctx = await setup()
const { value } = await call(ctx, 'cordis_define', { name: 'greeter', purpose: 'p', code: PROVIDER_CODE }) as { value: { id: string } }
const result = await call(ctx, 'cordis_run', { id: value.id })
expect(result.isError).toBe(false)
if (result.isError) throw new Error('expected cordis_run success')
expect(result.value).toEqual({ id: 'dyn-1', rev: 1, provides: ['greeter'], waitingFor: [] })
expect(text(result)).toContain('is running at rev 1: host half is running (provides: greeter)')
expect(ctx.get('greeter')).toBeDefined()
})
it('keeps a package whose host half waits for a service, naming what it waits for', async () => {
const ctx = await setup()
const { value } = await call(ctx, 'cordis_define', { name: 'consumer', purpose: 'p', code: CONSUMER_CODE }) as { value: { id: string } }
const result = await call(ctx, 'cordis_run', { id: value.id })
expect(result.isError).toBe(false)
expect(text(result)).toContain('host half is pending (missing services: greeter)')
expect(ctx.tools.get('greet')).toBeUndefined()
})
it('lets the agent give ITSELF a tool, callable on the next step', async () => {
const ctx = await setup()
await defineAndRun(ctx, REVERSE_TOOL_CODE)
expect(ctx.tools.get('reverse_text')).toBeDefined()
expect(text(await call(ctx, 'reverse_text', { text: 'abc' }))).toBe('cba')
})
it('runs a host-only package again without re-evaluating its host half', async () => {
const ctx = await setup()
const id = await defineAndRun(ctx, PROVIDER_CODE)
// Re-evaluating would collide on the provided service; binding a live host
// half is what lets a second call succeed at all.
const again = await call(ctx, 'cordis_run', { id })
expect(again.isError).toBe(false)
if (again.isError) throw new Error('expected the re-run to succeed')
expect(again.value).toMatchObject({ id, rev: 1 })
})
it('reports a sandbox failure as a tool error, leaving nothing running', async () => {
const ctx = await setup()
const { value } = await call(ctx, 'cordis_define', {
name: 'boom',
purpose: 'p',
code: 'throw new Error(\'host half exploded\')',
}) as { value: { id: string } }
const result = await call(ctx, 'cordis_run', { id: value.id })
expect(result.isError).toBe(true)
expect(text(result)).toContain('host half exploded')
expect(text(await call(ctx, 'cordis_runtime_inspect', { what: 'temporary' }))).toContain('[defined, not running]')
})
it('answers an unknown id with the memory-only explanation', async () => {
const ctx = await setup()
const result = await call(ctx, 'cordis_run', { id: 'dyn-99' })
expect(result.isError).toBe(true)
expect(text(result)).toContain('definitions live in memory only')
})
})
describe('cordis_stop', () => {
it('unwinds the package\'s registrations before it returns, and keeps the definition runnable', async () => {
const ctx = await setup()
const id = await defineAndRun(ctx, REVERSE_TOOL_CODE)
const result = await call(ctx, 'cordis_stop', { id })
expect(result.isError).toBe(false)
if (result.isError) throw new Error('expected cordis_stop success')
expect(result.value).toEqual({ id })
expect(text(result)).toContain('is stopped; its definition remains')
expect(ctx.tools.get('reverse_text')).toBeUndefined()
// Runnable again on a fresh revision, with no code re-sent.
const again = await call(ctx, 'cordis_run', { id })
expect(again.isError).toBe(false)
expect(ctx.tools.get('reverse_text')).toBeDefined()
})
it('refuses to stop a package that is not running', async () => {
const ctx = await setup()
const { value } = await call(ctx, 'cordis_define', { name: 'idle', purpose: 'p', code: PROVIDER_CODE }) as { value: { id: string } }
const result = await call(ctx, 'cordis_stop', { id: value.id })
expect(result.isError).toBe(true)
expect(text(result)).toContain('is not running')
})
})
describe('cordis_undefine', () => {
it('stops a running package, forgets it, and invalidates its id', async () => {
const ctx = await setup()
const log = vi.spyOn(console, 'log').mockImplementation(() => {})
const id = await defineAndRun(ctx, LISTENER_CODE)
const result = await call(ctx, 'cordis_undefine', { id })
expect(result.isError).toBe(false)
if (result.isError) throw new Error('expected cordis_undefine success')
expect(result.value).toEqual({ id, wasRunning: true })
expect(text(result)).toContain('is stopped and undefined')
const calls = log.mock.calls.length
ctx.tools.register({
name: 'post_undefine_trigger',
description: 'test trigger',
parameters: { type: 'object' as const, properties: {} },
output: { schema: { type: 'null' as const }, render: () => [] },
execute: async (): Promise<null> => null,
})
expect(log).toHaveBeenCalledTimes(calls)
expect((await call(ctx, 'cordis_run', { id })).isError).toBe(true)
vi.restoreAllMocks()
})
it('forgets a defined-but-never-run package', async () => {
const ctx = await setup()
const { value } = await call(ctx, 'cordis_define', { name: 'idle', purpose: 'p', code: PROVIDER_CODE }) as { value: { id: string } }
const result = await call(ctx, 'cordis_undefine', { id: value.id })
expect(result.isError).toBe(false)
if (result.isError) throw new Error('expected cordis_undefine success')
expect(result.value).toEqual({ id: value.id, wasRunning: false })
})
})
describe('session scope', () => {
it('hides another session\'s package from every verb', async () => {
const ctx = await setup()
const id = await defineAndRun(ctx, PROVIDER_CODE)
// A call from a different agent addresses a different definition space.
const other = await ctx.tools.execute({
signal: new AbortController().signal,
callId: 'call-other' as never,
name: 'cordis_run',
arguments: { id },
agent: { ...AGENT, id: 'S-other' } as never,
})
expect(other.isError).toBe(true)
expect(text(other)).toContain('no dynamic package')
})
it('refuses a dynamic-package call that arrives without an agent', async () => {
const ctx = await setup()
const result = await ctx.tools.execute({
signal: new AbortController().signal,
callId: 'call-agentless' as never,
name: 'cordis_define',
arguments: { name: 'x', purpose: 'p', code: PROVIDER_CODE },
})
expect(result.isError).toBe(true)
expect(text(result)).toContain('need a session')
})
})
describe('cordis_run with a browser half', () => {
it('reports what each half provides and waits for once a page carried it out', async () => {
const ctx = await setupWithBrowser(['someClientService'])
const defined = await call(ctx, 'cordis_define', {
name: 'both halves',
purpose: 'host + browser',
code: 'return { name: \'both-host\', apply(ctx) { ctx.provide(\'dynBoth\', {}) } }',
client: 'return () => {}',
})
if (defined.isError) throw new Error('define failed')
const result = await call(ctx, 'cordis_run', { id: (defined.value as { id: string }).id })
if (result.isError) throw new Error(text(result))
const value = result.value as { rev: number; provides: string[]; clientWaitingFor?: string[] }
expect(value.rev).toBe(1)
expect(value.provides).toEqual(['dynBoth'])
// The answering page's own parked services ride back to the model.
expect(value.clientWaitingFor).toEqual(['someClientService'])
expect(text(result)).toContain('browser half is pending (missing services: someClientService)')
})
it('reports a browser-only package as running even though no host fiber exists', async () => {
const ctx = await setupWithBrowser()
const defined = await call(ctx, 'cordis_define', {
name: 'browser only',
purpose: 'ui only',
client: 'return () => {}',
})
if (defined.isError) throw new Error('define failed')
const result = await call(ctx, 'cordis_run', { id: (defined.value as { id: string }).id })
if (result.isError) throw new Error(text(result))
const value = result.value as { rev: number; provides: string[]; waitingFor: string[] }
// No host half means no fiber to read provides/waits from — not an error.
expect(value.provides).toEqual([])
expect(value.waitingFor).toEqual([])
expect(text(result)).toContain('host half is running (provides: none)')
})
})
describe('cordis_undefine refusals', () => {
it('fails loud for an id the registry never minted', async () => {
const ctx = await setup()
const result = await call(ctx, 'cordis_undefine', { id: 'dyn-99' })
expect(result.isError).toBe(true)
expect(text(result)).toContain('dyn-99')
})
})
@@ -1,107 +0,0 @@
import { describe, expect, it } from 'vitest'
import { agentEvents } from '@deepseek-ai/dsh-agent'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import type { UserMessage } from '@deepseek-ai/dsh-session'
import { AGENT, call, setup, text } from './helpers.ts'
const HOST = 'return { apply() {} }'
async function preStep(ctx: Awaited<ReturnType<typeof setup>>, messages: UserMessage[]) {
return await agentEvents(ctx, AGENT).waterfall(
'agent/pre-step',
{ messages, turn: 1, step: 1, signal: new AbortController().signal },
() => Promise.resolve({ kind: 'enter' as const, messages }),
)
}
describe('versioned Cordis tools', () => {
it('defines Host and Client code under one code object and returns Host-minted identities', async () => {
const ctx = await setup()
const result = await call(ctx, 'cordis_define', {
plugin: { kind: 'new', idPrefix: 'clock' },
name: 'Clock',
purpose: 'show time',
code: { host: HOST, client: 'return { apply() {} }' },
})
expect(result.isError).toBe(false)
expect(result.value).toMatchObject({
pluginId: 'clock-1',
packageId: 'pkg-1',
hasHostHalf: true,
hasClientHalf: true,
})
expect(result.meta).toEqual({ pluginId: 'clock-1', packageId: 'pkg-1' })
expect(text(result)).toContain('clock-1/pkg-1')
})
it('runs an exact Package and persists Plugin, Package, and Plugin Run metadata', async () => {
const ctx = await setup()
const defined = await call(ctx, 'cordis_define', {
plugin: { kind: 'new', idPrefix: 'clock' },
name: 'Clock',
purpose: 'show time',
code: { host: HOST },
})
const { pluginId, packageId } = defined.value as { pluginId: string; packageId: string }
const result = await call(ctx, 'cordis_run', { pluginId, packageId, mode: 'run' })
expect(result.isError).toBe(false)
expect(result.value).toMatchObject({ pluginId, packageId, pluginRunId: 'run-1' })
expect(result.meta).toEqual({ pluginId, packageId, pluginRunId: 'run-1' })
})
it('injects a source-free Package reference and exposes source only through package inspection', async () => {
const ctx = await setup()
await call(ctx, 'cordis_define', {
plugin: { kind: 'new', idPrefix: 'clock' },
name: 'Clock',
purpose: 'show time',
code: { host: HOST },
})
const prompt = createUserMessage({
content: [{ type: 'text', text: '请修改 @clock-1 的显示' }],
source: { kind: 'user' },
})
const decision = await preStep(ctx, [prompt])
expect(decision.kind).toBe('enter')
if (decision.kind !== 'enter') return
const injected = decision.messages.at(-1)?.content
.flatMap(block => block.type === 'text' ? [block.text] : [])
.join('\n')
expect(injected).toContain('"pluginId": "clock-1"')
expect(injected).toContain('"packageId": "pkg-1"')
expect(injected).not.toContain(HOST)
expect(injected).toContain('cordis_package_inspect')
expect(injected).toContain('plugin.kind="existing"')
expect(injected).toContain('Do not create a new Plugin')
const inspected = await call(ctx, 'cordis_package_inspect', {
pluginId: 'clock-1',
packageId: 'pkg-1',
})
expect(inspected.isError).toBe(false)
expect(inspected.value).toMatchObject({
pluginId: 'clock-1',
packageId: 'pkg-1',
name: 'Clock',
purpose: 'show time',
code: { host: HOST },
})
expect(text(inspected)).toContain(HOST)
})
it('exposes Package inspection through the runtime API catalog', async () => {
const ctx = await setup()
const report = text(await call(ctx, 'cordis_runtime_inspect', {
what: 'api',
name: 'dynamicCordisRunner',
}))
expect(report).toContain('inspectPackage(')
expect(report).toContain('DynamicCordisPackageInspection')
})
})
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/extensions/ui-cordis/README.md
README.md: e3d9e0b13b2a1e2355d8e3e00567b1c63a590a3c
README.zh.md: 46fae942f88916e34fea2583c5cf903fe093c4e0
README.md: 5f354aa95939de57a385921199848d40d4486ed4
README.zh.md: 7109c77c6947d80975eba7b2e2cca24bb6ef60b7
@@ -3,6 +3,7 @@
exports[`single-slot mounting (declare + renderSlot) > folds class hashes and collapses svg internals in snapshots, leaving the live DOM alone 1`] = `
<div
data-slot="trt.panel"
style="display: contents;"
>
<div
class="frame plain"
@@ -24,6 +25,7 @@ exports[`single-slot mounting (declare + renderSlot) > folds class hashes and co
exports[`single-slot mounting edge arms > serializes childless svg untouched next to scoped classes 1`] = `
<div
data-slot="trt.panel"
style="display: contents;"
>
<div
class="frame"
@@ -46,18 +46,26 @@ describe('model-driven dsh-tools generation', () => {
summary: service?.summary,
methods: service?.members
.filter(member => member.kind === 'method' && !member.name.startsWith('['))
.map(member => ({
signature: member.signature,
jsDoc: member.jsDoc ?? '',
})),
}).toEqual(SERVICE_API.find(candidate => candidate.key === 'tools'))
.map(member => member.signature),
}).toEqual((() => {
const api = SERVICE_API.find(candidate => candidate.key === 'tools')
return {
key: api?.key,
summary: api?.summary,
methods: api?.methods.map(method => method.signature),
}
})())
expect(record?.model.events.filter(event => event.name.startsWith('tools/')).map(event => ({
name: event.name,
mode: event.mode,
signature: event.signature,
jsDoc: event.jsDoc ?? '',
summary: event.summary,
}))).toEqual(EVENT_API.filter(event => event.name.startsWith('tools/')))
}))).toEqual(EVENT_API.filter(event => event.name.startsWith('tools/')).map(event => ({
name: event.name,
mode: event.mode,
signature: event.signature,
summary: event.summary,
})))
expect(service?.types.find(type => type.name === 'ToolDefinition')).toEqual(
TYPE_API.find(type => type.name === 'ToolDefinition'),
)