mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
Merge pull request #2234 from deepseek-harness/feat/loader-entry-disabled-interpolation
feat(loader): interpolate the entry disabled field
This commit is contained in:
@@ -60,6 +60,8 @@ flowchart LR
|
||||
cfg --> plugin_dsh_base_sandbox_policy
|
||||
plugin_dsh_base_bash_sandbox["bash-sandbox<br/>@deepseek-ai/dsh-bash-sandbox"]
|
||||
cfg --> plugin_dsh_base_bash_sandbox
|
||||
plugin_dsh_base_pwsh_sandbox["pwsh-sandbox<br/>@deepseek-ai/dsh-pwsh-sandbox"]
|
||||
cfg --> plugin_dsh_base_pwsh_sandbox
|
||||
plugin_dsh_base_approval["approval<br/>@deepseek-ai/dsh-user-approval"]
|
||||
cfg --> plugin_dsh_base_approval
|
||||
plugin_dsh_base_permission["permission<br/>@deepseek-ai/dsh-permission"]
|
||||
@@ -68,6 +70,8 @@ flowchart LR
|
||||
cfg --> plugin_dsh_base_bash_env
|
||||
plugin_dsh_base_tool_bash["tool-bash<br/>@deepseek-ai/dsh-tool-bash"]
|
||||
cfg --> plugin_dsh_base_tool_bash
|
||||
plugin_dsh_base_tool_pwsh["tool-pwsh<br/>@deepseek-ai/dsh-tool-pwsh"]
|
||||
cfg --> plugin_dsh_base_tool_pwsh
|
||||
plugin_dsh_base_tool_tasks["tool-tasks<br/>@deepseek-ai/dsh-tool-tasks"]
|
||||
cfg --> plugin_dsh_base_tool_tasks
|
||||
plugin_dsh_base_fs_policy["fs-policy<br/>@deepseek-ai/dsh-fs-policy"]
|
||||
@@ -194,10 +198,12 @@ flowchart LR
|
||||
| `sandbox` | `@deepseek-ai/dsh-sandbox-local` |
|
||||
| `sandbox-policy` | `@deepseek-ai/dsh-sandbox-policy` |
|
||||
| `bash-sandbox` | `@deepseek-ai/dsh-bash-sandbox` |
|
||||
| `pwsh-sandbox` | `@deepseek-ai/dsh-pwsh-sandbox` |
|
||||
| `approval` | `@deepseek-ai/dsh-user-approval` |
|
||||
| `permission` | `@deepseek-ai/dsh-permission` |
|
||||
| `bash-env` | `@deepseek-ai/dsh-bash-env` |
|
||||
| `tool-bash` | `@deepseek-ai/dsh-tool-bash` |
|
||||
| `tool-pwsh` | `@deepseek-ai/dsh-tool-pwsh` |
|
||||
| `tool-tasks` | `@deepseek-ai/dsh-tool-tasks` |
|
||||
| `fs-policy` | `@deepseek-ai/dsh-fs-policy` |
|
||||
| `tool-fs` | `@deepseek-ai/dsh-tool-fs` |
|
||||
|
||||
@@ -45,11 +45,16 @@
|
||||
# publish `DSH_WEB_URL`/`DSH_WEB_MODE`, and a host row that injects a service is
|
||||
# the criterion for host-plane ownership — injection resolves before any session
|
||||
# exists, so there is no agent to key by. Behind a preset realm those variables
|
||||
# never reached the model's shell at all. `tool-bash` consumes the host registry
|
||||
# from here; the executor behind it (`bash-sandbox`) is host-plane too, where the
|
||||
# sandbox policy owns it.
|
||||
# never reached the model's shell at all. Both shell tools consume the host
|
||||
# registry from here; their executors (`bash-sandbox`/`pwsh-sandbox`) are
|
||||
# host-plane too.
|
||||
- id: tool-bash
|
||||
name: '@deepseek-ai/dsh-tool-bash'
|
||||
disabled: !!js process.platform === 'win32'
|
||||
|
||||
- id: tool-pwsh
|
||||
name: '@deepseek-ai/dsh-tool-pwsh'
|
||||
disabled: !!js process.platform !== 'win32'
|
||||
|
||||
# ── filesystem ──────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -39,11 +39,16 @@
|
||||
# publish `DSH_WEB_URL`/`DSH_WEB_MODE`, and a host row that injects a service is
|
||||
# the criterion for host-plane ownership — injection resolves before any session
|
||||
# exists, so there is no agent to key by. Behind a preset realm those variables
|
||||
# never reached the model's shell at all. `tool-bash` consumes the host registry
|
||||
# from here; the executor behind it (`bash-sandbox`) is host-plane too, where the
|
||||
# sandbox policy owns it.
|
||||
# never reached the model's shell at all. Both shell tools consume the host
|
||||
# registry from here; their executors (`bash-sandbox`/`pwsh-sandbox`) are
|
||||
# host-plane too.
|
||||
- id: tool-bash
|
||||
name: '@deepseek-ai/dsh-tool-bash'
|
||||
disabled: !!js process.platform === 'win32'
|
||||
|
||||
- id: tool-pwsh
|
||||
name: '@deepseek-ai/dsh-tool-pwsh'
|
||||
disabled: !!js process.platform !== 'win32'
|
||||
|
||||
# ── filesystem ──────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -38,11 +38,16 @@
|
||||
# publish `DSH_WEB_URL`/`DSH_WEB_MODE`, and a host row that injects a service is
|
||||
# the criterion for host-plane ownership — injection resolves before any session
|
||||
# exists, so there is no agent to key by. Behind a preset realm those variables
|
||||
# never reached the model's shell at all. `tool-bash` consumes the host registry
|
||||
# from here; the executor behind it (`bash-sandbox`) is host-plane too, where the
|
||||
# sandbox policy owns it.
|
||||
# never reached the model's shell at all. Both shell tools consume the host
|
||||
# registry from here; their executors (`bash-sandbox`/`pwsh-sandbox`) are
|
||||
# host-plane too.
|
||||
- id: tool-bash
|
||||
name: '@deepseek-ai/dsh-tool-bash'
|
||||
disabled: !!js process.platform === 'win32'
|
||||
|
||||
- id: tool-pwsh
|
||||
name: '@deepseek-ai/dsh-tool-pwsh'
|
||||
disabled: !!js process.platform !== 'win32'
|
||||
|
||||
# ── filesystem ──────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
type ConfigDumpLayer,
|
||||
} from '@deepseek-ai/dsh-app-boot'
|
||||
import { homePatchPath, prepareProfile, PROFILE_ROOT_FILENAME } from './profile-boot.ts'
|
||||
import { resolveWindowsShellLayer } from './windows-shell.ts'
|
||||
|
||||
const NAME = 'dsh'
|
||||
|
||||
@@ -34,12 +33,6 @@ export function runDumpConfig(profile: string, defaultOnly: boolean, patches: re
|
||||
label: layer.packageName,
|
||||
patches: layer.patches,
|
||||
}))
|
||||
// The win32 shell platform layer rides between bundles and user layers,
|
||||
// exactly where the boot applies it.
|
||||
const windowsShellLayer = resolveWindowsShellLayer(process.platform, loaded.layers, NAME)
|
||||
if (windowsShellLayer !== undefined) {
|
||||
layers.push({ label: windowsShellLayer.label, patches: windowsShellLayer.patches })
|
||||
}
|
||||
if (!defaultOnly) {
|
||||
if (existsSync(loaded.patchPath)) {
|
||||
layers.push({ label: loaded.patchPath, patches: loaded.patches })
|
||||
|
||||
@@ -39,7 +39,6 @@ const USER_PRESET_DIR = '.agent-presets'
|
||||
import { DSH_ENVIRONMENT_KEY, type EnvironmentSnapshot } from '@deepseek-ai/dsh-environment'
|
||||
import { provideCmdline } from '@deepseek-ai/dsh-cmdline'
|
||||
import { createProcessShutdown, type ProcessShutdown } from './process-shutdown.ts'
|
||||
import { resolveWindowsShellLayer } from './windows-shell.ts'
|
||||
|
||||
const NAME = 'dsh'
|
||||
|
||||
@@ -110,8 +109,6 @@ interface ComposedProfile {
|
||||
profile: Profile
|
||||
/** Bundle layers concatenated — the part below the user layers on a live reload. */
|
||||
bundlePatches: PatchOptions[]
|
||||
/** The win32 shell platform layer (the base bundle's `windows.cordis.patch.yml`), between bundles and user layers. */
|
||||
windowsShellPatches: PatchOptions[]
|
||||
/** The home-level user layer (`$DSH_HOME/cordis.patch.yml`), applied after the profile's own. */
|
||||
homePatches: PatchOptions[]
|
||||
/** Layers above the user layers on a live reload: `--patch` overlays and the telemetry switch. */
|
||||
@@ -127,7 +124,6 @@ interface ComposedProfile {
|
||||
function allPatches(composed: ComposedProfile): PatchOptions[] {
|
||||
return [
|
||||
...composed.bundlePatches,
|
||||
...composed.windowsShellPatches,
|
||||
...composed.profile.patches,
|
||||
...composed.homePatches,
|
||||
...composed.overlays,
|
||||
@@ -136,10 +132,10 @@ function allPatches(composed: ComposedProfile): PatchOptions[] {
|
||||
|
||||
/**
|
||||
* Load `name` and compose its effective patch stack: bundle layers in
|
||||
* `dsh.profile.bundles` order, the win32 shell platform layer (when the host
|
||||
* is Windows), the profile's user layer, the home-level user layer
|
||||
* (`$DSH_HOME/cordis.patch.yml` — machine-local preferences that apply to
|
||||
* every profile, so it outranks the per-profile layer), `--patch` overlays,
|
||||
* `dsh.profile.bundles` order (the base bundle gates the shell stacks by
|
||||
* platform on its own rows), the profile's user layer, the home-level user
|
||||
* layer (`$DSH_HOME/cordis.patch.yml` — machine-local preferences that apply
|
||||
* to every profile, so it outranks the per-profile layer), `--patch` overlays,
|
||||
* then the telemetry switch.
|
||||
* @param name - the profile name.
|
||||
* @param patchFiles - `--patch` overlay paths, in argv order.
|
||||
@@ -153,9 +149,8 @@ function composeProfile(
|
||||
const homePatches = loadOptionalPatches(NAME, homePatchPath()) ?? []
|
||||
const overlays = patchFiles.flatMap(file => loadOverlayPatches(NAME, resolve(file)))
|
||||
const bundlePatches = profile.layers.flatMap(layer => layer.patches)
|
||||
const windowsShellPatches = resolveWindowsShellLayer(process.platform, profile.layers, NAME)?.patches ?? []
|
||||
const rows = new Map<string, EntryOptions>()
|
||||
for (const row of composeEntries([bundlePatches, windowsShellPatches, profile.patches, homePatches, overlays])) {
|
||||
for (const row of composeEntries([bundlePatches, profile.patches, homePatches, overlays])) {
|
||||
if (typeof row.id === 'string') rows.set(row.id, row)
|
||||
}
|
||||
const composedOverlays = [...overlays]
|
||||
@@ -174,7 +169,7 @@ function composeProfile(
|
||||
}
|
||||
const telemetryPatch = resolveTelemetryPatch(process.env.DSH_TELEMETRY_DISABLED, rows.has(TELEMETRY_ROW_ID))
|
||||
if (telemetryPatch !== undefined) composedOverlays.push(telemetryPatch)
|
||||
return { profile, bundlePatches, windowsShellPatches, homePatches, overlays: composedOverlays, rows }
|
||||
return { profile, bundlePatches, homePatches, overlays: composedOverlays, rows }
|
||||
}
|
||||
|
||||
/** Options for {@link runProfile}. */
|
||||
@@ -246,7 +241,6 @@ export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Con
|
||||
// removing the override could never revert the row to the bundle default.
|
||||
const composeLive = (): PatchOptions[] => structuredClone([
|
||||
...composed.bundlePatches,
|
||||
...composed.windowsShellPatches,
|
||||
...loadOptionalPatches(NAME, composed.profile.patchPath) ?? [],
|
||||
...loadOptionalPatches(NAME, homePatchPath()) ?? [],
|
||||
...composed.overlays,
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
/**
|
||||
* The Windows shell platform layer: on win32 hosts the shipped profile
|
||||
* compositions swap the POSIX-only bash stack for the sandbox-confined
|
||||
* PowerShell stack (`@deepseek-ai/dsh-pwsh-sandbox` +
|
||||
* `@deepseek-ai/dsh-tool-pwsh`). The layer is the base bundle's
|
||||
* `windows.cordis.patch.yml`, injected by the launcher between the bundle
|
||||
* layers and the user layers so a user patch can still override it — the
|
||||
* only override channel is composition config, like every other roster
|
||||
* decision. POSIX hosts never receive the layer.
|
||||
* @module @deepseek-ai/dsh/windows-shell
|
||||
*/
|
||||
|
||||
import { join } from 'node:path'
|
||||
import type { PatchOptions } from '@deepseek-ai/cordis-plugin-include'
|
||||
import { loadOverlayPatches, type ProfileLayer } from '@deepseek-ai/dsh-app-boot'
|
||||
|
||||
/** The base bundle whose package carries the Windows shell patch. */
|
||||
export const BASE_BUNDLE = '@deepseek-ai/dsh-base'
|
||||
|
||||
/** The Windows shell patch filename inside the base bundle package. */
|
||||
export const WINDOWS_SHELL_PATCH_FILENAME = 'windows.cordis.patch.yml'
|
||||
|
||||
/** One Windows shell platform layer: its patch file and parsed patches. */
|
||||
export interface WindowsShellLayer {
|
||||
/** The patch file path, used as the config-dump provenance label. */
|
||||
label: string
|
||||
/** The parsed patch entries, applied after the bundle layers. */
|
||||
patches: PatchOptions[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the Windows shell platform layer for a profile composition.
|
||||
* @param platform - the host platform (`process.platform` at call sites).
|
||||
* @param layers - the profile's bundle layers, in application order.
|
||||
* @param binName - the diagnostic prefix on thrown errors (`dsh`).
|
||||
* @returns the pwsh layer on win32, else `undefined`. A custom profile that
|
||||
* mounts no base bundle is skipped (it owns its shell stack); a base
|
||||
* bundle whose Windows shell patch is missing fails loud in
|
||||
* {@link loadOverlayPatches} — the shipped package always carries it, so
|
||||
* a miss is a broken installation.
|
||||
*/
|
||||
export function resolveWindowsShellLayer(
|
||||
platform: NodeJS.Platform,
|
||||
layers: readonly ProfileLayer[],
|
||||
binName: string,
|
||||
): WindowsShellLayer | undefined {
|
||||
if (platform !== 'win32') return undefined
|
||||
const base = layers.find(layer => layer.packageName === BASE_BUNDLE)
|
||||
if (base === undefined) return undefined
|
||||
const label = join(base.packageDir, WINDOWS_SHELL_PATCH_FILENAME)
|
||||
return { label, patches: loadOverlayPatches(binName, label) }
|
||||
}
|
||||
@@ -1,73 +1,38 @@
|
||||
/**
|
||||
* The shipped shell composition: the base bundle gates both shell stacks by
|
||||
* platform on its own rows (`disabled: !!js process.platform`), so exactly
|
||||
* one shell stack mounts per host and no separate platform layer exists —
|
||||
* the launcher applies nothing beyond the bundle layers. The spec composes
|
||||
* the REAL shipped bundle layers (dsh-base + dsh-web-app resolved from the
|
||||
* app installation anchor) through the boot's patch algorithm and pins the
|
||||
* effective per-platform roster, the preset-level gates that keep tool-bash
|
||||
* out of win32 sessions and tool-pwsh out of POSIX sessions, and the
|
||||
* cold-start resolution closure for the pwsh rows' bare plugin names.
|
||||
*/
|
||||
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { mkdtempSync, writeFileSync, rmSync, mkdirSync, readFileSync } from 'node:fs'
|
||||
import { mkdtempSync, rmSync, readFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import type { ProfileLayer } from '@deepseek-ai/dsh-app-boot'
|
||||
import yaml from 'js-yaml'
|
||||
import { entryListSchema } from '@deepseek-ai/cordis-plugin-include'
|
||||
import { evaluate } from '@deepseek-ai/cordis-plugin-loader'
|
||||
import { composeEntries, initProfile, loadProfile, PROFILES_DIR } from '@deepseek-ai/dsh-app-boot'
|
||||
import {
|
||||
BASE_BUNDLE,
|
||||
resolveWindowsShellLayer,
|
||||
WINDOWS_SHELL_PATCH_FILENAME,
|
||||
} from '../src/windows-shell.ts'
|
||||
|
||||
const WINDOWS_PATCH = `- id: bash-sandbox
|
||||
disabled: true
|
||||
- insert:
|
||||
- id: pwsh-sandbox
|
||||
name: '@deepseek-ai/dsh-pwsh-sandbox'
|
||||
`
|
||||
|
||||
/** One fake bundle layer rooted in a temp directory. */
|
||||
function fakeLayer(packageName: string, dir: string): ProfileLayer {
|
||||
return { packageName, packageDir: dir, patchPath: join(dir, 'cordis.patch.yml'), patches: [] }
|
||||
}
|
||||
|
||||
/** A base bundle layer whose package carries the Windows shell patch. */
|
||||
function baseLayerWithPatch(dir: string): ProfileLayer {
|
||||
writeFileSync(join(dir, WINDOWS_SHELL_PATCH_FILENAME), WINDOWS_PATCH)
|
||||
return fakeLayer(BASE_BUNDLE, dir)
|
||||
}
|
||||
|
||||
describe('resolveWindowsShellLayer', () => {
|
||||
let base: string
|
||||
afterEach(() => { if (base !== undefined) rmSync(base, { recursive: true, force: true }) })
|
||||
const tempBase = (): string => {
|
||||
base = mkdtempSync(join(tmpdir(), 'dsh-windows-shell-'))
|
||||
return base
|
||||
/**
|
||||
* The effective disabled state of one row on one platform: a `!!js` expression
|
||||
* evaluates with a platform-scoped `process` so both outcomes pin on any host.
|
||||
*/
|
||||
function disabledOn(row: { disabled?: unknown }, platform: 'win32' | 'linux'): boolean {
|
||||
const value = row.disabled
|
||||
if (value !== null && typeof value === 'object' && '__jsExpr' in value) {
|
||||
return Boolean(evaluate({ process: { platform } }, (value as { __jsExpr: string }).__jsExpr))
|
||||
}
|
||||
return value === true
|
||||
}
|
||||
|
||||
it('never applies on POSIX hosts', () => {
|
||||
expect(resolveWindowsShellLayer('linux', [baseLayerWithPatch(tempBase())], 'dsh')).toBeUndefined()
|
||||
expect(resolveWindowsShellLayer('darwin', [baseLayerWithPatch(tempBase())], 'dsh')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('defaults Windows hosts to the pwsh platform layer', () => {
|
||||
const layer = resolveWindowsShellLayer('win32', [baseLayerWithPatch(tempBase())], 'dsh')
|
||||
expect(layer).toBeDefined()
|
||||
expect(layer?.label.endsWith(WINDOWS_SHELL_PATCH_FILENAME)).toBe(true)
|
||||
expect(layer?.patches).toEqual([
|
||||
{ id: 'bash-sandbox', disabled: true },
|
||||
{ insert: [{ id: 'pwsh-sandbox', name: '@deepseek-ai/dsh-pwsh-sandbox' }] },
|
||||
])
|
||||
})
|
||||
|
||||
it('skips custom profiles without a base bundle', () => {
|
||||
const other = fakeLayer('@deepseek-ai/dsh-custom', tempBase())
|
||||
expect(resolveWindowsShellLayer('win32', [other], 'dsh')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('fails loud when the base bundle ships no Windows shell patch', () => {
|
||||
const base = tempBase()
|
||||
mkdirSync(base, { recursive: true })
|
||||
// The overlay loader owns the fail-loud contract: the caller named this
|
||||
// file, so its absence is a misconfiguration, not "no overlay".
|
||||
expect(() => resolveWindowsShellLayer('win32', [fakeLayer(BASE_BUNDLE, base)], 'dsh'))
|
||||
.toThrow(/dsh: failed to read overlay .*windows\.cordis\.patch\.yml/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('the shipped Windows composition (real bundle layers)', () => {
|
||||
describe('the shipped shell composition (real bundle layers)', () => {
|
||||
let home: string
|
||||
afterEach(() => { if (home !== undefined) rmSync(home, { recursive: true, force: true }) })
|
||||
// The app installation anchor, mirroring profile-boot.ts: the bundle layers
|
||||
@@ -75,65 +40,98 @@ describe('the shipped Windows composition (real bundle layers)', () => {
|
||||
// suite composes the shipped patch files, not test fixtures.
|
||||
const anchor = fileURLToPath(new URL('../package.json', import.meta.url))
|
||||
|
||||
it('composes the win32 confined roster through the real patch layers', () => {
|
||||
it('composes the confined pwsh roster on win32 and the bash roster on POSIX from the same rows', () => {
|
||||
home = mkdtempSync(join(tmpdir(), 'dsh-windows-home-'))
|
||||
initProfile(join(home, PROFILES_DIR, 'web'), ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app'])
|
||||
const profile = loadProfile('dsh', 'web', anchor, home)
|
||||
const warnings: string[] = []
|
||||
const win32 = resolveWindowsShellLayer('win32', profile.layers, 'dsh')
|
||||
expect(win32).toBeDefined()
|
||||
const rows = composeEntries(
|
||||
[...profile.layers.map(layer => layer.patches), win32!.patches],
|
||||
profile.layers.map(layer => layer.patches),
|
||||
message => warnings.push(message),
|
||||
)
|
||||
const byId = new Map(rows.map(row => [row.id, row]))
|
||||
// Only the POSIX bash stack leaves the roster: the permission surface
|
||||
// (sandbox/sandbox-policy/fs-sandbox, permission, approval) stays enabled
|
||||
// exactly as on POSIX — the confined pwsh executor is what changes.
|
||||
for (const id of ['bash-sandbox', 'tool-bash']) {
|
||||
expect(byId.get(id)?.disabled, `row ${id}`).toBe(true)
|
||||
// One shared patch set, two rosters: the shell stacks gate themselves.
|
||||
for (const id of ['bash-sandbox', 'pwsh-sandbox', 'tool-bash', 'tool-pwsh']) {
|
||||
expect(byId.has(id), `row ${id}`).toBe(true)
|
||||
}
|
||||
expect(disabledOn(byId.get('bash-sandbox')!, 'win32'), 'bash-sandbox on win32').toBe(true)
|
||||
expect(disabledOn(byId.get('bash-sandbox')!, 'linux'), 'bash-sandbox on linux').toBe(false)
|
||||
expect(disabledOn(byId.get('pwsh-sandbox')!, 'win32'), 'pwsh-sandbox on win32').toBe(false)
|
||||
expect(disabledOn(byId.get('pwsh-sandbox')!, 'linux'), 'pwsh-sandbox on linux').toBe(true)
|
||||
// Host shell-tool rows are disabled on every platform; sessions mount
|
||||
// their own rows instead.
|
||||
expect(byId.get('tool-bash')?.disabled).toBe(true)
|
||||
expect(byId.get('tool-pwsh')?.disabled).toBe(true)
|
||||
// The permission surface never moves: the sandbox/policy rows, the
|
||||
// permission switcher, fs-sandbox, and the approval service stay enabled
|
||||
// exactly as on POSIX — the confined pwsh executor is what changes.
|
||||
for (const id of ['permission', 'ui-permission', 'sandbox', 'sandbox-policy', 'fs-sandbox', 'approval']) {
|
||||
expect(byId.get(id)?.disabled, `row ${id}`).not.toBe(true)
|
||||
}
|
||||
for (const id of ['pwsh-sandbox', 'tool-pwsh']) {
|
||||
expect(byId.has(id), `inserted row ${id}`).toBe(true)
|
||||
}
|
||||
// The launcher's cold-start module fallback BFS-links the apps/cli
|
||||
// dependency closure into the profile's node_modules (the pwsh-local
|
||||
// precedent), so every inserted bare plugin must resolve from there.
|
||||
// dependency closure into the profile's node_modules, so every bare
|
||||
// plugin name in the base patch must resolve from there.
|
||||
const cliManifest = JSON.parse(readFileSync(anchor, 'utf8')) as { dependencies?: Record<string, string> }
|
||||
for (const name of ['@deepseek-ai/dsh-pwsh-sandbox', '@deepseek-ai/dsh-tool-pwsh']) {
|
||||
expect(cliManifest.dependencies?.[name], `cold-start closure must reach ${name}`).toBeDefined()
|
||||
}
|
||||
// The patch touches only base-owned rows plus inserts, so the full web
|
||||
// profile composes without any no-match warning.
|
||||
expect(warnings).toEqual([])
|
||||
})
|
||||
|
||||
it('leaves POSIX untouched and base-only profiles compose without warnings', () => {
|
||||
it('base-only profiles carry both stacks with the same platform gating', () => {
|
||||
home = mkdtempSync(join(tmpdir(), 'dsh-windows-home-'))
|
||||
initProfile(join(home, PROFILES_DIR, 'web'), ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app'])
|
||||
const profile = loadProfile('dsh', 'web', anchor, home)
|
||||
// POSIX: no platform layer, the bash stack stays enabled.
|
||||
const posixRows = composeEntries(profile.layers.map(layer => layer.patches))
|
||||
const posixById = new Map(posixRows.map(row => [row.id, row]))
|
||||
expect(posixById.get('bash-sandbox')?.disabled).not.toBe(true)
|
||||
expect(posixById.has('pwsh-local')).toBe(false)
|
||||
expect(posixById.has('pwsh-sandbox')).toBe(false)
|
||||
|
||||
// A base-only custom profile (the DEFAULT_PROFILE_BUNDLES template): the
|
||||
// patch touches only base-owned rows (bash-sandbox/tool-bash) plus its
|
||||
// inserts, so the composition produces no no-match warning.
|
||||
initProfile(join(home, PROFILES_DIR, 'base-only'), ['@deepseek-ai/dsh-base'])
|
||||
const baseOnly = loadProfile('dsh', 'base-only', anchor, home)
|
||||
const baseWarnings: string[] = []
|
||||
const win32 = resolveWindowsShellLayer('win32', baseOnly.layers, 'dsh')
|
||||
expect(win32).toBeDefined()
|
||||
composeEntries(
|
||||
[...baseOnly.layers.map(layer => layer.patches), win32!.patches],
|
||||
message => baseWarnings.push(message),
|
||||
const profile = loadProfile('dsh', 'base-only', anchor, home)
|
||||
const warnings: string[] = []
|
||||
const rows = composeEntries(
|
||||
profile.layers.map(layer => layer.patches),
|
||||
message => warnings.push(message),
|
||||
)
|
||||
expect(baseWarnings).toEqual([])
|
||||
const byId = new Map(rows.map(row => [row.id, row]))
|
||||
for (const id of ['bash-sandbox', 'tool-bash', 'pwsh-sandbox', 'tool-pwsh']) {
|
||||
expect(byId.has(id), `row ${id}`).toBe(true)
|
||||
}
|
||||
// No web overlay: the tool rows keep their own gating too.
|
||||
expect(disabledOn(byId.get('tool-bash')!, 'win32'), 'tool-bash on win32').toBe(true)
|
||||
expect(disabledOn(byId.get('tool-bash')!, 'linux'), 'tool-bash on linux').toBe(false)
|
||||
expect(disabledOn(byId.get('tool-pwsh')!, 'win32'), 'tool-pwsh on win32').toBe(false)
|
||||
expect(disabledOn(byId.get('tool-pwsh')!, 'linux'), 'tool-pwsh on linux').toBe(true)
|
||||
expect(warnings).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('shipped agent presets gate both shell tools by platform', () => {
|
||||
const presetRoot = resolve(fileURLToPath(new URL('../package.json', import.meta.url)), '..', 'config', 'agent-presets')
|
||||
|
||||
it.each(['standard', 'code', 'cordis'])('preset %s gates its shell tool rows by platform', (preset) => {
|
||||
const entries: unknown = yaml.load(
|
||||
readFileSync(join(presetRoot, preset, 'agent.cordis.yml'), 'utf8'),
|
||||
{ schema: entryListSchema },
|
||||
)
|
||||
if (!Array.isArray(entries)) throw new TypeError(`preset ${preset} must parse to an entry array`)
|
||||
for (const [id, win32] of [['tool-bash', true], ['tool-pwsh', false]] as const) {
|
||||
const row = entries.find((entry): entry is Record<string, unknown> => (
|
||||
typeof entry === 'object' && entry !== null && (entry as Record<string, unknown>).id === id
|
||||
))
|
||||
if (row === undefined) throw new TypeError(`preset ${preset} must mount ${id}`)
|
||||
expect(row.disabled).toMatchObject({ __jsExpr: expect.any(String) as string })
|
||||
// A platform-scoped context pins both outcomes on every host.
|
||||
const expression = (row.disabled as { __jsExpr: string }).__jsExpr
|
||||
expect(Boolean(evaluate({ process: { platform: 'win32' } }, expression)), `${id} on win32`).toBe(win32)
|
||||
expect(Boolean(evaluate({ process: { platform: 'linux' } }, expression)), `${id} on linux`).toBe(!win32)
|
||||
}
|
||||
})
|
||||
|
||||
it('minimal mounts no shell tool row at all (its shell is the PTY stack)', () => {
|
||||
const entries: unknown = yaml.load(
|
||||
readFileSync(join(presetRoot, 'minimal', 'agent.cordis.yml'), 'utf8'),
|
||||
{ schema: entryListSchema },
|
||||
)
|
||||
if (!Array.isArray(entries)) throw new TypeError('minimal preset must parse to an entry array')
|
||||
for (const id of ['tool-bash', 'tool-pwsh']) {
|
||||
expect(entries.some(entry => (
|
||||
typeof entry === 'object' && entry !== null && (entry as Record<string, unknown>).id === id
|
||||
)), `${id} must be absent from minimal`).toBe(false)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user