8e79ec4942
- Updated README and README.zh to clarify packaging commands and added verification step. - Improved packaging scripts to automatically build the self-contained runtime and verify its integrity. - Introduced new scripts for verifying harness self-containment and booting the web profile. - Added comprehensive tests for relinking harness symlinks and ensuring self-containment. - Updated package.json scripts to reflect new build and verification processes.
166 lines
7.0 KiB
TypeScript
166 lines
7.0 KiB
TypeScript
import {
|
|
existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync,
|
|
} from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { dirname, join, resolve } from 'node:path'
|
|
import { afterEach, describe, expect, it } from 'vitest'
|
|
import {
|
|
assertSelfContained, findEscapingLinks, isWithin, probeSymlinkSupport, relinkHarness,
|
|
} from '../scripts/harness-relink.mjs'
|
|
|
|
const dirs: string[] = []
|
|
|
|
afterEach(() => {
|
|
for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
/**
|
|
* A repo whose layout mirrors the real one: `out` is the harness copy of the
|
|
* dev tree at `<repo>/apps/desktop/build/harness`, and the repo has a package
|
|
* the harness mirrors.
|
|
*/
|
|
function makeTree() {
|
|
const root = mkdtempSync(join(tmpdir(), 'dsh-relink-'))
|
|
dirs.push(root)
|
|
const repo = join(root, 'repo')
|
|
const out = join(repo, 'apps/desktop/build/harness')
|
|
mkdirSync(join(repo, 'packages/core/dsh-core'), { recursive: true })
|
|
writeFileSync(join(repo, 'packages/core/dsh-core/index.js'), 'export const x = 1\n')
|
|
mkdirSync(join(out, 'packages/core/dsh-core'), { recursive: true })
|
|
writeFileSync(join(out, 'packages/core/dsh-core/index.js'), 'export const x = 1\n')
|
|
mkdirSync(join(out, 'node_modules/@deepseek-ai'), { recursive: true })
|
|
return { root, repo, out }
|
|
}
|
|
|
|
/** The dir-symlink type to use when pre-creating a fixture link on this host. */
|
|
const dirLinkType = () => (process.platform === 'win32' ? 'junction' : 'dir')
|
|
|
|
/** Whether tests that create symlinks can run on this host. */
|
|
const canSymlink = probeSymlinkSupport()
|
|
|
|
describe('isWithin', () => {
|
|
it('accepts a path inside the root and rejects an escape', () => {
|
|
expect(isWithin('C:\\a\\b', 'C:\\a\\b\\c')).toBe(true)
|
|
expect(isWithin('C:\\a\\b', 'C:\\a\\other')).toBe(false)
|
|
expect(isWithin('C:\\a\\b', 'C:\\a\\b')).toBe(true)
|
|
})
|
|
|
|
it('rejects an absolute path on another drive', () => {
|
|
expect(isWithin('C:\\a\\b', 'D:\\x')).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe.skipIf(!canSymlink)('relinkHarness', () => {
|
|
it('leaves a relative in-tree link alone', () => {
|
|
const { out } = makeTree()
|
|
const link = join(out, 'node_modules/@deepseek-ai/dsh-core')
|
|
symlinkSync('../../packages/core/dsh-core', link, 'dir')
|
|
const report = relinkHarness(out, dirname(out))
|
|
expect(report.unchanged).toBeGreaterThanOrEqual(1)
|
|
expect(report.errors).toEqual([])
|
|
expect(findEscapingLinks(out)).toEqual([])
|
|
})
|
|
|
|
it('rewrites an absolute link to the dev tree into a relative in-tree link', () => {
|
|
const { repo, out } = makeTree()
|
|
const link = join(out, 'node_modules/@deepseek-ai/dsh-core')
|
|
const devTarget = join(repo, 'packages/core/dsh-core')
|
|
symlinkSync(devTarget, link, dirLinkType())
|
|
const report = relinkHarness(out, repo)
|
|
expect(report.normalized).toBeGreaterThanOrEqual(1)
|
|
expect(report.errors).toEqual([])
|
|
expect(lstatSync(link).isSymbolicLink()).toBe(true)
|
|
const raw = readlinkSync(link)
|
|
expect(resolve(dirname(link), raw)).toBe(resolve(join(out, 'packages/core/dsh-core')))
|
|
expect(findEscapingLinks(out)).toEqual([])
|
|
})
|
|
|
|
it('mirrors a node_modules/.pnpm store link into the harness', () => {
|
|
const { repo, out } = makeTree()
|
|
// The store entry the workspace link would point at, in both trees.
|
|
for (const base of [repo, out]) {
|
|
mkdirSync(join(base, 'node_modules/.pnpm/b@1/node_modules/b'), { recursive: true })
|
|
writeFileSync(join(base, 'node_modules/.pnpm/b@1/node_modules/b/index.js'), 'export const b = 2\n')
|
|
}
|
|
const link = join(out, 'node_modules/@deepseek-ai/a')
|
|
symlinkSync(join(repo, 'node_modules/.pnpm/b@1/node_modules/b'), link, dirLinkType())
|
|
const report = relinkHarness(out, repo)
|
|
expect(report.errors).toEqual([])
|
|
expect(resolve(dirname(link), readlinkSync(link)))
|
|
.toBe(resolve(join(out, 'node_modules/.pnpm/b@1/node_modules/b')))
|
|
expect(findEscapingLinks(out)).toEqual([])
|
|
})
|
|
|
|
it('dereferences a genuinely external target into real content', () => {
|
|
const { root, out } = makeTree()
|
|
const external = join(root, 'external')
|
|
mkdirSync(external, { recursive: true })
|
|
writeFileSync(join(external, 'index.js'), 'export const ext = 3\n')
|
|
const link = join(out, 'node_modules/external')
|
|
symlinkSync(external, link, dirLinkType())
|
|
const report = relinkHarness(out, dirname(out))
|
|
expect(report.dereferenced).toBeGreaterThanOrEqual(1)
|
|
expect(report.errors).toEqual([])
|
|
expect(lstatSync(link).isSymbolicLink()).toBe(false)
|
|
expect(readFileSync(join(link, 'index.js'), 'utf8')).toContain('ext')
|
|
expect(findEscapingLinks(out)).toEqual([])
|
|
})
|
|
|
|
it('drops a link whose target contains the harness (e.g. the desktop shell)', () => {
|
|
const { repo, out } = makeTree()
|
|
// `repo/apps/desktop` contains `out` (the harness): it is a workspace
|
|
// package the harness deliberately does not ship, so the link cannot be
|
|
// made self-contained and must be dropped, not copied.
|
|
const link = join(out, 'node_modules/desktop-shell')
|
|
symlinkSync(join(repo, 'apps/desktop'), link, dirLinkType())
|
|
const report = relinkHarness(out, repo)
|
|
expect(report.removed).toBeGreaterThanOrEqual(1)
|
|
expect(report.errors).toEqual([])
|
|
expect(existsSync(link)).toBe(false)
|
|
expect(findEscapingLinks(out)).toEqual([])
|
|
})
|
|
|
|
it('drops a link that points at a missing in-tree target (unshipped package)', () => {
|
|
const { out } = makeTree()
|
|
// A link to `out/python/sdk-runtime` — inside the tree lexically but absent
|
|
// (python is not part of the harness): it cannot resolve, so it is dropped.
|
|
const link = join(out, 'node_modules/dsh-sdk-runtime')
|
|
symlinkSync('../python/sdk-runtime', link, dirLinkType())
|
|
const report = relinkHarness(out, dirname(out))
|
|
expect(report.removed).toBeGreaterThanOrEqual(1)
|
|
expect(report.errors).toEqual([])
|
|
expect(existsSync(link)).toBe(false)
|
|
expect(findEscapingLinks(out)).toEqual([])
|
|
})
|
|
|
|
it('reports a dangling link as an error and leaves it detectable', () => {
|
|
const { root, out } = makeTree()
|
|
const link = join(out, 'node_modules/ghost')
|
|
symlinkSync(join(root, 'does-not-exist'), link, dirLinkType())
|
|
const report = relinkHarness(out, dirname(out))
|
|
expect(report.errors.some(e => e.includes('dangling'))).toBe(true)
|
|
expect(findEscapingLinks(out)).toContain(link)
|
|
})
|
|
})
|
|
|
|
describe.skipIf(!canSymlink)('findEscapingLinks / assertSelfContained', () => {
|
|
it('flags an escaping link and assertSelfContained throws', () => {
|
|
const { repo, out } = makeTree()
|
|
const link = join(out, 'node_modules/outside')
|
|
symlinkSync(join(repo, 'packages/core/dsh-core'), link, 'dir')
|
|
expect(findEscapingLinks(out)).toContain(link)
|
|
expect(() => assertSelfContained(out)).toThrow(/self-contained/)
|
|
})
|
|
|
|
it('assertSelfContained passes on a clean tree', () => {
|
|
const { out } = makeTree()
|
|
expect(() => assertSelfContained(out)).not.toThrow()
|
|
})
|
|
})
|
|
|
|
describe('probeSymlinkSupport', () => {
|
|
it('reports a boolean without throwing', () => {
|
|
expect(typeof probeSymlinkSupport()).toBe('boolean')
|
|
})
|
|
})
|