• feat(self-modification): add dynamic Cordis plugin runtime and UI

This commit is contained in:
imccyu
2026-08-12 23:51:31 +08:00
parent 0367506471
commit 4064198560
147 changed files with 20904 additions and 2412 deletions

View File

@@ -0,0 +1,88 @@
// Card view model: what a definition card can and cannot derive from the frozen
// call/result slice.
import { describe, expect, it } from 'vitest'
import type { RunningToolCall, ToolResultNode } from '@deepseek-ai/dsh-client-runtime/client'
import { cordisDefineCard } from '../src/client/card-model.ts'
const ARGS = '{"name":"clock","purpose":"顶栏时钟","code":{"client":"return {}","host":"harness.handle(\'now\', () => Date.now())"}}'
function running(over: Partial<RunningToolCall> = {}): RunningToolCall {
return {
callId: 'call-1', name: 'cordis_define', argsRaw: ARGS, turn: 1, step: 1, time: 1_000,
callView: null, subCalls: [], ...over,
}
}
function settled(over: Partial<ToolResultNode> = {}): ToolResultNode {
return {
kind: 'tool-result', seq: 2, time: 2_000, callId: 'call-1',
call: { name: 'cordis_define', argsRaw: ARGS }, callTime: 1_000,
content: [{ type: 'text', text: 'defined dyn-1' }], isError: false,
meta: { pluginId: 'dyn-1', packageId: 'pkg-1' }, callView: null, resultView: null, subCalls: [], ...over,
}
}
describe('cordisDefineCard', () => {
it('reads name, purpose and both code halves off the call arguments', () => {
const card = cordisDefineCard(running())
expect(card).toMatchObject({
name: 'clock', purpose: '顶栏时钟', clientCode: 'return {}', state: 'running', output: null,
})
expect(card.hostCode).toContain('harness.handle')
// The host mints the id during define, so an unsettled call has none and the
// card renders read-only.
expect(card.pluginId).toBeNull()
expect(card.packageId).toBeNull()
})
it('takes the minted id from the result presentation meta', () => {
expect(cordisDefineCard(settled()).pluginId).toBe('dyn-1')
expect(cordisDefineCard(settled()).packageId).toBe('pkg-1')
expect(cordisDefineCard(settled()).output).toBe('defined dyn-1')
expect(cordisDefineCard(settled()).state).toBe('ok')
})
it('renders read-only when the meta carries no usable id', () => {
expect(cordisDefineCard(settled({ meta: undefined })).pluginId).toBeNull()
expect(cordisDefineCard(settled({ meta: 'dyn-1' })).pluginId).toBeNull()
expect(cordisDefineCard(settled({ meta: { pluginId: '' } })).pluginId).toBeNull()
expect(cordisDefineCard(settled({ meta: { pluginId: 7 } })).pluginId).toBeNull()
})
it('classifies the define call’s own lifecycle and never operates a failed one', () => {
const failed = cordisDefineCard(settled({
isError: true, content: [{ type: 'text', text: 'SyntaxError: unexpected token\n at line 3' }],
}))
expect(failed.state).toBe('error')
expect(failed.errorSummary).toBe('SyntaxError: unexpected token')
// A definition that failed to register has nothing to run.
expect(failed.pluginId).toBeNull()
expect(cordisDefineCard(settled({ isError: true, error: { name: 'E', code: 'interrupted' } })).state).toBe('stopped')
expect(cordisDefineCard(settled({ content: [] })).output).toBeNull()
expect(cordisDefineCard(settled({ content: [], error: { name: 'E', code: 'boom' } })).output).toBe('E: boom')
// A non-text block has no display text of its own, so the row shows its JSON.
expect(cordisDefineCard(settled({ content: [{ type: 'reasoning', text: 'weighing it' }] })).output)
.toContain('"type": "reasoning"')
})
it('degrades on a truncated argument stream instead of dropping the row', () => {
expect(cordisDefineCard(running({ argsRaw: '{"name":"clo' })).name).toBe('{"name":"clo')
expect(cordisDefineCard(running({ argsRaw: '{"name":"clo' })).purpose).toBeNull()
expect(cordisDefineCard(running({ argsRaw: '"just a string"' })).name).toBe('"just a string"')
})
it('keeps the raw first line as the name when the arguments carry none', () => {
expect(cordisDefineCard(running({ argsRaw: '{"purpose":"顶栏时钟"}' })).name).toBe('{"purpose":"顶栏时钟"}')
expect(cordisDefineCard(running({ argsRaw: '{"name":"","purpose":"顶栏时钟"}' })).name).toBe('{"name":"","purpose":"顶栏时钟"}')
})
it('reports an unknown name when the event window cut the call head', () => {
// The host definition list answers identity and run state only, so the card
// has no label left to fall back on and names its own call instead.
const card = cordisDefineCard(settled({ call: null }))
expect(card.name).toBeNull()
expect(card.purpose).toBeNull()
})
})

