test-host-updates.ts 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354
  1. /** Run built Desktop Host task protection in a private development profile without Electron or installation. */
  2. import { spawn } from 'node:child_process'
  3. import { mkdir, mkdtemp, readFile, symlink, writeFile } from 'node:fs/promises'
  4. import { join, resolve } from 'node:path'
  5. import { fileURLToPath } from 'node:url'
  6. import { createDevelopmentProjectMetadata } from '../src/project-manager.ts'
  7. import { removeOwnedDirectory } from '../src/owned-directory.ts'
  8. import { DESKTOP_HOST_PROTOCOL_VERSION } from '../src/host-protocol.ts'
  9. const repo = resolve(import.meta.dirname, '../../..')
  10. const evidenceRoot = join(repo, 'apps/desktop/.desktop-build/qualification')
  11. await mkdir(evidenceRoot, { recursive: true })
  12. const root = await mkdtemp(join(evidenceRoot, 'host-updates-'))
  13. const project = join(root, 'project')
  14. const manifest = JSON.parse(await readFile(join(repo, 'apps/desktop/package.json'), 'utf8')) as { version: string }
  15. const pnpm = JSON.parse(await readFile(join(repo, 'apps/desktop/node_modules/pnpm/package.json'), 'utf8')) as { version: string }
  16. try {
  17. createDevelopmentProjectMetadata(project, {
  18. schemaVersion: 1, version: manifest.version, nodeVersion: process.versions.node,
  19. pnpmVersion: pnpm.version, hostProtocolVersion: DESKTOP_HOST_PROTOCOL_VERSION,
  20. })
  21. // This suite tests the Host, not dev.ts's dependency projection. Preserve pnpm's existing graph verbatim.
  22. await symlink(join(repo, 'node_modules/.pnpm/node_modules'), join(project, 'node_modules'),
  23. process.platform === 'win32' ? 'junction' : 'dir')
  24. await writeFile(join(project, 'cordis.patch.yml'), JSON.stringify([
  25. { id: 'webserver', config: { host: '127.0.0.1', port: 0 } },
  26. { id: 'llm-deepseek', disabled: true },
  27. { id: 'session-title-llm', disabled: true },
  28. { id: 'session-telemetry-otel', disabled: true },
  29. { id: 'agent-instructions', disabled: true },
  30. { id: 'agent-presets', config: { default: 'standard', includeUserRoot: false } },
  31. { insert: [{ id: 'update-qualification', name: new URL('../tests/fixtures/host-update-control.mjs', import.meta.url).href }] },
  32. ]))
  33. const environment = Object.fromEntries(Object.entries(process.env).filter(([name]) =>
  34. /^(?:path|systemroot|windir|comspec|pathext)$/iu.test(name)))
  35. const child = spawn(process.execPath, [fileURLToPath(new URL('../tests/fixtures/host-update-qualification.mjs', import.meta.url)), root], {
  36. cwd: root, env: { ...environment, DSH_HOME: join(root, 'home'), USERPROFILE: root, HOME: root,
  37. TEMP: root, TMP: root, TMPDIR: root }, stdio: 'inherit', windowsHide: true,
  38. })
  39. let timedOut = false
  40. const timeout = setTimeout(() => { timedOut = true; child.kill() }, 120_000)
  41. try {
  42. const result = await new Promise<{ code: number | null; signal: NodeJS.Signals | null }>((done, reject) => {
  43. child.once('error', reject)
  44. child.once('close', (code, signal) => { done({ code, signal }) })
  45. })
  46. if (timedOut || result.signal !== null || result.code !== 0) {
  47. throw new Error(`Host update qualification failed: timeout=${String(timedOut)}, signal=${String(result.signal)}, code=${String(result.code)}`)
  48. }
  49. console.log(`Host update evidence: ${root}`)
  50. } finally { clearTimeout(timeout) }
  51. } finally {
  52. removeOwnedDirectory(project)
  53. }