• feat(self-modification): add dynamic Cordis plugin runtime and UI
This commit is contained in:
@@ -1,7 +1,14 @@
|
||||
// Web e2e scenario for the opt-in Cordis tools. Record mode drives a real
|
||||
// model through inspect, mount, and unmount; replay pins the same shipped Web
|
||||
// composition, durable calls, generic rows, highlighted Plugin source, and
|
||||
// conversation accessibility tree.
|
||||
// model through inspect, define, run, and stop; replay pins the same shipped Web
|
||||
// composition, durable calls, generic rows, the define card's own source view,
|
||||
// and conversation accessibility tree.
|
||||
//
|
||||
// The approval is never in the fixture. The fixture pins what the MODEL said;
|
||||
// tools execute for real, so `cordis_run` genuinely blocks on a person and this
|
||||
// test is that person — which is what lets the run/approve boundary be asserted
|
||||
// instead of assumed. The package therefore carries a browser half whose only
|
||||
// job is to be visible (`[data-snapshot-probe]`): its absence before the answer
|
||||
// and presence after it is the v3 user gate, proven rather than described.
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import type { Browser, Page } from 'playwright'
|
||||
@@ -17,12 +24,21 @@ import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './suppor
|
||||
const FIXTURE = fileURLToPath(new URL('./snapshots/cordis-tool-round/session.jsonl', import.meta.url))
|
||||
const UI_EXPECTED = fileURLToPath(new URL('./snapshots/cordis-tool-round/ui.expected.md', import.meta.url))
|
||||
const MODE = webSnapshotMode()
|
||||
const CORDIS_TOOLS = ['cordis_inspect', 'cordis_mount', 'cordis_unmount'] as const
|
||||
const MOUNT_CODE = 'return { name: "snapshot-noop", apply(ctx) {} }'
|
||||
const PROMPT = 'Use only Cordis tools. First call cordis_inspect with what "temporary". '
|
||||
+ `Then call cordis_mount with this exact code: ${JSON.stringify(MOUNT_CODE)}. `
|
||||
+ 'Read its returned id and call cordis_unmount with that exact id. '
|
||||
+ 'After all three calls succeed, reply exactly CORDIS_UI_DONE and stop.'
|
||||
const CORDIS_TOOLS = ['cordis_runtime_inspect', 'cordis_package_inspect', 'cordis_define', 'cordis_run', 'cordis_stop'] as const
|
||||
const PACKAGE_CODE = 'return { name: "snapshot-noop", apply(ctx) {} }'
|
||||
// The browser half is the PROBE this scenario turns on: it renders a marker into
|
||||
// the frame-wide overlay, so "did the plugin actually run in this page" becomes a
|
||||
// DOM fact. A host-only package would sidestep the approval round trip entirely
|
||||
// (the host runs those immediately), which would drop the v3 user gate out of
|
||||
// coverage — the one thing this scenario exists to prove.
|
||||
const CLIENT_CODE = 'return { inject: ["slots"], apply(ctx) { ctx.slots.register('
|
||||
+ '{ name: "shell.overlay", id: "snapshot-probe" }, '
|
||||
+ '() => React.createElement("div", { "data-snapshot-probe": "loaded" })) } }'
|
||||
const PROMPT = 'Use only Cordis tools. First call cordis_runtime_inspect with what "temporary". '
|
||||
+ 'Then call cordis_define with name "snapshot noop", purpose "does nothing, for the snapshot", '
|
||||
+ `code exactly ${JSON.stringify(PACKAGE_CODE)} and client exactly ${JSON.stringify(CLIENT_CODE)}. `
|
||||
+ 'Read its returned id and call cordis_run with that exact id, then cordis_stop with the same id. '
|
||||
+ 'After all four calls succeed, reply exactly CORDIS_UI_DONE and stop.'
|
||||
|
||||
function assertCompleteCordisLifecycle(events: readonly SessionEvent[]): void {
|
||||
const turnEnd = events.findLast(
|
||||
@@ -82,6 +98,22 @@ describe('web e2e: Cordis tools use the generic row variants', () => {
|
||||
const settled = scaffold.whenTurnSettled()
|
||||
await input.fill(PROMPT)
|
||||
await input.press('Enter')
|
||||
|
||||
// `cordis_run` blocks host-side on a person's answer — no timer, no default.
|
||||
// The approval is the TEST's action in every mode: the fixture pins what the
|
||||
// model said, and the gate is a real round trip through the real panel.
|
||||
const badge = page.locator('[data-cordis-badge]')
|
||||
await expect.poll(() => badge.getAttribute('data-cordis-awaiting'), { timeout: 90_000 }).toBe('true')
|
||||
await badge.click()
|
||||
const approve = page.locator('[data-cordis-approve]').first()
|
||||
await approve.waitFor({ timeout: 10_000 })
|
||||
// The one assertion this scenario cannot give up: the model asking to run is
|
||||
// NOT the plugin running. Until a person answers, the browser half has not
|
||||
// been fetched, evaluated, or mounted anywhere on this page.
|
||||
expect(await page.locator('[data-snapshot-probe]').count()).toBe(0)
|
||||
await approve.click()
|
||||
await expect.poll(() => page.locator('[data-snapshot-probe]').count(), { timeout: 30_000 }).toBe(1)
|
||||
|
||||
const sessionId = await settled
|
||||
if (MODE === 'record') {
|
||||
assertCompleteCordisLifecycle(sessionEvents)
|
||||
@@ -100,20 +132,30 @@ describe('web e2e: Cordis tools use the generic row variants', () => {
|
||||
await expect.poll(() => page.getByText('CORDIS_UI_DONE', { exact: true }).count(), { timeout: 15_000 })
|
||||
.toBeGreaterThanOrEqual(1)
|
||||
|
||||
const inspectRow = page.locator('[data-tool="cordis_inspect"]').filter({ hasText: 'Inspect' }).first()
|
||||
const inspectRow = page.locator('[data-tool="cordis_runtime_inspect"]').filter({ hasText: 'Inspect' }).first()
|
||||
await inspectRow.waitFor({ timeout: 10_000 })
|
||||
|
||||
const mountRow = page.locator('[data-tool="cordis_mount"]').filter({ hasText: 'Mount temporary Plugin' }).first()
|
||||
await mountRow.waitFor({ timeout: 10_000 })
|
||||
// cordis_define does NOT go through the generic row: ui-cordis registers a
|
||||
// keyed toolview for it, and a keyed hit replaces the generic card. So the
|
||||
// title here is the CARD's ("Cordis Plugin"), and the expanded body is the
|
||||
// card's own two code sections rather than a generic args dump.
|
||||
const defineRow = page.locator('[data-tool="cordis_define"]').filter({ hasText: 'Cordis Plugin' }).first()
|
||||
await defineRow.waitFor({ timeout: 10_000 })
|
||||
// The whole summary row is the expand toggle (unified tool-row interaction).
|
||||
await mountRow.locator('[aria-expanded]').first().click()
|
||||
await expect.poll(() => mountRow.locator('pre.shiki').textContent(), { timeout: 10_000 })
|
||||
.toContain(MOUNT_CODE)
|
||||
await defineRow.locator('[aria-expanded]').first().click()
|
||||
await expect.poll(() => defineRow.textContent(), { timeout: 10_000 }).toContain(PACKAGE_CODE)
|
||||
await expect.poll(() => defineRow.textContent()).toContain('data-snapshot-probe')
|
||||
|
||||
const unmountRow = page.locator('[data-tool="cordis_unmount"]').filter({ hasText: 'Unmount temporary Plugin' }).first()
|
||||
await unmountRow.waitFor({ timeout: 10_000 })
|
||||
await expect.poll(() => unmountRow.textContent()).toContain('dyn-')
|
||||
await expect(unmountRow.getAttribute('data-state')).resolves.toBe('ok')
|
||||
const runRow = page.locator('[data-tool="cordis_run"]').filter({ hasText: 'Run dynamic package' }).first()
|
||||
await runRow.waitFor({ timeout: 10_000 })
|
||||
await expect.poll(() => runRow.textContent()).toContain('dyn-')
|
||||
|
||||
const stopRow = page.locator('[data-tool="cordis_stop"]').filter({ hasText: 'Stop dynamic package' }).first()
|
||||
await stopRow.waitFor({ timeout: 10_000 })
|
||||
await expect.poll(() => stopRow.textContent()).toContain('dyn-')
|
||||
await expect(stopRow.getAttribute('data-state')).resolves.toBe('ok')
|
||||
// Stopping withdraws the browser half from every page, probe included.
|
||||
await expect.poll(() => page.locator('[data-snapshot-probe]').count(), { timeout: 15_000 }).toBe(0)
|
||||
})
|
||||
|
||||
it.skipIf(MODE === 'record')('matches the conversation aria golden', async () => {
|
||||
|
||||
@@ -70,6 +70,7 @@ import SessionStore, {
|
||||
type SessionHeader,
|
||||
} from '@deepseek-ai/dsh-session'
|
||||
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
|
||||
import * as CordisHostRunner from '@deepseek-ai/dsh-cordis-host-runner'
|
||||
import * as ToolCordis from '@deepseek-ai/dsh-tool-cordis'
|
||||
// Empty type imports carry the webServer/agents/sessionPersistence Context merges.
|
||||
import type {} from '@deepseek-ai/dsh-host-webserver'
|
||||
@@ -460,8 +461,19 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
|
||||
// be able to change a golden, whatever roots a scenario asks for.
|
||||
: [{ id: 'agent-presets', config: { ...options.agentPresets, includeUserRoot: false } }],
|
||||
...options.toolsMode === undefined ? [] : [{ id: 'tools', config: { mode: options.toolsMode } }],
|
||||
// The host halves ride Loader builtins (below) so the shipped CLI keeps no
|
||||
// dependency on this opt-in package, but the two browser rows must carry
|
||||
// their real package names: the modules node half reads `dshClient` from a
|
||||
// row's resolved package root, and a `cordis:` builtin has none — it is
|
||||
// permanently not a client row, so a builtin here would silently drop the
|
||||
// browser half from the roster.
|
||||
...options.cordisTools === true
|
||||
? [{ insert: [{ id: 'tool-cordis', name: 'cordis:tool-cordis' }] }]
|
||||
? [{ insert: [
|
||||
{ id: 'cordis-host-runner', name: 'cordis:cordis-host-runner' },
|
||||
{ id: 'tool-cordis', name: 'cordis:tool-cordis' },
|
||||
{ id: 'cordis-client-runner', name: '@deepseek-ai/dsh-cordis-client-runner' },
|
||||
{ id: 'ui-cordis', name: '@deepseek-ai/dsh-client-ui-cordis' },
|
||||
] }]
|
||||
: [],
|
||||
...options.deepSeekSearch === undefined
|
||||
? []
|
||||
@@ -515,7 +527,10 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
|
||||
ctx.loader.builtins.group = Group
|
||||
// The shipped CLI deliberately has no dependency on this opt-in package.
|
||||
// Keep the Loader row real without broadening the product installation.
|
||||
if (options.cordisTools === true) ctx.loader.builtins['tool-cordis'] = ToolCordis
|
||||
if (options.cordisTools === true) {
|
||||
ctx.loader.builtins['cordis-host-runner'] = CordisHostRunner
|
||||
ctx.loader.builtins['tool-cordis'] = ToolCordis
|
||||
}
|
||||
await ctx.loader.create({
|
||||
name: 'cordis:include',
|
||||
config: { path: pathToFileURL(rootConfig).href, patches },
|
||||
|
||||
Reference in New Issue
Block a user