View File

@@ -0,0 +1,142 @@
// The inventory store: how the panel's rows arrive, what a failed read leaves
// behind, and why a read is single-flight.
import { describe, expect, it, vi } from 'vitest'
import { createCordisInventory } from '../src/client/inventory.ts'
import type { CordisDynamicPort, CordisInventoryRow } from '../src/client/dynamic-port.ts'
const ROW = {
id: 'dyn-1', name: 'clock', purpose: '顶栏时钟', agentId: 'sess-1', running: true,
} as unknown as CordisInventoryRow
/** A port whose inventory answer the test controls. */
function port(answer: () => Promise<readonly CordisInventoryRow[]>): { port: CordisDynamicPort; reads: () => number } {
let reads = 0
return {
port: {
inventory: () => { reads += 1; return answer() },
stop: () => Promise.reject(new Error('unused')),
remove: () => Promise.reject(new Error('unused')),
},
reads: () => reads,
}
}
describe('reading the registry', () => {
it('starts unread, then publishes the rows', async () => {
const seam = port(() => Promise.resolve([ROW]))
const inventory = createCordisInventory(seam.port, vi.fn())
// Unread is not empty: the panel must not claim "nothing defined" before a
// read settles.
expect(inventory.getSnapshot()).toEqual({ rows: [], removed: new Set(), read: false })
const seen = vi.fn()
const off = inventory.subscribe(seen)
inventory.refresh()
await vi.waitFor(() => { expect(inventory.getSnapshot().read).toBe(true) })
expect(inventory.getSnapshot().rows).toEqual([ROW])
expect(seen).toHaveBeenCalled()
off()
const before = seen.mock.calls.length
inventory.refresh()
await vi.waitFor(() => { expect(seam.reads()).toBe(2) })
expect(seen.mock.calls.length).toBe(before)
})
it('is single-flight: concurrent triggers read once', async () => {
let release: ((rows: readonly CordisInventoryRow[]) => void) | undefined
const seam = port(() => new Promise((resolve) => { release = resolve }))
const inventory = createCordisInventory(seam.port, vi.fn())
inventory.refresh()
inventory.refresh()
inventory.refresh()
expect(seam.reads()).toBe(1)
release?.([ROW])
await vi.waitFor(() => { expect(inventory.getSnapshot().read).toBe(true) })
// The slot frees once it settles, so the next trigger reads again.
inventory.refresh()
await vi.waitFor(() => { expect(seam.reads()).toBe(2) })
})
it('keeps the rows it had when a read fails, and says why', async () => {
let fail = false
const seam = port(() => (fail ? Promise.reject(new Error('socket closed')) : Promise.resolve([ROW])))
const onError = vi.fn()
const inventory = createCordisInventory(seam.port, onError)
inventory.refresh()
await vi.waitFor(() => { expect(inventory.getSnapshot().read).toBe(true) })
fail = true
inventory.refresh()
await vi.waitFor(() => { expect(inventory.getSnapshot().error).toBeDefined() })
// Dropping the rows would turn a transient wire failure into "nothing is
// defined", which is a different and wrong statement.
expect(inventory.getSnapshot().rows).toEqual([ROW])
expect(inventory.getSnapshot().read).toBe(true)
expect(inventory.getSnapshot().error).toBe('socket closed')
expect(onError).toHaveBeenCalled()
})
it('reports a non-Error rejection without inventing a message', async () => {
// oxlint-disable-next-line typescript/prefer-promise-reject-errors -- the non-Error rejection is the scenario.
const seam = port(() => Promise.reject('nope'))
const inventory = createCordisInventory(seam.port, vi.fn())
inventory.refresh()
await vi.waitFor(() => { expect(inventory.getSnapshot().error).toBeDefined() })
expect(inventory.getSnapshot().error).toBe('reading the cordis inventory failed')
})
it('forgets everything on reset, because the next host may be a new process', async () => {
const seam = port(() => Promise.resolve([ROW]))
const inventory = createCordisInventory(seam.port, vi.fn())
inventory.refresh()
await vi.waitFor(() => { expect(inventory.getSnapshot().read).toBe(true) })
inventory.reset()
expect(inventory.getSnapshot()).toEqual({ rows: [], removed: new Set(), read: false })
})
})
describe('a reconnect while a read is in flight', () => {
it('discards the previous connection’s answer and lets the fresh read through', async () => {
// Each read gets its own resolver, so the test can settle the stale one only.
const releases: ((rows: readonly CordisInventoryRow[]) => void)[] = []
const seam = port(() => new Promise((resolve) => { releases.push(resolve) }))
const inventory = createCordisInventory(seam.port, vi.fn())
inventory.refresh()
expect(seam.reads()).toBe(1)
// The in-flight read belongs to the host we just left; a reset frees the slot
// so the fresh read is not swallowed by it.
inventory.reset()
inventory.refresh()
expect(seam.reads()).toBe(2)
// The stale answer arriving late must not repopulate what reset cleared.
releases[0]?.([ROW])
await Promise.resolve()
await Promise.resolve()
expect(inventory.getSnapshot()).toEqual({ rows: [], removed: new Set(), read: false })
// The fresh read still lands.
releases[1]?.([ROW])
await vi.waitFor(() => { expect(inventory.getSnapshot().read).toBe(true) })
expect(inventory.getSnapshot().rows).toEqual([ROW])
})
it('swallows a stale read’s failure too, rather than blaming the new connection', async () => {
const rejects: ((reason: unknown) => void)[] = []
const seam = port(() => new Promise((_resolve, reject) => { rejects.push(reject) }))
const onError = vi.fn()
const inventory = createCordisInventory(seam.port, onError)
inventory.refresh()
inventory.reset()
rejects[0]?.(new Error('socket closed'))
await Promise.resolve()
await Promise.resolve()
// The failure belongs to a connection nobody is looking at any more.
expect(onError).not.toHaveBeenCalled()
expect(inventory.getSnapshot()).toEqual({ rows: [], removed: new Set(), read: false })
})
})

