mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-10-26 16:55:08 +00:00
* 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.
344 lines
14 KiB
TypeScript
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_')
|
|
}
|
|
})
|
|
})
|