diff --git a/apps/cli/tests/built-bin.e2e.ts b/apps/cli/tests/built-bin.e2e.ts index af8be201e1..fef2044a73 100644 --- a/apps/cli/tests/built-bin.e2e.ts +++ b/apps/cli/tests/built-bin.e2e.ts @@ -1,3 +1,4 @@ +import { stageStartupBundle } from './startup-bundle.ts' import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' @@ -411,7 +412,8 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)', '', ].join('\n')) try { - const result = await runBuiltBin(['--profile', 'sdk', '--patch', patch], { + stageStartupBundle(home, 'sdk', patch) + const result = await runBuiltBin(['--profile', 'sdk'], { DSH_HOME: home, DSH_TELEMETRY_DISABLED: '1', DEEPSEEK_API_KEY: 'built-sdk-startup-failure-no-call', @@ -757,18 +759,20 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)', } }, SPAWN_TIMEOUT_MS + 30_000) - it('keeps serving when an optional patch-overlay plugin fails', async () => { + it('rejects an external bundle that corrupts a builtin provider', async () => { const home = mkdtempSync(join(tmpdir(), 'dsh-invalid-patch-')) try { - const result = await runBuiltBin(['--profile', 'web', '--patch', invalidProvider, '--port', '0', '--no-open'], { + stageStartupBundle(home, 'web', invalidProvider) + const result = await runBuiltBin(['--profile', 'web', '--port', '0', '--no-open'], { DSH_HOME: home, DSH_BROWSER_OPEN_TEST_EXIT_ON_READY: '1', DEEPSEEK_API_KEY: 'keyless-invalid-config', DSH_TELEMETRY_DISABLED: '1', NODE_OPTIONS: `--import=${webReadyExitHook}`, }) - expect(result.code, result.stderr).toBe(0) - expect(result.stdout).toMatch(/^dsh web: http:\/\/127\.0\.0\.1:\d+\/\?token=[A-Za-z0-9_-]+$/u) + expect(result.code, result.stderr).toBe(1) + expect(result.stdout).not.toContain('dsh web: http://') + expect(result.stderr).toContain('required startup failure') expect(result.stderr).toContain('llm-pi-ai') } finally { rmSync(home, { recursive: true, force: true }) diff --git a/apps/cli/tests/fixtures/invalid-provider.cordis.yml b/apps/cli/tests/fixtures/invalid-provider.cordis.yml index 5aea6833ea..ac83ae27a1 100644 --- a/apps/cli/tests/fixtures/invalid-provider.cordis.yml +++ b/apps/cli/tests/fixtures/invalid-provider.cordis.yml @@ -1,4 +1,4 @@ -# Invalid optional provider override used to prove best-effort startup continues. +# Invalid override of a builtin provider: an external patch retains the target's required startup policy. - id: llm-pi-ai config: diff --git a/apps/cli/tests/profiles/headless/tests/headless.expected.e2e.ts b/apps/cli/tests/profiles/headless/tests/headless.expected.e2e.ts index bb31c90060..4433b89192 100644 --- a/apps/cli/tests/profiles/headless/tests/headless.expected.e2e.ts +++ b/apps/cli/tests/profiles/headless/tests/headless.expected.e2e.ts @@ -17,6 +17,7 @@ import { scanZstdFrames, } from '@deepseek-ai/dsh-session-persistence-jsonl/src/zstd.ts' import { describe, expect, it } from 'vitest' +import { stageStartupBundle } from '../../../startup-bundle.ts' const goldensDir = fileURLToPath(new URL('./expected/', import.meta.url)) const goalScenarioDir = join(goldensDir, 'goal-tools') @@ -277,10 +278,10 @@ describe('headless stream-json snapshots', () => { tempDirPrefix: 'headless-snapshot-startup-error-', binScript: dshBinScript, configPath: startupFailureConfigPath, + prepare: (cwd) => { stageStartupBundle(join(cwd, '.dsh'), 'headless', startupFailureConfigPath) }, binArgs: [ '--profile', 'headless', '--patch', headlessOverlayPath, - '--patch', startupFailureConfigPath, 'Complete the task despite the unrelated startup failure.', ], tsconfigPath, diff --git a/apps/cli/tests/profiles/web/tests/web-best-effort-startup.expected.e2e.ts b/apps/cli/tests/profiles/web/tests/web-best-effort-startup.expected.e2e.ts index f8485a7de7..cf6ad534d3 100644 --- a/apps/cli/tests/profiles/web/tests/web-best-effort-startup.expected.e2e.ts +++ b/apps/cli/tests/profiles/web/tests/web-best-effort-startup.expected.e2e.ts @@ -8,6 +8,7 @@ import type { Readable } from 'node:stream' import { fileURLToPath, pathToFileURL } from 'node:url' import { execa } from 'execa' import { describe, expect, it } from 'vitest' +import { stageStartupBundle } from '../../../startup-bundle.ts' const repoRoot = fileURLToPath(new URL('../../../../../../', import.meta.url)) const dshBin = join(repoRoot, 'apps/cli/lib/bin.js') @@ -46,6 +47,7 @@ function createFixture(): Fixture { '}', '', ].join('\n')) + writeFileSync(join(root, 'schema-failure.mjs'), 'export const Config = { "~standard": { version: 1, vendor: "startup", validate() { return { issues: [{ message: "web schema failure" }] } } } }; export function apply() {}\n') writeFileSync(join(root, 'sync-failure.mjs'), 'export function apply() { throw new Error("web sync apply failure") }\n') writeFileSync(join(root, 'async-failure.mjs'), [ 'export async function apply() {', @@ -61,10 +63,9 @@ function createFixture(): Fixture { ].join('\n')) const patch = join(root, 'failures.patch.yml') writeFileSync(patch, [ - '- id: tool-todo', - ' disabled: false', - ' config: {}', '- insert:', + ' - id: web-probe-schema-failure', + ` name: ${pathToFileURL(join(root, 'schema-failure.mjs')).href}`, ' - id: web-probe-good', ` name: ${pathToFileURL(join(root, 'good.mjs')).href}`, ' config:', @@ -134,10 +135,10 @@ async function waitForStartup( describe.skipIf(!builtArtifactsExist)('dsh Web profile best-effort startup', () => { it('serves the full Web app while unrelated entries fail to start', async () => { const fixture = createFixture() + stageStartupBundle(fixture.home, 'web', fixture.patch) const child = execa(process.execPath, [ dshBin, '--profile', 'web', - '--patch', fixture.patch, '--no-open', '--port', '0', ], { @@ -169,7 +170,7 @@ describe.skipIf(!builtArtifactsExist)('dsh Web profile best-effort startup', () expect(html).toContain('__DSH_BOOT__') expect(readFileSync(fixture.events, 'utf8')).toBe('good apply\n') expect(startup.stderr).toContain('web-probe-import-failure') - expect(startup.stderr).toContain('@deepseek-ai/dsh-tool-todo') + expect(startup.stderr).toContain('web schema failure') expect(startup.stderr).toContain('web sync apply failure') expect(startup.stderr).toContain('web async apply failure') expect(startup.stderr).toContain('pending (waiting for service: webProbeMissingService)') @@ -316,11 +317,11 @@ describe.skipIf(!builtArtifactsExist)('dsh Web profile best-effort startup', () ` name: ${pathToFileURL(plugin).href}`, '', ].join('\n')) + stageStartupBundle(fixture.home, 'web', fixture.patch) try { const result = await execa(process.execPath, [ dshBin, '--profile', 'web', - '--patch', fixture.patch, '--no-open', '--port', '0', ], { diff --git a/apps/cli/tests/profiles/web/tests/web-failure-matrix.expected.e2e.ts b/apps/cli/tests/profiles/web/tests/web-failure-matrix.expected.e2e.ts index d2d14a5ada..b907e3f133 100644 --- a/apps/cli/tests/profiles/web/tests/web-failure-matrix.expected.e2e.ts +++ b/apps/cli/tests/profiles/web/tests/web-failure-matrix.expected.e2e.ts @@ -8,6 +8,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url' import { execa } from 'execa' import { describe, expect, it } from 'vitest' import { FiberState } from '@deepseek-ai/cordis' +import { stageStartupBundle } from '../../../startup-bundle.ts' const repoRoot = fileURLToPath(new URL('../../../../../../', import.meta.url)) const bin = join(repoRoot, 'apps/cli/lib/bin.js') @@ -172,6 +173,8 @@ describe.skipIf(!built)('Web process failure matrix', () => { it.each(failures)(`${required ? 'required' : 'optional'} startup %s`, async (failure, diagnostic) => { const f = fixture() writeFileSync(f.patch, f.render(id, failure)) + stageStartupBundle(f.home, 'web', f.patch, required ? 'boot' : 'runtime') + writeFileSync(f.patch, '[]\n') const app = start(f) try { if (required) { @@ -360,7 +363,11 @@ describe.skipIf(!built)('Web process failure matrix', () => { const patch = JSON.stringify([{ insert: [f.observer, f.witness(0), { id: 'matrix-optional-http', name: pathToFileURL(plugin).href, config: { port: address.port }, }] }]) + '\n' - if (phase === 'startup') writeFileSync(f.patch, patch) + if (phase === 'startup') { + writeFileSync(f.patch, patch) + stageStartupBundle(f.home, 'web', f.patch) + writeFileSync(f.patch, '[]\n') + } app = start(f) const running = app await app.serves() diff --git a/apps/cli/tests/startup-bundle.ts b/apps/cli/tests/startup-bundle.ts new file mode 100644 index 0000000000..6d4a3c69a2 --- /dev/null +++ b/apps/cli/tests/startup-bundle.ts @@ -0,0 +1,38 @@ +/** Installed bundle provenance for product-profile startup acceptance tests. */ +import { existsSync, mkdirSync, writeFileSync } from 'node:fs' +import { join } from 'node:path' +import { + initProfile, loadOverlayPatches, PROFILE_TEMPLATES, readProfileManifest, writeProfileManifest, type ProfileLayer, +} from '@deepseek-ai/dsh-app-boot' + +/** + * Install a patch as a real profile dependency, preserving its module resolution anchors. + * @param home - the scenario's isolated Harness home. + * @param profile - the shipped profile under test. + * @param patchFile - the fixture patch whose relative module names resolve at its source. + * @param stage - whether this external bundle is required or optional at startup. + */ +export function stageStartupBundle( + home: string, + profile: 'web' | 'sdk' | 'headless', + patchFile: string, + stage: ProfileLayer['stage'] = 'runtime', +): void { + const dir = join(home, 'profiles', profile) + const template = PROFILE_TEMPLATES[profile] + if (template === undefined) throw new Error(`Missing shipped profile template: ${profile}`) + if (!existsSync(join(dir, 'package.json'))) initProfile(dir, template.bundles, template.patchReload) + const name = '@fixture/startup-policy' + const bundle = join(dir, 'node_modules', name) + mkdirSync(bundle, { recursive: true }) + writeFileSync(join(bundle, 'package.json'), JSON.stringify({ + name, version: '1.0.0', private: true, type: 'module', dsh: { bundle: { patch: './cordis.patch.json', stage } }, + })) + writeFileSync(join(bundle, 'cordis.patch.json'), JSON.stringify(loadOverlayPatches('startup fixture', patchFile))) + const manifest = readProfileManifest('startup fixture', dir) + writeProfileManifest(dir, { + ...manifest, + dependencies: { ...manifest.dependencies, [name]: '1.0.0' }, + dsh: { ...manifest.dsh, profile: { ...manifest.dsh?.profile, bundles: [...manifest.dsh?.profile?.bundles ?? [], name] } }, + }) +}