|
|
@@ -19,6 +19,10 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
|
|
|
|
/** Published-entry acceptance for argument errors, profile lifecycle, and boot-free config dumps. */
|
|
|
const repoRoot = fileURLToPath(new URL('../../../', import.meta.url))
|
|
|
+// The dsh built bin cold-starts slowly on the contended self-hosted Windows pool; the
|
|
|
+// execa deadline, its error text, the outer vitest case budget, and waitForFile all
|
|
|
+// share this value so a widening cannot leave a stale 25s diagnostic behind.
|
|
|
+const SPAWN_TIMEOUT_MS = 60_000
|
|
|
// The release version, including a prerelease such as 0.0.1-rc.1: `--version`
|
|
|
// prints what this manifest carries, so no test may pin it to a literal.
|
|
|
const cliVersion = (JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')) as { version: string }).version
|
|
|
@@ -36,7 +40,7 @@ async function runBuiltBin(
|
|
|
)
|
|
|
const result = await execa(process.execPath, [dshBin, ...args], {
|
|
|
input: '',
|
|
|
- timeout: 60_000,
|
|
|
+ timeout: SPAWN_TIMEOUT_MS,
|
|
|
killSignal: 'SIGKILL',
|
|
|
reject: false,
|
|
|
env: childEnv,
|
|
|
@@ -44,13 +48,13 @@ async function runBuiltBin(
|
|
|
...cwd === undefined ? {} : { cwd },
|
|
|
})
|
|
|
if (result.timedOut) {
|
|
|
- throw new Error(`dsh built bin did not exit within 25s. stdout:\n${result.stdout}\nstderr:\n${result.stderr}`)
|
|
|
+ throw new Error(`dsh built bin did not exit within ${SPAWN_TIMEOUT_MS / 1_000}s. stdout:\n${result.stdout}\nstderr:\n${result.stderr}`)
|
|
|
}
|
|
|
return { stdout: result.stdout, code: result.exitCode ?? -1, stderr: result.stderr }
|
|
|
}
|
|
|
|
|
|
async function waitForFile(file: string): Promise<void> {
|
|
|
- const deadline = Date.now() + 20_000
|
|
|
+ const deadline = Date.now() + SPAWN_TIMEOUT_MS
|
|
|
while (!existsSync(file)) {
|
|
|
if (Date.now() >= deadline) throw new Error(`dsh profile lifecycle marker did not appear: ${file}`)
|
|
|
await new Promise(resolve => setTimeout(resolve, 20))
|
|
|
@@ -310,7 +314,7 @@ function startStartupProfile(fixture: StartupFixture, args: readonly string[]) {
|
|
|
cwd: fixture.home,
|
|
|
input: '',
|
|
|
reject: false,
|
|
|
- timeout: 60_000,
|
|
|
+ timeout: SPAWN_TIMEOUT_MS,
|
|
|
killSignal: 'SIGKILL',
|
|
|
env: {
|
|
|
DSH_HOME: fixture.home,
|
|
|
@@ -335,7 +339,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
|
|
|
const result = await runBuiltBin(removed)
|
|
|
expect(result.code).toBe(1)
|
|
|
}
|
|
|
- }, 30_000)
|
|
|
+ }, SPAWN_TIMEOUT_MS + 30_000)
|
|
|
|
|
|
it('routes help and usage errors without activating startup-dependent rows', async () => {
|
|
|
const home = mkdtempSync(join(tmpdir(), 'dsh-app-help-'))
|
|
|
@@ -416,14 +420,14 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
|
|
|
} finally {
|
|
|
rmSync(home, { recursive: true, force: true })
|
|
|
}
|
|
|
- }, 30_000)
|
|
|
+ }, SPAWN_TIMEOUT_MS + 30_000)
|
|
|
|
|
|
it('serves the SDK protocol through the sdk profile and exits after shutdown', async () => {
|
|
|
const home = mkdtempSync(join(tmpdir(), 'dsh-built-sdk-'))
|
|
|
const child = execa(process.execPath, [dshBin, '--profile', 'sdk'], {
|
|
|
cwd: home,
|
|
|
reject: false,
|
|
|
- timeout: 60_000,
|
|
|
+ timeout: SPAWN_TIMEOUT_MS,
|
|
|
killSignal: 'SIGKILL',
|
|
|
env: {
|
|
|
...process.env,
|
|
|
@@ -471,7 +475,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
|
|
|
await child
|
|
|
rmSync(home, { recursive: true, force: true })
|
|
|
}
|
|
|
- }, 30_000)
|
|
|
+ }, SPAWN_TIMEOUT_MS + 30_000)
|
|
|
|
|
|
it('runs a mock-backed ACP turn through the acp profile and exits on disconnect', async () => {
|
|
|
const apiKey = 'built-acp-profile-key'
|
|
|
@@ -484,7 +488,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
|
|
|
const child = execa(process.execPath, [dshBin, '--profile', 'acp'], {
|
|
|
cwd: home,
|
|
|
reject: false,
|
|
|
- timeout: 60_000,
|
|
|
+ timeout: SPAWN_TIMEOUT_MS,
|
|
|
killSignal: 'SIGKILL',
|
|
|
env: {
|
|
|
...process.env,
|
|
|
@@ -555,7 +559,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
|
|
|
await server.close()
|
|
|
rmSync(home, { recursive: true, force: true })
|
|
|
}
|
|
|
- }, 30_000)
|
|
|
+ }, SPAWN_TIMEOUT_MS + 30_000)
|
|
|
|
|
|
it('runs the headless profile through its app-owned task positional', async () => {
|
|
|
const apiKey = 'built-dsh-headless-key'
|
|
|
@@ -583,7 +587,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
|
|
|
await server.close()
|
|
|
rmSync(home, { recursive: true, force: true })
|
|
|
}
|
|
|
- }, 30_000)
|
|
|
+ }, SPAWN_TIMEOUT_MS + 30_000)
|
|
|
|
|
|
it('does not load a project environment for --version', async () => {
|
|
|
const project = mkdtempSync(join(tmpdir(), 'dsh-version-project-'))
|
|
|
@@ -606,7 +610,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
|
|
|
} finally {
|
|
|
rmSync(home, { recursive: true, force: true })
|
|
|
}
|
|
|
- }, 30_000)
|
|
|
+ }, SPAWN_TIMEOUT_MS + 30_000)
|
|
|
|
|
|
it('uses the launching endpoint and managed credential through the published entry', async () => {
|
|
|
const apiKey = 'built-home-layer-key'
|
|
|
@@ -646,7 +650,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
|
|
|
rmSync(home, { recursive: true, force: true })
|
|
|
rmSync(project, { recursive: true, force: true })
|
|
|
}
|
|
|
- }, 30_000)
|
|
|
+ }, SPAWN_TIMEOUT_MS + 30_000)
|
|
|
|
|
|
it('reports a patch-overlay boot failure without hanging', async () => {
|
|
|
// The HMR main watcher's initial scan once refreshed the include
|
|
|
@@ -666,7 +670,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
|
|
|
} finally {
|
|
|
rmSync(home, { recursive: true, force: true })
|
|
|
}
|
|
|
- }, 30_000)
|
|
|
+ }, SPAWN_TIMEOUT_MS + 30_000)
|
|
|
|
|
|
it('lets a profile without a parser ignore app arguments and dispose on a startup-time signal', async () => {
|
|
|
const fixture = createProfileLifecycleFixture()
|
|
|
@@ -682,7 +686,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
|
|
|
child.kill('SIGKILL')
|
|
|
rmSync(fixture.home, { recursive: true, force: true })
|
|
|
}
|
|
|
- }, 30_000)
|
|
|
+ }, SPAWN_TIMEOUT_MS + 30_000)
|
|
|
|
|
|
it('fully settles a custom profile, hot-reloads its patch layer with removal reverting, and disposes on a signal', async () => {
|
|
|
const fixture = createProfileLifecycleFixture()
|
|
|
@@ -734,7 +738,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
|
|
|
child.kill('SIGKILL')
|
|
|
rmSync(fixture.home, { recursive: true, force: true })
|
|
|
}
|
|
|
- }, 30_000)
|
|
|
+ }, SPAWN_TIMEOUT_MS + 30_000)
|
|
|
|
|
|
it('hands the app arguments to the profile, which applies them before its rows start', async () => {
|
|
|
const fixture = createStartupFixture()
|
|
|
@@ -750,7 +754,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
|
|
|
child.kill('SIGKILL')
|
|
|
rmSync(fixture.home, { recursive: true, force: true })
|
|
|
}
|
|
|
- }, 30_000)
|
|
|
+ }, SPAWN_TIMEOUT_MS + 30_000)
|
|
|
|
|
|
it('starts a consumer on its composed value when the invocation carries no app arguments', async () => {
|
|
|
const fixture = createStartupFixture()
|
|
|
@@ -764,7 +768,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
|
|
|
child.kill('SIGKILL')
|
|
|
rmSync(fixture.home, { recursive: true, force: true })
|
|
|
}
|
|
|
- }, 30_000)
|
|
|
+ }, SPAWN_TIMEOUT_MS + 30_000)
|
|
|
|
|
|
it('keeps the app arguments across a user patch reload', async () => {
|
|
|
// A live edit recomposes every row while the provider service remains
|
|
|
@@ -798,7 +802,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
|
|
|
child.kill('SIGKILL')
|
|
|
rmSync(fixture.home, { recursive: true, force: true })
|
|
|
}
|
|
|
- }, 30_000)
|
|
|
+ }, SPAWN_TIMEOUT_MS + 30_000)
|
|
|
|
|
|
it("prints the app's own help, starts none of its rows, and exits", async () => {
|
|
|
const fixture = createStartupFixture()
|
|
|
@@ -811,7 +815,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
|
|
|
} finally {
|
|
|
rmSync(fixture.home, { recursive: true, force: true })
|
|
|
}
|
|
|
- }, 30_000)
|
|
|
+ }, SPAWN_TIMEOUT_MS + 30_000)
|
|
|
|
|
|
it('anchors a relative add spec to the invoking directory, not the profile', async () => {
|
|
|
// `dsh plugin --profile x add .` from a plugin checkout must install THAT
|
|
|
@@ -829,7 +833,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
|
|
|
const result = await execa(process.execPath, [dshBin, 'plugin', '--profile', 'anchor', 'add', '.'], {
|
|
|
cwd: checkout,
|
|
|
input: '',
|
|
|
- timeout: 60_000,
|
|
|
+ timeout: SPAWN_TIMEOUT_MS,
|
|
|
killSignal: 'SIGKILL',
|
|
|
reject: false,
|
|
|
env: { DSH_HOME: home },
|
|
|
@@ -897,7 +901,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
|
|
|
} finally {
|
|
|
rmSync(home, { recursive: true, force: true })
|
|
|
}
|
|
|
- }, 30_000)
|
|
|
+ }, SPAWN_TIMEOUT_MS + 30_000)
|
|
|
|
|
|
describe('config dump', () => {
|
|
|
let home: string
|
|
|
@@ -913,7 +917,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
|
|
|
expect(stdout).toContain('# == @deepseek-ai/dsh-base')
|
|
|
expect(stdout).toContain("name: '@deepseek-ai/dsh-host-webserver'")
|
|
|
expect(existsSync(join(home, 'profiles', 'node_modules'))).toBe(false)
|
|
|
- }, 30_000)
|
|
|
+ }, SPAWN_TIMEOUT_MS + 30_000)
|
|
|
|
|
|
it('prints the headless profile without Host or browser layers', async () => {
|
|
|
const { stdout, code, stderr } = await runBuiltBin(
|
|
|
@@ -926,7 +930,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
|
|
|
expect(stdout).not.toMatch(/name: '@deepseek-ai\/dsh-host-/)
|
|
|
expect(stdout).not.toContain("name: '@deepseek-ai/dsh-web-app'")
|
|
|
expect(stdout).not.toMatch(/name: '@deepseek-ai\/dsh-client-/)
|
|
|
- }, 30_000)
|
|
|
+ }, SPAWN_TIMEOUT_MS + 30_000)
|
|
|
|
|
|
it('prints the exact standalone sdk-minimal tree without dsh-base', async () => {
|
|
|
const { stdout, code, stderr } = await runBuiltBin(
|
|
|
@@ -959,7 +963,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
|
|
|
expect(stdout).toContain('# == @deepseek-ai/dsh-sdk-minimal')
|
|
|
expect(stdout).not.toContain('@deepseek-ai/dsh-base')
|
|
|
expect(stdout).not.toContain('@deepseek-ai/dsh-web-app')
|
|
|
- }, 30_000)
|
|
|
+ }, SPAWN_TIMEOUT_MS + 30_000)
|
|
|
|
|
|
it('composes the profile user layer and a --patch overlay in order', async () => {
|
|
|
// Auto-init the web profile first, then write its user layer.
|
|
|
@@ -998,6 +1002,6 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
|
|
|
// Both layers patched the row; the comment lists them in application order.
|
|
|
expect(stdout).toContain(`patched by ${profilePatch}, ${overlay}`)
|
|
|
expect(stderr).toContain('patch: entry "absent-row" not found')
|
|
|
- }, 30_000)
|
|
|
+ }, SPAWN_TIMEOUT_MS + 30_000)
|
|
|
})
|
|
|
})
|