import assert from 'node:assert/strict'; import { execFileSync } from 'node:child_process'; import { createHash } from 'node:crypto'; import { existsSync } from 'node:fs'; import { readdir, readFile } from 'node:fs/promises'; import path from 'node:path'; import test from 'node:test'; import { fileURLToPath } from 'node:url'; const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const DIST = path.join(ROOT, 'dist'); const ROOT_ALLOWLIST = new Set([ '.build', '.dockerignore', '.env', '.env.example', '.env.production.example', '.git', '.gitignore', '.project-docs', 'AGENTS.md', 'Dockerfile', 'LianSyn-platform', 'README.md', 'agent设计规范', 'archive', 'chrome-extension', 'control-plane', 'dist', 'docker-compose.yml', 'infra', 'mappings', 'node_modules', 'package-lock.json', 'package.json', 'pnpm-lock.yaml', 'quarantine', 'reports', 'samples', 'schemas', 'tools', 'tsconfig.json', ]); const SKIP_WALK = new Set(['.git', '.build', 'node_modules']); const RESIDUE_PATTERN = /(^\.DS_Store$|\.log$|\.tmp$|\.swp$|\.pid$|~$)/i; const TEXT_ARTIFACT_EXTENSIONS = new Set(['.css', '.html', '.js', '.json', '.md', '.mjs', '.py', '.ts', '.txt', '.yaml', '.yml']); function hashBytes(value) { return createHash('sha256').update(value).digest('hex'); } async function sha256(filePath) { return hashBytes(await readFile(filePath)); } function artifactContentHash(entry, value) { if (!TEXT_ARTIFACT_EXTENSIONS.has(path.extname(entry).toLowerCase())) return hashBytes(value); return hashBytes(Buffer.from(value.toString('utf8').replace(/\r\n/gu, '\n'), 'utf8')); } async function normalizedTextSha256(filePath) { return artifactContentHash(filePath, await readFile(filePath)); } async function walkFiles(directory, options = {}) { const { skip = SKIP_WALK } = options; const output = []; for (const entry of await readdir(directory, { withFileTypes: true })) { if (entry.isDirectory() && skip.has(entry.name)) continue; const absolute = path.join(directory, entry.name); if (entry.isDirectory()) output.push(...await walkFiles(absolute, options)); else if (entry.isFile() || entry.isSymbolicLink()) output.push(absolute); } return output; } async function relativeSourceFiles(sourceDirectory) { const files = await walkFiles(sourceDirectory, { skip: new Set() }); return files .filter((filePath) => path.basename(filePath) !== '.DS_Store') .map((filePath) => path.relative(sourceDirectory, filePath).split(path.sep).join('/')) .sort(); } function archiveFileEntries(archivePath) { return execFileSync('unzip', ['-Z1', archivePath], { encoding: 'utf8' }) .split(/\r?\n/) .map((value) => value.trim()) .filter((value) => value && !value.endsWith('/')) .sort(); } function archiveEntry(archivePath, entry) { return execFileSync('unzip', ['-p', archivePath, entry]); } async function assertArchiveMatchesSource({ artifactPath, sourcePath, ignore = new Set() }) { const sourceEntries = (await relativeSourceFiles(sourcePath)).filter((entry) => !ignore.has(entry)); const artifactEntries = archiveFileEntries(artifactPath).filter((entry) => !ignore.has(entry)); assert.deepEqual(artifactEntries, sourceEntries, `${path.basename(artifactPath)} 文件集合与源码不一致`); for (const entry of sourceEntries) { const sourceHash = artifactContentHash(entry, await readFile(path.join(sourcePath, ...entry.split('/')))); const archiveHash = artifactContentHash(entry, archiveEntry(artifactPath, entry)); assert.equal(archiveHash, sourceHash, `${path.basename(artifactPath)}:${entry} 与源码内容不一致`); } } test('root contains only governed long-lived entries', async () => { const names = (await readdir(ROOT)).sort(); const unexpected = names.filter((name) => !ROOT_ALLOWLIST.has(name)); assert.deepEqual(unexpected, []); for (const required of ['README.md', 'AGENTS.md', '.project-docs']) { assert.ok(names.includes(required), `缺少根目录治理文件 ${required}`); } }); test('project docs are the sole active project memory', async () => { const projectDocs = path.join(ROOT, '.project-docs'); const required = [ '00-brief/project-positioning.md', '05-agent-entry/concurrent-task-gate.md', '05-agent-entry/read-before-planning.md', '10-decisions/decision-index.md', '20-architecture/system-overview.md', '30-worklog/current-state.md', '30-worklog/task-template.md', '90-maintenance/doc-update-policy.md', ]; for (const relativePath of required) { assert.ok(existsSync(path.join(projectDocs, relativePath)), `缺少项目记忆文件 .project-docs/${relativePath}`); } for (const legacy of ['task_plan.md', 'findings.md', 'progress.md']) { assert.ok(!existsSync(path.join(ROOT, legacy)), `旧 Planning with Files 文件不得恢复到根目录:${legacy}`); } const positioning = await readFile(path.join(projectDocs, '00-brief/project-positioning.md'), 'utf8'); const currentState = await readFile(path.join(projectDocs, '30-worklog/current-state.md'), 'utf8'); const taskRecords = (await readdir(path.join(projectDocs, '30-worklog/tasks'))) .filter((name) => name.endsWith('.md')); assert.doesNotMatch(positioning, /\{(?:one-line project positioning|primary project goal|primary user or consumer)\}/u); assert.match(currentState, /^## Integrated Through$/mu); assert.match(currentState, /Commit `[a-f0-9]{40}`/u); assert.ok(taskRecords.some((name) => /^\d{8}-[a-z0-9-]+-[a-f0-9]{4,16}\.md$/u.test(name)), '缺少规范 task ID 的项目任务记录'); }); test('temporary residue and report outputs do not pollute the repository', async () => { const residue = (await walkFiles(ROOT)) .filter((filePath) => RESIDUE_PATTERN.test(path.basename(filePath))) .map((filePath) => path.relative(ROOT, filePath)); assert.deepEqual(residue, []); assert.deepEqual((await readdir(path.join(ROOT, 'reports'))).sort(), ['README.md']); }); test('dist is an exact versioned release set with valid hashes', async () => { const manifestPath = path.join(DIST, 'release-manifest.json'); const manifest = JSON.parse(await readFile(manifestPath, 'utf8')); assert.equal(manifest.manifest_version, 1); assert.equal(manifest.artifacts.length, 7); const expected = new Set(['README.md', 'release-manifest.json']); for (const artifact of manifest.artifacts) { assert.match(artifact.sha256, /^[a-f0-9]{64}$/); const artifactPath = path.resolve(ROOT, artifact.path); assert.equal(path.dirname(artifactPath), DIST, `${artifact.path} 必须直接位于 dist/`); assert.ok(existsSync(artifactPath), `发布物不存在:${artifact.path}`); assert.ok(existsSync(path.resolve(ROOT, artifact.source)), `发布源不存在:${artifact.source}`); assert.equal(await sha256(artifactPath), artifact.sha256, `${artifact.path} SHA-256 不一致`); expected.add(path.basename(artifactPath)); } assert.deepEqual((await readdir(DIST)).sort(), [...expected].sort()); assert.equal(manifest.artifacts.filter(({ kind }) => kind === 'chrome_extension').length, 1); assert.equal(manifest.artifacts.filter(({ kind }) => kind === 'skill').length, 5); assert.equal(manifest.artifacts.filter(({ kind }) => kind === 'business_instruction_docx').length, 1); const docx = manifest.artifacts.find(({ kind }) => kind === 'business_instruction_docx'); assert.equal(await normalizedTextSha256(path.resolve(ROOT, docx.source)), docx.source_sha256); assert.equal(await normalizedTextSha256(path.resolve(ROOT, docx.builder)), docx.builder_sha256); }); test('extension and release baselines agree', async () => { const release = JSON.parse(await readFile(path.join(DIST, 'release-manifest.json'), 'utf8')); const extension = JSON.parse(await readFile(path.join(ROOT, 'chrome-extension/ltjt-order-assistant/manifest.json'), 'utf8')); const mapping = JSON.parse(await readFile(path.join(ROOT, 'mappings/lifecycle.mapping.json'), 'utf8')); const platform = await readFile(path.join(ROOT, 'LianSyn-platform/app.js'), 'utf8'); const prompt = await readFile(path.join(ROOT, 'agent设计规范/agent-prompt.md'), 'utf8'); const platformVersion = platform.match(/REQUIRED_EXTENSION_VERSION\s*=\s*['"]([^'"]+)['"]/u)?.[1]; const promptVersion = prompt.match(/prompt_version:\s*([^`\s]+)/u)?.[1]; assert.equal(release.baselines.chrome_extension, extension.version); assert.equal(mapping.current_extension_version, extension.version); assert.equal(platformVersion, extension.version); assert.equal(promptVersion, release.baselines.agent_prompt); const extensionArtifact = release.artifacts.find(({ kind }) => kind === 'chrome_extension'); assert.equal(extensionArtifact.version, extension.version); assert.equal(path.basename(extensionArtifact.path), `ltjt-order-assistant-${extension.version}.zip`); for (const skill of release.artifacts.filter(({ kind }) => kind === 'skill')) { assert.equal(skill.version, release.baselines.skills); assert.equal(path.basename(skill.path), `${skill.name}-${skill.version}.skill`); } const docx = release.artifacts.find(({ kind }) => kind === 'business_instruction_docx'); assert.equal(docx.version, release.baselines.business_instruction_docx); assert.equal(path.basename(docx.path), `老挝联泰AI指令表-${docx.version}.docx`); }); test('packaged Skills and extension runtime match their project sources', async () => { const release = JSON.parse(await readFile(path.join(DIST, 'release-manifest.json'), 'utf8')); for (const artifact of release.artifacts.filter(({ kind }) => kind === 'skill')) { await assertArchiveMatchesSource({ artifactPath: path.resolve(ROOT, artifact.path), sourcePath: path.resolve(ROOT, artifact.source), }); } const extension = release.artifacts.find(({ kind }) => kind === 'chrome_extension'); await assertArchiveMatchesSource({ artifactPath: path.resolve(ROOT, extension.path), sourcePath: path.resolve(ROOT, extension.source), // README is compacted as project governance documentation; runtime bytes remain immutable. ignore: new Set(['README.md']), }); }); test('active Markdown links resolve and current docs do not depend on rolling history', async () => { const allMarkdown = (await walkFiles(ROOT)) .filter((filePath) => filePath.endsWith('.md')) .filter((filePath) => !filePath.startsWith(`${path.join(ROOT, 'archive')}${path.sep}`)); const broken = []; const forbidden = []; const linkPattern = /\[[^\]]*\]\(([^)]+)\)/gu; for (const filePath of allMarkdown) { const content = await readFile(filePath, 'utf8'); if (filePath.includes(`${path.sep}agent设计规范${path.sep}businesses${path.sep}`)) { if (/\.\.\/\.\.\/(findings|progress)\.md/u.test(content)) { forbidden.push(path.relative(ROOT, filePath)); } } if (/\]\([^)]*(HANDOFF|开发维护规范)\.md/u.test(content)) { forbidden.push(path.relative(ROOT, filePath)); } for (const match of content.matchAll(linkPattern)) { let target = match[1].trim(); if (target.startsWith('<') && target.endsWith('>')) target = target.slice(1, -1); if (/^[a-z][a-z0-9+.-]*:/iu.test(target) || target.startsWith('#')) continue; target = target.split('#', 1)[0].split('?', 1)[0]; if (!target) continue; try { target = decodeURIComponent(target); } catch { broken.push(`${path.relative(ROOT, filePath)} -> ${target}(URL 编码无效)`); continue; } const resolved = path.resolve(path.dirname(filePath), target); if (!existsSync(resolved)) broken.push(`${path.relative(ROOT, filePath)} -> ${target}`); } } assert.deepEqual([...new Set(forbidden)].sort(), []); assert.deepEqual(broken.sort(), []); }); test('archive README indexes remain navigable', async () => { const archiveRoot = path.join(ROOT, 'archive'); const readmes = (await walkFiles(archiveRoot, { skip: new Set() })) .filter((filePath) => path.basename(filePath) === 'README.md'); const broken = []; const linkPattern = /\[[^\]]*\]\(([^)]+)\)/gu; for (const filePath of readmes) { const content = await readFile(filePath, 'utf8'); for (const match of content.matchAll(linkPattern)) { let target = match[1].trim(); if (target.startsWith('<') && target.endsWith('>')) target = target.slice(1, -1); if (/^[a-z][a-z0-9+.-]*:/iu.test(target) || target.startsWith('#')) continue; target = target.split('#', 1)[0].split('?', 1)[0]; if (!target) continue; try { target = decodeURIComponent(target); } catch { broken.push(`${path.relative(ROOT, filePath)} -> ${target}(URL 编码无效)`); continue; } if (!existsSync(path.resolve(path.dirname(filePath), target))) { broken.push(`${path.relative(ROOT, filePath)} -> ${target}`); } } } assert.deepEqual(broken.sort(), []); }); test('single-source and generated-output boundaries remain explicit', async () => { const template = await readFile(path.join(ROOT, 'agent设计规范/templates/business-input-templates.md'), 'utf8'); const tsconfig = JSON.parse(await readFile(path.join(ROOT, 'tsconfig.json'), 'utf8')); const packageJson = JSON.parse(await readFile(path.join(ROOT, 'package.json'), 'utf8')); const dockerfile = await readFile(path.join(ROOT, 'Dockerfile'), 'utf8'); const compose = await readFile(path.join(ROOT, 'docker-compose.yml'), 'utf8'); const diagnoseServer = await readFile(path.join(ROOT, 'infra/diagnose-server.sh'), 'utf8'); const gitignore = await readFile(path.join(ROOT, '.gitignore'), 'utf8'); assert.match(template, /^交付版本:0\.5\.136\s*$/mu); assert.match(template, /^适用范围:当前 23 条业务路由,按 19 项运营场景分组;执行能力以生命周期发布门槛为准。\s*$/mu); assert.match(template, /^### 5\. 散拼团多个新增子单\s*$/mu); assert.doesNotMatch(template, /^事实基线:/mu); assert.equal(tsconfig.compilerOptions.outDir, '.build'); assert.match(packageJson.scripts.start, /\.build\/control-plane/u); assert.match(dockerfile, /\.build\/control-plane/u); assert.match(compose, /\.build\/control-plane/u); assert.match(compose, /x-default-logging:\s*&default-logging/u); assert.match(compose, /max-size:\s*"\$\{LOG_MAX_SIZE:-20m\}"/u); assert.match(compose, /max-file:\s*"\$\{LOG_MAX_FILES:-10\}"/u); assert.equal((compose.match(/logging:\s*\*default-logging/gu) || []).length, 3); assert.match(diagnoseServer, /health\/ready/u); assert.match(diagnoseServer, /docker compose --env-file \.env\.production/u); assert.doesNotMatch(diagnoseServer, /cat\s+\.env|printenv|env\s*$/mu); assert.match(gitignore, /^\.build\/$/mu); assert.match(gitignore, /^\.env$/mu); assert.ok(!existsSync(path.join(DIST, 'control-plane'))); assert.ok(!existsSync(path.join(DIST, 'ltjt-order-assistant.zip'))); }); test('runtime image includes both LibreOffice document converters', async () => { const dockerfile = await readFile(path.join(ROOT, 'Dockerfile'), 'utf8'); assert.match(dockerfile, /apt-get install[^\n]*libreoffice-calc/u); assert.match(dockerfile, /apt-get install[^\n]*libreoffice-writer/u); });