test(ci): [CI-TYPES-02] gate installed types across all libraries

This commit is contained in:
Harvey Zhao committed 2026-09-15 20:15:58 +08:00
1 parent 00c131bff1
commit 01f3d00e4c
17 files changed
+974 -8

No files matched your search

+38
View File
@@ -33,3 +33,41 @@ Maintain the module with `yarn typecheck:docs-tools`, `yarn lint`, and
`node --test test/installed-types.test.js` (also included in `yarn test:node`).
The tests compile a real isolated package and reject a missing declaration,
`any` widening, and a package link outside the consumer.
## Complete ecosystem gate
`yarn test:ecosystem-types` rebuilds public declarations, all package bundles and
core languages, then runs the four-package shared consumer plus all 17 specialized
package type commands sequentially. `ecosystem.ts` validates that this explicit
roster covers every library exactly once; the VitePress site retains its separate
site/editor checks. A new library or missing command fails instead of silently
reducing scope. Existing historical diagnostics, conditional exports and accurate
runtime entry fixtures remain owned by their specialized checkers.
Preparation failure stops before any installed validation, avoiding stale bundle
acceptance. A package failure records its actual exit code and continues through
the remaining packages, then fails the aggregate command. Each child writes stdout
and stderr to a dedicated file; after each child, the report records duration,
exit code or launch error, log fingerprint and package ownership. Results live in
`refactor/.cache/ecosystem-types/run-*/`. Specialized checkers keep their own detailed
reports and fingerprints under `refactor/.cache/*-package-types-*/`.
This command intentionally regenerates tracked distribution files using the
normal build commands. Review any real output drift before committing; formatting
normalization alone is not a runtime change. Do not run another source edit/build
concurrently. Checks run one at a time to avoid competing installs and excessive
compiler memory. The CI consumer job runs this gate after its existing runtime,
framework, history and performance checks on each OS, with the canonical Node
restored. CI retains the aggregate directory and detailed JSON/log artifacts on
failure. The existing job timeout remains in force.
The shared four-package check updates `refactor/.cache/packages/latest.json`.
That is why CI runs this gate after the existing runtime/browser consumers, which
already selected their own map. Before a later full installed browser run, prepare
its twenty-package map again with `yarn test:package --browser`; never pass the
four-package type preparation off as the complete browser artifact roster.
Successful completion means every selected checker met its documented type
contract, including exact expected historical failures. It does not approve the
pending Thumbnail runtime compatibility choice or replace media/device/SDK tests,
release reviews, or actual remote CI evidence.
+95
View File
@@ -0,0 +1,95 @@
import assert from 'node:assert/strict'
import { spawn } from 'node:child_process'
import { createHash } from 'node:crypto'
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import { fileURLToPath } from 'node:url'
export const ecosystemChecks = [
{ script: 'test:package', args: ['--include=artplayer-plugin-audio-track,artplayer-plugin-hls-control'], packages: ['artplayer', 'artplayer-plugin-chapter', 'artplayer-plugin-audio-track', 'artplayer-plugin-hls-control'] },
...[
['ads', 'artplayer-plugin-ads'],
['ambilight', 'artplayer-plugin-ambilight'],
['asr', 'artplayer-plugin-asr'],
['auto-thumbnail', 'artplayer-plugin-auto-thumbnail'],
['canvas', 'artplayer-proxy-canvas'],
['chromecast', 'artplayer-plugin-chromecast'],
['danmuku', 'artplayer-plugin-danmuku'],
['danmuku-mask', 'artplayer-plugin-danmuku-mask'],
['dash', 'artplayer-plugin-dash-control'],
['dpip', 'artplayer-plugin-document-pip'],
['iframe', 'artplayer-tool-iframe'],
['jassub', 'artplayer-plugin-jassub'],
['mediabunny', 'artplayer-proxy-mediabunny'],
['multiple-subtitles', 'artplayer-plugin-multiple-subtitles'],
['thumbnail', 'artplayer-tool-thumbnail'],
['vast', 'artplayer-plugin-vast'],
['vtt-thumbnail', 'artplayer-plugin-vtt-thumbnail'],
].map(([name, pkg]) => ({ script: `test:${name}-types-package`, args: [], packages: [pkg!] })),
]
export function verifyEcosystemScope(directory: string, checks = ecosystemChecks) {
const names = fs.readdirSync(path.join(directory, 'packages')).filter(name => name !== 'artplayer-vitepress' && fs.existsSync(path.join(directory, 'packages', name, 'package.json'))).sort()
const covered = checks.flatMap(check => check.packages).sort()
assert.deepEqual(covered, names, 'Every library package needs exactly one installed type check')
const manifest = JSON.parse(fs.readFileSync(path.join(directory, 'package.json'), 'utf8')) as { scripts: Record<string, string> }
for (const check of checks)
assert(manifest.scripts[check.script], `Missing consumer command: ${check.script}`)
return names
}
export async function runConsumerCommand(executable: string, args: string[], cwd: string, log: string) {
const started = Date.now()
const file = fs.openSync(log, 'w')
try {
const result = await new Promise<{ code: number | null, signal: NodeJS.Signals | null, error?: string }>((resolve) => {
const child = spawn(executable, args, { cwd, windowsHide: true, stdio: ['ignore', file, file], env: { ...process.env, NODE_PATH: '' } })
child.once('error', error => resolve({ code: null, signal: null, error: error.message }))
child.once('close', (code, signal) => resolve({ code, signal }))
})
return { ...result, elapsedMs: Date.now() - started, log, sha256: createHash('sha256').update(fs.readFileSync(log)).digest('hex') }
}
finally {
fs.closeSync(file)
}
}
export async function checkEcosystem(directory = fileURLToPath(new URL('../../', import.meta.url))) {
assert.equal(process.env.npm_config_user_agent?.split(' ')[0], 'yarn/1.22.22', 'Use the pinned Yarn')
const yarn = process.env.npm_execpath
assert(yarn && fs.existsSync(yarn), 'Missing Yarn executable')
const packages = verifyEcosystemScope(directory)
const parent = path.join(directory, 'refactor/.cache/ecosystem-types')
fs.mkdirSync(parent, { recursive: true })
const output = fs.mkdtempSync(path.join(parent, 'run-'))
const results: { command: string[], packages: string[], outcome: Awaited<ReturnType<typeof runConsumerCommand>> }[] = []
const save = () => fs.writeFileSync(path.join(output, 'report.json'), `${JSON.stringify({ packages, node: process.version, results, scope: 'Installed type contracts, including explicitly recorded historical diagnostics. Not runtime/device or release acceptance.' }, null, 2)}\n`)
console.log(`Ecosystem installed types: ${output}`)
for (const args of [['build:types'], ['build', 'all'], ['build:i18n']]) {
const outcome = await runConsumerCommand(process.execPath, [yarn, ...args], directory, path.join(output, `prepare-${results.length}.log`))
results.push({ command: args, packages: [], outcome })
save()
assert.equal(outcome.code, 0, `Cannot test stale packages after failed preparation; see ${outcome.log}`)
}
let failed = false
for (const [index, check] of ecosystemChecks.entries()) {
const args = [check.script, ...check.args]
console.log(`[${index + 1}/${ecosystemChecks.length}] ${args.join(' ')}`)
const outcome = await runConsumerCommand(process.execPath, [yarn, ...args], directory, path.join(output, `${check.script.replaceAll(':', '-')}.log`))
results.push({ command: args, packages: check.packages, outcome })
save()
if (outcome.code !== 0)
failed = true
console.log(`${check.script}: ${outcome.code === 0 ? 'passed' : 'FAILED'} (${Math.round(outcome.elapsedMs / 1000)}s); ${outcome.log}`)
}
assert(!failed, `Installed ecosystem type checks failed; all available results retained in ${output}`)
return { output, results }
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
checkEcosystem().catch((error: unknown) => {
console.error(error)
process.exitCode = 1
})
}