mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-09 20:26:16 -08:00
test(refactor): [ENG-COVERAGE-01] index public contracts and exact candidate evidence
This commit is contained in:
1 parent
6abac891ce
commit
f35fb87615
22 files changed
+11007
-7
No files matched your search
@@ -0,0 +1,8 @@
|
||||
export default async function* contractReporter(events) {
|
||||
for await (const event of events) {
|
||||
if (['test:pass', 'test:fail', 'test:summary'].includes(event.type)) {
|
||||
const { name, file, nesting, skip, todo, counts, success, details } = event.data
|
||||
yield `${JSON.stringify({ type: event.type, name, file, nesting, skip, todo, counts, success, kind: details?.type, error: details?.error?.message })}\n`
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { createHash } from 'node:crypto'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import ts from 'typescript'
|
||||
import { repositoryPath } from './impact-model.mjs'
|
||||
|
||||
export const sha256 = value => createHash('sha256').update(value).digest('hex')
|
||||
export const readJson = (directory, file) => JSON.parse(fs.readFileSync(localFile(directory, file), 'utf8'))
|
||||
|
||||
export function localFile(directory, file) {
|
||||
assert.equal(repositoryPath(file), file, `Noncanonical contract path: ${file}`)
|
||||
const absolute = path.join(directory, file)
|
||||
const relative = path.relative(fs.realpathSync(directory), fs.realpathSync(absolute))
|
||||
assert(relative && !relative.startsWith('..') && !path.isAbsolute(relative), `Redirected contract path: ${file}`)
|
||||
assert(fs.statSync(absolute).isFile(), `Contract evidence is not a file: ${file}`)
|
||||
return absolute
|
||||
}
|
||||
|
||||
export function pointer(document, value) {
|
||||
assert(/^\//.test(value), 'Expected a JSON pointer')
|
||||
return value.slice(1).split('/').reduce((item, part) => {
|
||||
const key = part.replaceAll('~1', '/').replaceAll('~0', '~')
|
||||
assert(item && Object.hasOwn(item, key), `Missing version evidence pointer: ${value}`)
|
||||
return item[key]
|
||||
}, document)
|
||||
}
|
||||
|
||||
export function candidateInputs(directory, model) {
|
||||
const files = new Set(['package.json', 'yarn.lock', 'refactor/contract-policy.json', '.node-version'])
|
||||
const walk = (folder) => {
|
||||
if (!fs.existsSync(path.join(directory, folder)))
|
||||
return
|
||||
for (const entry of fs.readdirSync(path.join(directory, folder), { withFileTypes: true })) {
|
||||
const file = `${folder}/${entry.name}`
|
||||
assert(!entry.isSymbolicLink(), `Review redirected contract input: ${file}`)
|
||||
if (entry.isDirectory())
|
||||
walk(file)
|
||||
else files.add(file)
|
||||
}
|
||||
}
|
||||
for (const pkg of model.policy.packages) {
|
||||
files.add(`packages/${pkg.name}/package.json`)
|
||||
for (const folder of ['src', 'types', 'public']) walk(`packages/${pkg.name}/${folder}`)
|
||||
}
|
||||
for (const folder of ['scripts', 'test', 'types', 'refactor/scripts', 'refactor/fixtures']) walk(folder)
|
||||
for (const version of model.policy.versions) files.add(version.file)
|
||||
const inputs = [...files].sort().map(file => ({ file, sha256: sha256(fs.readFileSync(localFile(directory, file))) }))
|
||||
return { digest: sha256(JSON.stringify(inputs)), inputs }
|
||||
}
|
||||
|
||||
function validateTest(directory, item) {
|
||||
assert(/^CT-[A-Z0-9-]+$/.test(item.id), 'Invalid stable contract test ID')
|
||||
assert(item.command === 'yarn test:contracts' && item.runner === 'node', 'Unsupported contract runner/command')
|
||||
const source = ts.createSourceFile(item.file, fs.readFileSync(localFile(directory, item.file), 'utf8'), ts.ScriptTarget.Latest, true)
|
||||
assert.equal(source.parseDiagnostics.length, 0, `Malformed contract test: ${item.file}`)
|
||||
const imported = source.statements.some(statement => ts.isImportDeclaration(statement) && statement.moduleSpecifier.text === 'node:test'
|
||||
&& (statement.importClause?.name?.text === 'test' || statement.importClause?.namedBindings?.elements?.some(element => element.name.text === 'test' && (!element.propertyName || element.propertyName.text === 'test'))))
|
||||
assert(imported, `Contract case requires the Node test binding: ${item.id}`)
|
||||
const matches = []
|
||||
function visit(node) {
|
||||
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'test' && node.arguments[0] && ts.isStringLiteralLike(node.arguments[0]) && node.arguments[0].text === item.title)
|
||||
matches.push(node)
|
||||
ts.forEachChild(node, visit)
|
||||
}
|
||||
visit(source)
|
||||
assert.equal(matches.length, 1, `Contract test title must identify exactly one literal test: ${item.id}`)
|
||||
assert(item.scope?.length && item.limitations?.length, `Missing assertion scope/limits: ${item.id}`)
|
||||
}
|
||||
|
||||
export function readContractModel(directory) {
|
||||
const policy = readJson(directory, 'refactor/contract-policy.json')
|
||||
assert.equal(policy.schemaVersion, 1)
|
||||
const scripts = readJson(directory, 'package.json').scripts
|
||||
assert.equal(scripts?.['test:contracts'], 'node refactor/scripts/run-contracts.mjs', 'Contract runner command changed; review its evidence adapter')
|
||||
assert.equal(scripts?.['check:contracts'], 'node refactor/scripts/contracts.mjs', 'Contract index command changed')
|
||||
const stages = scripts['ci:check']?.split(' && ') || []
|
||||
assert(stages.includes('yarn test:contracts') && stages.indexOf('yarn check:contracts --report') > stages.indexOf('yarn test:contracts'), 'CI must run contract observations before checking their report')
|
||||
const tasks = readJson(directory, 'refactor/tasks.json').tasks
|
||||
const inventory = readJson(directory, 'refactor/package-inventory.json').packages
|
||||
const families = [...fs.readFileSync(localFile(directory, 'refactor/compatibility.md'), 'utf8').matchAll(/^\| (API-\d{2}) \| ([^|]+) \|/gm)].map(match => ({ id: match[1], name: match[2].trim() }))
|
||||
assert(families.length > 0)
|
||||
assert.deepEqual(policy.families, families, 'Public contract families changed; update their coverage ownership')
|
||||
const names = inventory.map(pkg => pkg.name).sort()
|
||||
assert.deepEqual(policy.packages.map(pkg => pkg.name).sort(), names, 'Contract package ownership is incomplete or duplicated')
|
||||
const actual = fs.readdirSync(path.join(directory, 'packages')).filter(name => fs.existsSync(path.join(directory, 'packages', name, 'package.json'))).sort()
|
||||
assert.deepEqual(actual, names, 'New package must be registered in inventory and contracts')
|
||||
const unique = (items, label) => assert.equal(new Set(items.map(item => item.id)).size, items.length, `Duplicate ${label} ID`)
|
||||
unique(policy.versions, 'version')
|
||||
unique(policy.cases, 'case')
|
||||
assert(policy.cases.length > 0, 'Contract runner requires explicit cases; empty selection must not discover unrelated tests')
|
||||
assert.equal(new Set(policy.cases.map(item => JSON.stringify([item.file, item.title]))).size, policy.cases.length, 'Duplicate file/title under different contract IDs')
|
||||
for (const version of policy.versions) {
|
||||
assert(names.includes(version.package), 'Version references unknown package')
|
||||
const origin = pointer(readJson(directory, version.file), version.pointer)
|
||||
assert.equal(origin.name, version.package, `Wrong package in version evidence: ${version.id}`)
|
||||
assert.equal(origin.version, version.version, `Wrong version evidence: ${version.id}`)
|
||||
if (version.kind === 'published-baseline') {
|
||||
assert(/^[a-f\d]{64}$/.test(origin.sha256) && origin.integrity?.startsWith('sha512-'), `Published version needs archived integrity: ${version.id}`)
|
||||
assert.equal(version.archiveSha256, origin.sha256, `Published archive identity changed: ${version.id}`)
|
||||
}
|
||||
else {
|
||||
assert.equal(version.kind, 'workspace-baseline')
|
||||
assert.equal(version.file, 'refactor/package-inventory.json', 'Workspace version must use the frozen inventory')
|
||||
}
|
||||
}
|
||||
for (const pkg of policy.packages) {
|
||||
assert.deepEqual(Object.keys(pkg.owners).sort(), families.map(family => family.id).sort(), `Missing contract owner: ${pkg.name}`)
|
||||
for (const owner of Object.values(pkg.owners)) {
|
||||
const task = tasks.find(task => task.id === owner)
|
||||
assert(task && (task.scope.includes(pkg.name) || task.scope.includes('workspace')), `Invalid contract responsibility: ${pkg.name}/${owner}`)
|
||||
}
|
||||
assert(pkg.versionIds.length && pkg.versionIds.every(id => policy.versions.some(version => version.id === id && version.package === pkg.name)), `Missing package version basis: ${pkg.name}`)
|
||||
assert.equal(pkg.supportWindow, 'not-inferred-from-baseline', 'A baseline must not become an invented supported version range')
|
||||
assert(pkg.scopeReview?.length, 'Contract applicability must remain explicit')
|
||||
}
|
||||
for (const item of policy.cases) {
|
||||
const pkg = policy.packages.find(pkg => pkg.name === item.package)
|
||||
assert(pkg && item.contracts.length && item.contracts.every(id => families.some(family => family.id === id)), 'Unknown case package/contract')
|
||||
assert(item.versionIds.length && item.versionIds.every(id => policy.versions.some(version => version.id === id && version.package === item.package)), 'Unknown test version basis')
|
||||
validateTest(directory, item)
|
||||
}
|
||||
const historical = tasks.flatMap(task => task.evidence.filter(file => /\.json$/.test(file)).map(file => ({ task: task.id, scope: task.scope, file: `refactor/${file}` })))
|
||||
for (const item of historical) localFile(directory, item.file)
|
||||
return { policy, families, tasks, historical }
|
||||
}
|
||||
|
||||
export function evaluateRun(model, report, currentDigest) {
|
||||
assert.equal(report.schemaVersion, 1)
|
||||
assert.equal(report.command, 'yarn test:contracts')
|
||||
assert(/^[a-f\d]{40}$/.test(report.head) && report.environment?.node && report.environment?.platform, 'Run lacks candidate/environment provenance')
|
||||
assert.equal(report.candidate.digest, sha256(JSON.stringify(report.candidate.inputs)), 'Candidate input fingerprint mismatch')
|
||||
assert(report.candidate.inputs.length && report.candidate.inputs.every(input => /^[a-f\d]{64}$/.test(input.sha256)), 'Invalid candidate inputs')
|
||||
for (const input of report.candidate.inputs) repositoryPath(input.file)
|
||||
assert.equal(new Set(report.cases.map(item => item.id)).size, report.cases.length, 'Duplicate run case identity')
|
||||
assert(report.events.length && ['passed', 'failed'].includes(report.outcome), 'Run is missing test events/outcome')
|
||||
const summaries = report.events.filter(event => event.type === 'test:summary' && !event.file)
|
||||
assert.equal(summaries.length, 1, 'Run requires one process summary')
|
||||
assert.equal(report.outcome === 'passed', summaries[0].success === true, 'Run outcome contradicts process summary')
|
||||
if (report.outcome === 'passed')
|
||||
assert(!report.events.some(event => event.type === 'test:fail' && !event.todo), 'Passed run contains a failed test')
|
||||
return report.cases.map((item) => {
|
||||
const defined = model.policy.cases.find(candidate => candidate.id === item.id)
|
||||
assert(defined, `Unknown reported test ID: ${item.id}`)
|
||||
assert.equal(item.definition?.id, item.id, `Wrong recorded definition: ${item.id}`)
|
||||
assert.equal(item.definitionSha256, sha256(JSON.stringify(item.definition)), `Reported test definition fingerprint changed: ${item.id}`)
|
||||
assert.equal(item.definition.package, defined.package, `Stable test ID was reassigned to another package: ${item.id}`)
|
||||
const matches = report.events.filter(event => ['test:pass', 'test:fail'].includes(event.type) && event.file === item.definition.file && event.name === item.definition.title)
|
||||
assert.equal(matches.length, 1, `Missing or ambiguous executed test: ${item.id}`)
|
||||
const event = matches[0]
|
||||
const status = event.skip || event.todo ? 'skipped' : event.type === 'test:fail' ? 'failed' : 'passed'
|
||||
assert.equal(item.status, status, `Reported test outcome mismatch: ${item.id}`)
|
||||
return { id: item.id, package: item.definition.package, contracts: item.definition.contracts, versionIds: item.definition.versionIds, status, freshness: item.definitionSha256 !== sha256(JSON.stringify(defined)) ? 'historical-definition' : report.candidate.digest === currentDigest ? 'current-inputs' : 'historical-inputs', scope: item.definition.scope }
|
||||
})
|
||||
}
|
||||
|
||||
export function contractStatus(directory, model) {
|
||||
const current = candidateInputs(directory, model)
|
||||
const registry = readJson(directory, 'refactor/contract-runs.json')
|
||||
assert.equal(registry.schemaVersion, 1)
|
||||
assert.equal(new Set(registry.runs.map(run => run.file)).size, registry.runs.length, 'Duplicate registered report')
|
||||
const observations = []
|
||||
for (const ref of registry.runs) {
|
||||
const bytes = fs.readFileSync(localFile(directory, ref.file))
|
||||
assert.equal(sha256(bytes.toString('utf8').replaceAll('\r\n', '\n')), ref.sha256LF, `Contract report changed: ${ref.file}`)
|
||||
const report = JSON.parse(bytes)
|
||||
observations.push({ file: ref.file, outcome: report.outcome, cases: evaluateRun(model, report, current.digest) })
|
||||
}
|
||||
const latest = 'refactor/.cache/ci/contracts-run.json'
|
||||
if (fs.existsSync(path.join(directory, latest))) {
|
||||
const report = readJson(directory, latest)
|
||||
observations.push({ file: latest, outcome: report.outcome, cases: evaluateRun(model, report, current.digest) })
|
||||
}
|
||||
const rows = model.policy.packages.flatMap(pkg => model.families.map((family) => {
|
||||
const cases = model.policy.cases.filter(item => item.package === pkg.name && item.contracts.includes(family.id))
|
||||
const results = observations.flatMap(run => run.cases.filter(item => item.package === pkg.name && item.contracts.includes(family.id)).map(item => ({ ...item, report: run.file, runOutcome: run.outcome })))
|
||||
return { package: pkg.name, contract: family.id, owner: pkg.owners[family.id], versionIds: pkg.versionIds, supportWindow: pkg.supportWindow, state: results.length ? 'partial-observations' : cases.length ? 'planned-not-observed' : 'tests-not-indexed', cases: cases.map(item => item.id), observations: results, remaining: 'Family-wide acceptance, version combinations and environment scope are not implied by individual test observations' }
|
||||
}))
|
||||
return { schemaVersion: 1, candidateDigest: current.digest, rows, observations, historicalRecords: model.historical.map(item => ({ ...item, state: 'recorded-not-normalized' })), limitation: 'Index status is not package compatibility or npm release approval' }
|
||||
}
|
||||
|
||||
export function renderContracts(model) {
|
||||
const lines = ['# 公开契约覆盖索引', '', '> 由 contract-policy.json 生成;`yarn check:contracts --write` 更新。动态候选/报告状态见 `yarn check:contracts --report`。', '', '覆盖12类兼容契约与22包的验证归属;不是每个公开成员的穷尽测试证明。没有固定测试ID的行仍需逐成员补齐。', '版本均为精确冻结对照点,不能推导连续支持区间。历史报告保留原状态,只有精确用例和候选指纹对应时才列为观察结果。', '', '| 包 | 契约 | 责任任务 | 固定测试ID |', '| --- | --- | --- | --- |']
|
||||
for (const pkg of model.policy.packages) {
|
||||
for (const family of model.families) {
|
||||
const cases = model.policy.cases.filter(item => item.package === pkg.name && item.contracts.includes(family.id))
|
||||
lines.push(`| ${pkg.name} | ${family.id} ${family.name} | ${pkg.owners[family.id]} | ${cases.map(item => item.id).join(', ') || '待索引;不代表没有历史测试'} |`)
|
||||
}
|
||||
}
|
||||
lines.push('', '## 精确测试及限制', '')
|
||||
for (const item of model.policy.cases)
|
||||
lines.push(`- ${item.id}: [${item.file}](../${item.file}) — ${item.title}。${item.scope}。限制:${item.limitations}。命令:\`${item.command}\`。版本依据:${item.versionIds.join(', ')}。`)
|
||||
lines.push('', '## 版本依据', '')
|
||||
for (const version of model.policy.versions)
|
||||
lines.push(`- ${version.id}: ${version.package}@${version.version} (${version.kind});[来源](../${version.file}) JSON pointer \`${version.pointer}\`。`)
|
||||
return `${lines.join('\n')}\n`
|
||||
}
|
||||
|
||||
export const currentHead = directory => execFileSync('git', ['rev-parse', 'HEAD'], { cwd: directory, encoding: 'utf8' }).trim()
|
||||
@@ -0,0 +1,23 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { contractStatus, readContractModel, renderContracts } from './contracts-model.mjs'
|
||||
|
||||
const root = fileURLToPath(new URL('../../', import.meta.url))
|
||||
const args = process.argv.slice(2)
|
||||
assert(args.every(arg => ['--write', '--report'].includes(arg)), 'Use --write and/or --report')
|
||||
const model = readContractModel(root)
|
||||
const markdown = renderContracts(model)
|
||||
const file = path.join(root, 'refactor/contract-index.md')
|
||||
if (args.includes('--write'))
|
||||
fs.writeFileSync(file, markdown)
|
||||
else assert.equal(fs.readFileSync(file, 'utf8').replaceAll('\r\n', '\n'), markdown, 'Contract index is stale; run yarn check:contracts --write')
|
||||
const report = contractStatus(root, model)
|
||||
if (args.includes('--report')) {
|
||||
fs.mkdirSync(path.join(root, 'refactor/.cache/ci'), { recursive: true })
|
||||
fs.writeFileSync(path.join(root, 'refactor/.cache/ci/contracts.json'), `${JSON.stringify(report, null, 2)}\n`)
|
||||
}
|
||||
console.log(`Contracts: ${model.policy.packages.length} packages, ${report.rows.length} assigned families, ${model.policy.cases.length} indexed cases, ${report.observations.length} structured runs`)
|
||||
console.log(`${report.rows.filter(row => row.state === 'tests-not-indexed').length} family rows need test indexing; historical records are not auto-promoted to current acceptance`)
|
||||
@@ -0,0 +1,185 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Contract evidence validation uses real Node runner results and isolated files.
|
||||
import test from 'node:test'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
import { candidateInputs, contractStatus, evaluateRun, localFile, pointer, readContractModel, renderContracts, sha256 } from './contracts-model.mjs'
|
||||
|
||||
const root = fileURLToPath(new URL('../../', import.meta.url))
|
||||
const actual = readContractModel(root)
|
||||
const cache = path.join(root, 'refactor/.cache')
|
||||
|
||||
function fixture(t) {
|
||||
fs.mkdirSync(cache, { recursive: true })
|
||||
const directory = fs.mkdtempSync(path.join(cache, 'contracts-test-'))
|
||||
t.after(() => {
|
||||
const relative = path.relative(cache, directory)
|
||||
assert(relative.startsWith('contracts-test-') && !relative.includes(path.sep))
|
||||
fs.rmSync(directory, { recursive: true, force: true })
|
||||
})
|
||||
const write = (file, value) => {
|
||||
fs.mkdirSync(path.dirname(path.join(directory, file)), { recursive: true })
|
||||
fs.writeFileSync(path.join(directory, file), typeof value === 'string' ? value : JSON.stringify(value))
|
||||
}
|
||||
const policy = structuredClone(actual.policy)
|
||||
const save = () => write('refactor/contract-policy.json', policy)
|
||||
save()
|
||||
write('refactor/tasks.json', { tasks: actual.tasks.map(task => ({ ...task, evidence: [] })) })
|
||||
for (const file of new Set(['refactor/package-inventory.json', 'refactor/compatibility.md', ...policy.versions.map(version => version.file)]))
|
||||
write(file, fs.readFileSync(path.join(root, file), 'utf8'))
|
||||
for (const pkg of policy.packages) {
|
||||
write(`packages/${pkg.name}/package.json`, { name: pkg.name })
|
||||
write(`packages/${pkg.name}/src/index.ts`, 'export const value = 1')
|
||||
}
|
||||
for (const file of new Set(policy.cases.map(item => item.file)))
|
||||
write(file, `import test from 'node:test'\n${policy.cases.filter(item => item.file === file).map(item => `test(${JSON.stringify(item.title)}, () => {})`).join('\n')}`)
|
||||
for (const file of ['package.json', 'yarn.lock', '.node-version']) write(file, fs.readFileSync(path.join(root, file), 'utf8'))
|
||||
write('refactor/contract-runs.json', { schemaVersion: 1, runs: [] })
|
||||
return { directory, policy, save, write, model: () => readContractModel(directory) }
|
||||
}
|
||||
|
||||
test('Actual contract model assigns all 264 family rows and separates exact published versions from workspace snapshots', () => {
|
||||
assert.equal(actual.policy.packages.length, 22)
|
||||
assert.equal(actual.families.length, 12)
|
||||
assert.equal(actual.policy.versions.filter(version => version.kind === 'published-baseline').length, 7)
|
||||
assert.equal(actual.policy.versions.filter(version => version.kind === 'workspace-baseline').length, 22)
|
||||
assert.equal(actual.policy.cases.length, 10)
|
||||
const markdown = renderContracts(actual)
|
||||
assert.equal(markdown.split('\n').filter(line => /^\| artplayer[^|]* \| API-/.test(line)).length, 264)
|
||||
assert.equal(pointer({ 'a/b': { '~': 2 } }, '/a~1b/~0'), 2)
|
||||
})
|
||||
|
||||
test('Missing families, package owners, tasks and unsupported version claims cannot silently pass', (t) => {
|
||||
for (const edit of [
|
||||
(policy) => { policy.families.pop() },
|
||||
(policy) => { policy.packages.pop() },
|
||||
(policy) => { delete policy.packages[0].owners['API-01'] },
|
||||
(policy) => { policy.packages[0].owners['API-01'] = 'NONEXISTENT' },
|
||||
(policy) => { policy.packages[0].supportWindow = 'all-historical-versions' },
|
||||
(policy) => { policy.packages[0].versionIds = ['invented'] },
|
||||
]) {
|
||||
const repo = fixture(t)
|
||||
edit(repo.policy)
|
||||
repo.save()
|
||||
assert.throws(repo.model)
|
||||
}
|
||||
const repo = fixture(t)
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(repo.directory, 'package.json'), 'utf8'))
|
||||
manifest.scripts['ci:check'] = manifest.scripts['ci:check'].replace('yarn test:contracts && ', '')
|
||||
repo.write('package.json', manifest)
|
||||
assert.throws(repo.model, /CI must run/)
|
||||
})
|
||||
|
||||
test('Version evidence checks JSON pointer, package identity, exact version and archive integrity', (t) => {
|
||||
for (const edit of [
|
||||
(version) => { version.pointer = '/releases/99' },
|
||||
(version) => { version.version = '99.0.0' },
|
||||
(version) => { version.package = 'artplayer-plugin-chapter' },
|
||||
(version) => { version.archiveSha256 = 'a'.repeat(64) },
|
||||
(version) => { version.file = '../outside.json' },
|
||||
]) {
|
||||
const repo = fixture(t)
|
||||
edit(repo.policy.versions.find(version => version.kind === 'published-baseline'))
|
||||
repo.save()
|
||||
assert.throws(repo.model)
|
||||
}
|
||||
})
|
||||
|
||||
test('Stable IDs require an actual unique literal test and valid paths; comments, skips and renamed assertions are not enough', (t) => {
|
||||
for (const source of [
|
||||
title => `import test from 'node:test'; // test(${JSON.stringify(title)}, () => {})`,
|
||||
title => `import test from 'node:test'; test.skip(${JSON.stringify(title)}, () => {})`,
|
||||
() => `import test from 'node:test'; test('renamed', () => {})`,
|
||||
title => `import test from 'node:test'; test(${JSON.stringify(title)}, () => {}); test(${JSON.stringify(title)}, () => {})`,
|
||||
]) {
|
||||
const repo = fixture(t)
|
||||
repo.write(repo.policy.cases[0].file, source(repo.policy.cases[0].title))
|
||||
assert.throws(repo.model, /exactly one literal/)
|
||||
}
|
||||
const repo = fixture(t)
|
||||
repo.policy.cases.push(repo.policy.cases[0])
|
||||
repo.save()
|
||||
assert.throws(repo.model, /Duplicate case/)
|
||||
repo.policy.cases[repo.policy.cases.length - 1] = { ...repo.policy.cases[0], id: 'CT-DUPLICATE-ASSERTION' }
|
||||
repo.save()
|
||||
assert.throws(repo.model, /Duplicate file\/title/)
|
||||
repo.policy.cases = []
|
||||
repo.save()
|
||||
assert.throws(repo.model, /requires explicit cases/)
|
||||
for (const file of ['../outside', 'D:/outside', '/absolute', 'test\\outside.js'])
|
||||
assert.throws(() => localFile(repo.directory, file))
|
||||
})
|
||||
|
||||
function observation(model, candidate, item, status = 'passed') {
|
||||
const event = { type: status === 'failed' ? 'test:fail' : 'test:pass', name: item.title, file: item.file, ...(status === 'skipped' ? { skip: 'unsupported fixture' } : {}) }
|
||||
return { schemaVersion: 1, command: 'yarn test:contracts', head: 'a'.repeat(40), environment: { node: process.version, platform: process.platform }, candidate, outcome: status === 'failed' ? 'failed' : 'passed', events: [event, { type: 'test:summary', success: status !== 'failed' }], cases: [{ id: item.id, definition: item, definitionSha256: sha256(JSON.stringify(item)), status }] }
|
||||
}
|
||||
|
||||
test('Evidence distinguishes current, stale, skipped and failed; wrong titles, fingerprints and process outcomes fail', (t) => {
|
||||
const repo = fixture(t)
|
||||
const model = repo.model()
|
||||
const candidate = candidateInputs(repo.directory, model)
|
||||
const item = model.policy.cases[0]
|
||||
const passed = observation(model, candidate, item)
|
||||
assert.equal(evaluateRun(model, passed, candidate.digest)[0].freshness, 'current-inputs')
|
||||
repo.write('packages/artplayer/src/index.ts', 'export const value = 2')
|
||||
assert.equal(evaluateRun(model, passed, candidateInputs(repo.directory, model).digest)[0].freshness, 'historical-inputs')
|
||||
for (const status of ['skipped', 'failed'])
|
||||
assert.equal(evaluateRun(model, observation(model, candidate, item, status), candidate.digest)[0].status, status)
|
||||
for (const edit of [
|
||||
(report) => { report.events[0].name = 'different assertion' },
|
||||
(report) => { report.cases[0].status = 'skipped' },
|
||||
(report) => { report.candidate.digest = 'a'.repeat(64) },
|
||||
(report) => { report.cases[0].definitionSha256 = 'b'.repeat(64) },
|
||||
(report) => { report.events.at(-1).success = false },
|
||||
(report) => { report.events.push(report.events[0]) },
|
||||
]) {
|
||||
const report = structuredClone(passed)
|
||||
edit(report)
|
||||
assert.throws(() => evaluateRun(model, report, candidate.digest))
|
||||
}
|
||||
const modified = structuredClone(model)
|
||||
modified.policy.cases[0].scope = 'A revised assertion scope'
|
||||
assert.equal(evaluateRun(modified, passed, candidate.digest)[0].freshness, 'historical-definition')
|
||||
})
|
||||
|
||||
test('Report registry checks immutable bytes and never upgrades individual observations into complete families', (t) => {
|
||||
const repo = fixture(t)
|
||||
const model = repo.model()
|
||||
const report = observation(model, candidateInputs(repo.directory, model), model.policy.cases[0])
|
||||
const file = 'refactor/observation.json'
|
||||
repo.write(file, report)
|
||||
const bytes = fs.readFileSync(path.join(repo.directory, file))
|
||||
repo.write('refactor/contract-runs.json', { schemaVersion: 1, runs: [{ file, sha256LF: sha256(bytes) }] })
|
||||
const result = contractStatus(repo.directory, model)
|
||||
assert.equal(result.rows.length, 264)
|
||||
assert.equal(result.rows.find(row => row.package === 'artplayer' && row.contract === 'API-01').state, 'partial-observations')
|
||||
assert(result.rows.some(row => row.state === 'planned-not-observed'))
|
||||
assert(result.rows.some(row => row.state === 'tests-not-indexed'))
|
||||
repo.policy.cases[0].contracts = ['API-09']
|
||||
repo.save()
|
||||
const revised = contractStatus(repo.directory, repo.model())
|
||||
assert.equal(revised.rows.find(row => row.package === 'artplayer' && row.contract === 'API-09').observations.length, 0)
|
||||
assert.equal(revised.rows.find(row => row.package === 'artplayer' && row.contract === 'API-01').observations[0].freshness, 'historical-definition')
|
||||
repo.write(file, { ...report, outcome: 'failed' })
|
||||
assert.throws(() => contractStatus(repo.directory, model), /Contract report changed/)
|
||||
})
|
||||
|
||||
test('Real Node reporter retains exact file/name, failure, skip, todo and overall process failure', (t) => {
|
||||
const repo = fixture(t)
|
||||
repo.write('runner.test.mjs', 'import test from \'node:test\';test(\'passing\',()=>{});test(\'failure\',()=>{throw Error(\'fixture failure\')});test(\'skipped\',{skip:\'no environment\'},()=>{});test(\'todo\',{todo:\'pending\'},()=>{});')
|
||||
const env = { ...process.env }
|
||||
delete env.NODE_TEST_CONTEXT
|
||||
delete env.NODE_OPTIONS
|
||||
const result = spawnSync(process.execPath, ['--test', `--test-reporter=${pathToFileURL(path.join(root, 'refactor/scripts/contract-reporter.mjs')).href}`, 'runner.test.mjs'], { cwd: repo.directory, env, encoding: 'utf8' })
|
||||
assert.equal(result.status, 1, result.stderr)
|
||||
const events = result.stdout.trim().split(/\r?\n/).map(line => JSON.parse(line))
|
||||
assert(events.some(event => event.name === 'passing' && event.type === 'test:pass' && event.file === path.join(repo.directory, 'runner.test.mjs')))
|
||||
assert(events.some(event => event.name === 'failure' && event.type === 'test:fail'))
|
||||
assert(events.some(event => event.name === 'skipped' && event.skip))
|
||||
assert(events.some(event => event.name === 'todo' && event.todo))
|
||||
assert.equal(events.find(event => event.type === 'test:summary' && !event.file).success, false)
|
||||
})
|
||||
@@ -0,0 +1,54 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { candidateInputs, currentHead, evaluateRun, readContractModel, sha256 } from './contracts-model.mjs'
|
||||
|
||||
const root = fileURLToPath(new URL('../../', import.meta.url))
|
||||
assert.equal(process.argv.length, 2, 'Contract runner does not accept filters or alternate artifacts')
|
||||
const model = readContractModel(root)
|
||||
const candidate = candidateInputs(root, model)
|
||||
const head = currentHead(root)
|
||||
const directory = path.join(root, 'refactor/.cache/contracts')
|
||||
fs.mkdirSync(directory, { recursive: true })
|
||||
const output = fs.mkdtempSync(path.join(directory, 'run-'))
|
||||
const files = [...new Set(model.policy.cases.map(item => item.file))]
|
||||
const env = { ...process.env }
|
||||
for (const key of Object.keys(env)) {
|
||||
if (key.startsWith('ARTPLAYER_') || ['NODE_OPTIONS', 'NODE_TEST_CONTEXT', 'NODE_V8_COVERAGE'].includes(key))
|
||||
delete env[key]
|
||||
}
|
||||
const command = ['--test', '--test-reporter=./refactor/scripts/contract-reporter.mjs', ...files]
|
||||
const executed = spawnSync(process.execPath, command, { cwd: root, env, encoding: 'utf8', timeout: 180000, maxBuffer: 16 * 1024 * 1024 })
|
||||
fs.writeFileSync(path.join(output, 'events.jsonl'), executed.stdout || '')
|
||||
fs.writeFileSync(path.join(output, 'stderr.log'), executed.stderr || '')
|
||||
assert(!executed.error, `Contract process failed: ${executed.error?.message}; evidence ${output}`)
|
||||
assert.equal(candidateInputs(root, model).digest, candidate.digest, 'Contract inputs changed during execution')
|
||||
const events = executed.stdout.split(/\r?\n/).filter(Boolean).map(line => JSON.parse(line)).map(event => ({ ...event, ...(event.file ? { file: path.relative(root, event.file).split(path.sep).join('/') } : {}) }))
|
||||
const report = {
|
||||
schemaVersion: 1,
|
||||
command: 'yarn test:contracts',
|
||||
invocation: [process.execPath, ...command],
|
||||
head,
|
||||
capturedAt: new Date().toISOString(),
|
||||
environment: { node: process.version, platform: process.platform, arch: process.arch, artifactOverrides: 'cleared' },
|
||||
candidate,
|
||||
events,
|
||||
outcome: executed.status === 0 ? 'passed' : 'failed',
|
||||
cases: model.policy.cases.map((item) => {
|
||||
const matches = events.filter(event => ['test:pass', 'test:fail'].includes(event.type) && event.file === item.file && event.name === item.title)
|
||||
assert.equal(matches.length, 1, `Missing or ambiguous runner result: ${item.id}; evidence ${output}`)
|
||||
const event = matches[0]
|
||||
return { id: item.id, definition: item, definitionSha256: sha256(JSON.stringify(item)), status: event.skip || event.todo ? 'skipped' : event.type === 'test:pass' ? 'passed' : 'failed' }
|
||||
}),
|
||||
limitations: 'Selected source/controlled-host assertions only; not full family, package tarball, browser, device, SDK or support-range acceptance',
|
||||
}
|
||||
evaluateRun(model, report, candidate.digest)
|
||||
fs.writeFileSync(path.join(output, 'report.json'), `${JSON.stringify(report, null, 2)}\n`)
|
||||
fs.mkdirSync(path.join(root, 'refactor/.cache/ci'), { recursive: true })
|
||||
fs.writeFileSync(path.join(root, 'refactor/.cache/ci/contracts-run.json'), `${JSON.stringify(report, null, 2)}\n`)
|
||||
fs.writeFileSync(path.join(directory, 'latest.json'), `${JSON.stringify({ output: path.relative(root, output).split(path.sep).join('/') }, null, 2)}\n`)
|
||||
console.log(`Contract observations: ${report.cases.length} indexed cases; process ${report.outcome}; ${path.relative(root, output)}`)
|
||||
process.exitCode = executed.status === 0 && report.cases.every(item => item.status === 'passed') ? 0 : 1
|
||||
Reference in new issue
Block a user