mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-30 04:40:37 +00:00
Replace the TypeScript SDK's public arbitrary command/argv launch surface with the same-version dsh CLI, a named profile, ordered per-launch patches, optional process cwd, and explicit Harness home selection. Installed consumers use the built CLI; clean source checkouts use the package's src/bin.ts through an absolute tsx/esm loader and a source-only patch that omits build-generated Typert loading. Materialize explicit or inherited environments at spawn time, keep profile-internal patches below caller patches, and resolve every caller-relative path before the child starts. The SDK subagent provider validates its optional CLI and patch files at plugin load and requires an isolated absolute child home. Treat JSON-RPC initialize as the Loader-owned readiness point: Loader settlement joins entry imports, fiber lifecycle work, and synchronous effect registration, so the server needs no scheduler tick. A delayed profile entry registers a private adapter before initialize resolves, proving caller-supplied plugin routes are visible without fallback. Update sdk-app ownership, server diagnostics, TypeScript SDK examples, nested-loader coverage, and session-upload replay layering together. The following commit contains only the regenerated SDK transcripts, keeping this API and lifecycle change directly reviewable.
170 lines
6.5 KiB
TypeScript
170 lines
6.5 KiB
TypeScript
/** Public dsh launch resolution for the TypeScript SDK. */
|
|
|
|
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { join, resolve } from 'node:path'
|
|
import { pathToFileURL } from 'node:url'
|
|
import { afterEach, describe, expect, it } from 'vitest'
|
|
import {
|
|
DEFAULT_INITIALIZE_TIMEOUT_MS,
|
|
installedDshBin,
|
|
resolveDshNodeLaunchFromManifests,
|
|
resolveDshBinFromManifests,
|
|
resolveDshLaunch,
|
|
} from '../src/launch.ts'
|
|
|
|
const cleanups: string[] = []
|
|
afterEach(() => {
|
|
for (const path of cleanups.splice(0)) rmSync(path, { recursive: true, force: true })
|
|
})
|
|
|
|
function manifestPair(dsh: object, client: object): { dshUrl: string; clientUrl: string; root: string } {
|
|
const root = mkdtempSync(join(tmpdir(), 'dsh-sdk-manifests-'))
|
|
cleanups.push(root)
|
|
const dshPath = join(root, 'dsh-package.json')
|
|
const clientPath = join(root, 'client-package.json')
|
|
writeFileSync(dshPath, JSON.stringify(dsh))
|
|
writeFileSync(clientPath, JSON.stringify(client))
|
|
return {
|
|
dshUrl: pathToFileURL(dshPath).href,
|
|
clientUrl: pathToFileURL(clientPath).href,
|
|
root,
|
|
}
|
|
}
|
|
|
|
describe('SDK dsh launch resolution', () => {
|
|
it('resolves the same-version installed dsh entry by default', () => {
|
|
const bin = installedDshBin()
|
|
expect(bin.endsWith(join('apps', 'cli', 'lib', 'bin.js'))).toBe(true)
|
|
const launch = resolveDshLaunch()
|
|
expect(launch.command).toBe(process.execPath)
|
|
expect(launch.args).toEqual(existsSync(bin)
|
|
? [bin, '--profile', 'sdk']
|
|
: [
|
|
'--import', import.meta.resolve('tsx/esm'), resolve(bin, '..', '..', 'src/bin.ts'),
|
|
'--profile', 'sdk',
|
|
'--patch', resolve(bin, '..', '..', 'src/sdk-source.cordis.patch.yml'),
|
|
])
|
|
expect(launch.initializeTimeoutMs).toBe(DEFAULT_INITIALIZE_TIMEOUT_MS)
|
|
expect(launch.description).toBe('dsh profile "sdk"')
|
|
})
|
|
|
|
it('makes every filesystem input absolute before spawn and preserves patch order', () => {
|
|
const caller = resolve('/tmp', 'sdk-launch-caller')
|
|
const launch = resolveDshLaunch({
|
|
dshBin: './bin/dsh',
|
|
profile: 'custom-sdk',
|
|
patches: ['./first.yml', '../second.yml'],
|
|
dshHome: './home',
|
|
processCwd: './worker',
|
|
env: { PATH: '/bin', DSH_HOME: '/stale' },
|
|
initializeTimeoutMs: 123,
|
|
requestTimeoutMs: 456,
|
|
shutdownTimeoutMs: 789,
|
|
disposeEofGraceMs: 12,
|
|
disposeGraceMs: 34,
|
|
}, caller)
|
|
expect(launch).toMatchObject({
|
|
command: process.execPath,
|
|
args: [
|
|
join(caller, 'bin/dsh'),
|
|
'--profile', 'custom-sdk',
|
|
'--patch', join(caller, 'first.yml'),
|
|
'--patch', resolve(caller, '../second.yml'),
|
|
],
|
|
cwd: join(caller, 'worker'),
|
|
description: 'dsh profile "custom-sdk"',
|
|
initializeTimeoutMs: 123,
|
|
requestTimeoutMs: 456,
|
|
shutdownTimeoutMs: 789,
|
|
disposeEofGraceMs: 12,
|
|
disposeGraceMs: 34,
|
|
})
|
|
expect(launch.environment()).toEqual({ PATH: '/bin', DSH_HOME: join(caller, 'home') })
|
|
})
|
|
|
|
it('falls back to the same package source entry through an absolute tsx loader', () => {
|
|
const pair = manifestPair({ version: '1.0.0', bin: 'lib/bin.js' }, { version: '1.0.0' })
|
|
const sourceBin = join(pair.root, 'src/bin.ts')
|
|
const sourcePatch = join(pair.root, 'src/sdk-source.cordis.patch.yml')
|
|
const sourceTsconfig = join(pair.root, 'tsconfig.json')
|
|
mkdirSync(join(pair.root, 'src'))
|
|
writeFileSync(sourceBin, '')
|
|
writeFileSync(sourcePatch, '[]\n')
|
|
writeFileSync(sourceTsconfig, '{}\n')
|
|
|
|
expect(resolveDshNodeLaunchFromManifests(pair.dshUrl, pair.clientUrl, 'file:///tsx-loader.mjs'))
|
|
.toEqual({
|
|
nodeArgs: ['--import', 'file:///tsx-loader.mjs', sourceBin],
|
|
patches: [sourcePatch],
|
|
environment: { TSX_TSCONFIG_PATH: sourceTsconfig },
|
|
})
|
|
expect(resolveDshNodeLaunchFromManifests(pair.dshUrl, pair.clientUrl))
|
|
.toEqual({
|
|
nodeArgs: ['--import', import.meta.resolve('tsx/esm'), sourceBin],
|
|
patches: [sourcePatch],
|
|
environment: { TSX_TSCONFIG_PATH: sourceTsconfig },
|
|
})
|
|
})
|
|
|
|
it('uses the built entry when the manifest bin exists', () => {
|
|
const pair = manifestPair({ version: '1.0.0', bin: 'lib/bin.js' }, { version: '1.0.0' })
|
|
const bin = join(pair.root, 'lib/bin.js')
|
|
mkdirSync(join(pair.root, 'lib'))
|
|
writeFileSync(bin, '')
|
|
|
|
expect(resolveDshNodeLaunchFromManifests(pair.dshUrl, pair.clientUrl)).toEqual({
|
|
nodeArgs: [bin],
|
|
patches: [],
|
|
environment: {},
|
|
})
|
|
})
|
|
|
|
it.each([0, 1, 2])('fails loud when a source launch is missing required file set %s', (presentCount) => {
|
|
const pair = manifestPair({ version: '1.0.0', bin: 'lib/bin.js' }, { version: '1.0.0' })
|
|
mkdirSync(join(pair.root, 'src'))
|
|
const sourceFiles = ['src/bin.ts', 'src/sdk-source.cordis.patch.yml', 'tsconfig.json']
|
|
for (const source of sourceFiles.slice(0, presentCount)) writeFileSync(join(pair.root, source), '')
|
|
expect(() => resolveDshNodeLaunchFromManifests(pair.dshUrl, pair.clientUrl, 'file:///tsx-loader.mjs'))
|
|
.toThrow('is missing its built executable')
|
|
})
|
|
|
|
it('reads explicit and inherited environments when the child starts', () => {
|
|
const explicit: NodeJS.ProcessEnv = { MARKER: 'before' }
|
|
const explicitLaunch = resolveDshLaunch({ dshBin: '/bin/dsh', env: explicit })
|
|
explicit.MARKER = 'after'
|
|
expect(explicitLaunch.environment().MARKER).toBe('after')
|
|
|
|
const inheritedLaunch = resolveDshLaunch({ dshBin: '/bin/dsh' })
|
|
process.env.DSH_SDK_LATE_ENV_TEST = 'late'
|
|
try {
|
|
expect(inheritedLaunch.environment().DSH_SDK_LATE_ENV_TEST).toBe('late')
|
|
} finally {
|
|
delete process.env.DSH_SDK_LATE_ENV_TEST
|
|
}
|
|
})
|
|
|
|
it.each([2, '2.0.0'])(
|
|
'rejects a dsh version that differs from the client (%j)',
|
|
(version) => {
|
|
const pair = manifestPair({ version, bin: 'bin.js' }, { version: '1.0.0' })
|
|
expect(() => resolveDshBinFromManifests(pair.dshUrl, pair.clientUrl))
|
|
.toThrow(`requires the same dsh version, got ${String(version)}`)
|
|
},
|
|
)
|
|
|
|
it('accepts the string npm bin form', () => {
|
|
const pair = manifestPair({ version: '1.0.0', bin: './bin.js' }, { version: '1.0.0' })
|
|
expect(resolveDshBinFromManifests(pair.dshUrl, pair.clientUrl)).toBe(join(pair.root, 'bin.js'))
|
|
})
|
|
|
|
it.each([null, {}, ''])(
|
|
'rejects a manifest without a usable dsh executable (%j)',
|
|
(bin) => {
|
|
const pair = manifestPair({ version: '1.0.0', bin }, { version: '1.0.0' })
|
|
expect(() => resolveDshBinFromManifests(pair.dshUrl, pair.clientUrl))
|
|
.toThrow('declares no dsh executable')
|
|
},
|
|
)
|
|
})
|