mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
`apps/cli/config/base.cordis.yml` and `web.cordis.yml` no longer exist — the
bundle split replaced them with `packages/bundle/{base,web-app}/cordis.patch.yml`
— so this file booted a path that was deleted under it and every CI run since
the merge failed at `ENOENT`.
It now boots what `dsh web` boots: an empty preset root with the two bundle
patches over it. That root sits outside the workspace, so bare plugin names
cannot resolve by Node's upward walk and the flat module fallback the preset
boot maintains is what makes them resolvable — the same mechanism, not a
test-only shim. That fallback links each package's PUBLISHED entry, so this
file now consumes the artifact plane and moves to the lane that builds first
(`.e2e.ts`, beside `built-bin.e2e.ts`), rather than the coverage lane, which
installs and runs on a clean tree.
238 lines
11 KiB
TypeScript
238 lines
11 KiB
TypeScript
import { mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises'
|
|
import { tmpdir } from 'node:os'
|
|
import { fileURLToPath } from 'node:url'
|
|
import { join } from 'node:path'
|
|
import { Context } from 'cordis'
|
|
import { boot, healProfilesModuleFallback, loadOverlayPatches } from '@deepseek-ai/dsh-app-boot'
|
|
import { SessionId } from '@deepseek-ai/dsh-session'
|
|
import type { Agent } from '@deepseek-ai/dsh-agent'
|
|
import type { PatchOptions } from '@cordisjs/plugin-include'
|
|
import { beforeAll, describe, expect, it } from 'vitest'
|
|
import type {} from '@deepseek-ai/dsh-agent-presets'
|
|
import type {} from '@deepseek-ai/dsh-tools'
|
|
|
|
const CONFIG_DIR = fileURLToPath(new URL('../config/', import.meta.url))
|
|
const REPO_ROOT = fileURLToPath(new URL('../../..', import.meta.url))
|
|
/** The shipped Web surface: the dsh-base and dsh-web-app bundle patches over an empty preset root. */
|
|
const BASE_PATCH = join(REPO_ROOT, 'packages/bundle/base/cordis.patch.yml')
|
|
const WEB_PATCH = join(REPO_ROOT, 'packages/bundle/web-app/cordis.patch.yml')
|
|
/** The installation anchor whose dependency surface the preset module fallback mirrors. */
|
|
const INSTALL_ANCHOR = join(REPO_ROOT, 'apps/cli/package.json')
|
|
|
|
/**
|
|
* Boot the shipped Web composition, minus the rows that would bind a port,
|
|
* touch the network, or write outside the test. Everything that decides an
|
|
* agent's capabilities is the real thing, including both shipped presets.
|
|
*/
|
|
async function bootWeb(): Promise<Context> {
|
|
const patches: PatchOptions[] = [
|
|
...loadOverlayPatches('dsh-test', BASE_PATCH),
|
|
...loadOverlayPatches('dsh-test', WEB_PATCH),
|
|
// Host rows with side effects outside this process: a bound port, a
|
|
// served asset tree, a telemetry exporter.
|
|
{ id: 'webserver', disabled: true },
|
|
// Waits for `httpServer`, which the disabled webserver above provides.
|
|
{ id: 'web-runtime', disabled: true },
|
|
{ id: 'telemetry-otel', disabled: true },
|
|
{ id: 'modules', disabled: true },
|
|
{ id: 'connection', disabled: true },
|
|
// NOT a side-effect row: the api-proxy cannot mount in THIS layer at all,
|
|
// because it injects `subagents` and the subagent registry moved into the
|
|
// presets here. That is the breakage a later layer returns to the host
|
|
// plane; when it does, this line comes out and the boot audit covers the
|
|
// whole host-plane injection graph again.
|
|
{ id: 'api-gateway', disabled: true },
|
|
{ id: 'directory-picker', disabled: true },
|
|
// The roster AppCLIEntry would patch in; only the shipped root, so a
|
|
// developer's own `~/.dsh/.preset` cannot change this test's outcome.
|
|
{
|
|
id: 'agent-presets',
|
|
config: { default: 'standard', roots: [{ path: join(CONFIG_DIR, 'agent-presets'), trust: 'system' }] },
|
|
},
|
|
]
|
|
// The composition boots from an empty preset root, exactly as `dsh web`
|
|
// does: the root's own directory is outside this workspace, so bare plugin
|
|
// names cannot resolve by Node's upward walk. The flat fallback the preset
|
|
// boot maintains is what makes them resolvable — the same mechanism, not a
|
|
// test-only shim. It links each package's published entry, so this file
|
|
// consumes the ARTIFACT plane and lives in the lane that builds.
|
|
const home = await mkdtemp(join(tmpdir(), 'dsh-web-presets-'))
|
|
healProfilesModuleFallback(INSTALL_ANCHOR, home)
|
|
const presetDir = join(home, 'profiles', 'spec')
|
|
await mkdir(presetDir, { recursive: true })
|
|
const rootConfig = join(presetDir, 'cordis.yml')
|
|
await writeFile(rootConfig, '[]\n')
|
|
return await boot('dsh-test', rootConfig, patches)
|
|
}
|
|
|
|
const toolNames = (ctx: Context, agent?: Agent): string[] =>
|
|
ctx.tools.schemas(agent).map(schema => schema.name).sort()
|
|
|
|
let ctx: Context
|
|
beforeAll(async () => {
|
|
ctx = await bootWeb()
|
|
}, 120_000)
|
|
|
|
describe('the shipped Web composition', () => {
|
|
it('leaves the global tool layer empty', () => {
|
|
// Every model-facing tool belongs to a preset, `ask_user_question`
|
|
// included: a tool in the global layer reaches EVERY agent regardless of
|
|
// which preset composed it, so a two-tool benchmark surface would really
|
|
// present three. A regression here means an agent-plane row came back to
|
|
// the host composition.
|
|
expect(toolNames(ctx)).toEqual([])
|
|
})
|
|
|
|
it('supplies both shipped presets, and only those, from the system root', async () => {
|
|
const listed = await ctx.agentPresets.list()
|
|
|
|
expect(listed.map(preset => preset.id).sort()).toEqual(['core-web', 'standard'])
|
|
expect(listed.every(preset => preset.trust === 'system')).toBe(true)
|
|
expect(ctx.agentPresets.defaultId).toBe('standard')
|
|
})
|
|
|
|
it('composes the full agent from `standard`', async () => {
|
|
const handle = await ctx.agents.create({
|
|
sessionId: SessionId('preset-standard'),
|
|
setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'standard').then(() => undefined),
|
|
})
|
|
try {
|
|
// The EXACT catalog, not a spot-check: an omission is this design's
|
|
// quietest failure mode, because a row that registers into the wrong
|
|
// layer mounts cleanly and simply contributes nothing. `glob`/`grep` are
|
|
// excluded for the reason the TUI composition e2e excludes them — they
|
|
// depend on ripgrep being present on the machine.
|
|
expect(toolNames(ctx, handle.agent).filter(name => name !== 'glob' && name !== 'grep')).toEqual([
|
|
'ask_user_question', 'bash', 'create_goal', 'edit', 'exit_plan_mode',
|
|
'get_goal', 'list_agents', 'ralph', 'read', 'send_message', 'skill',
|
|
'str_replace_editor', 'subagent', 'subagent_fork', 'task_kill',
|
|
'task_list', 'task_output', 'todo_write', 'update_goal', 'web_search',
|
|
'workflow', 'write',
|
|
])
|
|
} finally {
|
|
await handle.dispose()
|
|
}
|
|
})
|
|
|
|
it('composes exactly two tools from `core-web`', async () => {
|
|
const handle = await ctx.agents.create({
|
|
sessionId: SessionId('preset-core-web'),
|
|
setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'core-web').then(() => undefined),
|
|
})
|
|
try {
|
|
// Exactly what the preset lists — nothing arrives from the host.
|
|
expect(toolNames(ctx, handle.agent)).toEqual(['bash', 'str_replace_editor'])
|
|
} finally {
|
|
await handle.dispose()
|
|
}
|
|
})
|
|
|
|
it('keeps two differently composed sessions independent', async () => {
|
|
const full = await ctx.agents.create({
|
|
sessionId: SessionId('preset-both-full'),
|
|
setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'standard').then(() => undefined),
|
|
})
|
|
const minimal = await ctx.agents.create({
|
|
sessionId: SessionId('preset-both-minimal'),
|
|
setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'core-web').then(() => undefined),
|
|
})
|
|
try {
|
|
expect(toolNames(ctx, minimal.agent)).toEqual(['bash', 'str_replace_editor'])
|
|
expect(toolNames(ctx, full.agent).length).toBeGreaterThan(10)
|
|
|
|
await minimal.dispose()
|
|
|
|
// Tearing the minimal session down leaves the full one whole.
|
|
expect(toolNames(ctx, full.agent).length).toBeGreaterThan(10)
|
|
expect(toolNames(ctx)).toEqual([])
|
|
} finally {
|
|
await full.dispose()
|
|
}
|
|
})
|
|
|
|
it('never rewrites the preset file it composed from', async () => {
|
|
// The Loader persists a tree whose plugin self-disposed, and tearing an
|
|
// agent down disposes its whole subtree. Inherited, that rewrote the
|
|
// shipped composition — truncating it to `[]` the first time a session
|
|
// ended — so `PresetTree` refuses to write at all.
|
|
const path = join(CONFIG_DIR, 'agent-presets', 'standard', 'agent.cordis.yml')
|
|
const before = await readFile(path, 'utf8')
|
|
|
|
const handle = await ctx.agents.create({
|
|
sessionId: SessionId('preset-readonly'),
|
|
setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'standard').then(() => undefined),
|
|
})
|
|
await handle.dispose()
|
|
// Slack, not a race the number has to win. The write is driven by the
|
|
// Loader's fiber-unload listener, which fires as the subtree's fibers
|
|
// settle rather than when `dispose()` resolves, and the Loader exposes no
|
|
// flush to await. A regression writes synchronously inside that listener,
|
|
// so any wait past settlement fails; a longer one only slows the test.
|
|
await new Promise(resolve => setTimeout(resolve, 50))
|
|
|
|
expect(await readFile(path, 'utf8')).toBe(before)
|
|
})
|
|
|
|
it('gives each session its own persona', async () => {
|
|
const handle = await ctx.agents.create({
|
|
sessionId: SessionId('preset-persona'),
|
|
setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'core-web').then(() => undefined),
|
|
})
|
|
try {
|
|
const assembly = await ctx.systemPrompt.assemble({ scope: handle.agent })
|
|
expect(assembly.sections.find(section => section.name === 'deployment:persona')?.text)
|
|
.toContain('You are a coding agent powered by')
|
|
} finally {
|
|
await handle.dispose()
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('a forked session', () => {
|
|
it('inherits the composition its seeded history was produced under', async () => {
|
|
const parent = await ctx.agents.create({
|
|
sessionId: SessionId('preset-fork-parent'),
|
|
meta: { agentPreset: 'core-web' },
|
|
setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'core-web').then(() => undefined),
|
|
})
|
|
const inherited = parent.agent.session.header.agentPreset
|
|
const child = await ctx.agents.create({
|
|
sessionId: SessionId('preset-fork-child'),
|
|
meta: {
|
|
parentSession: SessionId('preset-fork-parent'),
|
|
seedLength: 0,
|
|
...inherited === undefined ? {} : { agentPreset: inherited },
|
|
},
|
|
setup: agentCtx => ctx.agentPresets.mount(agentCtx, inherited).then(() => undefined),
|
|
})
|
|
try {
|
|
// Composing nothing would leave the child empty: this layer moved every
|
|
// model-facing row out of the host plane, so there is nothing to inherit
|
|
// for free any more.
|
|
expect(toolNames(ctx, child.agent)).toEqual(toolNames(ctx, parent.agent))
|
|
expect(toolNames(ctx, child.agent).length).toBeGreaterThan(0)
|
|
} finally {
|
|
await child.dispose()
|
|
await parent.dispose()
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('a session keeps the preset it was created with', () => {
|
|
it('records the preset the gateway guard reads', async () => {
|
|
const handle = await ctx.agents.create({
|
|
sessionId: SessionId('preset-locked'),
|
|
meta: { agentPreset: 'core-web' },
|
|
setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'core-web').then(() => undefined),
|
|
})
|
|
try {
|
|
// The api-proxy guard reads exactly this: the header records what the
|
|
// session runs, so naming anything else is a caller error rather than a
|
|
// switch. Its history was produced under `core-web`'s two tools.
|
|
expect(handle.agent.session.header.agentPreset).toBe('core-web')
|
|
} finally {
|
|
await handle.dispose()
|
|
}
|
|
})
|
|
})
|