windows-shell.spec.ts 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. import { afterEach, describe, expect, it } from 'vitest'
  2. import { mkdtempSync, writeFileSync, rmSync, mkdirSync } from 'node:fs'
  3. import { tmpdir } from 'node:os'
  4. import { join } from 'node:path'
  5. import { fileURLToPath } from 'node:url'
  6. import type { ProfileLayer } from '@deepseek-ai/dsh-app-boot'
  7. import { composeEntries, initProfile, loadProfile, PROFILES_DIR } from '@deepseek-ai/dsh-app-boot'
  8. import {
  9. BASE_BUNDLE,
  10. resolveWindowsShellLayer,
  11. WINDOWS_SHELL_PATCH_FILENAME,
  12. } from '../src/windows-shell.ts'
  13. const WINDOWS_PATCH = `- id: bash-sandbox
  14. disabled: true
  15. - insert:
  16. - id: pwsh-sandbox
  17. name: '@deepseek-ai/dsh-pwsh-sandbox'
  18. `
  19. /** One fake bundle layer rooted in a temp directory. */
  20. function fakeLayer(packageName: string, dir: string): ProfileLayer {
  21. return { packageName, packageDir: dir, patchPath: join(dir, 'cordis.patch.yml'), patches: [] }
  22. }
  23. /** A base bundle layer whose package carries the Windows shell patch. */
  24. function baseLayerWithPatch(dir: string): ProfileLayer {
  25. writeFileSync(join(dir, WINDOWS_SHELL_PATCH_FILENAME), WINDOWS_PATCH)
  26. return fakeLayer(BASE_BUNDLE, dir)
  27. }
  28. describe('resolveWindowsShellLayer', () => {
  29. let base: string
  30. afterEach(() => { if (base !== undefined) rmSync(base, { recursive: true, force: true }) })
  31. const tempBase = (): string => {
  32. base = mkdtempSync(join(tmpdir(), 'dsh-windows-shell-'))
  33. return base
  34. }
  35. it('never applies on POSIX hosts', () => {
  36. expect(resolveWindowsShellLayer('linux', [baseLayerWithPatch(tempBase())], 'dsh')).toBeUndefined()
  37. expect(resolveWindowsShellLayer('darwin', [baseLayerWithPatch(tempBase())], 'dsh')).toBeUndefined()
  38. })
  39. it('defaults Windows hosts to the pwsh platform layer', () => {
  40. const layer = resolveWindowsShellLayer('win32', [baseLayerWithPatch(tempBase())], 'dsh')
  41. expect(layer).toBeDefined()
  42. expect(layer?.label.endsWith(WINDOWS_SHELL_PATCH_FILENAME)).toBe(true)
  43. expect(layer?.patches).toEqual([
  44. { id: 'bash-sandbox', disabled: true },
  45. { insert: [{ id: 'pwsh-sandbox', name: '@deepseek-ai/dsh-pwsh-sandbox' }] },
  46. ])
  47. })
  48. it('skips custom profiles without a base bundle', () => {
  49. const other = fakeLayer('@deepseek-ai/dsh-custom', tempBase())
  50. expect(resolveWindowsShellLayer('win32', [other], 'dsh')).toBeUndefined()
  51. })
  52. it('fails loud when the base bundle ships no Windows shell patch', () => {
  53. const base = tempBase()
  54. mkdirSync(base, { recursive: true })
  55. expect(() => resolveWindowsShellLayer('win32', [fakeLayer(BASE_BUNDLE, base)], 'dsh'))
  56. .toThrow(/@deepseek-ai\/dsh-base ships no windows\.cordis\.patch\.yml/)
  57. })
  58. })
  59. describe('the shipped Windows composition (real bundle layers)', () => {
  60. let home: string
  61. afterEach(() => { if (home !== undefined) rmSync(home, { recursive: true, force: true }) })
  62. // The app installation anchor, mirroring profile-boot.ts: the bundle layers
  63. // resolve from the REAL dsh-base/dsh-web-app packages through it, so this
  64. // suite composes the shipped patch files, not test fixtures.
  65. const anchor = fileURLToPath(new URL('../package.json', import.meta.url))
  66. it('composes the win32 confined roster through the real patch layers', () => {
  67. home = mkdtempSync(join(tmpdir(), 'dsh-windows-home-'))
  68. initProfile(join(home, PROFILES_DIR, 'web'), ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app'])
  69. const profile = loadProfile('dsh', 'web', anchor, home)
  70. const warnings: string[] = []
  71. const win32 = resolveWindowsShellLayer('win32', profile.layers, 'dsh')
  72. expect(win32).toBeDefined()
  73. const rows = composeEntries(
  74. [...profile.layers.map(layer => layer.patches), win32!.patches],
  75. message => warnings.push(message),
  76. )
  77. const byId = new Map(rows.map(row => [row.id, row]))
  78. // Only the POSIX bash stack leaves the roster: the permission surface
  79. // (sandbox/sandbox-policy/fs-sandbox, permission, approval) stays enabled
  80. // exactly as on POSIX — the confined pwsh executor is what changes.
  81. for (const id of ['bash-sandbox', 'tool-bash']) {
  82. expect(byId.get(id)?.disabled, `row ${id}`).toBe(true)
  83. }
  84. for (const id of ['permission', 'ui-permission', 'sandbox', 'sandbox-policy', 'fs-sandbox', 'approval']) {
  85. expect(byId.get(id)?.disabled, `row ${id}`).not.toBe(true)
  86. }
  87. for (const id of ['pwsh-sandbox', 'tool-pwsh']) {
  88. expect(byId.has(id), `inserted row ${id}`).toBe(true)
  89. }
  90. // The patch touches only base-owned rows plus inserts, so the full web
  91. // profile composes without any no-match warning.
  92. expect(warnings).toEqual([])
  93. })
  94. it('leaves POSIX untouched and base-only profiles compose without warnings', () => {
  95. home = mkdtempSync(join(tmpdir(), 'dsh-windows-home-'))
  96. initProfile(join(home, PROFILES_DIR, 'web'), ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app'])
  97. const profile = loadProfile('dsh', 'web', anchor, home)
  98. // POSIX: no platform layer, the bash stack stays enabled.
  99. const posixRows = composeEntries(profile.layers.map(layer => layer.patches))
  100. const posixById = new Map(posixRows.map(row => [row.id, row]))
  101. expect(posixById.get('bash-sandbox')?.disabled).not.toBe(true)
  102. expect(posixById.has('pwsh-local')).toBe(false)
  103. expect(posixById.has('pwsh-sandbox')).toBe(false)
  104. // A base-only custom profile (the DEFAULT_PROFILE_BUNDLES template): the
  105. // patch touches only base-owned rows (bash-sandbox/tool-bash) plus its
  106. // inserts, so the composition produces no no-match warning.
  107. initProfile(join(home, PROFILES_DIR, 'base-only'), ['@deepseek-ai/dsh-base'])
  108. const baseOnly = loadProfile('dsh', 'base-only', anchor, home)
  109. const baseWarnings: string[] = []
  110. const win32 = resolveWindowsShellLayer('win32', baseOnly.layers, 'dsh')
  111. expect(win32).toBeDefined()
  112. composeEntries(
  113. [...baseOnly.layers.map(layer => layer.patches), win32!.patches],
  114. message => baseWarnings.push(message),
  115. )
  116. expect(baseWarnings).toEqual([])
  117. })
  118. })