mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-05-17 06:13:08 +08:00
332 lines
12 KiB
JavaScript
332 lines
12 KiB
JavaScript
/**
|
|
* Tests for scripts/operator-readiness-dashboard.js
|
|
*/
|
|
|
|
const assert = require('assert');
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
const { execFileSync, spawnSync } = require('child_process');
|
|
|
|
const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'operator-readiness-dashboard.js');
|
|
const { buildReport, parseArgs, renderMarkdown, renderText } = require(SCRIPT);
|
|
|
|
function createTempDir(prefix) {
|
|
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
}
|
|
|
|
function cleanup(dirPath) {
|
|
fs.rmSync(dirPath, { recursive: true, force: true });
|
|
}
|
|
|
|
function writeFile(rootDir, relativePath, content) {
|
|
const targetPath = path.join(rootDir, relativePath);
|
|
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
fs.writeFileSync(targetPath, content);
|
|
}
|
|
|
|
function seedRepo(rootDir, overrides = {}) {
|
|
const files = {
|
|
'package.json': JSON.stringify({
|
|
name: 'everything-claude-code',
|
|
files: [
|
|
'scripts/observability-readiness.js',
|
|
'scripts/operator-readiness-dashboard.js',
|
|
'scripts/platform-audit.js'
|
|
],
|
|
scripts: {
|
|
'discussion:audit': 'node scripts/discussion-audit.js',
|
|
'observability:ready': 'node scripts/observability-readiness.js',
|
|
'operator:dashboard': 'node scripts/operator-readiness-dashboard.js',
|
|
'platform:audit': 'node scripts/platform-audit.js',
|
|
'security:ioc-scan': 'node scripts/ci/scan-supply-chain-iocs.js',
|
|
'security:advisory-sources': 'node scripts/ci/supply-chain-advisory-sources.js'
|
|
}
|
|
}, null, 2),
|
|
'scripts/operator-readiness-dashboard.js': 'operator dashboard generator',
|
|
'docs/ECC-2.0-GA-ROADMAP.md': [
|
|
'https://linear.app/itomarkets/project/ecc-platform-roadmap-52b328ee03e1',
|
|
'Linear ITO-44 ITO-59',
|
|
'AgentShield PR #86 #78-#86',
|
|
'AgentShield Enterprise Iteration',
|
|
'ECC-Tools PR #73',
|
|
'hosted promotion',
|
|
'announcementGate',
|
|
'ITO-55'
|
|
].join('\n'),
|
|
'docs/releases/2.0.0-rc.1/publication-readiness.md': 'Claude plugin Codex plugin',
|
|
'docs/releases/2.0.0-rc.1/naming-and-publication-matrix.md': 'Claude plugin Codex plugin npm package Publication Paths',
|
|
'docs/releases/2.0.0-rc.1/preview-pack-manifest.md': 'publication-readiness.md release-notes.md quickstart.md',
|
|
'docs/releases/2.0.0-rc.1/release-notes.md': 'release notes',
|
|
'docs/releases/2.0.0-rc.1/x-thread.md': 'x thread',
|
|
'docs/releases/2.0.0-rc.1/linkedin-post.md': 'linkedin post',
|
|
'docs/releases/2.0.0-rc.1/operator-readiness-dashboard-2026-05-15.md': [
|
|
'This dashboard is generated by `npm run operator:dashboard`',
|
|
'operator:dashboard',
|
|
'Prompt-To-Artifact Checklist',
|
|
'Next Work Order',
|
|
'ITO-44',
|
|
'ITO-59',
|
|
'PR queue',
|
|
'Not complete'
|
|
].join('\n'),
|
|
'docs/HERMES-SETUP.md': 'Hermes setup',
|
|
'skills/hermes-imports/SKILL.md': 'Hermes imports',
|
|
'docs/stale-pr-salvage-ledger.md': 'Manual review tail',
|
|
'docs/architecture/progress-sync-contract.md': 'GitHub PRs/issues/discussions Linear project local handoff repo roadmap scripts/work-items.js',
|
|
'docs/architecture/observability-readiness.md': 'observability-readiness.js',
|
|
'docs/security/supply-chain-incident-response.md': 'TanStack Mini Shai-Hulud node-ipc scan-supply-chain-iocs.js supply-chain-advisory-sources.js',
|
|
'docs/releases/2.0.0-rc.1/publication-evidence-2026-05-15.md': 'TanStack Mini Shai-Hulud Node IPC follow-up node-ipc IOC scan',
|
|
'.github/workflows/supply-chain-watch.yml': 'name: Supply-Chain Watch supply-chain-advisory-sources.js supply-chain-advisory-sources.json'
|
|
};
|
|
|
|
for (const [relativePath, content] of Object.entries({ ...files, ...overrides })) {
|
|
if (content === null) {
|
|
continue;
|
|
}
|
|
writeFile(rootDir, relativePath, content);
|
|
}
|
|
}
|
|
|
|
function run(args = [], options = {}) {
|
|
return execFileSync('node', [SCRIPT, ...args], {
|
|
cwd: options.cwd || path.join(__dirname, '..', '..'),
|
|
encoding: 'utf8',
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
timeout: 10000
|
|
});
|
|
}
|
|
|
|
function runProcess(args = [], options = {}) {
|
|
return spawnSync('node', [SCRIPT, ...args], {
|
|
cwd: options.cwd || path.join(__dirname, '..', '..'),
|
|
encoding: 'utf8',
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
timeout: 10000
|
|
});
|
|
}
|
|
|
|
function test(name, fn) {
|
|
try {
|
|
fn();
|
|
console.log(` PASS ${name}`);
|
|
return true;
|
|
} catch (error) {
|
|
console.log(` FAIL ${name}`);
|
|
console.log(` Error: ${error.message}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function runTests() {
|
|
console.log('\n=== Testing operator-readiness-dashboard.js ===\n');
|
|
|
|
let passed = 0;
|
|
let failed = 0;
|
|
|
|
if (test('parseArgs accepts dashboard flags and rejects invalid values', () => {
|
|
const rootDir = createTempDir('operator-dashboard-args-');
|
|
|
|
try {
|
|
const parsed = parseArgs([
|
|
'node',
|
|
'script',
|
|
'--format=json',
|
|
`--root=${rootDir}`,
|
|
'--skip-github',
|
|
'--allow-untracked',
|
|
'docs/drafts/',
|
|
'--repo',
|
|
'affaan-m/everything-claude-code',
|
|
'--generated-at',
|
|
'2026-05-15T00:00:00.000Z'
|
|
]);
|
|
|
|
assert.strictEqual(parsed.format, 'json');
|
|
assert.strictEqual(parsed.root, path.resolve(rootDir));
|
|
assert.strictEqual(parsed.skipGithub, true);
|
|
assert.deepStrictEqual(parsed.allowUntracked, ['docs/drafts/']);
|
|
assert.deepStrictEqual(parsed.repos, ['affaan-m/everything-claude-code']);
|
|
assert.strictEqual(parsed.generatedAt, '2026-05-15T00:00:00.000Z');
|
|
|
|
assert.throws(() => parseArgs(['node', 'script', '--format', 'xml']), /Invalid format/);
|
|
assert.throws(() => parseArgs(['node', 'script', '--write', 'dashboard.md', '--format', 'text']), /--write requires/);
|
|
assert.throws(() => parseArgs(['node', 'script', '--max-open-prs', 'x']), /Invalid --max-open-prs/);
|
|
assert.throws(() => parseArgs(['node', 'script', '--unknown']), /Unknown argument/);
|
|
} finally {
|
|
cleanup(rootDir);
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
if (test('seeded repo emits an objective audit with remaining work', () => {
|
|
const rootDir = createTempDir('operator-dashboard-report-');
|
|
|
|
try {
|
|
seedRepo(rootDir);
|
|
const report = buildReport({
|
|
allowUntracked: [],
|
|
exitCode: false,
|
|
format: 'json',
|
|
generatedAt: '2026-05-15T00:00:00.000Z',
|
|
help: false,
|
|
repos: [],
|
|
root: rootDir,
|
|
skipGithub: true,
|
|
thresholds: { maxOpenPrs: 20, maxOpenIssues: 20, maxDirtyFiles: 0 },
|
|
useEnvGithubToken: false,
|
|
writePath: null
|
|
});
|
|
|
|
assert.strictEqual(report.schema_version, 'ecc.operator-readiness-dashboard.v1');
|
|
assert.strictEqual(report.generatedAt, '2026-05-15T00:00:00.000Z');
|
|
assert.strictEqual(report.dashboardReady, true);
|
|
assert.strictEqual(report.ready, false);
|
|
assert.strictEqual(report.publicationReady, false);
|
|
assert.ok(report.requirements.some(item => item.id === 'completion-dashboard' && item.status === 'complete'));
|
|
assert.ok(report.requirements.some(item => item.id === 'ecc-tools-next-level' && item.status === 'in_progress'));
|
|
assert.ok(report.top_actions.some(item => item.id === 'naming-and-plugin-publication'));
|
|
} finally {
|
|
cleanup(rootDir);
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
if (test('markdown output can be written as the dashboard artifact', () => {
|
|
const rootDir = createTempDir('operator-dashboard-markdown-');
|
|
const outputPath = path.join(rootDir, 'artifacts', 'dashboard.md');
|
|
|
|
try {
|
|
seedRepo(rootDir);
|
|
const stdout = run([
|
|
'--markdown',
|
|
'--skip-github',
|
|
`--root=${rootDir}`,
|
|
'--generated-at=2026-05-15T00:00:00.000Z',
|
|
'--write',
|
|
outputPath
|
|
], { cwd: rootDir });
|
|
const written = fs.readFileSync(outputPath, 'utf8');
|
|
|
|
assert.strictEqual(stdout, written);
|
|
assert.ok(written.includes('# ECC Operator Readiness Dashboard'));
|
|
assert.ok(written.includes('Generated: 2026-05-15T00:00:00.000Z'));
|
|
assert.ok(written.includes('## Prompt-To-Artifact Checklist'));
|
|
assert.ok(written.includes('Build ITO-44 completion dashboard into a repeatable command'));
|
|
assert.ok(written.includes('## Next Work Order'));
|
|
} finally {
|
|
cleanup(rootDir);
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
if (test('text output renders compact status and top actions', () => {
|
|
const rootDir = createTempDir('operator-dashboard-text-');
|
|
|
|
try {
|
|
seedRepo(rootDir);
|
|
const stdout = run([
|
|
'--format=text',
|
|
'--skip-github',
|
|
`--root=${rootDir}`,
|
|
'--generated-at=2026-05-15T00:00:00.000Z'
|
|
], { cwd: rootDir });
|
|
|
|
assert.ok(stdout.includes('ECC Operator Readiness Dashboard'));
|
|
assert.ok(stdout.includes('work remaining'));
|
|
assert.ok(stdout.includes('Dashboard ready: true'));
|
|
assert.ok(stdout.includes('Publication ready: false'));
|
|
assert.ok(stdout.includes('Top actions:'));
|
|
assert.ok(stdout.includes('naming-and-plugin-publication'));
|
|
} finally {
|
|
cleanup(rootDir);
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
if (test('renderers handle a ready report with no top actions', () => {
|
|
const report = {
|
|
dashboardReady: true,
|
|
generatedAt: '2026-05-15T00:00:00.000Z',
|
|
head: 'abc123',
|
|
next_work_order: ['Ship release evidence'],
|
|
platform: {
|
|
blockingDirtyCount: 0,
|
|
discussionsMissingAcceptedAnswer: 0,
|
|
discussionsNeedingMaintainerTouch: 0,
|
|
githubSkipped: false,
|
|
ignoredDirtyCount: 0,
|
|
openIssues: 1,
|
|
openPrs: 1,
|
|
ready: true
|
|
},
|
|
publicationReady: true,
|
|
ready: true,
|
|
requirements: [
|
|
{
|
|
artifact: 'artifact.md',
|
|
evidence: 'verified',
|
|
gap: '',
|
|
id: 'release',
|
|
requirement: 'Release is approved',
|
|
status: 'complete'
|
|
}
|
|
],
|
|
top_actions: []
|
|
};
|
|
|
|
const text = renderText(report);
|
|
assert.ok(text.includes('objective ready'));
|
|
assert.ok(text.includes('Commit: abc123'));
|
|
assert.ok(text.includes(' none'));
|
|
|
|
const markdown = renderMarkdown(report);
|
|
assert.ok(markdown.includes('Status: objective ready'));
|
|
assert.ok(markdown.includes('| PR queue | Current | 1 open PRs across tracked repos |'));
|
|
assert.ok(markdown.includes('| Publication | Ready |'));
|
|
assert.ok(markdown.includes('- none'));
|
|
})) passed++; else failed++;
|
|
|
|
if (test('exit-code mode fails closed while macro objective has gaps', () => {
|
|
const rootDir = createTempDir('operator-dashboard-exit-');
|
|
|
|
try {
|
|
seedRepo(rootDir);
|
|
const result = runProcess([
|
|
'--json',
|
|
'--skip-github',
|
|
`--root=${rootDir}`,
|
|
'--generated-at=2026-05-15T00:00:00.000Z',
|
|
'--exit-code'
|
|
], { cwd: rootDir });
|
|
|
|
assert.strictEqual(result.status, 2);
|
|
assert.strictEqual(result.stderr, '');
|
|
assert.ok(result.stdout.includes('"ready": false'));
|
|
assert.ok(result.stdout.includes('"publicationReady": false'));
|
|
} finally {
|
|
cleanup(rootDir);
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
if (test('cli help exits successfully and invalid cli flags fail before reporting', () => {
|
|
const help = runProcess(['--help']);
|
|
assert.strictEqual(help.status, 0);
|
|
assert.strictEqual(help.stderr, '');
|
|
assert.ok(help.stdout.includes('Usage: node scripts/operator-readiness-dashboard.js'));
|
|
assert.ok(help.stdout.includes('--write <path>'));
|
|
|
|
const invalid = runProcess(['--format=xml']);
|
|
assert.strictEqual(invalid.status, 1);
|
|
assert.strictEqual(invalid.stdout, '');
|
|
assert.match(invalid.stderr, /Error: Invalid format/);
|
|
})) passed++; else failed++;
|
|
|
|
console.log(`\nPassed: ${passed}`);
|
|
console.log(`Failed: ${failed}`);
|
|
|
|
if (failed > 0) {
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
if (require.main === module) {
|
|
runTests();
|
|
}
|