mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
Merge remote-tracking branch 'origin/master' into fix/open-file-fail
This commit is contained in:
@@ -198,9 +198,10 @@
|
||||
toolName: subagent_fork
|
||||
backgroundMode: continuable
|
||||
|
||||
# Production dsh does not install these optional providers. An opting-in
|
||||
# Profile mounts each provider once on the host plane; copy this preset,
|
||||
# then remove `disabled` from the matching tool row.
|
||||
# Production dsh does not install these optional providers. Install the
|
||||
# matching Bundle in this Profile and restart the Host, then copy this
|
||||
# preset and remove `disabled` from the matching tool row. Host availability
|
||||
# alone grants no tool.
|
||||
- id: tool-subagent-codex
|
||||
name: '@deepseek-ai/dsh-tool-subagent'
|
||||
disabled: true
|
||||
|
||||
@@ -185,9 +185,10 @@
|
||||
toolName: subagent_fork
|
||||
backgroundMode: continuable
|
||||
|
||||
# Production dsh does not install these optional providers. An opting-in
|
||||
# Profile mounts each provider once on the host plane; copy this preset,
|
||||
# then remove `disabled` from the matching tool row.
|
||||
# Production dsh does not install these optional providers. Install the
|
||||
# matching Bundle in this Profile and restart the Host, then copy this
|
||||
# preset and remove `disabled` from the matching tool row. Host availability
|
||||
# alone grants no tool.
|
||||
- id: tool-subagent-codex
|
||||
name: '@deepseek-ai/dsh-tool-subagent'
|
||||
disabled: true
|
||||
|
||||
@@ -123,7 +123,16 @@ After a clean mount-validation, ask the user to start a session on the new prese
|
||||
|
||||
## Native product subagents
|
||||
|
||||
Codex and Claude Code providers belong on the host plane but are not installed by production `dsh`. The active Profile must install and mount the selected provider before a preset can expose its ordinary delegation-tool row; never move a product provider into the preset and never add a product-specific settings field.
|
||||
Codex and Claude Code providers are independent optional Profile Bundles. Install only the products a Profile needs, then restart the Profile so its Host registers those providers:
|
||||
|
||||
```sh
|
||||
dsh plugin --profile <name> add @deepseek-ai/dsh-subagent-codex
|
||||
dsh plugin --profile <name> add @deepseek-ai/dsh-subagent-claude-code
|
||||
dsh plugin --profile <name> remove @deepseek-ai/dsh-subagent-codex
|
||||
dsh plugin --profile <name> remove @deepseek-ai/dsh-subagent-claude-code
|
||||
```
|
||||
|
||||
Each Bundle owns its Host availability; the preset separately grants one Agent its ordinary delegation tool. Never move a product provider into the preset and never add a product-specific settings field. Removing one package withdraws only that provider on the next Profile start.
|
||||
|
||||
Copy these disabled templates from a shipped full preset and remove `disabled` only for the products the user requested:
|
||||
|
||||
@@ -149,7 +158,7 @@ Copy these disabled templates from a shipped full preset and remove `disabled` o
|
||||
|
||||
For additional named Codex or Claude Code instances, mount a separate host-plane provider row for each instance with a unique `providerName`, then add a separate preset tool row whose `provider` exactly matches that name and whose `toolName` is also unique. Keep the shipped rows for the default `codex` and `claude-code` names; do not reuse one tool row for several providers or derive either name from permission or environment settings.
|
||||
|
||||
The two rows are independent. Leaving both disabled preserves the copied preset, enabling one exposes only that product tool, and enabling both exposes both. Production `dsh` does not install or mount either optional provider: before enabling a row, the Profile must install the matching `@deepseek-ai/dsh-subagent-codex` or `@deepseek-ai/dsh-subagent-claude-code` package and mount the required provider instances on the host plane. A preset cannot provide that host dependency. `backgroundMode: one-shot` keeps omitted or `false` calls in the foreground and lets explicit `run_in_background: true` return a generic Job id. Full presets already carry `tool-jobs`, while the base host carries the job registry; retain both so `job_output`, `job_list`, `job_kill`, cancellation, and completion notices stay available. The host must also provide `codex` or `claude` on `PATH`; the preset does not install, authenticate, select a model for, or probe either product.
|
||||
The two rows are independent. Leaving both disabled preserves the copied preset, enabling one exposes only that product tool, and enabling both exposes both. Production `dsh` does not install either optional provider: before enabling a row, install the matching `@deepseek-ai/dsh-subagent-codex` or `@deepseek-ai/dsh-subagent-claude-code` Bundle in the Profile and restart it. Each Bundle registers its dormant default provider and exclusively uses its pinned package-local platform CLI; additional named instances use extra host-plane rows from the same installed package. A preset cannot provide that host dependency. `backgroundMode: one-shot` keeps omitted or `false` calls in the foreground and lets explicit `run_in_background: true` return a generic Job id. Full presets already carry `tool-jobs`, while the base host carries the job registry; retain both so `job_output`, `job_list`, `job_kill`, cancellation, and completion notices stay available. Installing a Bundle or composing a preset row does not start a product, authenticate an account, select a model, probe credentials, or manage native product settings.
|
||||
|
||||
## What not to move into a preset
|
||||
|
||||
|
||||
@@ -197,9 +197,10 @@
|
||||
toolName: subagent_fork
|
||||
backgroundMode: continuable
|
||||
|
||||
# Production dsh does not install these optional providers. An opting-in
|
||||
# Profile mounts each provider once on the host plane; copy this preset,
|
||||
# then remove `disabled` from the matching tool row.
|
||||
# Production dsh does not install these optional providers. Install the
|
||||
# matching Bundle in this Profile and restart the Host, then copy this
|
||||
# preset and remove `disabled` from the matching tool row. Host availability
|
||||
# alone grants no tool.
|
||||
- id: tool-subagent-codex
|
||||
name: '@deepseek-ai/dsh-tool-subagent'
|
||||
disabled: true
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write apps/cli/reference/README.md
|
||||
README.md: 0be64fdfc0ad4e81d23f25a26881fa89f37565b0
|
||||
README.zh.md: 649bf15df814abf4875fed794c2e76c494bfcc25
|
||||
README.md: 7828f55a2e4adfd85a0018baada6945ea75aacb0
|
||||
README.zh.md: e14e13731c314efd4d39913b91f2e90ba624e55c
|
||||
|
||||
@@ -42,6 +42,18 @@ dsh --profile web --patch ./extra.yml --dump-config
|
||||
|
||||
`dsh plugin --profile <name> <args...>` initializes the profile when missing (shipped template, or `@deepseek-ai/dsh-base` alone for other names), then forwards `<args...>` to `pnpm` with the profile directory as working directory — `add`, `remove`, `why`, `update`, and every other pnpm verb work unchanged; pnpm must be on PATH. Relative path specs (`.`, `../plugin`, and their `file:`/`link:` forms) are anchored to the invoking directory first, so `add .` from a plugin checkout installs that checkout, not the profile. After every successful run, `dsh.profile.bundles` is reconciled against the installed state: each dependency resolving to a package whose manifest declares `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` joins the layer stack (so an `update` that gains the declaration activates it), a bundle-less dependency stays plain with a one-time warning, and a removed dependency leaves the stack.
|
||||
|
||||
The Codex and Claude Code subagent providers are separate optional Bundles. Add either package, both in one command, or remove either package independently:
|
||||
|
||||
```sh
|
||||
dsh plugin --profile <name> add @deepseek-ai/dsh-subagent-codex
|
||||
dsh plugin --profile <name> add @deepseek-ai/dsh-subagent-claude-code
|
||||
dsh plugin --profile <name> add @deepseek-ai/dsh-subagent-codex @deepseek-ai/dsh-subagent-claude-code
|
||||
dsh plugin --profile <name> remove @deepseek-ai/dsh-subagent-codex
|
||||
dsh plugin --profile <name> remove @deepseek-ai/dsh-subagent-claude-code
|
||||
```
|
||||
|
||||
The successful pnpm operation changes the Profile manifest and Bundle list on disk; a running Profile keeps the Bundle set from its current start. Restart that Profile after adding, removing, or updating a Bundle. This startup boundary applies to Bundle membership, while ordinary edits to the Profile or home `cordis.patch.yml` take effect through hot reload. On the next start, each installed Bundle registers only its dormant Host provider; a copied Preset must separately enable the matching tool row for new Agents. The [Codex provider README](../../../packages/subagent/subagent-codex/README.md) and [Claude Code provider README](../../../packages/subagent/subagent-claude-code/README.md) own executable, authentication, payload, and failure details; the [base Bundle reference](../../../packages/bundle/base/README.md) owns the default dependency closure.
|
||||
|
||||
```sh
|
||||
dsh plugin --profile tui add github:deepseek-harness/turtle-ui
|
||||
dsh plugin --profile tui remove turtle-ui
|
||||
|
||||
@@ -42,6 +42,18 @@ dsh --profile web --patch ./extra.yml --dump-config
|
||||
|
||||
`dsh plugin --profile <name> <args...>` 在 profile 缺失时先初始化它(有随附模板的用模板,其他名称只装 `@deepseek-ai/dsh-base`),然后以 profile 目录为工作目录,把 `<args...>` 转发给 `pnpm`:`add`、`remove`、`why`、`update` 及其他所有 pnpm 子命令都照常可用;pnpm 必须在 PATH 上。相对路径 spec(`.`、`../plugin` 及其 `file:`/`link:` 形式)会先锚定到调用目录,因此在插件 checkout 中执行 `add .` 安装的是该 checkout,而不是 profile。每次成功运行后,系统都会根据当前安装状态更新 `dsh.profile.bundles`:如果某项依赖解析到的包在 manifest 中声明了 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`,该依赖就会加入配置层栈;如果某项依赖在 `update` 后获得该声明,也会随即激活。没有组合包声明的依赖仍作为普通依赖保留,并显示一次性警告;已移除的依赖则从配置层栈中删除。
|
||||
|
||||
Codex 与 Claude Code subagent provider 是两个彼此独立的可选 Bundle。可以只添加一个包、在同一命令中添加两个包,或独立移除任一包:
|
||||
|
||||
```sh
|
||||
dsh plugin --profile <name> add @deepseek-ai/dsh-subagent-codex
|
||||
dsh plugin --profile <name> add @deepseek-ai/dsh-subagent-claude-code
|
||||
dsh plugin --profile <name> add @deepseek-ai/dsh-subagent-codex @deepseek-ai/dsh-subagent-claude-code
|
||||
dsh plugin --profile <name> remove @deepseek-ai/dsh-subagent-codex
|
||||
dsh plugin --profile <name> remove @deepseek-ai/dsh-subagent-claude-code
|
||||
```
|
||||
|
||||
pnpm 操作成功后只会改变磁盘上的 Profile manifest 与 Bundle 列表;正在运行的 Profile 会保留本次启动时的 Bundle 集合。添加、移除或更新 Bundle 后须重启该 Profile。这个启动边界只适用于 Bundle 成员变化,Profile 或 home 中普通 `cordis.patch.yml` 的编辑通过热重载生效。下一次启动时,每个已安装 Bundle 只注册自己的休眠 Host provider;还须在复制出的 Preset 中单独启用对应工具行,新 Agent 才能看到该工具。[Codex provider README](../../../packages/subagent/subagent-codex/README.md)与 [Claude Code provider README](../../../packages/subagent/subagent-claude-code/README.md)负责可执行文件、身份验证、载荷与失败细节;[base Bundle 参考](../../../packages/bundle/base/README.md)负责默认依赖闭包。
|
||||
|
||||
```sh
|
||||
dsh plugin --profile tui add github:deepseek-harness/turtle-ui
|
||||
dsh plugin --profile tui remove turtle-ui
|
||||
|
||||
@@ -650,6 +650,21 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
|
||||
}
|
||||
expect(Object.keys(manifest.dependencies)).toEqual(['anchored-bundle'])
|
||||
expect(manifest.dsh.profile.bundles).toContain('anchored-bundle')
|
||||
|
||||
const removed = await runBuiltBin(
|
||||
['plugin', '--profile', 'anchor', 'remove', 'anchored-bundle'],
|
||||
{ DSH_HOME: home },
|
||||
checkout,
|
||||
)
|
||||
expect(removed.code).toBe(0)
|
||||
const afterRemove = JSON.parse(
|
||||
readFileSync(join(home, 'profiles', 'anchor', 'package.json'), 'utf8'),
|
||||
) as {
|
||||
dependencies?: Record<string, string>
|
||||
dsh: { profile: { bundles: string[] } }
|
||||
}
|
||||
expect(Object.keys(afterRemove.dependencies ?? {})).toEqual([])
|
||||
expect(afterRemove.dsh.profile.bundles).not.toContain('anchored-bundle')
|
||||
} finally {
|
||||
rmSync(home, { recursive: true, force: true })
|
||||
rmSync(checkout, { recursive: true, force: true })
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { mkdir, mkdtemp, readFile, stat, writeFile } from 'node:fs/promises'
|
||||
import { mkdir, mkdtemp, readFile, stat, symlink, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { boot, healProfilesModuleFallback, loadOverlayPatches } from '@deepseek-ai/dsh-app-boot'
|
||||
import { boot, healProfilesModuleFallback, loadOverlayPatches, loadProfile } from '@deepseek-ai/dsh-app-boot'
|
||||
import { provideCmdline } from '@deepseek-ai/dsh-cmdline'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import type { PatchOptions } from '@deepseek-ai/cordis-plugin-include'
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
|
||||
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'
|
||||
@@ -26,9 +26,10 @@ 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')
|
||||
const CODEX_PACKAGE_DIR = join(REPO_ROOT, 'packages/subagent/subagent-codex')
|
||||
const CLAUDE_CODE_PACKAGE_DIR = join(REPO_ROOT, 'packages/subagent/subagent-claude-code')
|
||||
/** 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.
|
||||
@@ -47,12 +48,11 @@ const MINIMAL_BASH_DESCRIPTION = `Run commands in a bash shell
|
||||
async function bootWeb(
|
||||
settingsFile: string,
|
||||
extra: PatchOptions[] = [],
|
||||
extraInstallAnchor?: string,
|
||||
profilePackages: readonly string[] = [],
|
||||
profileBundles?: readonly string[],
|
||||
): Promise<Context> {
|
||||
const storageRoot = join(dirname(settingsFile), 'storages')
|
||||
const patches: PatchOptions[] = [
|
||||
...loadOverlayPatches('dsh-test', BASE_PATCH),
|
||||
...loadOverlayPatches('dsh-test', WEB_PATCH),
|
||||
const overrides: PatchOptions[] = [
|
||||
// The settings row defaults to `$DSH_HOME/settings.yaml`. Left alone it
|
||||
// reads the developer's own document — and since the default preset is a
|
||||
// setting, a stored `agent-presets.default` would decide this file's
|
||||
@@ -115,12 +115,34 @@ async function bootWeb(
|
||||
// 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 })
|
||||
// Product Bundles are installed into the Profile, not the dsh app. Model
|
||||
// pnpm's package link for only the selected products; their own production
|
||||
// dependencies resolve from the linked workspace packages, while shared
|
||||
// peers still resolve through the installation fallback above.
|
||||
for (const packageDir of profilePackages) {
|
||||
const manifest = JSON.parse(await readFile(join(packageDir, 'package.json'), 'utf8')) as { name: string }
|
||||
const link = join(profileDir, 'node_modules', manifest.name)
|
||||
await mkdir(dirname(link), { recursive: true })
|
||||
await symlink(packageDir, link, 'junction')
|
||||
}
|
||||
let bundlePatches: PatchOptions[] = [
|
||||
...loadOverlayPatches('dsh-test', BASE_PATCH),
|
||||
...loadOverlayPatches('dsh-test', WEB_PATCH),
|
||||
]
|
||||
if (profileBundles !== undefined) {
|
||||
await writeFile(join(profileDir, 'package.json'), JSON.stringify({
|
||||
private: true,
|
||||
dependencies: Object.fromEntries(profileBundles.map(name => [name, 'workspace:*'])),
|
||||
dsh: { profile: { bundles: profileBundles } },
|
||||
}, null, 2) + '\n')
|
||||
const profile = loadProfile('dsh-test', 'spec', INSTALL_ANCHOR, home, { userLayer: false })
|
||||
bundlePatches = profile.layers.flatMap(layer => layer.patches)
|
||||
}
|
||||
const rootConfig = join(profileDir, 'cordis.yml')
|
||||
await writeFile(rootConfig, '[]\n')
|
||||
return await boot('dsh-test', rootConfig, patches, (bootCtx) => {
|
||||
return await boot('dsh-test', rootConfig, [...bundlePatches, ...overrides], (bootCtx) => {
|
||||
provideCmdline(bootCtx, { args: [], exit: () => {} })
|
||||
})
|
||||
}
|
||||
@@ -434,17 +456,18 @@ describe('the shipped Web composition', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('product subagent rows in user presets', () => {
|
||||
let productCtx: Context
|
||||
const ids = ['products-none', 'products-codex', 'products-claude', 'products-both'] as const
|
||||
describe('product Bundle and user-preset intersection', () => {
|
||||
const presetIds = ['products-none', 'products-codex', 'products-claude', 'products-both'] as const
|
||||
type Product = 'codex' | 'claude-code'
|
||||
type PresetId = typeof presetIds[number]
|
||||
|
||||
beforeAll(async () => {
|
||||
async function bootProducts(installed: readonly Product[]): Promise<Context> {
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-product-presets-'))
|
||||
const userRoot = join(root, 'presets')
|
||||
const settingsFile = join(root, 'settings.yaml')
|
||||
const standard = await readFile(join(CONFIG_DIR, 'agent-presets', 'standard', 'agent.cordis.yml'), 'utf8')
|
||||
await writeFile(settingsFile, '{}\n')
|
||||
for (const id of ids) {
|
||||
for (const id of presetIds) {
|
||||
let composition = standard
|
||||
if (id === 'products-codex' || id === 'products-both') {
|
||||
composition = enablePresetTool(composition, 'tool-subagent-codex')
|
||||
@@ -456,11 +479,15 @@ describe('product subagent rows in user presets', () => {
|
||||
await mkdir(directory, { recursive: true })
|
||||
await writeFile(join(directory, 'agent.cordis.yml'), composition)
|
||||
}
|
||||
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' },
|
||||
] },
|
||||
const packageDir = (product: Product): string => (
|
||||
product === 'codex' ? CODEX_PACKAGE_DIR : CLAUDE_CODE_PACKAGE_DIR
|
||||
)
|
||||
const packageName = (product: Product): string => (
|
||||
product === 'codex'
|
||||
? '@deepseek-ai/dsh-subagent-codex'
|
||||
: '@deepseek-ai/dsh-subagent-claude-code'
|
||||
)
|
||||
return await bootWeb(settingsFile, [
|
||||
{
|
||||
id: 'agent-presets',
|
||||
config: {
|
||||
@@ -472,46 +499,68 @@ describe('product subagent rows in user presets', () => {
|
||||
includeUserRoot: false,
|
||||
},
|
||||
},
|
||||
], EXAMPLES_INSTALL_ANCHOR)
|
||||
}, 120_000)
|
||||
|
||||
afterAll(async () => {
|
||||
await productCtx.fiber.dispose()
|
||||
})
|
||||
|
||||
it('composes none, either product, or both without changing the shared host registry', async () => {
|
||||
const expected = new Map<string, string[]>([
|
||||
['products-none', []],
|
||||
['products-codex', ['subagent_codex']],
|
||||
['products-claude', ['subagent_claude_code']],
|
||||
['products-both', ['subagent_claude_code', 'subagent_codex']],
|
||||
], installed.map(packageDir), [
|
||||
'@deepseek-ai/dsh-base',
|
||||
'@deepseek-ai/dsh-web-app',
|
||||
...installed.map(packageName),
|
||||
])
|
||||
expect(productCtx.subagents.list()).toEqual(expect.arrayContaining([
|
||||
'spawn', 'fork', 'codex', 'claude-code',
|
||||
]))
|
||||
}
|
||||
|
||||
for (const [id, productTools] of expected) {
|
||||
const handle = await productCtx.agents.create({
|
||||
sessionId: SessionId(`preset-${id}`),
|
||||
setup: agentCtx => productCtx.agentPresets.mount(agentCtx, id).then(() => undefined),
|
||||
})
|
||||
it('composes the intersection of installed Bundles and enabled preset rows', async () => {
|
||||
const enabledByPreset: Record<PresetId, Product[]> = {
|
||||
'products-none': [],
|
||||
'products-codex': ['codex'],
|
||||
'products-claude': ['claude-code'],
|
||||
'products-both': ['codex', 'claude-code'],
|
||||
}
|
||||
const scenarios: Array<{ installed: Product[]; presets: readonly PresetId[] }> = [
|
||||
{ installed: [], presets: ['products-both'] },
|
||||
{ installed: ['codex'], presets: ['products-both'] },
|
||||
{ installed: ['claude-code'], presets: ['products-both'] },
|
||||
{ installed: ['codex', 'claude-code'], presets: presetIds },
|
||||
]
|
||||
|
||||
for (const { installed, presets } of scenarios) {
|
||||
const productCtx = await bootProducts(installed)
|
||||
const spawn = vi.spyOn(productCtx.subprocess, 'spawn')
|
||||
try {
|
||||
const tools = toolNames(productCtx, handle.agent)
|
||||
expect(tools.filter(name => name === 'subagent_codex' || name === 'subagent_claude_code'))
|
||||
.toEqual(productTools)
|
||||
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',
|
||||
])
|
||||
expect(productCtx.subagents.list()
|
||||
.filter(name => name === 'codex' || name === 'claude-code')
|
||||
.sort())
|
||||
.toEqual([...installed].sort())
|
||||
for (const id of presets) {
|
||||
const handle = await productCtx.agents.create({
|
||||
sessionId: SessionId(`preset-${id}-${installed.join('-') || 'none'}-${randomUUID()}`),
|
||||
setup: agentCtx => productCtx.agentPresets.mount(agentCtx, id).then(() => undefined),
|
||||
})
|
||||
try {
|
||||
const productTools = enabledByPreset[id]
|
||||
.filter(product => installed.includes(product))
|
||||
.map(product => product === 'codex' ? 'subagent_codex' : 'subagent_claude_code')
|
||||
.sort()
|
||||
const tools = toolNames(productCtx, handle.agent)
|
||||
expect(tools.filter(name => name === 'subagent_codex' || name === 'subagent_claude_code'))
|
||||
.toEqual(productTools)
|
||||
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',
|
||||
])
|
||||
}
|
||||
} finally {
|
||||
await handle.dispose()
|
||||
}
|
||||
}
|
||||
expect(spawn).not.toHaveBeenCalled()
|
||||
} finally {
|
||||
await handle.dispose()
|
||||
spawn.mockRestore()
|
||||
await productCtx.fiber.dispose()
|
||||
}
|
||||
}
|
||||
})
|
||||
}, 120_000)
|
||||
|
||||
it('applies a product-row edit only to later sessions on the preset', async () => {
|
||||
const productCtx = await bootProducts(['codex'])
|
||||
const preset = await productCtx.agentPresets.resolve('products-none')
|
||||
const original = await readFile(preset.path, 'utf8')
|
||||
const existing = await productCtx.agents.create({
|
||||
@@ -535,8 +584,9 @@ describe('product subagent rows in user presets', () => {
|
||||
} finally {
|
||||
await existing.dispose()
|
||||
await writeFile(preset.path, original)
|
||||
await productCtx.fiber.dispose()
|
||||
}
|
||||
})
|
||||
}, 120_000)
|
||||
})
|
||||
|
||||
describe('a switch survives the session', () => {
|
||||
|
||||
@@ -17,7 +17,7 @@ const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/skill-tool-row', import.
|
||||
const UI_EXPECTED = fileURLToPath(new URL('./snapshots/skill-tool-row/ui.expected.md', import.meta.url))
|
||||
const MODE = webSnapshotMode()
|
||||
const SEED_ID = 'skill-tool-row-web-e2e'
|
||||
const PROMPT = 'Load the snapshot-skill skill with the skill tool, then reply DONE.'
|
||||
const PROMPT = 'Load the editing-cordis-compositions skill with the skill tool, then reply DONE.'
|
||||
|
||||
describe.skipIf(MODE === 'record')('web e2e: dedicated Skill tool row', () => {
|
||||
let scaffold: WebScaffold
|
||||
@@ -53,17 +53,17 @@ describe.skipIf(MODE === 'record')('web e2e: dedicated Skill tool row', () => {
|
||||
it('expands the loaded skill to its exact recorded instructions', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-skill-tool-row'))
|
||||
const call = page.locator('[data-tool="skill"]')
|
||||
const row = call.getByRole('button', { name: 'Skill snapshot-skill' })
|
||||
const row = call.getByRole('button', { name: 'Skill editing-cordis-compositions' })
|
||||
await expect.poll(() => row.getAttribute('aria-expanded')).toBe('false')
|
||||
expect(await call.getByText('snapshot-skill', { exact: true }).count()).toBe(1)
|
||||
expect(await call.getByText('editing-cordis-compositions', { exact: true }).count()).toBe(1)
|
||||
|
||||
await row.click()
|
||||
await expect.poll(() => row.getAttribute('aria-expanded')).toBe('true')
|
||||
await call.getByText('Instructions', { exact: true }).waitFor()
|
||||
const output = call.locator('pre')
|
||||
await output.waitFor()
|
||||
expect(await output.textContent()).toContain('<skill_content name="snapshot-skill">')
|
||||
expect(await output.textContent()).toContain('Follow these snapshot-only instructions.')
|
||||
expect(await output.textContent()).toContain('<skill_content name="editing-cordis-compositions">')
|
||||
expect(await output.textContent()).toContain('Each Bundle registers its dormant default provider and exclusively uses its pinned package-local platform CLI')
|
||||
expect(await output.evaluate(element => getComputedStyle(element.parentElement!).maxHeight)).toBe('260px')
|
||||
|
||||
const snapshot = (await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd))
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user