Files
deepseek-harness/scripts/verify-client-packages.spec.ts
T

527 lines
23 KiB
TypeScript

/** Tests for client package modes and module requests. */
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import {
collectClientPackageViolations,
collectLocalSourceSpecifiers,
collectRuntimeLocalSourceSpecifiers,
collectRuntimeSourcePackageUses,
collectRuntimeSourceSpecifiers,
collectSourceAssetSpecifiers,
collectSourcePackageUses,
collectStylesheetAssetUrls,
fixClientPackageManifests,
readClientDeclarations,
readStylesheetAssetReferences,
type ClientDeclaration,
type ClientPackage,
type ClientPackageFacts,
} from './verify-client-packages.ts'
const CORDIS = '@deepseek-ai/cordis'
const roots: string[] = []
afterEach(() => {
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
})
function declaration(
short: string,
fields: Partial<Omit<ClientDeclaration, 'name' | 'manifest'>> = {},
): ClientDeclaration {
return {
name: short.startsWith('@') ? short : '@deepseek-ai/dsh-client-' + short,
manifest: 'packages/client/' + short.replace(/^.*\//, '') + '/package.json',
dynamic: true,
platform: 'web',
external: [],
inject: [],
runtimeSourceUses: {},
runtimeSourceSpecifiers: {},
assetExport: undefined,
assetReferences: [],
...fields,
}
}
function pkg(
short: string,
fields: Partial<Omit<ClientPackage, 'name' | 'manifest'>> = {},
): ClientPackage {
return {
...declaration(short),
staticLinked: false,
sourceUses: {},
runtimeSourceUses: {},
dependencies: {},
peerDependencies: { [CORDIS]: 'workspace:^' },
devDependencies: { [CORDIS]: 'workspace:^' },
...fields,
}
}
function facts(
packages: readonly ClientPackage[],
options: Partial<Omit<ClientPackageFacts, 'packages'>> = {},
): ClientPackageFacts {
return {
packages,
declarations: options.declarations ?? packages,
staticLinkedPackages: options.staticLinkedPackages ?? new Set(
packages.filter(item => item.staticLinked).map(item => item.name),
),
platformModules: options.platformModules ?? [],
preloadedExternals: options.preloadedExternals ?? [],
parserPreloadIds: options.parserPreloadIds
?? (options.preloadedExternals ?? []).map(value => value.replace(/\/client$/, '')),
malformed: options.malformed ?? [],
}
}
describe('source package uses', () => {
it('includes root-relative runtime requests only when explicitly requested', () => {
const source = [
"import './local.ts'",
"import '/src/value.ts'",
"export * from '/src/export.ts'",
"const load = () => import('/src/lazy.ts')",
"const legacy = require('/src/legacy.ts')",
"import type { A } from '/src/import-type.ts'",
"import { type B } from '/src/import-type-only.ts'",
"export type { A } from '/src/export-type.ts'",
"export { type B } from '/src/export-type-only.ts'",
].join('\n')
expect([...collectRuntimeLocalSourceSpecifiers('feature.ts', source)]).toEqual(['./local.ts'])
expect([...collectRuntimeLocalSourceSpecifiers('feature.ts', source, true)]).toEqual([
'./local.ts', '/src/value.ts', '/src/export.ts', '/src/lazy.ts', '/src/legacy.ts',
])
})
it('counts type imports, module augmentations, dynamic imports, and JSX', () => {
const uses = collectSourcePackageUses('feature.tsx', [
"import type { A } from '@deepseek-ai/dsh-a/subpath'",
"declare module '@deepseek-ai/dsh-client-ui-slots' {}",
"const load = () => import('@deepseek-ai/dsh-b/remote')",
'export const view = <div />',
"export type { Local } from './local.ts'",
].join('\n'))
expect([...uses].sort()).toEqual([
'@deepseek-ai/dsh-a',
'@deepseek-ai/dsh-b',
'@deepseek-ai/dsh-client-ui-slots',
'react',
])
expect([...collectRuntimeSourcePackageUses('feature.tsx', [
"import type { A } from '@deepseek-ai/dsh-a/subpath'",
"declare module '@deepseek-ai/dsh-client-ui-slots' {}",
"const load = () => import('@deepseek-ai/dsh-b')",
'export const view = <div />',
].join('\n'))].sort()).toEqual([
'@deepseek-ai/dsh-b',
'react',
])
expect([...collectRuntimeSourceSpecifiers('feature.tsx', [
"import type { A } from '@deepseek-ai/dsh-a/subpath'",
"const load = () => import('@deepseek-ai/dsh-b/remote')",
'export const view = <div />',
].join('\n'))].sort()).toEqual([
'@deepseek-ai/dsh-b/remote',
'react',
])
expect([...collectLocalSourceSpecifiers('feature.ts', [
"import type { A } from './types.ts'",
"export { value } from './value.ts'",
"const load = () => import('./lazy.ts')",
"const legacy = require('./legacy.ts')",
"declare module './augmentation.ts' {}",
"import '@deepseek-ai/dsh-a'",
].join('\n'))].sort()).toEqual([
'./lazy.ts',
'./legacy.ts',
'./types.ts',
'./value.ts',
])
})
})
describe('package modes', () => {
it('accepts one dynamic package and one statically linked package', () => {
const dynamic = pkg('feature')
const shell = pkg('ui-slots', { dynamic: false, staticLinked: true })
expect(collectClientPackageViolations(facts([dynamic, shell]))).toEqual([])
})
it('rejects a package with both modes or neither mode', () => {
const both = pkg('both', { staticLinked: true })
const neither = pkg('neither', { dynamic: false })
const found = collectClientPackageViolations(facts([both, neither]))
expect(found).toHaveLength(2)
expect(found.join('\n')).toContain('must be dynamic or statically linked, not both')
expect(found.join('\n')).toContain('has no supported client package mode')
})
it('requires seeded workspace packages to use staticLinked and preloads to name dynamic rows', () => {
const slots = declaration('ui-slots', { dynamic: false })
const bootstrap = declaration('bootstrap', { dynamic: false })
const found = collectClientPackageViolations(facts([], {
declarations: [slots, bootstrap],
platformModules: [slots.name],
preloadedExternals: [bootstrap.name + '/client'],
}))
expect(found).toHaveLength(2)
expect(found.join('\n')).toContain('does not use the staticLinked preset')
expect(found.join('\n')).toContain('has no dynamic dsh.client row')
})
it('requires every preloaded external to have a parser preload row', () => {
const bootstrap = declaration('bootstrap')
expect(collectClientPackageViolations(facts([], {
declarations: [bootstrap],
preloadedExternals: [bootstrap.name + '/client'],
parserPreloadIds: [],
}))).toEqual([
'packages/client/web/src/platform.ts: parser-preloaded external '
+ '"@deepseek-ai/dsh-client-bootstrap/client" has no matching PARSER_PRELOAD_IDS row in '
+ 'packages/client/modules/src/index.ts',
])
})
})
describe('module requests', () => {
it('rejects runtime requests from one client feature package to another dynamic row', () => {
const ui = declaration('ui', {
external: ['@deepseek-ai/dsh-client-slots/client'],
runtimeSourceUses: {
'@deepseek-ai/dsh-client-slots': ['packages/client/ui/src/client/index.ts'],
},
})
const slots = declaration('slots')
expect(collectClientPackageViolations(facts([], { declarations: [ui, slots] }))).toEqual([
ui.manifest + ': client feature package requests runtime external '
+ '"@deepseek-ai/dsh-client-slots/client"; import shared types only or call an injected Cordis service',
])
})
it('rejects stale externals and accepts a runtime import outside client feature packages', () => {
const gateway = {
...declaration('@deepseek-ai/dsh-api-gateway'), manifest: 'packages/api/gateway/package.json',
}
const stale = { ...declaration('@deepseek-ai/dsh-api-stale', {
external: ['@deepseek-ai/dsh-api-gateway/client'],
}), manifest: 'packages/api/stale/package.json' }
const live = { ...declaration('@deepseek-ai/dsh-api-live', {
external: ['@deepseek-ai/dsh-api-gateway/client'],
runtimeSourceUses: {
'@deepseek-ai/dsh-api-gateway': ['packages/api/live/src/client/index.ts'],
},
runtimeSourceSpecifiers: {
'@deepseek-ai/dsh-api-gateway/client': ['packages/api/live/src/client/index.ts'],
},
}), manifest: 'packages/api/live/package.json' }
expect(collectClientPackageViolations(facts([], {
declarations: [gateway, stale, live],
}))).toEqual([
stale.manifest + ': dsh.client.external "@deepseek-ai/dsh-api-gateway/client"'
+ ' has no runtime import or re-export in production source; remove the stale declaration',
])
})
it('requires the exact external subpath to be imported at runtime', () => {
const gateway = {
...declaration('@deepseek-ai/dsh-api-gateway'), manifest: 'packages/api/gateway/package.json',
}
const subject = { ...declaration('@deepseek-ai/dsh-api-session-controller', {
external: ['@deepseek-ai/dsh-api-gateway/client'],
runtimeSourceUses: {
'@deepseek-ai/dsh-api-gateway': ['packages/api/session-controller/src/client/index.ts'],
},
runtimeSourceSpecifiers: {
'@deepseek-ai/dsh-api-gateway/remote': ['packages/api/session-controller/src/client/index.ts'],
},
}), manifest: 'packages/api/session-controller/package.json' }
expect(collectClientPackageViolations(facts([], { declarations: [gateway, subject] }))).toEqual([
subject.manifest + ': dsh.client.external "@deepseek-ai/dsh-api-gateway/client"'
+ ' has no runtime import or re-export in production source; remove the stale declaration',
])
})
it('rejects an explicit baseline request', () => {
const ui = declaration('ui', { external: ['react'] })
expect(collectClientPackageViolations(facts([], {
declarations: [ui],
platformModules: ['react'],
}))).toEqual([
ui.manifest + ': dsh.client.external repeats baseline module "react"; remove the explicit declaration',
])
})
it('rejects duplicates, empty values, self-requests, and missing suppliers', () => {
const ui = declaration('ui', {
external: ['', '@deepseek-ai/dsh-client-ui', '@deepseek-ai/dsh-missing', '@deepseek-ai/dsh-missing'],
inject: ['', '@deepseek-ai/dsh-a', '@deepseek-ai/dsh-a'],
})
const found = collectClientPackageViolations(facts([], { declarations: [ui] }))
expect(found).toHaveLength(6)
expect(found.join('\n')).toContain('dsh.client.external contains an empty value')
expect(found.join('\n')).toContain('dsh.client.inject contains an empty value')
expect(found.join('\n')).toContain('names its own row')
expect(found.join('\n')).toContain('has no supplier')
})
it('rejects synchronous module-request cycles but ignores inject cycles', () => {
const a = { ...declaration('@deepseek-ai/dsh-api-a', {
external: ['@deepseek-ai/dsh-api-b'],
inject: ['@deepseek-ai/dsh-api-b'],
runtimeSourceUses: { '@deepseek-ai/dsh-api-b': ['packages/api/a/src/client.ts'] },
runtimeSourceSpecifiers: { '@deepseek-ai/dsh-api-b': ['packages/api/a/src/client.ts'] },
}), manifest: 'packages/api/a/package.json' }
const b = { ...declaration('@deepseek-ai/dsh-api-b', {
external: ['@deepseek-ai/dsh-api-a'],
inject: ['@deepseek-ai/dsh-api-a'],
runtimeSourceUses: { '@deepseek-ai/dsh-api-a': ['packages/client/b/src/client.ts'] },
runtimeSourceSpecifiers: { '@deepseek-ai/dsh-api-a': ['packages/client/b/src/client.ts'] },
}), manifest: 'packages/api/b/package.json' }
const found = collectClientPackageViolations(facts([], { declarations: [a, b] }))
expect(found).toHaveLength(1)
expect(found[0]).toContain('synchronous dsh.client.external cycle')
})
})
describe('package assets', () => {
const art = { file: 'packages/client/ui/src/client/View.tsx', reference: '"./assets/art.webp"' }
const mask = { file: 'packages/client/ui/src/client/View.module.css', reference: 'url("../assets/mask.png")' }
it('collects relative runtime module specifiers with a package-asset extension', () => {
expect([...collectSourceAssetSpecifiers('View.tsx', [
"import art from './assets/art.webp'",
"import back from '../assets/icons/back.SVG'",
"import './assets/side-effect.png'",
"export { default as photo } from './assets/photo.jpeg'",
"const lazy = () => import('./assets/lazy.jpg')",
"const legacy = require('./assets/legacy.svg')",
"import type Art from './assets/type-only.webp'",
"import styles from './View.module.css'",
"import font from './assets/font.woff2'",
"import raw from './assets/raw.svg?raw'",
"import shared from '@deepseek-ai/dsh-client-other/assets/shared.svg'",
"import rooted from '/assets/rooted.svg'",
].join('\n'))]).toEqual([
'./assets/art.webp',
'../assets/icons/back.SVG',
'./assets/side-effect.png',
'./assets/photo.jpeg',
'./assets/lazy.jpg',
'./assets/legacy.svg',
])
})
it.each(['View.module.css', 'global.css'])('collects relative url() values from %s', (path) => {
expect(collectStylesheetAssetUrls(path, [
".plain { background: url('./x.png'); }",
'.parent { mask: url("../assets/x.png") center / 100% no-repeat; }',
'.bare { background: url(./bare.svg); }',
".empty { background: url(''); }",
'.inline { background: url("data:image/svg+xml,%3Csvg%3E%3C/svg%3E"); }',
".fragment { filter: url('#frag'); }",
".rooted { background: url('/abs/x.png'); }",
'.network { background: url(//cdn.example.com/x.png); }',
".remote { background: url('https://example.com/x.png'); }",
"@supports (mask-image: url('')) { .nested { mask: url(./nested.svg); } }",
].join('\n'))).toEqual(['./x.png', '../assets/x.png', './bare.svg', './nested.svg'])
})
it('rejects an unparsable stylesheet with its path', () => {
expect(() => collectStylesheetAssetUrls('src/View.module.css', '.a { composes: b; } .c .d { composes: a; }'))
.toThrow('verify-client-packages: cannot parse stylesheet src/View.module.css: The `composes` property')
})
it('reads url() references only from package stylesheets that Client sources import', () => {
const root = mkdtempSync(join(tmpdir(), 'client-package-assets-'))
roots.push(root)
const files: Record<string, string> = {
'packages/client/ui/src/client/View.module.css': ".view { mask: url('../assets/mask.png'); background: url(data:,x); }",
'packages/client/ui/src/client/global.css': '.global { background: url(./assets/global.svg); }',
'packages/client/ui/src/client/text.css': '.text { background: url("./assets/text.webp"); }',
'packages/client/ui/src/styles/static.css': '@font-face { src: url(./static.woff2); }',
'packages/client/ui/styles/outside.css': '.outside { background: url(./outside.png); }',
}
for (const [path, value] of Object.entries(files)) {
mkdirSync(dirname(join(root, path)), { recursive: true })
writeFileSync(join(root, path), value)
}
expect(readStylesheetAssetReferences(root, 'packages/client/ui', new Map([
['packages/client/ui/src/client/View.tsx', ['./View.module.css', './global.css', './assets/art.webp']],
['packages/client/ui/src/client/lazy.ts', ['./text.css?inline', '../../styles/outside.css', './View.module.css']],
['packages/client/ui/src/index.ts', ['katex/dist/katex.css', './client/missing.css.ts']],
]))).toEqual({
stylesheets: [
'packages/client/ui/src/client/View.module.css',
'packages/client/ui/src/client/global.css',
'packages/client/ui/src/client/text.css',
],
references: [
{ file: 'packages/client/ui/src/client/View.module.css', reference: 'url("../assets/mask.png")' },
{ file: 'packages/client/ui/src/client/global.css', reference: 'url("./assets/global.svg")' },
{ file: 'packages/client/ui/src/client/text.css', reference: 'url("./assets/text.webp")' },
],
})
})
it('reads the Web platform and the raw asset export from manifests', () => {
const root = mkdtempSync(join(tmpdir(), 'client-package-asset-manifests-'))
roots.push(root)
const files: Record<string, unknown> = {
'packages/g/web/package.json': {
name: '@f/web', dsh: { client: { platform: 'web' } }, exports: { './assets/*': './lib/assets/*' },
},
'packages/g/conditional/package.json': {
name: '@f/conditional', exports: { './assets/*': { default: './lib/assets/*' } },
},
'packages/g/none/package.json': { name: '@f/none', dsh: { client: { platform: 1 } }, exports: './lib/index.js' },
}
for (const [path, value] of Object.entries(files)) {
mkdirSync(dirname(join(root, path)), { recursive: true })
writeFileSync(join(root, path), JSON.stringify(value))
}
expect(readClientDeclarations(root).declarations.map(entry => [entry.name, entry.platform, entry.assetExport]))
.toEqual([
['@f/conditional', undefined, { default: './lib/assets/*' }],
['@f/none', undefined, undefined],
['@f/web', 'web', './lib/assets/*'],
])
})
it('accepts referenced assets with the export and unreferenced packages without it', () => {
expect(collectClientPackageViolations(facts([], {
declarations: [
declaration('ui', { assetExport: './lib/assets/*', assetReferences: [mask, art] }),
declaration('plain'),
declaration('shell', { dynamic: false, platform: undefined }),
],
}))).toEqual([])
})
it.each([
['./lib/assets/**', '"./lib/assets/**"'],
[{ default: './lib/assets/*' }, '{"default":"./lib/assets/*"}'],
[null, 'null'],
])('rejects the asset export target %j', (target, shown) => {
const ui = declaration('ui', { assetExport: target, assetReferences: [art] })
expect(collectClientPackageViolations(facts([], { declarations: [ui] }))).toEqual([
ui.manifest + ': exports["./assets/*"] is ' + shown + '; it must be exactly "./lib/assets/*"',
])
})
it.each([
['a statically linked package', { dynamic: false, platform: undefined }],
['a row for another platform', { platform: 'desktop' }],
['a row without a platform', { platform: undefined }],
] as const)('rejects the asset export on %s', (_label, fields) => {
const ui = declaration('ui', { ...fields, assetExport: './lib/assets/*', assetReferences: [art] })
expect(collectClientPackageViolations(facts([], { declarations: [ui] }))).toEqual([
ui.manifest + ': ' + ui.name + ' declares exports["./assets/*"] without a dsh.client row for platform "web";'
+ ' only dynamic Web Client packages publish package assets',
])
})
it('rejects a referenced asset without the export and names the first reference', () => {
const ui = declaration('ui', { assetReferences: [mask, art] })
expect(collectClientPackageViolations(facts([], { declarations: [ui] }))).toEqual([
ui.manifest + ': ' + ui.name + ' references package asset url("../assets/mask.png") in '
+ 'packages/client/ui/src/client/View.module.css but declares no exports["./assets/*"];'
+ ' add "./assets/*": "./lib/assets/*" and publish "lib/assets"',
])
})
it('rejects an unused asset export', () => {
const ui = declaration('ui', { assetExport: './lib/assets/*' })
expect(collectClientPackageViolations(facts([], { declarations: [ui] }))).toEqual([
ui.manifest + ': ' + ui.name + ' declares an unused ./assets/* export;'
+ ' no Client source or stylesheet it imports references a package asset',
])
})
})
describe('manifest declarations', () => {
it('reports malformed arrays without hiding other packages', () => {
const root = mkdtempSync(join(tmpdir(), 'client-packages-'))
roots.push(root)
const files: Record<string, unknown> = {
'packages/g/a/package.json': {
name: '@f/a', dsh: { client: { external: 'react', inject: ['@f/b', 1] } },
},
'packages/g/b/package.json': { name: '@f/b', dsh: { client: {} } },
}
for (const [path, value] of Object.entries(files)) {
mkdirSync(dirname(join(root, path)), { recursive: true })
writeFileSync(join(root, path), JSON.stringify(value))
}
const result = readClientDeclarations(root)
expect(result.declarations).toHaveLength(2)
expect(result.malformed).toEqual([
'packages/g/a/package.json: @f/a dsh.client.external must be a string array',
'packages/g/a/package.json: @f/a dsh.client.inject must be a string array',
])
})
it('fixes malformed declaration entries without changing dependency sections', () => {
const root = mkdtempSync(join(tmpdir(), 'client-packages-fix-'))
roots.push(root)
const subject = pkg('feature', {
external: ['', 'react', '@deepseek-ai/dsh-client-feature', '@deepseek-ai/dsh-missing'],
inject: ['', '@deepseek-ai/dsh-agent', '@deepseek-ai/dsh-agent'],
sourceUses: {
'@deepseek-ai/dsh-agent': ['packages/client/feature/src/index.ts'],
'@deepseek-ai/dsh-client-ui-slots': ['packages/client/feature/src/view.tsx'],
},
dependencies: {
[CORDIS]: 'workspace:^',
'@deepseek-ai/dsh-agent': 'workspace:*',
},
peerDependencies: {
'@deepseek-ai/dsh-client-ui-slots': 'workspace:^',
'@deepseek-ai/cordis-plugin-loader': 'workspace:^',
},
devDependencies: {},
})
const slots = declaration('ui-slots', { dynamic: false })
const manifest = {
name: subject.name,
dsh: { client: { external: subject.external, inject: subject.inject, platform: 'web' } },
dependencies: subject.dependencies,
peerDependencies: subject.peerDependencies,
devDependencies: subject.devDependencies,
}
mkdirSync(dirname(join(root, subject.manifest)), { recursive: true })
writeFileSync(join(root, subject.manifest), JSON.stringify(manifest))
writeFileSync(join(root, 'package.json'), JSON.stringify({ private: true }))
expect(fixClientPackageManifests(root, facts([subject], {
declarations: [subject, slots],
staticLinkedPackages: new Set([slots.name]),
platformModules: ['react', slots.name],
}))).toEqual([subject.manifest])
const fixed = JSON.parse(readFileSync(join(root, subject.manifest), 'utf8')) as {
dsh: { client: { external: string[]; inject: string[] } }
dependencies?: Record<string, string>
peerDependencies: Record<string, string>
devDependencies: Record<string, string>
}
expect(fixed.dsh.client).toMatchObject({
external: ['@deepseek-ai/dsh-missing'],
inject: ['@deepseek-ai/dsh-agent'],
})
expect(fixed.dependencies).toEqual(subject.dependencies)
expect(fixed.peerDependencies).toEqual(subject.peerDependencies)
expect(fixed.devDependencies).toEqual(subject.devDependencies)
})
})