fix: validate optional Python resources and isolate downloads
This commit is contained in:
1 parent
305021fcd6
commit
4fd85be130
19 files changed
+335
-149
No files matched your search
@@ -1,10 +1,12 @@
|
||||
import { chmod, mkdir, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises'
|
||||
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, expect, it } from 'vitest'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { c as tar } from 'tar'
|
||||
import { copyOfficeSidecar, downloadOfficeSidecar } from './build-exe-for-python-sdk-office.ts'
|
||||
import { downloadOfficeSidecar, runtimeNpmArchive } from './build-exe-for-python-sdk-office.ts'
|
||||
|
||||
import { officePackageDirectories } from './libreoffice-packages.mjs'
|
||||
|
||||
const temporaryDirectories: string[] = []
|
||||
|
||||
@@ -26,33 +28,8 @@ async function fixture() {
|
||||
return { root, staging, destination, packageAt }
|
||||
}
|
||||
|
||||
it('copies package-owned data, licenses, helper permissions, and nested dependencies', async () => {
|
||||
const { staging, destination, packageAt } = await fixture()
|
||||
const entry = await packageAt('@deepseek-ai/libreoffice-kit', { optionalDependencies: { '@deepseek-ai/libreoffice-kit-wasm': '0.0.1' }, dependencies: { decoder: '1' } })
|
||||
const engine = await packageAt('@deepseek-ai/libreoffice-kit-wasm')
|
||||
const decoder = await packageAt('decoder', { dependencies: { codec: '1' } }, entry)
|
||||
await packageAt('codec', {}, decoder)
|
||||
await packageAt('codec', { version: '2.0.0' })
|
||||
const assets = ['assets/soffice.data', 'prebuilds.json', 'licenses/LICENSE', 'bin/helper']
|
||||
for (const name of assets) {
|
||||
const path = join(engine, name)
|
||||
await mkdir(dirname(path), { recursive: true })
|
||||
await writeFile(path, name)
|
||||
}
|
||||
await chmod(join(engine, 'bin/helper'), 0o755)
|
||||
|
||||
const packages = await copyOfficeSidecar(staging, destination, { platform: 'linux', arch: 'x64' })
|
||||
|
||||
expect(packages).toHaveLength(4)
|
||||
expect(await readFile(join(destination, 'node_modules/@deepseek-ai/libreoffice-kit-wasm/assets/soffice.data'), 'utf8')).toBe('assets/soffice.data')
|
||||
expect(await readFile(join(destination, 'node_modules/@deepseek-ai/libreoffice-kit-wasm/licenses/LICENSE'), 'utf8')).toBe('licenses/LICENSE')
|
||||
expect(await readFile(join(destination, 'node_modules/@deepseek-ai/libreoffice-kit/node_modules/decoder/node_modules/codec/package.json'), 'utf8')).toContain('codec')
|
||||
await expect(stat(join(destination, 'node_modules/codec'))).rejects.toMatchObject({ code: 'ENOENT' })
|
||||
if (process.platform !== 'win32') expect((await stat(join(destination, 'node_modules/@deepseek-ai/libreoffice-kit-wasm/bin/helper'))).mode & 0o111).toBe(0o111)
|
||||
})
|
||||
|
||||
it('copies installed target optionals and leaves other platforms and absent optionals out', async () => {
|
||||
const { staging, destination, packageAt } = await fixture()
|
||||
it('locates installed target optionals and leaves other platforms and absent optionals out', async () => {
|
||||
const { staging, packageAt } = await fixture()
|
||||
await packageAt('@deepseek-ai/libreoffice-kit', {
|
||||
optionalDependencies: { native: '1', foreign: '1', absent: '1' },
|
||||
})
|
||||
@@ -60,77 +37,74 @@ it('copies installed target optionals and leaves other platforms and absent opti
|
||||
await packageAt('native', { os: ['linux'], cpu: ['x64'] })
|
||||
await packageAt('foreign', { os: ['darwin'], cpu: ['arm64'] })
|
||||
|
||||
const packages = await copyOfficeSidecar(staging, destination, { platform: 'linux', arch: 'x64' })
|
||||
const packages = await officePackageDirectories(staging, { platform: 'linux', arch: 'x64' })
|
||||
|
||||
expect(packages.map(path => path.replaceAll('\\', '/'))).toEqual([
|
||||
expect(packages.map(path => path.slice(staging.length + 1).replaceAll('\\', '/'))).toEqual([
|
||||
'node_modules/@deepseek-ai/libreoffice-kit',
|
||||
'node_modules/@deepseek-ai/libreoffice-kit-wasm',
|
||||
'node_modules/native',
|
||||
])
|
||||
})
|
||||
|
||||
it('rejects a missing required dependency before producing a sidecar', async () => {
|
||||
const { staging, destination, packageAt } = await fixture()
|
||||
it('rejects a missing required dependency in the deployed closure', async () => {
|
||||
const { staging, packageAt } = await fixture()
|
||||
await packageAt('@deepseek-ai/libreoffice-kit', { dependencies: { 'dsh-missing-office-fixture': '1' } })
|
||||
|
||||
await expect(copyOfficeSidecar(staging, destination, { platform: 'linux', arch: 'x64' }))
|
||||
await expect(officePackageDirectories(staging, { platform: 'linux', arch: 'x64' }))
|
||||
.rejects.toThrow('dsh-missing-office-fixture required by @deepseek-ai/libreoffice-kit is missing')
|
||||
await expect(stat(destination)).rejects.toMatchObject({ code: 'ENOENT' })
|
||||
})
|
||||
|
||||
it('rejects an incomplete installed optional instead of omitting it', async () => {
|
||||
const { staging, destination, packageAt } = await fixture()
|
||||
const { staging, packageAt } = await fixture()
|
||||
await packageAt('@deepseek-ai/libreoffice-kit', { optionalDependencies: { broken: '1' } })
|
||||
await mkdir(join(staging, 'node_modules/broken'), { recursive: true })
|
||||
|
||||
await expect(copyOfficeSidecar(staging, destination, { platform: 'linux', arch: 'x64' }))
|
||||
await expect(officePackageDirectories(staging, { platform: 'linux', arch: 'x64' }))
|
||||
.rejects.toMatchObject({ code: 'ENOENT' })
|
||||
})
|
||||
|
||||
it('rejects an ancestor dependency outside the deployed closure', async () => {
|
||||
const { root, staging, destination, packageAt } = await fixture()
|
||||
const { root, staging, packageAt } = await fixture()
|
||||
await packageAt('@deepseek-ai/libreoffice-kit', { dependencies: { 'ancestor-office-fixture': '1' } })
|
||||
await packageAt('ancestor-office-fixture', {}, root)
|
||||
|
||||
await expect(copyOfficeSidecar(staging, destination, { platform: 'linux', arch: 'x64' }))
|
||||
await expect(officePackageDirectories(staging, { platform: 'linux', arch: 'x64' }))
|
||||
.rejects.toThrow('outside the deployed closure')
|
||||
})
|
||||
|
||||
it.each([true, false])('requires a staged WASM engine when declared=%s even when an ancestor has one', async (declared) => {
|
||||
const { root, staging, destination, packageAt } = await fixture()
|
||||
const { root, staging, packageAt } = await fixture()
|
||||
await packageAt('@deepseek-ai/libreoffice-kit', declared ? { optionalDependencies: { '@deepseek-ai/libreoffice-kit-wasm': '0.0.1' } } : {})
|
||||
await packageAt('@deepseek-ai/libreoffice-kit-wasm', {}, root)
|
||||
await expect(copyOfficeSidecar(staging, destination, { platform: 'linux', arch: 'x64' }))
|
||||
await expect(officePackageDirectories(staging, { platform: 'linux', arch: 'x64' }))
|
||||
.rejects.toThrow('Office engine @deepseek-ai/libreoffice-kit-wasm required for linux/x64 is missing.')
|
||||
await expect(stat(destination)).rejects.toMatchObject({ code: 'ENOENT' })
|
||||
})
|
||||
|
||||
it.each([
|
||||
['darwin', 'arm64', 'darwin-arm64'], ['darwin', 'x64', 'darwin-x64'],
|
||||
['win32', 'arm64', 'win32-arm64'], ['win32', 'x64', 'win32-x64'],
|
||||
['linux', 'x64', 'linux-x64'], ['linux', 'arm64', 'wasm'], ['freebsd', 'x64', 'wasm'],
|
||||
])('copies only the %s/%s engine even when other engines are staged', async (platform, arch, selected) => {
|
||||
const { staging, destination, packageAt } = await fixture()
|
||||
])('locates only the %s/%s engine even when other engines are staged', async (platform, arch, selected) => {
|
||||
const { staging, packageAt } = await fixture()
|
||||
const targets = ['darwin-arm64', 'darwin-x64', 'win32-arm64', 'win32-x64', 'linux-x64', 'wasm']
|
||||
const names = targets.map(target => `@deepseek-ai/libreoffice-kit-${target}`)
|
||||
await packageAt('@deepseek-ai/libreoffice-kit', { optionalDependencies: Object.fromEntries(names.map(name => [name, '0.0.1'])) })
|
||||
for (const name of names) await packageAt(name)
|
||||
const expected = `@deepseek-ai/libreoffice-kit-${selected}`
|
||||
const packages = await copyOfficeSidecar(staging, destination, { platform, arch })
|
||||
expect(packages.map(path => path.replaceAll('\\', '/'))).toEqual([
|
||||
const packages = await officePackageDirectories(staging, { platform, arch })
|
||||
expect(packages.map(path => path.slice(staging.length + 1).replaceAll('\\', '/'))).toEqual([
|
||||
'node_modules/@deepseek-ai/libreoffice-kit', `node_modules/${expected}`,
|
||||
])
|
||||
})
|
||||
|
||||
it.each(['darwin', 'win32', 'linux'])('%s requires its native package even when WASM is staged', async (platform) => {
|
||||
const { staging, destination, packageAt } = await fixture()
|
||||
const { staging, packageAt } = await fixture()
|
||||
await packageAt('@deepseek-ai/libreoffice-kit', { optionalDependencies: {
|
||||
'@deepseek-ai/libreoffice-kit-wasm': '0.0.1', [`@deepseek-ai/libreoffice-kit-${platform}-arm64`]: '0.0.1',
|
||||
} })
|
||||
await packageAt('@deepseek-ai/libreoffice-kit-wasm')
|
||||
await expect(copyOfficeSidecar(staging, destination, { platform, arch: 'arm64' }))
|
||||
await expect(officePackageDirectories(staging, { platform, arch: 'arm64' }))
|
||||
.rejects.toThrow(`Office engine @deepseek-ai/libreoffice-kit-${platform}-arm64 required for ${platform}/arm64 is missing.`)
|
||||
await expect(stat(destination)).rejects.toMatchObject({ code: 'ENOENT' })
|
||||
})
|
||||
|
||||
it('downloads pinned npm packages into the sidecar without resolving or executing package scripts', async () => {
|
||||
@@ -157,3 +131,18 @@ it('downloads pinned npm packages into the sidecar without resolving or executin
|
||||
await writeFile(join(cache, checksum.toString('hex')), 'corrupted archive')
|
||||
await expect(downloadOfficeSidecar([artifact], destination, cache)).rejects.toThrow('checksum mismatch')
|
||||
})
|
||||
|
||||
|
||||
it('takes npm integrity from the workspace lock without querying registry metadata', async () => {
|
||||
const { root } = await fixture()
|
||||
const lockfile = join(root, 'pnpm-lock.yaml')
|
||||
const integrity = `sha512-${Buffer.alloc(64, 7).toString('base64')}`
|
||||
await writeFile(lockfile, JSON.stringify({ packages: { '@scope/[email protected]': { resolution: { integrity } } } }))
|
||||
expect(await runtimeNpmArchive('@scope/fixture', '1.2.3', lockfile)).toEqual({
|
||||
name: '@scope/fixture', version: '1.2.3', integrity,
|
||||
url: 'https://registry.npmjs.org/@scope/fixture/-/fixture-1.2.3.tgz',
|
||||
})
|
||||
await expect(runtimeNpmArchive('@scope/fixture', '1.2.4', lockfile)).rejects.toThrow('missing SHA-512 integrity')
|
||||
await writeFile(lockfile, JSON.stringify({ packages: { '@scope/[email protected]': { resolution: { integrity: 'sha256-invalid' } } } }))
|
||||
await expect(runtimeNpmArchive('@scope/fixture', '1.2.3', lockfile)).rejects.toThrow('missing SHA-512 integrity')
|
||||
})
|
||||
@@ -1,7 +1,8 @@
|
||||
/** Keep LibreOffice workers, prebuilt engines, and their dependencies on the real filesystem. */
|
||||
import { cp, mkdir, readFile, rm } from 'node:fs/promises'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { isAbsolute, join, relative, sep } from 'node:path'
|
||||
import { downloadRuntimeNpmPackage, type RuntimeNpmArchive } from './primary-runtime/prepare.ts'
|
||||
import { load as parseYaml } from 'js-yaml'
|
||||
import { officePackageDirectories } from './libreoffice-packages.mjs'
|
||||
|
||||
/** pkg applies these exclusions to dependency `files` as well as root asset globs. */
|
||||
@@ -10,33 +11,6 @@ export const OFFICE_ASSET_IGNORES = [
|
||||
'**/node_modules/@deepseek-ai/libreoffice-kit-*/**',
|
||||
]
|
||||
|
||||
/**
|
||||
* Copy the installed Office dependency tree without changing package contents or executable modes.
|
||||
* Harness sidecars require the kit's declared target native engine, or WASM for other targets.
|
||||
* Missing target engines fail with their package name; missing required dependencies or paths outside the deployed closure also fail.
|
||||
* @param staging - Symlink-free deployed Node closure.
|
||||
* @param destination - Target-specific Office directory beside the executable; replaced when present.
|
||||
* @param target - Node platform and CPU of the executable.
|
||||
* @returns Relative package directories included in the sidecar.
|
||||
*/
|
||||
export async function copyOfficeSidecar(
|
||||
staging: string,
|
||||
destination: string,
|
||||
target: { platform: string; arch: string },
|
||||
): Promise<string[]> {
|
||||
const directories = await officePackageDirectories(staging, target)
|
||||
await rm(destination, { recursive: true, force: true })
|
||||
await mkdir(destination, { recursive: true })
|
||||
for (const source of directories) {
|
||||
const nestedModules = join(source, 'node_modules')
|
||||
await cp(source, join(destination, relative(staging, source)), {
|
||||
recursive: true,
|
||||
filter: path => path !== nestedModules && !path.startsWith(nestedModules + sep),
|
||||
})
|
||||
}
|
||||
return directories.map(directory => relative(staging, directory))
|
||||
}
|
||||
|
||||
/** One npm package at its existing sidecar dependency location. */
|
||||
export interface OfficePackageArchive extends RuntimeNpmArchive {
|
||||
/** Relative package directory inside the sidecar. */
|
||||
@@ -44,19 +18,19 @@ export interface OfficePackageArchive extends RuntimeNpmArchive {
|
||||
}
|
||||
|
||||
/**
|
||||
* Read exact npm tarball metadata for a version already selected by the workspace lock.
|
||||
* Resolve one installed npm version against its reviewed workspace lock integrity.
|
||||
* @param name - Published package name.
|
||||
* @param version - Exact installed version.
|
||||
* @returns Immutable npm tarball identity.
|
||||
* @param lockfile - Workspace lock recording the exact published archive.
|
||||
* @returns Locked npm tarball identity; rejects versions without SHA-512 integrity.
|
||||
*/
|
||||
export async function runtimeNpmArchive(name: string, version: string): Promise<RuntimeNpmArchive> {
|
||||
const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(name)}/${encodeURIComponent(version)}`)
|
||||
if (!response.ok) throw new Error(`runtime npm metadata: HTTP ${String(response.status)} for ${name}@${version}`)
|
||||
const metadata = await response.json() as { name: string; version: string; dist: { tarball: string; integrity: string } }
|
||||
if (metadata.name !== name || metadata.version !== version || !metadata.dist.integrity.startsWith('sha512-')) {
|
||||
throw new Error(`runtime npm metadata: invalid identity for ${name}@${version}`)
|
||||
export async function runtimeNpmArchive(name: string, version: string, lockfile = join(import.meta.dirname, '../pnpm-lock.yaml')): Promise<RuntimeNpmArchive> {
|
||||
const lock = parseYaml(await readFile(lockfile, 'utf8')) as { packages?: Record<string, { resolution?: { integrity?: string } }> }
|
||||
const integrity = lock.packages?.[`${name}@${version}`]?.resolution?.integrity
|
||||
if (typeof integrity !== 'string' || !/^sha512-[A-Za-z0-9+/]+={0,2}$/u.test(integrity)) {
|
||||
throw new Error(`runtime npm lock: missing SHA-512 integrity for ${name}@${version}`)
|
||||
}
|
||||
return { name, version, url: metadata.dist.tarball, integrity: metadata.dist.integrity }
|
||||
return { name, version, url: `https://registry.npmjs.org/${name}/-/${name.split('/').at(-1)}-${version}.tgz`, integrity }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -342,6 +342,8 @@ class SingleExeBuild {
|
||||
const patch = { bin: ENTRY_BIN, pkg: { assets: ASSET_GLOBS, ignore: [
|
||||
...OFFICE_ASSET_IGNORES,
|
||||
`${sourceModules}/**`, `${sourceModules}/.pnpm/**`,
|
||||
...['packages', 'apps', 'vendor', 'native', 'scripts'].map(directory => `${join(root, directory).replaceAll('\\', '/')}/**`),
|
||||
`${root.replaceAll('\\', '/')}/tsconfig*.json`,
|
||||
'**/*.map', '**/*.d.ts', '**/*.d.mts', '**/*.d.cts',
|
||||
] } }
|
||||
const manifestPath = join(this.staging, 'package.json')
|
||||
@@ -355,11 +357,24 @@ class SingleExeBuild {
|
||||
if (!existsSync(join(this.staging, ENTRY_BIN))) {
|
||||
throw new Error(`build-exe-for-python-sdk: staged bootstrap ${join(this.staging, ENTRY_BIN)} is missing.`)
|
||||
}
|
||||
await bundle({ config: false, tsconfig: false, inputOptions: { tsconfig: false }, target: 'es2024', entry: { 'primary-runtime': join(root, 'scripts/primary-runtime/prepare.ts'),
|
||||
'office-sidecar': join(root, 'scripts/build-exe-for-python-sdk-office.ts') },
|
||||
outDir: this.staging, clean: false, format: 'esm', platform: 'node', dts: false,
|
||||
deps: { neverBundle: [/^@deepseek-ai\//u], alwaysBundle: [/.*/u] }, shims: true, outExtensions: () => ({ js: '.mjs' }),
|
||||
define: { 'import.meta.main': 'false' } })
|
||||
const helpers = [
|
||||
{ name: 'primary-runtime', source: 'scripts/primary-runtime/prepare.ts', exports: 'downloadNodeRuntime, preparePrimaryRuntime' },
|
||||
{ name: 'office-sidecar', source: 'scripts/build-exe-for-python-sdk-office.ts', exports: 'downloadOfficeSidecar' },
|
||||
]
|
||||
const entries: Record<string, string> = {}
|
||||
for (const helper of helpers) {
|
||||
const entry = join(this.staging, `.${helper.name}-entry.ts`)
|
||||
entries[helper.name] = entry
|
||||
await writeFile(entry, `export { ${helper.exports} } from ${JSON.stringify(join(root, helper.source).replaceAll('\\', '/'))}\n`)
|
||||
}
|
||||
try {
|
||||
await bundle({ config: false, tsconfig: false, inputOptions: { tsconfig: false }, target: 'es2024', entry: entries,
|
||||
outDir: this.staging, clean: false, format: 'esm', platform: 'node', dts: false,
|
||||
deps: { neverBundle: [/^@deepseek-ai\//u], alwaysBundle: [/.*/u] }, shims: true, outExtensions: () => ({ js: '.mjs' }),
|
||||
define: { 'import.meta.main': 'false' } })
|
||||
} finally {
|
||||
for (const entry of Object.values(entries)) await rm(entry, { force: true })
|
||||
}
|
||||
const manifest = JSON.parse(await readFile(manifestPath, 'utf8')) as Record<string, unknown>
|
||||
await writeFile(manifestPath, `${JSON.stringify({ ...manifest, ...patch }, null, 2)}\n`)
|
||||
await this.run('resource helper imports', process.execPath, ['--input-type=module', '--eval',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
||||
import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { basename, join } from 'node:path'
|
||||
import { zipSync } from 'fflate'
|
||||
@@ -61,7 +61,11 @@ it('extracts a hash-verified cached library without a Python installer or networ
|
||||
const hash = createHash('sha256').update(libraryWheel).digest('hex')
|
||||
const archive = join(root, hash)
|
||||
await writeFile(archive, libraryWheel)
|
||||
const original = await stat(archive)
|
||||
expect(await downloadPrimaryRuntimeAsset('https://unused.invalid/library.whl', hash, root)).toBe(archive)
|
||||
const reused = await stat(archive)
|
||||
expect(reused.ino).toBe(original.ino)
|
||||
expect(reused.mtimeMs).toBe(original.mtimeMs)
|
||||
await unpackPrimaryRuntimeWheel(archive, join(root, 'site-packages'))
|
||||
expect(await readFile(join(root, 'site-packages/sample.py'), 'utf8')).toBe('sample = 42\n')
|
||||
await writeFile(archive, 'corrupt archive')
|
||||
|
||||
@@ -25,8 +25,10 @@ export async function downloadPrimaryRuntimeAsset(url: string, sha256: string, c
|
||||
mkdirSync(cache, { recursive: true })
|
||||
const destination = join(cache, sha256)
|
||||
let bytes: Buffer
|
||||
let cached = true
|
||||
try { bytes = readFileSync(destination) } catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
|
||||
cached = false
|
||||
const timeout = Number(process.env.DSH_RESOURCE_DOWNLOAD_TIMEOUT_MS ?? '300000')
|
||||
if (!Number.isSafeInteger(timeout) || timeout < 1 || timeout > 2_147_483_647) throw new Error('DSH_RESOURCE_DOWNLOAD_TIMEOUT_MS must be an integer from 1 to 2147483647')
|
||||
const response = await fetch(url, { signal: AbortSignal.timeout(timeout) })
|
||||
@@ -37,6 +39,7 @@ export async function downloadPrimaryRuntimeAsset(url: string, sha256: string, c
|
||||
rmSync(destination, { force: true })
|
||||
throw new Error(`primary runtime download: checksum mismatch for ${url}`)
|
||||
}
|
||||
if (cached) return destination
|
||||
const temporary = join(cache, `.${randomUUID()}`)
|
||||
try {
|
||||
writeFileSync(temporary, bytes)
|
||||
|
||||
@@ -23,15 +23,17 @@ async function fixture() {
|
||||
await file(join(root, 'node/node_modules/@deepseek-ai/dsh-sandbox-windows-acl/package.json'), '{"type":"module","exports":{"./runner":"./runner.mjs"}}')
|
||||
await file(join(root, 'node/node_modules/@deepseek-ai/dsh-sandbox-windows-acl/runner.mjs'), '')
|
||||
await file(join(root, 'node/node_modules/@deepseek-ai/dsh/package.json'), '{"type":"module"}')
|
||||
await file(join(root, 'node/node_modules/@deepseek-ai/dsh/lib/bin.js'), 'export async function runCli() { console.log(JSON.stringify({ primary: process.env.DSH_BUNDLED_PRIMARY_RUNTIME, office: process.env.DSH_BUNDLED_OFFICE_CLI })) }')
|
||||
await file(join(root, 'node/node_modules/@deepseek-ai/dsh/lib/bin.js'), 'export async function runCli() { if (process.argv[2]) { console.log(JSON.stringify({ argv: process.argv.slice(2) })); return } console.log(JSON.stringify({ primary: process.env.DSH_BUNDLED_PRIMARY_RUNTIME, office: process.env.DSH_BUNDLED_OFFICE_CLI })) }')
|
||||
await file(join(root, 'node/primary-runtime.mjs'), `
|
||||
import { mkdir, writeFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
export async function downloadNodeRuntime(target, destination) {
|
||||
if (process.env.DSH_RUNTIME_DOWNLOAD !== undefined) throw new Error("private selector leaked");
|
||||
await mkdir(join(destination, 'bin'), { recursive: true });
|
||||
await writeFile(join(destination, 'bin', process.platform === 'win32' ? 'node.exe' : 'node'), 'node');
|
||||
}
|
||||
export async function preparePrimaryRuntime({ output }) {
|
||||
if (process.env.DSH_RUNTIME_DOWNLOAD !== undefined) throw new Error("private selector leaked");
|
||||
await mkdir(join(output, 'primary-runtime'), { recursive: true });
|
||||
await writeFile(join(output, 'primary-runtime/runtime.json'), '{}');
|
||||
}
|
||||
@@ -51,9 +53,9 @@ async function fixture() {
|
||||
}
|
||||
}
|
||||
`)
|
||||
function run(flag?: string, extra: NodeJS.ProcessEnv = {}) {
|
||||
const child = spawn(process.execPath, [bootstrap, ...flag === undefined ? [] : [flag]], {
|
||||
env: { ...process.env, DSH_RESOURCE_CACHE: cache, DSH_BUNDLED_PRIMARY_RUNTIME: undefined,
|
||||
function run(resource?: string, extra: NodeJS.ProcessEnv = {}, args: string[] = []) {
|
||||
const child = spawn(process.execPath, [bootstrap, ...args], {
|
||||
env: { ...process.env, DSH_RESOURCE_CACHE: cache, DSH_RUNTIME_DOWNLOAD: resource, DSH_BUNDLED_PRIMARY_RUNTIME: undefined,
|
||||
DSH_OFFICE_SIDECAR: undefined, DSH_BUNDLED_OFFICE_CLI: undefined, ...extra },
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
})
|
||||
@@ -78,32 +80,32 @@ it('starts without downloading, independently downloads either resource, and reu
|
||||
try {
|
||||
expect(await run().done).toMatchObject({ code: 0, stdout: '{}\n' })
|
||||
await expect(readdir(cache)).rejects.toMatchObject({ code: 'ENOENT' })
|
||||
const primary = await run('--download-primary-runtime').done
|
||||
const primary = await run('primary').done
|
||||
expect(primary.code, primary.stderr).toBe(0)
|
||||
expect(JSON.parse(primary.stdout)).toBe(join(cache, 'a'.repeat(64), 'primary', 'primary-runtime'))
|
||||
await expect(readdir(join(cache, 'a'.repeat(64), 'office'))).rejects.toMatchObject({ code: 'ENOENT' })
|
||||
const office = await run('--download-office').done
|
||||
const office = await run('office').done
|
||||
expect(office.code, office.stderr).toBe(0)
|
||||
const location = JSON.parse(office.stdout) as string
|
||||
expect(await readFile(join(location, 'node_modules/@deepseek-ai/libreoffice-kit/lib/cli.js'), 'utf8')).toBe('cli')
|
||||
expect(await run('--download-office', { DSH_FIXTURE_FAIL: '1' }).done).toMatchObject({ code: 0, stdout: office.stdout })
|
||||
expect(await run('office', { DSH_FIXTURE_FAIL: '1' }).done).toMatchObject({ code: 0, stdout: office.stdout })
|
||||
} finally { await rm(root, { recursive: true, force: true }) }
|
||||
})
|
||||
|
||||
it('cleans failed staging and allows an explicit retry', async () => {
|
||||
const { root, cache, run } = await fixture()
|
||||
try {
|
||||
const failed = await run('--download-office', { DSH_FIXTURE_FAIL: '1' }).done
|
||||
const failed = await run('office', { DSH_FIXTURE_FAIL: '1' }).done
|
||||
expect(failed.code).toBe(1)
|
||||
expect(failed.stderr).toContain('fixture download failed')
|
||||
expect(await readdir(join(cache, 'a'.repeat(64)))).toEqual([])
|
||||
expect((await run('--download-office').done).code).toBe(0)
|
||||
expect((await run('office').done).code).toBe(0)
|
||||
} finally { await rm(root, { recursive: true, force: true }) }
|
||||
})
|
||||
|
||||
it('publishes one complete cache when two processes download concurrently', async () => {
|
||||
const { root, cache, run } = await fixture()
|
||||
const jobs = [run('--download-office', { DSH_FIXTURE_BARRIER: '1' }), run('--download-office', { DSH_FIXTURE_BARRIER: '1' })]
|
||||
const jobs = [run('office', { DSH_FIXTURE_BARRIER: '1' }), run('office', { DSH_FIXTURE_BARRIER: '1' })]
|
||||
try {
|
||||
await Promise.all(jobs.map(job => job.ready))
|
||||
for (const job of jobs) job.child.stdin.end('continue')
|
||||
@@ -117,3 +119,17 @@ it('publishes one complete cache when two processes download concurrently', asyn
|
||||
await rm(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
it('leaves ordinary CLI arguments to the CLI and rejects an invalid private download selector', async () => {
|
||||
const { root, cache, run } = await fixture()
|
||||
try {
|
||||
expect(await run(undefined, {}, ['--download-office']).done).toMatchObject({
|
||||
code: 0, stdout: JSON.stringify({ argv: ['--download-office'] }) + '\n',
|
||||
})
|
||||
const rejected = await run('invalid').done
|
||||
expect(rejected.code).toBe(1)
|
||||
expect(rejected.stderr).toContain('DSH_RUNTIME_DOWNLOAD must select office or primary')
|
||||
await expect(readdir(cache)).rejects.toMatchObject({ code: 'ENOENT' })
|
||||
} finally { await rm(root, { recursive: true, force: true }) }
|
||||
})
|
||||
@@ -884,9 +884,16 @@ def main() -> None:
|
||||
|
||||
def smoke_sdk_authoring(base_url: str, executable: Path, update_snapshots: bool) -> None:
|
||||
"""Query the bundled Python and switch skills without replacing that environment."""
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="dsh-sdk-authoring-cache-") as cache_directory:
|
||||
_smoke_sdk_authoring_cached(base_url, executable, update_snapshots, Path(cache_directory))
|
||||
|
||||
|
||||
def _smoke_sdk_authoring_cached(base_url: str, executable: Path, update_snapshots: bool, cache: Path) -> None:
|
||||
from deepseek_harness import DeepSeekHarness
|
||||
|
||||
primary = Path(json.loads(subprocess.check_output([str(executable), "--download-primary-runtime"], text=True)))
|
||||
cache_environment = {**os.environ, "DSH_RESOURCE_CACHE": str(cache), "DSH_RUNTIME_DOWNLOAD": "primary"}
|
||||
primary = Path(json.loads(subprocess.check_output([str(executable)], encoding="utf-8", env=cache_environment)))
|
||||
resources = primary.parent
|
||||
manifest = json.loads((primary / "runtime.json").read_text())
|
||||
for mode in ("default", "replacement", "disabled"):
|
||||
@@ -904,7 +911,7 @@ def smoke_sdk_authoring(base_url: str, executable: Path, update_snapshots: bool)
|
||||
provider="deepseek-official", model="smoke-model", cwd=str(root),
|
||||
dsh_bin=str(executable), dsh_home=str(home), patches=(str(patch),),
|
||||
api_key="sk-keyless-smoke", base_url=base_url,
|
||||
env={"DSH_PERMISSION_MODE": "danger-full-access", "DSH_TELEMETRY_DISABLED": "1"},
|
||||
env={"DSH_PERMISSION_MODE": "danger-full-access", "DSH_TELEMETRY_DISABLED": "1", "DSH_RESOURCE_CACHE": str(cache)},
|
||||
request_timeout_seconds=60,
|
||||
) as harness:
|
||||
result = harness.run(AUTHORING_PROMPT, session_id="authoring")
|
||||
@@ -960,7 +967,7 @@ def smoke_sdk_office(executable: Path) -> None:
|
||||
else:
|
||||
shutil.copy2(source, destination)
|
||||
cache_environment = {**os.environ, "DSH_RESOURCE_CACHE": str(root / "cache")}
|
||||
office = Path(json.loads(subprocess.check_output([str(relocated), "--download-office"], text=True, env=cache_environment)))
|
||||
office = Path(json.loads(subprocess.check_output([str(relocated)], encoding="utf-8", env={**cache_environment, "DSH_RUNTIME_DOWNLOAD": "office"})))
|
||||
assert not (office.parent / "primary").exists(), "Office download unexpectedly prepared the authoring environment"
|
||||
adapter = office / "node_modules/@deepseek-ai/libreoffice-kit/package.json"
|
||||
native = stem.removeprefix("deepseek-harness-sdk-runtime-").replace("win-", "win32-").replace("macos-", "darwin-")
|
||||
@@ -1034,7 +1041,7 @@ recalculate(root / 'input.xlsx', root / 'calculated.xlsx')
|
||||
with zipfile.ZipFile(root / 'calculated.xlsx') as archive:
|
||||
sheet = ElementTree.fromstring(archive.read('xl/worksheets/sheet1.xml'))
|
||||
assert sheet.find('.//{*}c[@r="A1"]/{*}v').text == '3'
|
||||
""", str(root), expected_backend], env={**cache_environment, "DSH_OFFICE_SIDECAR": str(office)}, check=True, timeout=180)
|
||||
""", str(root), expected_backend], env={name: value for name, value in cache_environment.items() if name != "DSH_OFFICE_SIDECAR"}, check=True, timeout=180)
|
||||
assert not (office.parent / "primary").exists(), "Python Office API unexpectedly downloaded authoring resources"
|
||||
print(f"smoke-python-runtime: relocated Office {result['backend']} DOCX produced {len(pdf)} PDF bytes")
|
||||
|
||||
|
||||
Reference in new issue
Block a user