|
|
@@ -1,76 +1,40 @@
|
|
|
+/**
|
|
|
+ * 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 gate that keeps tool-bash
|
|
|
+ * out of win32 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, resolve } from 'node:path'
|
|
|
import { fileURLToPath } from 'node:url'
|
|
|
import yaml from 'js-yaml'
|
|
|
import { entryListSchema } from '@deepseek-ai/cordis-plugin-include'
|
|
|
import { evaluate } from '@deepseek-ai/cordis-plugin-loader'
|
|
|
-import type { ProfileLayer } from '@deepseek-ai/dsh-app-boot'
|
|
|
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 composed row on one platform: a `!!js`
|
|
|
+ * expression evaluates with a platform-scoped context (the `with` scope
|
|
|
+ * shadows the global `process`) so both outcomes pin on every host; a plain
|
|
|
+ * boolean is the value itself.
|
|
|
+ */
|
|
|
+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
|
|
|
@@ -78,66 +42,64 @@ 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-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)
|
|
|
+ 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)
|
|
|
+ // The Web surface owns the host tool-bash row on every platform: sessions
|
|
|
+ // mount their own preset rows instead.
|
|
|
+ expect(byId.get('tool-bash')?.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([])
|
|
|
})
|
|
|
})
|
|
|
|
|
|
@@ -155,9 +117,9 @@ describe('shipped agent presets keep tool-bash off the win32 roster', () => {
|
|
|
))
|
|
|
if (row === undefined) throw new TypeError(`preset ${preset} must mount tool-bash`)
|
|
|
expect(row.disabled).toMatchObject({ __jsExpr: expect.any(String) as string })
|
|
|
- // The platform patch disables the host's tool-bash row on win32; the
|
|
|
- // preset row must not re-enable it there. Evaluate the shipped expression
|
|
|
- // with a platform-scoped context (the `with` scope shadows the global
|
|
|
+ // The base patch gates the host tool-bash row on win32; the preset row
|
|
|
+ // must not re-enable it there. Evaluate the shipped expression with a
|
|
|
+ // platform-scoped context (the `with` scope shadows the global
|
|
|
// `process`) so both outcomes pin on every host.
|
|
|
const expression = (row.disabled as { __jsExpr: string }).__jsExpr
|
|
|
expect(Boolean(evaluate({ process: { platform: 'win32' } }, expression))).toBe(true)
|