diff --git a/__tests__/runtime-publication.test.mjs b/__tests__/runtime-publication.test.mjs new file mode 100644 index 0000000..6c6003e --- /dev/null +++ b/__tests__/runtime-publication.test.mjs @@ -0,0 +1,151 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { execFileSync, spawnSync } from 'node:child_process' +import { createHash } from 'node:crypto' +import { lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs' +import { builtinModules } from 'node:module' +import { devNull, tmpdir } from 'node:os' +import { dirname, join, resolve } from 'node:path' +import { fileURLToPath, pathToFileURL } from 'node:url' + +const repository = fileURLToPath(new URL('../', import.meta.url)) +const toolDirectory = join(repository, '.github/runtime-publication') +const tool = join(toolDirectory, 'publisher.cjs') +const digest = value => createHash('sha256').update(value).digest('hex') + +test('standalone publisher has pinned bytes, bounded provenance and Node-only externals', () => { + const metadata = JSON.parse(readFileSync(join(toolDirectory, 'publisher.metadata.json'), 'utf8')) + assert.equal(metadata.formatVersion, 1) + assert.equal(metadata.profile, 'config-center-runtime-publisher-node-cjs-v1') + assert.match(metadata.sourceCommit, /^[a-f0-9]{40}$/) + assert.equal(digest(readFileSync(tool)), metadata.sha256) + assert.ok(metadata.inputs.length > 0 && metadata.inputs.length <= 2048) + assert.equal(new Set(metadata.inputs.map(input => input.path)).size, metadata.inputs.length) + for (const input of metadata.inputs) { + assert.match(input.sha256, /^[a-f0-9]{64}$/) + assert.ok(input.path.length <= 512 && !/[\\:\x00-\x1f]/.test(input.path)) + assert.ok(input.path.split('/').every(part => part && part !== '.' && part !== '..')) + assert.match(input.path, /^(?:node_modules\/|packages\/(?:schemas|shared)\/src\/|scripts\/sync\/)/) + } + const builtin = new Set(builtinModules.map(name => name.replace(/^node:/, ''))) + assert.ok(metadata.externalImports.length > 0) + for (const name of metadata.externalImports) { + assert.ok(name.startsWith('node:') && builtin.has(name.slice(5))) + } + assert.ok(readFileSync(join(toolDirectory, 'LICENSE.DesireCore')).length > 0) + assert.ok(readFileSync(join(toolDirectory, 'THIRD_PARTY_NOTICES.txt')).length > 0) +}) + +test('standalone publication ignores docs/tests but requires a new version for changed runtime bytes', { timeout: 120_000 }, async t => { + // 私有无凭据 fixture,不联网、不执行作者脚本、不触碰工作区或远端 runtime。 + const scratch = mkdtempSync(join(tmpdir(), 'config-center-publication-test-')) + t.after(() => rmSync(scratch, { recursive: true, force: true })) + const home = join(scratch, 'home'), hooks = join(scratch, 'hooks'), template = join(scratch, 'template') + for (const path of [home, hooks, template]) mkdirSync(path) + const clean = Object.fromEntries(['PATH', 'Path', 'SystemRoot', 'windir', 'TEMP', 'TMP', 'TMPDIR'] + .flatMap(key => process.env[key] === undefined ? [] : [[key, process.env[key]]])) + const environment = { ...clean, HOME: home, USERPROFILE: home, XDG_CONFIG_HOME: home, + GIT_CONFIG_GLOBAL: devNull, GIT_CONFIG_SYSTEM: devNull, GIT_CONFIG_NOSYSTEM: '1', + GIT_TEMPLATE_DIR: template, GIT_TERMINAL_PROMPT: '0', GIT_ALLOW_PROTOCOL: 'file', + GIT_OPTIONAL_LOCKS: '0', GIT_NO_REPLACE_OBJECTS: '1', GIT_NO_LAZY_FETCH: '1' } + const git = args => execFileSync('git', ['-c', `core.hooksPath=${hooks}`, '-c', 'core.fsmonitor=false', + '-c', 'gc.auto=0', '-c', 'maintenance.auto=false', ...args], { + cwd: scratch, env: environment, encoding: 'utf8', timeout: 30_000, maxBuffer: 8 * 1024 * 1024, stdio: 'pipe', + }).trim() + const source = join(scratch, 'source'), output = join(scratch, 'runtime.git') + const initialHead = git(['-C', repository, 'rev-parse', 'HEAD']) + git(['clone', '--no-local', '--no-checkout', '--no-hardlinks', '--depth=1', pathToFileURL(repository).href, source]) + git(['-C', source, 'checkout', '--detach', initialHead]) + const publish = () => spawnSync(process.execPath, [tool, '--source', source, + '--source-commit', git(['-C', source, 'rev-parse', 'HEAD']), '--output', output, '--apply', + '--author-name', 'Runtime Fixture', '--author-email', 'fixture@example.invalid'], { + cwd: scratch, env: clean, encoding: 'utf8', timeout: 30_000, maxBuffer: 2 * 1024 * 1024, + }) + const success = () => { + const result = publish() + assert.equal(result.error, undefined) + assert.equal(result.status, 0, result.stderr) + const receipt = JSON.parse(result.stdout.split('\n')[0]) + assert.equal(receipt.validationBoundary, 'runtime-payload-and-client-contract-only') + assert.equal(receipt.dryRun, false) + return receipt + } + const commit = (path, text) => { + const target = resolve(source, path) + assert.ok(target.startsWith(source + '/')) + mkdirSync(dirname(target), { recursive: true }) + writeFileSync(target, text) + git(['-C', source, 'add', '--', path]) + git(['-C', source, '-c', 'user.name=Runtime Fixture', '-c', 'user.email=fixture@example.invalid', + 'commit', '-m', `fixture: ${path}`]) + assert.equal(git(['-C', source, 'status', '--porcelain']), '') + } + const bareSnapshot = () => { + const files = [] + const visit = (directory, prefix = '') => { + for (const name of readdirSync(directory).sort()) { + const path = join(directory, name), relative = prefix + name, info = lstatSync(path) + assert.ok(!info.isSymbolicLink()) + if (info.isDirectory()) visit(path, relative + '/') + else { assert.ok(info.isFile()); files.push([relative, digest(readFileSync(path))]) } + } + } + visit(output) + return files + } + const manifestText = readFileSync(join(source, 'manifest.json'), 'utf8') + const first = success() + assert.equal(first.changed, true) + assert.equal(first.dataVersion, JSON.parse(manifestText).presetDataVersion) + const ref = () => git([`--git-dir=${output}`, 'rev-parse', 'refs/heads/runtime']) + assert.equal(ref(), first.runtimeCommit) + assert.equal(git([`--git-dir=${output}`, 'rev-list', '--count', 'runtime']), '1') + const paths = git([`--git-dir=${output}`, 'ls-tree', '-r', '--name-only', 'runtime']).split('\n') + const marker = JSON.parse(git([`--git-dir=${output}`, 'show', 'runtime:.desirecore-runtime.json'])) + assert.deepEqual(paths.sort(), [...marker.files.map(file => file.path), '.desirecore-runtime.json'].sort()) + assert.equal(paths.length, first.fileCount + 1) + assert.ok(paths.every(path => !/^(?:docs|__tests__|scripts|\.github)\//.test(path))) + const baseline = bareSnapshot() + + for (const [name, path, text] of [ + ['documentation-only', 'docs/runtime-publication-fixture.md', '# 私有无变化回归夹具\n'], + ['test-only', '__tests__/runtime-publication-fixture.test.mjs', '// 私有无变化回归夹具\n'], + ]) { + await t.test(`${name} is a true no-op without even rewriting bare files`, () => { + commit(path, text) + const receipt = success() + assert.equal(receipt.changed, false) + assert.equal(receipt.sourceCommit, git(['-C', source, 'rev-parse', 'HEAD'])) + for (const key of ['runtimeCommit', 'dataVersion', 'payloadDigest', 'fileCount']) assert.equal(receipt[key], first[key]) + assert.equal(readFileSync(join(source, 'manifest.json'), 'utf8'), manifestText) + assert.equal(ref(), first.runtimeCommit) + assert.deepEqual(bareSnapshot(), baseline) + }) + } + + await t.test('changed runtime bytes at the same version fail without mutating the last good publication', () => { + const manifest = JSON.parse(manifestText) + manifest.description += ' (private fixture)' + commit('manifest.json', JSON.stringify(manifest, null, 2) + '\n') + const result = publish() + assert.equal(result.error, undefined) + assert.equal(result.status, 1) + assert.equal(result.stderr.trim(), 'config_center_runtime_changed_payload_requires_new_data_version') + assert.equal(ref(), first.runtimeCommit) + assert.deepEqual(bareSnapshot(), baseline) + }) + + await t.test('a valid higher-version payload advances only the independent runtime parent chain', () => { + const manifest = JSON.parse(readFileSync(join(source, 'manifest.json'), 'utf8')) + manifest.presetDataVersion++ + commit('manifest.json', JSON.stringify(manifest, null, 2) + '\n') + const next = success() + assert.equal(next.changed, true) + assert.equal(next.dataVersion, first.dataVersion + 1) + assert.notEqual(next.payloadDigest, first.payloadDigest) + assert.equal(ref(), next.runtimeCommit) + assert.equal(git([`--git-dir=${output}`, 'rev-list', '--parents', '-1', 'runtime']), `${next.runtimeCommit} ${first.runtimeCommit}`) + assert.equal(git([`--git-dir=${output}`, 'rev-list', '--count', 'runtime']), '2') + assert.equal(success().changed, false) + }) +})