base.spec.ts 3.5 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
  1. /**
  2. * The bundle's substance is its patch file: the `dsh.bundle.patch` manifest
  3. * field must name a real, parseable patch list.
  4. */
  5. import { existsSync, readFileSync } from 'node:fs'
  6. import { fileURLToPath } from 'node:url'
  7. import { resolve } from 'node:path'
  8. import { describe, expect, it } from 'vitest'
  9. import * as yaml from 'js-yaml'
  10. import { entryListSchema } from '@deepseek-ai/cordis-plugin-include'
  11. import { evaluate } from '@deepseek-ai/cordis-plugin-loader'
  12. describe('dsh-base bundle', () => {
  13. it('declares a parseable patch list through the dsh.bundle.patch manifest field', () => {
  14. const root = fileURLToPath(new URL('..', import.meta.url))
  15. const manifest = JSON.parse(
  16. readFileSync(resolve(root, 'package.json'), 'utf8'),
  17. ) as {
  18. dependencies?: Record<string, string>
  19. dsh?: { bundle?: { patch?: string } }
  20. }
  21. expect(manifest.dsh?.bundle?.patch).toBe('./cordis.patch.yml')
  22. const parsed = yaml.load(
  23. readFileSync(resolve(root, manifest.dsh!.bundle!.patch!), 'utf8'),
  24. { schema: entryListSchema },
  25. )
  26. expect(Array.isArray(parsed)).toBe(true)
  27. // The base layer is one insert list over the empty profile root.
  28. const rows = (parsed as { insert?: { id?: string }[] }[]).flatMap(
  29. patch => patch.insert ?? [],
  30. )
  31. expect(rows.length).toBeGreaterThan(50)
  32. expect(rows.some(row => row.id === 'agent-loop')).toBe(true)
  33. expect(rows.filter(row => row.id === 'subagent-codex')).toHaveLength(1)
  34. expect(rows.filter(row => row.id === 'subagent-claude-code')).toHaveLength(1)
  35. expect(manifest.dependencies).toMatchObject({
  36. '@deepseek-ai/dsh-subagent-codex': 'workspace:^',
  37. '@deepseek-ai/dsh-subagent-claude-code': 'workspace:^',
  38. })
  39. })
  40. it('gates each shell stack by platform with a symmetric disabled expression', () => {
  41. const root = fileURLToPath(new URL('..', import.meta.url))
  42. const parsed = yaml.load(
  43. readFileSync(resolve(root, 'cordis.patch.yml'), 'utf8'),
  44. { schema: entryListSchema },
  45. )
  46. if (!Array.isArray(parsed)) throw new TypeError('base patch must parse to a patch list')
  47. const rows = parsed.flatMap((patch): Record<string, unknown>[] =>
  48. typeof patch === 'object' && patch !== null
  49. ? (patch as { insert?: Record<string, unknown>[] }).insert ?? []
  50. : [],
  51. )
  52. // Symmetric gating: each stack's executor and tool rows carry the same
  53. // platform fact, inverted between the bash and pwsh twins, so exactly one
  54. // shell stack mounts per host. Evaluate with a platform-scoped context
  55. // (the `with` scope shadows the global `process`) so both outcomes pin on
  56. // every host.
  57. for (const [id, win32, linux] of [
  58. ['bash-sandbox', true, false],
  59. ['tool-bash', true, false],
  60. ['pwsh-sandbox', false, true],
  61. ['tool-pwsh', false, true],
  62. ] as const) {
  63. const row = rows.find(candidate => candidate.id === id)
  64. if (row === undefined) throw new Error(`base patch must mount ${id}`)
  65. const expression = (row.disabled as { __jsExpr?: string } | undefined)?.__jsExpr
  66. if (expression === undefined) throw new Error(`${id} must gate on a !!js disabled expression`)
  67. expect(Boolean(evaluate({ process: { platform: 'win32' } }, expression)), `${id} on win32`).toBe(win32)
  68. expect(Boolean(evaluate({ process: { platform: 'linux' } }, expression)), `${id} on linux`).toBe(linux)
  69. }
  70. // The platform layer folded into these rows: no separate patch file ships.
  71. expect(existsSync(resolve(root, 'windows.cordis.patch.yml'))).toBe(false)
  72. })
  73. })