Files
deepseek-harness/scripts/client-build-environment.client.spec.ts
imccyu 1276a8e13a ci: split pull-request CI into Linux, Windows, and Python workflows (#6898)
* ci: run native Windows PR jobs in a separate workflow

GitHub re-runs failed jobs only after a whole run finishes, so slow Windows jobs blocked re-running failed Linux jobs. CI Windows now owns the native Windows jobs and the Windows x64 Python runtime target, with its own required 'windows checks passed' verdict.

Refs #6893

* ci: run Python PR jobs in a separate workflow

Python rarely fails while the Windows lanes run long, so the Python SDK suite and the Linux/Windows x64 runtime matrix move into CI Python. The three pull-request workflows can now be re-run without waiting for each other, and branch protection gains "python checks passed" beside the existing verdicts.

Refs #6893

* docs: record the three PR verdicts in the failover runbook

* test: pin the shared environment across the PR workflows

* test: read the python SDK lane from CI Python

* test(sdk): parse session JSONL frames in the keyless smoke

Node 24.19 returned only the first frame of a concatenated zstd log, so the single JSON.parse saw just the header; 24.22 returns every frame and the JSONL body broke the parse. Read frames explicitly and select the main session by its header so both versions agree.
2026-10-22 04:09:35 +00:00

344 lines
14 KiB
TypeScript

import { execFileSync } from 'node:child_process'
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, join, resolve } from 'node:path'
import yaml from 'js-yaml'
import { afterEach, describe, expect, it, vi } from 'vitest'
import {
assertClientBuildEnvironment,
clientBuildEnvironmentDefines,
clientBuildProcessEnvironment,
clientBuildValueDefines,
officialClientBuildEnvironment,
readClientBuildRecord,
repositoryClientBuildEnvironment,
repositoryCommitHash,
repositoryGitDirty,
repositoryVersion,
resolveClientBuildEnvironment,
writeClientBuildRecord,
} from './client-build-environment.ts'
import { clientBundle } from '../packages/client/tsdown.client.ts'
const root = resolve(import.meta.dirname, '..')
const PROBE_NAME = 'DSH_CLIENT_BUILD_TEST'
const COMMIT_HASH = '0123456789abcdef0123456789abcdef01234567'
const PROBE_KEY = `process.env.${PROBE_NAME}`
const originalProbe = process.env[PROBE_NAME]
const roots: string[] = []
const dshBuildWorkflows = [
'build-exe-for-python-sdk.yml',
'ci.yml',
'ci-python.yml',
'ci-windows.yml',
'e2e.yml',
'release.yml',
'release-publish.yml',
'sandbox.yml',
]
afterEach(() => {
if (originalProbe === undefined) Reflect.deleteProperty(process.env, PROBE_NAME)
else process.env[PROBE_NAME] = originalProbe
vi.resetModules()
for (const fixtureRoot of roots.splice(0)) rmSync(fixtureRoot, { recursive: true, force: true })
})
function write(path: string, content: string): void {
mkdirSync(dirname(path), { recursive: true })
writeFileSync(path, content)
}
function buildFixture(environment: Record<string, string>): string {
const fixtureRoot = mkdtempSync(join(tmpdir(), 'dsh-client-build-'))
roots.push(fixtureRoot)
write(join(fixtureRoot, 'apps/web/dist/index.html'), '<main></main>')
write(join(fixtureRoot, 'packages/client/example/lib/client.js'), 'module.exports = {}\n')
writeClientBuildRecord(fixtureRoot, environment)
return fixtureRoot
}
function git(root: string, args: readonly string[]): string {
return execFileSync('git', [...args], {
cwd: root,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
}).trim()
}
function repositoryFixture(version = '1.2.3-rc.4'): string {
const fixtureRoot = mkdtempSync(join(tmpdir(), 'dsh-client-build-repository-'))
roots.push(fixtureRoot)
write(join(fixtureRoot, 'package.json'), `${JSON.stringify({ version })}\n`)
write(join(fixtureRoot, 'tracked.txt'), 'committed\n')
git(fixtureRoot, ['init'])
git(fixtureRoot, ['config', 'user.name', 'DSH test'])
git(fixtureRoot, ['config', 'user.email', 'dsh-test@example.invalid'])
git(fixtureRoot, ['config', 'commit.gpgsign', 'false'])
git(fixtureRoot, ['add', 'package.json', 'tracked.txt'])
git(fixtureRoot, ['commit', '-m', 'fixture'])
return fixtureRoot
}
describe('client build environment', () => {
it('requires an exact public environment for a named artifact profile', () => {
const expected = {
DSH_CLIENT_BUILD_PROFILE: 'official',
DSH_CLIENT_COMMIT_HASH: COMMIT_HASH.slice(0, 7),
DSH_CLIENT_TITLE: 'DeepSeek Harness',
DSH_CLIENT_VERSION: '1.2.3',
} as const
expect(() => { assertClientBuildEnvironment({ PATH: '/bin', ...expected }, expected) }).not.toThrow()
expect(() => { assertClientBuildEnvironment({}, expected) }).toThrow(/DSH_CLIENT_TITLE/)
expect(() => { assertClientBuildEnvironment({ DSH_CLIENT_TITLE: 'Other' }, expected) }).toThrow(/DSH_CLIENT_TITLE/)
expect(() => {
assertClientBuildEnvironment({ ...expected, DSH_CLIENT_UNDECLARED: 'value' }, expected)
}).toThrow(/DSH_CLIENT_UNDECLARED/)
})
it('inherits public values by default and isolates an explicit official profile', () => {
const parent = {
PATH: '/bin',
DSH_BUILD_CLIENT_PROFILE: 'official',
DSH_CLIENT_BUILD_PROFILE: 'local',
DSH_CLIENT_COMMIT_HASH: COMMIT_HASH.slice(0, 7),
DSH_CLIENT_GIT_DIRTY: 'true',
DSH_CLIENT_TITLE: 'Local title',
DSH_CLIENT_VERSION: '1.2.3',
DSH_CLIENT_EXTRA: 'local-extra',
}
expect(resolveClientBuildEnvironment({ DSH_CLIENT_TITLE: 'Local title' })).toEqual({
DSH_CLIENT_TITLE: 'Local title',
})
expect(resolveClientBuildEnvironment(parent)).toEqual({
DSH_CLIENT_BUILD_PROFILE: 'official',
DSH_CLIENT_COMMIT_HASH: COMMIT_HASH.slice(0, 7),
DSH_CLIENT_TITLE: 'DeepSeek Harness',
DSH_CLIENT_VERSION: '1.2.3',
})
expect(() => {
resolveClientBuildEnvironment({ DSH_BUILD_CLIENT_PROFILE: 'official' })
}).toThrow(/DSH_CLIENT_COMMIT_HASH/)
expect(() => {
resolveClientBuildEnvironment({
DSH_BUILD_CLIENT_PROFILE: 'official',
DSH_CLIENT_COMMIT_HASH: COMMIT_HASH.slice(0, 7),
})
}).toThrow(/DSH_CLIENT_VERSION/)
expect(() => { resolveClientBuildEnvironment({}, 'unknown') }).toThrow(/unknown client build profile/)
expect(clientBuildProcessEnvironment(parent, {
DSH_CLIENT_BUILD_PROFILE: 'official',
DSH_CLIENT_COMMIT_HASH: COMMIT_HASH.slice(0, 7),
DSH_CLIENT_TITLE: 'DeepSeek Harness',
DSH_CLIENT_VERSION: '1.2.3',
})).toEqual({
PATH: '/bin',
DSH_CLIENT_BUILD_PROFILE: 'official',
DSH_CLIENT_COMMIT_HASH: COMMIT_HASH.slice(0, 7),
DSH_CLIENT_TITLE: 'DeepSeek Harness',
DSH_CLIENT_VERSION: '1.2.3',
})
expect(repositoryCommitHash('/unused', { DSH_CLIENT_COMMIT_HASH: COMMIT_HASH })).toBe(COMMIT_HASH.slice(0, 7))
})
it.each([undefined, 'true', 'false'])('isolates local branding while retaining repository metadata (dirty=%s)', (dirty) => {
const parent = {
DSH_BUILD_CLIENT_PROFILE: 'official',
DSH_CLIENT_BUILD_PROFILE: 'official',
DSH_CLIENT_TITLE: 'DeepSeek Harness',
DSH_CLIENT_COMMIT_HASH: COMMIT_HASH.slice(0, 7),
DSH_CLIENT_VERSION: '1.2.3',
DSH_CLIENT_GIT_DIRTY: dirty,
DSH_CLIENT_EXTRA: 'inherited',
}
const local = resolveClientBuildEnvironment(parent, 'local')
expect(local).toEqual({
DSH_CLIENT_BUILD_PROFILE: 'local',
DSH_CLIENT_COMMIT_HASH: COMMIT_HASH.slice(0, 7),
DSH_CLIENT_VERSION: '1.2.3',
...(dirty === 'true' ? { DSH_CLIENT_GIT_DIRTY: 'true' } : {}),
})
expect(clientBuildProcessEnvironment(parent, local)).toEqual(local)
expect(resolveClientBuildEnvironment({ ...parent, DSH_BUILD_CLIENT_PROFILE: 'local' })).toEqual(local)
expect(resolveClientBuildEnvironment({ ...parent, ...local }, 'official')).toEqual({
DSH_CLIENT_BUILD_PROFILE: 'official',
DSH_CLIENT_COMMIT_HASH: COMMIT_HASH.slice(0, 7),
DSH_CLIENT_TITLE: 'DeepSeek Harness',
DSH_CLIENT_VERSION: '1.2.3',
})
})
it('requires source metadata for an explicit local build', () => {
expect(() => { resolveClientBuildEnvironment({}, 'local') }).toThrow(/DSH_CLIENT_COMMIT_HASH/)
expect(() => {
resolveClientBuildEnvironment({ DSH_CLIENT_COMMIT_HASH: COMMIT_HASH.slice(0, 7) }, 'local')
}).toThrow(/DSH_CLIENT_VERSION/)
})
it('owns repository version, commit, and dirty metadata for complete builds', () => {
const fixtureRoot = repositoryFixture()
const commit = git(fixtureRoot, ['rev-parse', '--short=7', 'HEAD'])
expect(repositoryVersion(fixtureRoot)).toBe('1.2.3-rc.4')
expect(repositoryGitDirty(fixtureRoot)).toBe(false)
expect(repositoryClientBuildEnvironment(fixtureRoot, {
DSH_CLIENT_COMMIT_HASH: COMMIT_HASH,
DSH_CLIENT_EXTRA: 'preserved',
DSH_CLIENT_GIT_DIRTY: 'true',
DSH_CLIENT_VERSION: 'spoofed',
})).toEqual({
DSH_CLIENT_COMMIT_HASH: COMMIT_HASH.slice(0, 7),
DSH_CLIENT_EXTRA: 'preserved',
DSH_CLIENT_VERSION: '1.2.3-rc.4',
})
expect(officialClientBuildEnvironment(fixtureRoot)).toEqual({
DSH_CLIENT_BUILD_PROFILE: 'official',
DSH_CLIENT_COMMIT_HASH: commit,
DSH_CLIENT_TITLE: 'DeepSeek Harness',
DSH_CLIENT_VERSION: '1.2.3-rc.4',
})
write(join(fixtureRoot, '.gitignore'), 'ignored.txt\n')
git(fixtureRoot, ['add', '.gitignore'])
git(fixtureRoot, ['commit', '-m', 'ignore fixture'])
write(join(fixtureRoot, 'ignored.txt'), 'ignored\n')
expect(repositoryGitDirty(fixtureRoot)).toBe(false)
rmSync(join(fixtureRoot, 'ignored.txt'))
write(join(fixtureRoot, 'tracked.txt'), 'unstaged\n')
expect(repositoryGitDirty(fixtureRoot)).toBe(true)
write(join(fixtureRoot, 'tracked.txt'), 'committed\n')
expect(repositoryGitDirty(fixtureRoot)).toBe(false)
write(join(fixtureRoot, 'tracked.txt'), 'staged\n')
git(fixtureRoot, ['add', 'tracked.txt'])
expect(repositoryGitDirty(fixtureRoot)).toBe(true)
git(fixtureRoot, ['commit', '-m', 'staged fixture'])
expect(repositoryGitDirty(fixtureRoot)).toBe(false)
write(join(fixtureRoot, 'untracked.txt'), 'untracked\n')
expect(repositoryGitDirty(fixtureRoot)).toBe(true)
expect(repositoryClientBuildEnvironment(fixtureRoot, {
DSH_CLIENT_COMMIT_HASH: COMMIT_HASH,
})).toEqual({
DSH_CLIENT_COMMIT_HASH: COMMIT_HASH.slice(0, 7),
DSH_CLIENT_GIT_DIRTY: 'true',
DSH_CLIENT_VERSION: '1.2.3-rc.4',
})
rmSync(join(fixtureRoot, 'untracked.txt'))
const submoduleSource = repositoryFixture('9.8.7')
git(fixtureRoot, ['-c', 'protocol.file.allow=always', 'submodule', 'add', submoduleSource, 'submodule'])
git(fixtureRoot, ['commit', '-am', 'submodule fixture'])
expect(repositoryGitDirty(fixtureRoot)).toBe(false)
write(join(fixtureRoot, 'submodule/tracked.txt'), 'modified submodule\n')
expect(repositoryGitDirty(fixtureRoot)).toBe(true)
})
it('omits dirty metadata when repository metadata is unavailable', () => {
const fixtureRoot = mkdtempSync(join(tmpdir(), 'dsh-client-build-no-git-'))
roots.push(fixtureRoot)
write(join(fixtureRoot, 'package.json'), '{"version":"2.0.0"}\n')
expect(repositoryGitDirty(fixtureRoot)).toBeUndefined()
expect(repositoryClientBuildEnvironment(fixtureRoot, {
DSH_CLIENT_COMMIT_HASH: COMMIT_HASH,
DSH_CLIENT_GIT_DIRTY: 'true',
})).toEqual({
DSH_CLIENT_COMMIT_HASH: COMMIT_HASH.slice(0, 7),
DSH_CLIENT_VERSION: '2.0.0',
})
})
it('defines only public client values over a non-enumerable fallback', () => {
const environment = {
PATH: '/bin',
DSH_TEST_API_KEY: 'secret',
DSH_CLIENT_VARIANT: 'quoted "value"',
DSH_CLIENT_EMPTY: '',
DSH_CLIENT_UNSET: undefined,
}
const values = {
'process.env.DSH_CLIENT_EMPTY': '""',
'process.env.DSH_CLIENT_VARIANT': '"quoted \\"value\\""',
}
expect(clientBuildEnvironmentDefines(environment)).toEqual({ 'process.env': '{}', ...values })
// Node faces keep their runtime `process.env` and fix only the public values.
expect(clientBuildValueDefines(environment)).toEqual(values)
})
it('feeds the same build-process value to dynamic tsdown bundles, their Node faces, and the Vite shell', async () => {
process.env[PROBE_NAME] = 'shared-value'
const configs = clientBundle(join(import.meta.dirname, '../packages/client/ui-sidebar'), [
'lib/types/index.js',
])({ env: { DSH_BUILD_FACE: 'client' } })
if (!Array.isArray(configs)) throw new TypeError('client bundle config must be an array')
const dynamic = configs.find(config => config.name === '@deepseek-ai/dsh-client-ui-sidebar/client')
expect(dynamic?.define).toMatchObject({
'process.env': '{}',
[PROBE_KEY]: '"shared-value"',
})
const node = configs.find(config => config.name === '@deepseek-ai/dsh-client-ui-sidebar')
expect(node?.define).toMatchObject({ [PROBE_KEY]: '"shared-value"' })
expect(node?.define).not.toHaveProperty(['process.env'])
const viteConfigPath = '../apps/web/vite.config.ts'
const viteModule: unknown = await import(viteConfigPath)
if (typeof viteModule !== 'object' || viteModule === null) {
throw new TypeError('web Vite config module must be an object')
}
const viteConfig: unknown = Reflect.get(viteModule, 'default')
if (typeof viteConfig === 'function') throw new TypeError('web Vite config must be an object')
if (typeof viteConfig !== 'object' || viteConfig === null) {
throw new TypeError('web Vite config must be an object')
}
expect(Reflect.get(viteConfig, 'define')).toMatchObject({
'process.env': '{}',
[PROBE_KEY]: '"shared-value"',
})
})
it('binds the recorded environment to a complete set of client artifacts', () => {
const officialEnvironment = {
DSH_CLIENT_BUILD_PROFILE: 'official',
DSH_CLIENT_COMMIT_HASH: COMMIT_HASH.slice(0, 7),
DSH_CLIENT_TITLE: 'DeepSeek Harness',
DSH_CLIENT_VERSION: '1.2.3',
}
const official = buildFixture(officialEnvironment)
const defaultBuild = buildFixture({})
expect(readClientBuildRecord(official, officialEnvironment).environment).toEqual(officialEnvironment)
expect(() => { readClientBuildRecord(defaultBuild, officialEnvironment) }).toThrow(/DSH_CLIENT_/)
expect(() => { readClientBuildRecord(join(defaultBuild, 'missing')) }).toThrow(/record.*missing/)
write(join(official, 'apps/web/dist/index.html'), '<main>changed</main>')
expect(() => { readClientBuildRecord(official) }).toThrow(/artifacts differ/)
const chunked = buildFixture(officialEnvironment)
write(join(chunked, 'packages/client/example/lib/client.pdf.js'), 'module.exports = {}\n')
expect(() => { readClientBuildRecord(chunked) }).toThrow(/artifacts differ/)
const dual = buildFixture(officialEnvironment)
write(join(dual, 'packages/client/example/lib/client.node.js'), 'export const node = true\n')
write(join(dual, 'packages/client/example/lib/client.node.helper.js'), 'export const helper = true\n')
expect(() => { readClientBuildRecord(dual) }).not.toThrow()
write(join(dual, 'packages/client/example/lib/client.browser.js'), 'module.exports = { browser: true }\n')
expect(() => { readClientBuildRecord(dual) }).toThrow(/artifacts differ/)
})
it('keeps public client values out of workflow-wide environments', () => {
for (const name of dshBuildWorkflows) {
const path = `.github/workflows/${name}`
const document: unknown = yaml.load(readFileSync(resolve(root, path), 'utf8'))
if (typeof document !== 'object' || document === null || Array.isArray(document)) {
throw new TypeError(`${path} must contain a workflow object`)
}
expect(JSON.stringify(document), path).not.toContain('DSH_CLIENT_')
}
})
})