Merge commit '5ae8c6487ad9556b8793b890a4fe5ece24c55805' into codex/product-subagent-one-shot-background
# Conflicts: # packages/subagent/subagent-codex/README.i18n.yaml # packages/subagent/subagent-codex/README.zh.md # packages/subagent/tool-subagent/tests/tool-subagent.spec.ts
This commit is contained in:
4
apps/cli/tests/fixtures/dsh-badge/cordis.yml
vendored
4
apps/cli/tests/fixtures/dsh-badge/cordis.yml
vendored
@@ -1,9 +1,9 @@
|
||||
- id: skill-badge
|
||||
disabled: false
|
||||
|
||||
- id: skill-local
|
||||
- id: skill-filesystem
|
||||
config:
|
||||
watch: false
|
||||
|
||||
- id: telemetry-otel
|
||||
- id: session-telemetry-otel
|
||||
disabled: true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
- id: skill-local
|
||||
- id: skill-filesystem
|
||||
config:
|
||||
watch: false
|
||||
|
||||
- id: telemetry-otel
|
||||
- id: session-telemetry-otel
|
||||
disabled: true
|
||||
|
||||
@@ -31,7 +31,7 @@ try {
|
||||
steer: () => {},
|
||||
inject: () => { throw new Error('dsh-badge snapshot must receive the catalog at the step boundary') },
|
||||
cancel: () => {},
|
||||
runMaintenance: task => task(new AbortController().signal),
|
||||
runMaintenance: job => job(new AbortController().signal),
|
||||
whenIdle: () => Promise.resolve(),
|
||||
}
|
||||
const decision = await agentEvents(ctx, agent).waterfall(
|
||||
|
||||
@@ -21,15 +21,22 @@ import { describe, expect, it } from 'vitest'
|
||||
const repoRoot = fileURLToPath(new URL('../../../', import.meta.url))
|
||||
const builtBin = join(repoRoot, 'apps/cli/lib/bin.js')
|
||||
const webDist = join(repoRoot, 'apps/web/dist/index.html')
|
||||
// The web bundle's patch owns the session-query-sqlite lazy-open row.
|
||||
const configPath = join(repoRoot, 'packages/bundle/web-app/cordis.patch.yml')
|
||||
// Full-text session search ships off (`openAt: never` on both layers): the
|
||||
// base patch carries the default, and the web restatement must not re-enable it.
|
||||
const baseConfigPath = join(repoRoot, 'packages/bundle/base/cordis.patch.yml')
|
||||
const webConfigPath = join(repoRoot, 'packages/bundle/web-app/cordis.patch.yml')
|
||||
const requireBuiltArtifacts = process.env.DSH_REQUIRE_BUILT_CLI_SMOKE === '1'
|
||||
|
||||
interface ConfigRow {
|
||||
id?: string
|
||||
disabled?: unknown
|
||||
config?: { openAt?: unknown }
|
||||
}
|
||||
|
||||
interface PatchEntry extends ConfigRow {
|
||||
insert?: ConfigRow[]
|
||||
}
|
||||
|
||||
const jsExprType = new yaml.Type('tag:yaml.org,2002:js', {
|
||||
kind: 'scalar',
|
||||
construct: value => String(value),
|
||||
@@ -92,12 +99,20 @@ function runBuiltWeb(cwd: string): Promise<{ stdout: string; stderr: string; cod
|
||||
}
|
||||
|
||||
describe.skipIf(!requireBuiltArtifacts)('built CLI lazy-search startup', () => {
|
||||
it('boots and disposes the shipped composition without a SQLite startup warning', async () => {
|
||||
it('boots and disposes the shipped composition with full-text search off by default', async () => {
|
||||
expect(existsSync(builtBin), `missing built CLI ${resolve(builtBin)}; run pnpm build`).toBe(true)
|
||||
expect(existsSync(webDist), `missing Web dist ${resolve(webDist)}; run pnpm run build:web`).toBe(true)
|
||||
const rows = yaml.load(await readFile(configPath, 'utf8'), { schema: configSchema }) as ConfigRow[]
|
||||
const searchRow = rows.find(row => row.id === 'session-query-sqlite')
|
||||
expect(searchRow?.config?.openAt).toBe('first-search')
|
||||
const baseRows = (yaml.load(await readFile(baseConfigPath, 'utf8'), { schema: configSchema }) as PatchEntry[])
|
||||
.flatMap(entry => entry.insert ?? [entry])
|
||||
const webRows = (yaml.load(await readFile(webConfigPath, 'utf8'), { schema: configSchema }) as PatchEntry[])
|
||||
.flatMap(entry => entry.insert ?? [entry])
|
||||
const baseRow = baseRows.find(row => row.id === 'session-query-sqlite')
|
||||
const webRow = webRows.find(row => row.id === 'session-query-sqlite')
|
||||
expect(baseRow?.config?.openAt).toBe('never')
|
||||
expect(baseRow?.disabled).toBeUndefined()
|
||||
// The web restatement keeps the shipped default; opting in is a later layer's override.
|
||||
expect(webRow?.config?.openAt).toBe('never')
|
||||
expect(webRow?.disabled).toBeUndefined()
|
||||
|
||||
const cwd = await mkdtemp(join(tmpdir(), 'dsh-cli-lazy-search-'))
|
||||
try {
|
||||
|
||||
@@ -12,7 +12,7 @@ import type { Context } from '@deepseek-ai/cordis'
|
||||
import type { PatchOptions } from '@deepseek-ai/cordis-plugin-include'
|
||||
import { boot, loadOverlayPatches } from '@deepseek-ai/dsh-app-boot'
|
||||
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||
import ToolRegistry from '@deepseek-ai/dsh-tools'
|
||||
import ToolRuntime from '@deepseek-ai/dsh-tools'
|
||||
import * as McpClient from '@deepseek-ai/dsh-mcp-client/src/index.ts'
|
||||
|
||||
interface ExampleContract {
|
||||
@@ -123,7 +123,7 @@ describe('third-party memory MCP example overlays', () => {
|
||||
(ctx) => {
|
||||
liveContexts.add(ctx)
|
||||
ctx.loader.builtins['memory-test-system-prompt'] = SystemPrompt
|
||||
ctx.loader.builtins['memory-test-tools'] = ToolRegistry
|
||||
ctx.loader.builtins['memory-test-tools'] = ToolRuntime
|
||||
ctx.loader.builtins['memory-test-mcp-client'] = McpClient
|
||||
},
|
||||
)
|
||||
|
||||
@@ -2,14 +2,14 @@ import { describe, expect, it } from 'vitest'
|
||||
import { resolveTelemetryPatch } from '../src/profile-boot.ts'
|
||||
|
||||
describe('resolveTelemetryPatch', () => {
|
||||
it('keeps telemetry enabled when the switch is unset or empty', () => {
|
||||
it('preserves the configured telemetry mode when the hard-disable switch is unset or empty', () => {
|
||||
expect(resolveTelemetryPatch(undefined, true)).toBeUndefined()
|
||||
expect(resolveTelemetryPatch('', true)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('disables on ANY non-empty value, including falsy-looking ones', () => {
|
||||
for (const value of ['1', '0', 'false', 'no']) {
|
||||
expect(resolveTelemetryPatch(value, true)).toEqual({ id: 'telemetry-otel', disabled: true })
|
||||
expect(resolveTelemetryPatch(value, true)).toEqual({ id: 'session-telemetry-otel', disabled: true })
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import { settingsNamespace } from '@deepseek-ai/dsh-settings'
|
||||
import { resolveSessionPreset, SETTINGS_NAMESPACE } from '@deepseek-ai/dsh-agent-presets'
|
||||
import { applyChildComposition, childSessionMeta } from '@deepseek-ai/dsh-subagent'
|
||||
import { CallId } from '@deepseek-ai/dsh-llm'
|
||||
import type {} from '@deepseek-ai/dsh-compact-basic'
|
||||
import type {} from '@deepseek-ai/dsh-compaction-basic'
|
||||
import type {} from '@deepseek-ai/dsh-skill'
|
||||
import type {} from '@deepseek-ai/dsh-tools'
|
||||
// Type-only: resolves `ctx.get('sessionProjections')` and `ctx.get('tokenMeter')`.
|
||||
@@ -28,6 +28,7 @@ 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')
|
||||
const EXAMPLES_INSTALL_ANCHOR = join(REPO_ROOT, 'examples/package.json')
|
||||
const MINIMAL_PROMPT = 'You are a helpful software engineer assistant.'
|
||||
const MINIMAL_BASH_DESCRIPTION = `Run commands in a bash shell
|
||||
* When invoking this tool, the contents of the "command" parameter does NOT need to be XML-escaped.
|
||||
@@ -43,7 +44,11 @@ const MINIMAL_BASH_DESCRIPTION = `Run commands in a bash shell
|
||||
* 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(settingsFile: string, extra: PatchOptions[] = []): Promise<Context> {
|
||||
async function bootWeb(
|
||||
settingsFile: string,
|
||||
extra: PatchOptions[] = [],
|
||||
extraInstallAnchor?: string,
|
||||
): Promise<Context> {
|
||||
const storageRoot = join(dirname(settingsFile), 'storages')
|
||||
const patches: PatchOptions[] = [
|
||||
...loadOverlayPatches('dsh-test', BASE_PATCH),
|
||||
@@ -67,12 +72,12 @@ async function bootWeb(settingsFile: string, extra: PatchOptions[] = []): Promis
|
||||
// moved into the presets that a host row still waits for. The boot audit
|
||||
// is that assertion.
|
||||
{ id: 'webserver', disabled: true },
|
||||
// The web bundle's runtime row injects `httpServer`, so it cannot
|
||||
// The web bundle's runtime row injects `webServer`, so it cannot
|
||||
// activate without the bound port disabled above. It owns dist serving
|
||||
// and the URL prompt line — surface glue, not anything that decides an
|
||||
// agent's capabilities, which is all this file asserts.
|
||||
{ id: 'web-runtime', disabled: true },
|
||||
{ id: 'telemetry-otel', disabled: true },
|
||||
{ id: 'session-telemetry-otel', disabled: true },
|
||||
// A deployment-level skill on the host registry's GLOBAL layer — the same
|
||||
// registration shape a repository plugin's skill root uses. The layered
|
||||
// skills test below proves it reaches preset-composed agents.
|
||||
@@ -88,7 +93,7 @@ async function bootWeb(settingsFile: string, extra: PatchOptions[] = []): Promis
|
||||
{ id: 'directory-picker', disabled: true },
|
||||
{ insert: [
|
||||
{ id: 'directory-picker-browse', name: '@deepseek-ai/dsh-host-directory-picker-browse' },
|
||||
{ id: 'ui-directory-picker', name: '@deepseek-ai/dsh-client-ui-directory-picker' },
|
||||
{ id: 'ui-directory-picker-browse', name: '@deepseek-ai/dsh-client-ui-directory-picker-browse' },
|
||||
] },
|
||||
// The roster AppCLIEntry would patch in; only the shipped root, so a
|
||||
// developer's own `~/.dsh/.preset` cannot change this test's outcome.
|
||||
@@ -110,6 +115,7 @@ async function bootWeb(settingsFile: string, extra: PatchOptions[] = []): Promis
|
||||
// them resolvable — the same mechanism, not a test-only shim.
|
||||
const home = dirname(settingsFile)
|
||||
healProfilesModuleFallback(INSTALL_ANCHOR, home)
|
||||
if (extraInstallAnchor !== undefined) healProfilesModuleFallback(extraInstallAnchor, home)
|
||||
const profileDir = join(home, 'profiles', 'spec')
|
||||
await mkdir(profileDir, { recursive: true })
|
||||
const rootConfig = join(profileDir, 'cordis.yml')
|
||||
@@ -209,9 +215,8 @@ describe('the shipped Web composition', () => {
|
||||
// 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', 'interrupt_agent', 'list_agents', 'ralph', 'read', 'read_image', 'send_message', 'skill',
|
||||
'subagent', 'subagent_fork', 'task_kill',
|
||||
'task_list', 'task_output', 'todo_write', 'update_goal', 'web_search',
|
||||
'get_goal', 'interrupt_agent', 'job_kill', 'job_list', 'job_output', 'list_agents', 'ralph', 'read', 'read_image', 'send_message', 'skill',
|
||||
'subagent', 'subagent_fork', 'todo_write', 'update_goal', 'web_search',
|
||||
'workflow', 'write',
|
||||
])
|
||||
} finally {
|
||||
@@ -233,8 +238,8 @@ describe('the shipped Web composition', () => {
|
||||
expect(assembly.tools.find(tool => tool.name === 'bash')?.description).toBe(MINIMAL_BASH_DESCRIPTION)
|
||||
expect(JSON.stringify(assembly.tools.find(tool => tool.name === 'str_replace_editor')?.parameters))
|
||||
.toContain('Absolute path')
|
||||
expect(ctx.agentPresets.serviceFor(handle.agent, 'compact')).toBeUndefined()
|
||||
expect(handle.agent.ctx.get('compact')).toBeUndefined()
|
||||
expect(ctx.agentPresets.serviceFor(handle.agent, 'compaction')).toBeUndefined()
|
||||
expect(handle.agent.ctx.get('compaction')).toBeUndefined()
|
||||
} finally {
|
||||
await handle.dispose()
|
||||
}
|
||||
@@ -271,7 +276,10 @@ describe('the shipped Web composition', () => {
|
||||
try {
|
||||
const tools = toolNames(ctx, handle.agent)
|
||||
// The self-referential toolset is what distinguishes this preset.
|
||||
expect(tools).toEqual(expect.arrayContaining(['cordis_inspect', 'cordis_mount', 'cordis_unmount']))
|
||||
expect(tools).toEqual(expect.arrayContaining([
|
||||
'cordis_inspect_list', 'cordis_inspect_query', 'cordis_inspect_self',
|
||||
'cordis_define', 'cordis_run', 'cordis_stop', 'cordis_undefine',
|
||||
]))
|
||||
// And it keeps the standard agent's own tools rather than replacing them.
|
||||
expect(tools).toEqual(expect.arrayContaining(['bash', 'read', 'edit', 'skill']))
|
||||
expect(tools).not.toContain('str_replace_editor')
|
||||
@@ -325,7 +333,7 @@ describe('the shipped Web composition', () => {
|
||||
})
|
||||
try {
|
||||
// Editing the live runtime is opt-in per session, not ambient.
|
||||
expect(toolNames(ctx, handle.agent)).not.toContain('cordis_mount')
|
||||
expect(toolNames(ctx, handle.agent)).not.toContain('cordis_define')
|
||||
} finally {
|
||||
await handle.dispose()
|
||||
}
|
||||
@@ -362,7 +370,7 @@ describe('the shipped Web composition', () => {
|
||||
})
|
||||
try {
|
||||
// The host (global) view carries the deployment-level provider alone:
|
||||
// local discovery moved behind the presets with `skill-local`.
|
||||
// local discovery moved behind the presets with `skill-filesystem`.
|
||||
expect((await ctx.skills.list({ cwd: proj })).map(skill => skill.name)).toEqual(['dsh-badge'])
|
||||
|
||||
// The standard agent's view merges the global layer with its preset's
|
||||
@@ -448,17 +456,23 @@ describe('product subagent rows in user presets', () => {
|
||||
await mkdir(directory, { recursive: true })
|
||||
await writeFile(join(directory, 'agent.cordis.yml'), composition)
|
||||
}
|
||||
productCtx = await bootWeb(settingsFile, [{
|
||||
id: 'agent-presets',
|
||||
config: {
|
||||
default: 'standard',
|
||||
roots: [
|
||||
{ path: join(CONFIG_DIR, 'agent-presets'), trust: 'system' },
|
||||
{ path: userRoot, trust: 'user' },
|
||||
],
|
||||
includeUserRoot: false,
|
||||
productCtx = await bootWeb(settingsFile, [
|
||||
{ insert: [
|
||||
{ id: 'subagent-codex', name: '@deepseek-ai/dsh-subagent-codex' },
|
||||
{ id: 'subagent-claude-code', name: '@deepseek-ai/dsh-subagent-claude-code' },
|
||||
] },
|
||||
{
|
||||
id: 'agent-presets',
|
||||
config: {
|
||||
default: 'standard',
|
||||
roots: [
|
||||
{ path: join(CONFIG_DIR, 'agent-presets'), trust: 'system' },
|
||||
{ path: userRoot, trust: 'user' },
|
||||
],
|
||||
includeUserRoot: false,
|
||||
},
|
||||
},
|
||||
}])
|
||||
], EXAMPLES_INSTALL_ANCHOR)
|
||||
}, 120_000)
|
||||
|
||||
afterAll(async () => {
|
||||
@@ -485,7 +499,7 @@ describe('product subagent rows in user presets', () => {
|
||||
const tools = toolNames(productCtx, handle.agent)
|
||||
expect(tools.filter(name => name === 'subagent_codex' || name === 'subagent_claude_code'))
|
||||
.toEqual(productTools)
|
||||
expect(tools).toEqual(expect.arrayContaining(['task_kill', 'task_list', 'task_output']))
|
||||
expect(tools).toEqual(expect.arrayContaining(['job_kill', 'job_list', 'job_output']))
|
||||
for (const productTool of productTools) {
|
||||
expect(toolParameterNames(productCtx, handle.agent, productTool)).toEqual([
|
||||
'description', 'prompt', 'run_in_background',
|
||||
|
||||
Reference in New Issue
Block a user