Files
deepseek-harness/apps/cli/tests/shipped-composition.e2e.ts
T
NI0317 d558cb4fd4 feat(cli): even out the shipped tool rosters across both surfaces
The two shipped surfaces offered different tools for no recorded reason:
session checkpoints, tool-result pruning, the goal tools, and Ralph were in
`tui.cordis.yml`; `tool-todo` and web search were in `web.cordis.yml`. Neither
offered session search, a string-replacement editor, or a repeat-tool guard,
though none of the three is surface-specific.

Move the rows that are not surface-specific into `base.cordis.yml` and add
those three. Web search moves there too — the TUI decision the change that
made it a Web default deferred. Both surfaces now assemble the same 27 tools.

This adds only. No row is removed from either surface and no existing row's
configuration is edited: executors, sandbox composition, access defaults,
`tools.mode`, and the workflow tool are exactly what they were. Two rows stay
surface-specific: `tmux-context` (no terminal multiplexer in a browser) and
`session-reference` (its index has one writer owner).

Ship `dsh-mcp-client` as a dependency without a row: the plugin mounts one
server per instance and `command` is required, so a default would name a
third-party server and spawn it outside `ctx.bash` on every launch. The CLI
README carries the YAML for mounting one from a personal config.
2026-07-31 17:45:49 +08:00

127 lines
5.5 KiB
TypeScript

import { readdir, readFile } from 'node:fs/promises'
import { fileURLToPath } from 'node:url'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
import { LOADER_SMOKE_TEST_TIMEOUT_MS } from '@deepseek-ai/dsh-loader-smoke'
import type { SessionEvent } from '@deepseek-ai/dsh-session'
import { COMPOSITION_REPLY_TEXT } from './fixtures/composition-echo-llm.ts'
import { COMPOSITION_SETTLED_MARKER } from './fixtures/composition-settled.ts'
import { runTuiPtySmoke } from './pty-harness.ts'
const dshBinScript = fileURLToPath(new URL('../src/bin.ts', import.meta.url))
const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
// An overlay over the shipped tree, so the catalog under test is the one
// `base.cordis.yml` + `tui.cordis.yml` assemble; the tail only swaps the model
// and redirects session artifacts.
const keylessTail = fileURLToPath(new URL('./fixtures/composition-keyless-tail.cordis.yml', import.meta.url))
/**
* The catalog the shipped `dsh` TUI puts in front of the model, as the loop
* logged it, minus the ripgrep-dependent pair below.
* The absences are the composition's security decisions, not incidental gaps:
* `workflow` and the `cordis_*` toolset both execute model-written JavaScript
* that no sandbox row confines, `web_fetch` chooses its own request target, and
* `mcp_*` servers spawn outside `ctx.bash`. `ralph` proves the narrower
* alternative survives: it runs a script this repository fixed at build time,
* so the engine stays mounted without a model-code tool on top of it. The composition Agent Note owns
* the rationale and its sources.
*/
const EXPECTED_TUI_TOOLS = [
'ask_user_question',
'bash',
'create_goal',
'edit',
'exit_plan_mode',
'get_goal',
'ralph',
'read',
'session_event_read',
'session_event_search',
'session_event_trace',
'session_search',
'session_trace',
'skill',
'str_replace_editor',
'subagent',
'subagent_fork',
'task_kill',
'task_list',
'task_output',
'todo_write',
'update_goal',
'web_search',
'workflow',
'write',
]
/**
* `glob` and `grep` come from `dsh-tool-fs-search`, which probes `command -v rg`
* through the mounted bash executor at load and registers neither tool when
* ripgrep is absent. That is a host dependency, not a composition decision, so the
* pair is asserted separately — present together or absent together.
*/
const RIPGREP_TOOLS = ['glob', 'grep']
/** The assembled request header the smoke asserts on. */
interface LoggedHeader {
/** Assembled tool names, sorted. */
names: string[]
/** `bash`'s assembled parameter properties; the escalation pair is present only under a confining executor. */
bashArguments: Record<string, unknown>
}
/**
* Read the request header the loop assembled for its first request from the
* session log the smoke's workspace persisted — the model-visible composition
* itself, not a registry projection taken beside it.
* @param cwd - the smoke's temporary workspace.
* @returns the assembled catalog, system prompt, and `bash` argument shape.
*/
async function loggedHeader(cwd: string): Promise<LoggedHeader> {
const sessionsDir = join(cwd, '.sessions')
const entries = await readdir(sessionsDir, { recursive: true })
// A single keyless run writes one session log.
const logRelPath = entries.find(name => name.endsWith('.jsonl'))
if (logRelPath === undefined) throw new Error(`no session log written under ${sessionsDir}`)
const lines = (await readFile(join(sessionsDir, logRelPath), 'utf8')).split('\n').filter(Boolean)
for (const line of lines) {
const event = JSON.parse(line) as SessionEvent
if (event.type !== 'request/header') continue
const tools = event.data.header.tools ?? []
const bash = tools.find(schema => schema.name === 'bash')
return {
names: tools.map(schema => schema.name).sort(),
bashArguments: (bash?.parameters as { properties?: Record<string, unknown> } | undefined)?.properties ?? {},
}
}
throw new Error(`session log ${logRelPath} has no request/header event`)
}
describe('shipped dsh composition (real Loader tree in a PTY)', () => {
it('assembles exactly the shipped TUI catalog', async () => {
let observed: LoggedHeader | undefined
const output = await runTuiPtySmoke({
label: 'dsh shipped composition',
tempDirPrefix: 'dsh-shipped-tui-',
binScript: dshBinScript,
tsconfigPath,
configPath: keylessTail,
env: { DEEPSEEK_API_KEY: 'keyless-composition-no-call', DSH_TELEMETRY_DISABLED: '1' },
// Artifact CI builds and smokes concurrently on a contended runner.
...(process.env.DSH_EXAMPLE_MODE === 'lib' ? { timeoutMs: 60_000 } : {}),
actions: [
{ waitFor: COMPOSITION_SETTLED_MARKER, send: 'Describe the shipped composition.\r' },
{ waitFor: COMPOSITION_REPLY_TEXT, send: '/exit\r' },
],
inspect: async (cwd) => { observed = await loggedHeader(cwd) },
})
expect(output).toContain(COMPOSITION_REPLY_TEXT)
expect(observed?.names.filter(name => !RIPGREP_TOOLS.includes(name))).toEqual(EXPECTED_TUI_TOOLS)
expect([[], RIPGREP_TOOLS]).toContainEqual(observed?.names.filter(name => RIPGREP_TOOLS.includes(name)))
// The TUI mounts the unrestricted local executors, so `tool-bash` emits no
// escalation pair. Pinning its absence keeps a later sandbox change from
// arriving here unannounced.
expect(Object.keys(observed?.bashArguments ?? {})).not.toContain('sandbox_permissions')
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
})