import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join, resolve, sep } from 'node:path' import { describe, expect, it, vi } from 'vitest' import { Context } from 'cordis' import SystemPrompt, { renderPrompt } from '@deepseek-ai/dsh-system-prompt' import { addHarnessSourceSection, assertEntriesActivated, assertEntriesLoaded, boot, FAIL_LOUD_RELEASE_TIMEOUT_MS, HARNESS_SOURCE_SECTION, installFailLoud, loadEnv, loadOverlayPatches, resolveConfigPath, type FailLoudProcess, } from '../src/index.ts' const NAME = 'dsh-test-bin' const tmp = (): string => mkdtempSync(join(tmpdir(), 'dsh-app-boot-')) describe('resolveConfigPath', () => { it('resolves relative to the given cwd outside replay mode', () => { expect(resolveConfigPath('./cordis.yml', undefined, `${sep}base`)).toBe(resolve(`${sep}base`, 'cordis.yml')) expect(resolveConfigPath('conf/app.yaml', 'record', `${sep}base`)).toBe(resolve(`${sep}base`, 'conf/app.yaml')) }) it('swaps a cordis.yml/.yaml basename for cordis.snapshot.yml in replay mode', () => { expect(resolveConfigPath('./cordis.yml', 'replay', `${sep}base`)).toBe(resolve(`${sep}base`, 'cordis.snapshot.yml')) expect(resolveConfigPath('deep/cordis.yaml', 'replay', `${sep}base`)).toBe(resolve(`${sep}base`, 'deep/cordis.snapshot.yml')) }) it('leaves a non-cordis basename alone in replay mode and defaults cwd to the process cwd', () => { expect(resolveConfigPath('custom.yml', 'replay', `${sep}base`)).toBe(resolve(`${sep}base`, 'custom.yml')) expect(resolveConfigPath('./x.yml', undefined)).toBe(resolve(process.cwd(), 'x.yml')) }) }) describe('loadEnv', () => { it('loads variables from .env in the given dir', () => { const dir = tmp() writeFileSync(join(dir, '.env'), 'DSH_APP_BOOT_SPEC_VAR=loaded\n') const warn = vi.fn() loadEnv(NAME, dir, warn) expect(process.env['DSH_APP_BOOT_SPEC_VAR']).toBe('loaded') expect(warn).not.toHaveBeenCalled() delete process.env['DSH_APP_BOOT_SPEC_VAR'] }) it('stays silent when no .env exists (ambient environment wins)', () => { const warn = vi.fn() loadEnv(NAME, tmp(), warn) expect(warn).not.toHaveBeenCalled() }) it('warns (labelled, single line) when .env exists but cannot be loaded', () => { const dir = tmp() mkdirSync(join(dir, '.env')) // a directory named .env: present, unreadable as a file const warn = vi.fn() loadEnv(NAME, dir, warn) expect(warn).toHaveBeenCalledTimes(1) expect(warn.mock.calls[0]?.[0]).toMatch(new RegExp(`^${NAME}: failed to load \\.env: `)) }) it('defaults dir to the process cwd and warn to a stderr write', () => { const dir = tmp() writeFileSync(join(dir, '.env'), 'DSH_APP_BOOT_SPEC_DEFAULTS=yes\n') const previous = process.cwd() process.chdir(dir) try { loadEnv(NAME) // happy path: the default warn sink is never invoked } finally { process.chdir(previous) } expect(process.env['DSH_APP_BOOT_SPEC_DEFAULTS']).toBe('yes') delete process.env['DSH_APP_BOOT_SPEC_DEFAULTS'] // The default warn sink itself: point it at a broken .env with stderr // spied, so the arrow body runs without polluting the test output. const broken = tmp() mkdirSync(join(broken, '.env')) const write = vi.spyOn(process.stderr, 'write').mockImplementation(() => true) let written: string[] try { loadEnv(NAME, broken) written = write.mock.calls.map(call => String(call[0])) } finally { write.mockRestore() } expect(written).toHaveLength(1) expect(written[0]).toContain(`${NAME}: failed to load .env: `) }) }) describe('installFailLoud', () => { function fakeProc(): FailLoudProcess & { handlers: Array<(err: unknown) => void>; written: string[]; exits: number[] } { const handlers: Array<(err: unknown) => void> = [] const written: string[] = [] const exits: number[] = [] return { handlers, written, exits, on: (_event, handler) => { handlers.push(handler) }, off: (_event, handler) => { handlers.splice(handlers.indexOf(handler), 1) }, stderr: { write: (chunk: string) => { written.push(chunk) } }, exit: (code: number) => { exits.push(code) }, } } it('writes one labelled line with the stack and exits 1 on an Error rejection', () => { const proc = fakeProc() installFailLoud(NAME, proc) const error = new Error('boom') proc.handlers[0]!(error) expect(proc.written[0]).toContain(`${NAME}: fatal load failure: `) expect(proc.written[0]).toContain(error.stack) expect(proc.exits).toEqual([1]) }) // One rejection is reported per install: the first is the diagnosis, so each // formatting case needs its own handler rather than reusing a latched one. it('stringifies a non-Error rejection and an Error without a stack falls back to its message', () => { const plain = fakeProc() installFailLoud(NAME, plain) plain.handlers[0]!('plain failure') expect(plain.written[0]).toContain('plain failure') expect(plain.exits).toEqual([1]) const stackless = new Error('no stack') delete (stackless as { stack?: string }).stack const bare = fakeProc() installFailLoud(NAME, bare) bare.handlers[0]!(stackless) expect(bare.written[0]).toContain('no stack') expect(bare.exits).toEqual([1]) }) it('returns an uninstaller that removes the handler (and defaults to the real process)', () => { const proc = fakeProc() const uninstall = installFailLoud(NAME, proc) expect(proc.handlers).toHaveLength(1) uninstall() expect(proc.handlers).toHaveLength(0) // Default-proc arm: install on the real process, then immediately uninstall // so the suite leaks no handler and can never exit the runner. const before = process.listenerCount('unhandledRejection') const uninstallReal = installFailLoud(NAME) expect(process.listenerCount('unhandledRejection')).toBe(before + 1) uninstallReal() expect(process.listenerCount('unhandledRejection')).toBe(before) }) it('does not report an activation rejection shared by entries in the boot audit', async () => { const proc = fakeProc() installFailLoud(NAME, proc) const error = new Error('assembled activation failure') const audit = assertEntriesActivated({ loader: { entries: () => ['broken-a', 'broken-b'].map(name => ({ options: { name }, fiber: { state: 3, inject: {}, ctx: { get: () => undefined }, await: async () => { throw error }, }, })), }, } as unknown as Context, NAME) await Promise.resolve() await Promise.resolve() proc.handlers[0]!(error) expect(proc.written).toEqual([]) expect(proc.exits).toEqual([]) await expect(audit).rejects.toThrow('assembled activation failure') proc.handlers[0]!(error) expect(proc.exits).toEqual([1]) }) // The Loader mounts entries concurrently, so a terminal-owning surface can // already hold raw mode when a sibling entry rejects. Exiting without running // its teardown strands the terminal on the user's shell. it('awaits the release hook before exiting so the terminal owner can restore it', async () => { const proc = fakeProc() const order: string[] = [] installFailLoud(NAME, proc, async () => { await Promise.resolve() order.push('released') }) proc.handlers[0]!(new Error('sibling entry rejected')) expect(proc.written[0]).toContain(`${NAME}: fatal load failure: `) // The release is in flight, so the exit has not committed yet. expect(proc.exits).toEqual([]) await vi.waitFor(() => { expect(proc.exits).toEqual([1]) }) expect(order).toEqual(['released']) }) it('still exits when the release hook rejects', async () => { const proc = fakeProc() installFailLoud(NAME, proc, () => Promise.reject(new Error('terminal stop failed'))) proc.handlers[0]!(new Error('boom')) await vi.waitFor(() => { expect(proc.exits).toEqual([1]) }) }) it('exits without waiting when a release hook never settles', async () => { vi.useFakeTimers() try { const proc = fakeProc() installFailLoud(NAME, proc, () => new Promise(() => {})) proc.handlers[0]!(new Error('boom')) expect(proc.exits).toEqual([]) await vi.advanceTimersByTimeAsync(FAIL_LOUD_RELEASE_TIMEOUT_MS) expect(proc.exits).toEqual([1]) } finally { vi.useRealTimers() } }) // Loader failures arrive in bursts, and teardown's own disposers may reject. // Only the first rejection is the diagnosis; the handler must stay installed // so a later one cannot become uncaught and kill the process mid-teardown. it('reports only the first rejection and keeps handling later ones during the release', async () => { const proc = fakeProc() let released = false installFailLoud(NAME, proc, async () => { await Promise.resolve() released = true }) proc.handlers[0]!(new Error('first rejection')) proc.handlers[0]!(new Error('second rejection')) expect(proc.handlers).toHaveLength(1) expect(proc.written).toHaveLength(1) expect(proc.written[0]).toContain('first rejection') await vi.waitFor(() => { expect(proc.exits).toEqual([1]) }) expect(released).toBe(true) }) }) describe('assertEntriesLoaded', () => { const ctxWith = (entries: Array<{ fiber?: unknown; disabled?: boolean; options: { name?: string } }>): Context => ({ loader: { entries: () => entries } }) as unknown as Context it('passes when every enabled entry has a fiber', () => { expect(() => { assertEntriesLoaded(ctxWith([ { fiber: {}, options: { name: 'a' } }, { disabled: true, options: { name: 'off' } }, ]), NAME) }).not.toThrow() }) it('throws naming every enabled fiber-less entry', () => { expect(() => { assertEntriesLoaded(ctxWith([ { fiber: {}, options: { name: 'ok' } }, { options: { name: 'broken-a' } }, { options: { name: 'broken-b' } }, ]), NAME) }).toThrow(`${NAME}: plugin(s) failed to load: broken-a, broken-b`) }) }) describe('assertEntriesActivated', () => { interface FakeFiber { state: number inject: Record ctx: { get(name: string): unknown } await(): Promise } const ctxWith = (entries: Array<{ fiber?: FakeFiber; disabled?: boolean; options: { name: string } }>): Context => ({ loader: { entries: () => entries }, }) as unknown as Context const fiber = ( state: number, error?: unknown, inject: Record = {}, services: string[] = [], ): FakeFiber => ({ state, inject, ctx: { get: name => services.includes(name) ? {} : undefined }, await: error === undefined ? async () => undefined : async () => { throw error }, }) it('passes active entries and ignores disabled entries', async () => { let awaitCalls = 0 const active = fiber(2) active.await = async () => { awaitCalls++ return undefined } const disabled = fiber(3, new Error('disabled failure')) disabled.await = async () => { awaitCalls++ throw new Error('disabled failure') } await expect(assertEntriesActivated(ctxWith([ { fiber: active, options: { name: 'active' } }, { fiber: disabled, disabled: true, options: { name: 'disabled' } }, ]), NAME)).resolves.toBeUndefined() expect(awaitCalls).toBe(0) }) it('reports the plugin name and original activation stack instead of fiber state 3', async () => { const original = new Error('actual plugin failure') await expect(assertEntriesActivated(ctxWith([ { fiber: fiber(3, original), options: { name: 'broken-plugin' } }, ]), NAME)).rejects.toThrow(`${NAME}: 1 entry did not activate\nbroken-plugin: ${original.stack!}`) }) it('formats stackless and non-Error activation failures', async () => { const stackless = new Error('stackless failure') delete (stackless as { stack?: string }).stack await expect(assertEntriesActivated(ctxWith([ { fiber: fiber(3, stackless), options: { name: 'stackless' } }, { fiber: fiber(3, 'plain failure'), options: { name: 'plain' } }, ]), NAME)).rejects.toThrow(`${NAME}: 2 entries did not activate\nstackless: stackless failure\nplain: plain failure`) }) it('reports unresolved services for pending entries', async () => { let awaitCalls = 0 const expected = [ `${NAME}: 3 entries did not activate`, 'waiting: pending (waiting for services: missingA, missingB)', 'single-wait: pending (waiting for service: missing)', 'unknown-wait: pending (waiting for services: unknown)', ].join('\n') const waiting = fiber(0, undefined, { ready: {}, missingA: {}, missingB: {} }, ['ready']) const singleWait = fiber(0, undefined, { missing: {} }) const unknownWait = fiber(0) for (const item of [waiting, singleWait, unknownWait]) { item.await = async () => { awaitCalls++ return undefined } } await expect(assertEntriesActivated(ctxWith([ { fiber: waiting, options: { name: 'waiting' } }, { fiber: singleWait, options: { name: 'single-wait' } }, { fiber: unknownWait, options: { name: 'unknown-wait' } }, ]), NAME)).rejects.toThrow(expected) expect(awaitCalls).toBe(0) }) it('retains the numeric diagnostic for a settled unexpected state', async () => { await expect(assertEntriesActivated(ctxWith([ { fiber: fiber(4), options: { name: 'disposed' } }, ]), NAME)).rejects.toThrow('disposed: fiber state 4') }) }) describe('loadOverlayPatches', () => { it('loads expressions and rejects missing, malformed, non-array, and non-mapping overlays', () => { const dir = tmp() const valid = join(dir, 'valid.yml') writeFileSync(valid, '- id: target\n config:\n value: !!js process.env.VALUE\n') expect(loadOverlayPatches(NAME, valid)).toEqual([{ id: 'target', config: { value: { __jsExpr: 'process.env.VALUE' } } }]) expect(() => loadOverlayPatches(NAME, join(dir, 'missing.yml'))).toThrow(`${NAME}: failed to read overlay`) const malformed = join(dir, 'malformed.yml') writeFileSync(malformed, ': bad') expect(() => loadOverlayPatches(NAME, malformed)).toThrow(`${NAME}: failed to parse overlay`) const mapping = join(dir, 'mapping.yml') writeFileSync(mapping, 'id: target\n') expect(() => loadOverlayPatches(NAME, mapping)).toThrow('must be a top-level YAML array') const scalar = join(dir, 'scalar.yml') writeFileSync(scalar, '- scalar\n') expect(() => loadOverlayPatches(NAME, scalar)).toThrow('entry 1') }) }) describe('boot', () => { it('boots a leaf config through the real Loader and settles the tree', async () => { const dir = tmp() writeFileSync(join(dir, 'noop.mjs'), 'export const name = "noop"\nexport function apply() {}\n') writeFileSync(join(dir, 'cordis.yml'), '- id: noop\n name: ./noop.mjs\n') const ctx = await boot(NAME, join(dir, 'cordis.yml')) try { const entries = [...ctx.loader.entries()] expect(entries.some(entry => entry.options.name === './noop.mjs' && entry.fiber !== undefined)).toBe(true) } finally { await ctx.fiber.dispose() } }) it('runs host preparation before the Loader tree mounts', async () => { const dir = tmp() writeFileSync(join(dir, 'noop.mjs'), 'export const name = "noop"\nexport function apply() {}\n') writeFileSync(join(dir, 'cordis.yml'), '- id: noop\n name: ./noop.mjs\n') const prepared: Context[] = [] const ctx = await boot(NAME, join(dir, 'cordis.yml'), undefined, (hostCtx) => { expect(hostCtx.loader).toBeDefined() expect([...hostCtx.loader.entries()]).toEqual([]) prepared.push(hostCtx) }) try { expect(prepared).toEqual([ctx]) } finally { await ctx.fiber.dispose() } }) it('disposes partial host setup and labels non-Error preparation failures', async () => { const dir = tmp() const failure = 42 let disposed = false const task = boot(NAME, join(dir, 'cordis.yml'), undefined, (ctx) => { ctx.effect(() => () => { disposed = true }) throw failure }) await expect(task).rejects.toMatchObject({ message: `${NAME}: host preparation failed: ${failure}`, cause: failure, }) expect(disposed).toBe(true) }) it('exposes dshHomePath to Loader config expressions', async () => { const dir = tmp() const dshHome = join(dir, 'home') vi.stubEnv('DSH_HOME', dshHome) writeFileSync(join(dir, 'capture.mjs'), [ 'export const name = "capture"', 'export function apply(ctx, config) {', ' ctx.provide("capturedPath", config.path)', '}', '', ].join('\n')) writeFileSync(join(dir, 'cordis.yml'), [ '- id: capture', ' name: ./capture.mjs', ' config:', " path: !!js dshHomePath('sessions')", '', ].join('\n')) let ctx: Context | undefined try { ctx = await boot(NAME, join(dir, 'cordis.yml')) expect(ctx.get('capturedPath')).toBe(join(dshHome, 'sessions')) } finally { await ctx?.fiber.dispose() vi.unstubAllEnvs() } }) it('returns instead of asserting over a tree a surface disposed mid-startup', async () => { // A surface can dispose the root fiber while boot() is still awaiting the // Loader, before the last entry settles. The Loader service goes with the // tree, so reading it for the post-boot assertions would crash an app that // exited exactly as the user asked. const dir = tmp() writeFileSync(join(dir, 'exiting.mjs'), [ 'export const name = "exiting"', 'export function apply(ctx) {', ' void ctx.root.fiber.dispose()', '}', '', ].join('\n')) writeFileSync(join(dir, 'cordis.yml'), '- id: exiting\n name: ./exiting.mjs\n') const ctx = await boot(NAME, join(dir, 'cordis.yml')) expect(ctx.get('loader')).toBeUndefined() }) it('rejects (never exits 0 half-empty) when a config names a plugin that cannot be imported', async () => { const dir = tmp() writeFileSync(join(dir, 'cordis.yml'), '- id: ghost\n name: ./missing.mjs\n') await expect(boot(NAME, join(dir, 'cordis.yml'))).rejects.toThrow( `${NAME}: plugin tree failed to load: failed to apply loader entry`, ) }) it('appends the deepest cause with its original stack to the load failure', async () => { const dir = tmp() writeFileSync(join(dir, 'failing.mjs'), [ 'export function apply() {', " const failure = new Error('pinned activation failure')", " failure.stack = 'Error: pinned activation failure\\n at failing-fixture'", ' throw failure', '}', '', ].join('\n')) writeFileSync(join(dir, 'cordis.yml'), '- id: failing\n name: ./failing.mjs\n') await expect(boot(NAME, join(dir, 'cordis.yml'))).rejects.toThrow(new RegExp([ String.raw`failed to apply loader entry failing \(\./failing\.mjs\): pinned activation failure\n`, String.raw`Error: pinned activation failure\n {4}at failing-fixture$`, ].join(''))) }) it('falls back to the deepest cause message when its stack was erased', async () => { const dir = tmp() const deepest = new Error('stackless deep failure') delete (deepest as { stack?: string }).stack await expect(boot(NAME, join(dir, 'cordis.yml'), undefined, () => { throw new Error('wrapped setup failure', { cause: deepest }) })).rejects.toThrow( `${NAME}: host preparation failed: wrapped setup failure\nstackless deep failure`, ) }) it('reports a pending real Loader fiber and the service unresolved in its own context', async () => { const dir = tmp() writeFileSync(join(dir, 'waiting.mjs'), 'export const inject = ["neverProvided"]\nexport function apply() {}\n') writeFileSync(join(dir, 'cordis.yml'), '- id: waiting\n name: ./waiting.mjs\n') await expect(boot(NAME, join(dir, 'cordis.yml'))).rejects.toThrow([ `${NAME}: 1 entry did not activate`, './waiting.mjs: pending (waiting for service: neverProvided)', ].join('\n')) }) }) describe('addHarnessSourceSection', () => { const SOURCE_ROOT = `${sep}opt${sep}harness-src` const EXPECTED = `The DeepSeek Harness implementation checkout is at ${SOURCE_ROOT}. The checkout location and current working directory are separate values and may differ; never infer the working directory from this path. Use pwd to determine the current working directory. Use this checkout only to inspect or extend DSH itself.` it('distinguishes the source path from the current workdir between identity and persona', async () => { const ctx = new Context() try { await ctx.plugin(SystemPrompt, { persona: 'You are a coding agent.' }) const dispose = addHarnessSourceSection(ctx, SOURCE_ROOT) expect(dispose).toBeTypeOf('function') const systemPrompt = ctx.get('systemPrompt')! const rendered = renderPrompt(await systemPrompt.assemble()) expect(rendered).toContain(EXPECTED) // Harness-owned opener (-100) → source (-99) → persona (0). The >= 0 guards // keep a drifted opener/persona string from a false pass through `-1 < n`. const identityAt = rendered.indexOf('You are an AI agent powered by the DeepSeek Harness SDK.') const sourceAt = rendered.indexOf(EXPECTED) const personaAt = rendered.indexOf('You are a coding agent.') expect(identityAt).toBeGreaterThanOrEqual(0) expect(personaAt).toBeGreaterThanOrEqual(0) expect(identityAt).toBeLessThan(sourceAt) expect(sourceAt).toBeLessThan(personaAt) } finally { await ctx.fiber.dispose() } }) it('is a no-op returning undefined when no systemPrompt service is mounted', async () => { const ctx = new Context() try { expect(addHarnessSourceSection(ctx, SOURCE_ROOT)).toBeUndefined() } finally { await ctx.fiber.dispose() } }) it('disposes the section it added, so a systemPrompt reload leaves no residue', async () => { const ctx = new Context() try { await ctx.plugin(SystemPrompt, {}) const systemPrompt = ctx.get('systemPrompt')! const dispose = addHarnessSourceSection(ctx, SOURCE_ROOT)! const present = await systemPrompt.assemble() expect(present.sections.some(section => section.name === HARNESS_SOURCE_SECTION)).toBe(true) dispose() const gone = await systemPrompt.assemble() expect(gone.sections.some(section => section.name === HARNESS_SOURCE_SECTION)).toBe(false) } finally { await ctx.fiber.dispose() } }) })