/** * 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 #89 #78-#89', 'AgentShield Enterprise Iteration', 'ECC-Tools PR #76', '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 ')); 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(); }