View File

@@ -0,0 +1,133 @@
import { describe, expect, it, vi } from 'vitest'
import type { DynamicCordisLivePackage } from '@deepseek-ai/dsh-cordis-client-runner/client'
import type {
CordisDynamicPackageId, CordisDynamicPluginId, CordisDynamicPluginRunId,
DynamicCordisInventoryRow,
} from '../src/client/events.ts'
import { cordisDefineCard, cordisRunCard } from '../src/client/card-model.ts'
import { CordisRunCardRegistry, cordisToolViewKey } from '../src/client/run-card-index.ts'
import { cordisVisibleStatus } from '../src/client/status.ts'
const PLUGIN = 'clock-1' as CordisDynamicPluginId
const PACKAGE = 'pkg-1' as CordisDynamicPackageId
const RUN = 'run-1' as CordisDynamicPluginRunId
const row = (client: boolean): DynamicCordisInventoryRow => ({
pluginId: PLUGIN,
agentId: 'session-1' as DynamicCordisInventoryRow['agentId'],
packages: [{
packageId: PACKAGE,
name: 'Clock',
purpose: 'show time',
hasHostHalf: true,
hasClientHalf: client,
}],
currentPackageId: PACKAGE,
activeRun: { packageId: PACKAGE, pluginRunId: RUN },
})
describe('versioned Cordis card models', () => {
it('reads symmetric Host and Client source fields from cordis_define', () => {
const card = cordisDefineCard({
callId: 'call-1',
name: 'cordis_define',
argsRaw: JSON.stringify({
plugin: { kind: 'new', idPrefix: 'clock' },
name: 'Clock',
purpose: 'show time',
code: { host: 'HOST_CODE', client: 'CLIENT_CODE' },
}),
turn: 1,
step: 1,
time: 1,
callView: null,
subCalls: [],
})
expect(card).toMatchObject({
pluginId: null,
packageId: null,
hostCode: 'HOST_CODE',
clientCode: 'CLIENT_CODE',
state: 'running',
})
})
it('reads exact activation metadata from a successful cordis_run result', () => {
const card = cordisRunCard({
kind: 'tool-result',
seq: 9,
time: 2,
callId: 'call-2',
call: { name: 'cordis_run', argsRaw: JSON.stringify({ pluginId: PLUGIN, packageId: PACKAGE, mode: 'run' }) },
callTime: 1,
content: [{ type: 'text', text: 'running' }],
isError: false,
meta: { pluginId: PLUGIN, packageId: PACKAGE, pluginRunId: RUN },
callView: null,
resultView: null,
subCalls: [],
})
expect(card).toMatchObject({
pluginId: PLUGIN,
packageId: PACKAGE,
pluginRunId: RUN,
mode: 'run',
seq: 9,
state: 'ok',
})
})
it('keeps the target identities while cordis_run waits for approval', () => {
const card = cordisRunCard({
callId: 'call-3',
name: 'cordis_run',
argsRaw: JSON.stringify({ pluginId: PLUGIN, packageId: PACKAGE, mode: 'update' }),
turn: 1,
step: 1,
time: 1,
callView: null,
subCalls: [],
})
expect(card).toMatchObject({
pluginId: PLUGIN,
packageId: PACKAGE,
pluginRunId: null,
mode: 'update',
state: 'running',
})
})
})
describe('Cordis run-card ownership', () => {
it('keeps the greatest Session log sequence for one Plugin and Package', () => {
const store = new CordisRunCardRegistry().forSession('session-1' as DynamicCordisInventoryRow['agentId'])
const changed = vi.fn()
store.subscribe(changed)
const key = cordisToolViewKey(PLUGIN, PACKAGE)
store.observe({ key, callId: 'new', seq: 20, pluginRunId: RUN })
store.observe({ key, callId: 'old', seq: 10, pluginRunId: 'run-0' as CordisDynamicPluginRunId })
expect(store.getSnapshot().get(key)?.callId).toBe('new')
expect(changed).toHaveBeenCalledTimes(1)
})
})
describe('Cordis visible status', () => {
it('distinguishes Host-only running, Client pending, and fully loaded', () => {
expect(cordisVisibleStatus(row(false), PACKAGE, [])).toBe('running')
expect(cordisVisibleStatus(row(true), PACKAGE, [])).toBe('client-pending')
const loaded: DynamicCordisLivePackage[] = [{
pluginId: PLUGIN,
packageId: PACKAGE,
pluginRunId: RUN,
name: 'Clock',
slots: [],
styleCount: 0,
}]
expect(cordisVisibleStatus(row(true), PACKAGE, loaded)).toBe('running')
})
})