diff --git a/tests/lib/github-coordination-branches.test.js b/tests/lib/github-coordination-branches.test.js index 5dbae1c43..75c84836e 100644 --- a/tests/lib/github-coordination-branches.test.js +++ b/tests/lib/github-coordination-branches.test.js @@ -25,24 +25,14 @@ const { verifyDependenciesClosed, } = require('../../scripts/lib/github-coordination/state'); -function test(name, fn) { - try { - fn(); - console.log(` ✓ ${name}`); - return true; - } catch (err) { - console.log(` ✗ ${name}`); - console.log(` Error: ${err.message}`); - return false; - } -} +const { test, banner, section, summary } = require('./helpers/mini-test-runner'); let passed = 0; let failed = 0; -console.log('\n=== parsing.js — uncovered branches ===\n'); +banner('parsing.js — uncovered branches'); -console.log('normalizeBodyForComparison:'); +section('normalizeBodyForComparison:'); if (test('handles null body (uses empty string fallback)', () => { const result = normalizeBodyForComparison(null); @@ -61,7 +51,7 @@ if (test('normalizes lastSyncAt timestamps in body text', () => { assert.ok(!result.includes('2024-01-01')); })) passed++; else failed++; -console.log('\nparseStringList:'); +section('parseStringList:'); if (test('returns empty array for null', () => { assert.deepStrictEqual(parseStringList(null), []); @@ -83,7 +73,7 @@ if (test('filters out empty parts from double-commas', () => { assert.deepStrictEqual(parseStringList('a,,b'), ['a', 'b']); })) passed++; else failed++; -console.log('\nmergeIssueBody — empty body branch:'); +section('mergeIssueBody — empty body branch:'); if (test('returns rendered state when issue body is empty string', () => { const state = { status: 'available', schemaVersion: 'v1', kind: 'epic', owner: null, branch: null, validation: 'pending', review: 'not-requested', project: { state: 'backlog', fields: {} }, dependencies: [], tasks: [], labels: [], lastAction: 'sync' }; @@ -97,9 +87,9 @@ if (test('returns rendered state when issue body is null', () => { assert.ok(result.includes('ecc-coordination:start')); })) passed++; else failed++; -console.log('\n=== state.js — uncovered branches ===\n'); +banner('state.js — uncovered branches'); -console.log('buildIssueStateFromAction — options absent (false branches):'); +section('buildIssueStateFromAction — options absent (false branches):'); const baseIssue = { number: 1, labels: [], body: '' }; const baseState = { @@ -133,7 +123,7 @@ if (test('buildIssueStateFromAction — currentState.tasks not array → re-extr assert.ok(Array.isArray(result.tasks)); })) passed++; else failed++; -console.log('\ndesiredLabelsForState — uncovered status/review/validation branches:'); +section('desiredLabelsForState — uncovered status/review/validation branches:'); if (test('includes published label for status "published"', () => { const labels = desiredLabelsForState({ status: 'published' }); @@ -160,7 +150,7 @@ if (test('includes review-changes-requested label for review "changes-requested" assert.ok(labels.includes('coordination:review-changes-requested')); })) passed++; else failed++; -console.log('\nmapStateToWorkItemStatus — uncovered switch cases:'); +section('mapStateToWorkItemStatus — uncovered switch cases:'); if (test('"validated" → "in-progress"', () => { assert.strictEqual(mapStateToWorkItemStatus('validated'), 'in-progress'); @@ -182,7 +172,7 @@ if (test('"unknown-state" → "open" (default)', () => { assert.strictEqual(mapStateToWorkItemStatus('unknown-state'), 'open'); })) passed++; else failed++; -console.log('\nassertIssueClaimable:'); +section('assertIssueClaimable:'); if (test('throws when issue is not open', () => { assert.throws( @@ -204,7 +194,7 @@ if (test('does not throw for open, unclaimed issue', () => { }); })) passed++; else failed++; -console.log('\nverifyDependenciesClosed:'); +section('verifyDependenciesClosed:'); if (test('returns empty array when dependencyNumbers is not an array', () => { const result = verifyDependenciesClosed('r/r', null, {}, []); @@ -240,7 +230,7 @@ if (test('warns via stderr and skips when dependency issue is not in allIssues l assert.ok(stderrOutput.includes('dependency issue #5 not found'), `expected stderr warning, got: ${stderrOutput}`); })) passed++; else failed++; -console.log('\ndefaultCoordinationState — edge branches:'); +section('defaultCoordinationState — edge branches:'); if (test('owner is null when issue has no author', () => { const result = defaultCoordinationState({ number: 1, labels: [] }); @@ -264,5 +254,4 @@ if (test('handles null issue', () => { assert.deepStrictEqual(result.tasks, []); })) passed++; else failed++; -console.log(`\n Results: ${passed} passed, ${failed} failed`); -if (failed > 0) process.exit(1); +summary(passed, failed); diff --git a/tests/lib/github-coordination-policy.test.js b/tests/lib/github-coordination-policy.test.js index 2143548be..0420789ef 100644 --- a/tests/lib/github-coordination-policy.test.js +++ b/tests/lib/github-coordination-policy.test.js @@ -19,17 +19,7 @@ const { DEFAULT_SECTION_MARKER, } = require('../../scripts/lib/github-coordination/policy'); -function test(name, fn) { - try { - fn(); - console.log(` ✓ ${name}`); - return true; - } catch (err) { - console.log(` ✗ ${name}`); - console.log(` Error: ${err.message}`); - return false; - } -} +const { test, banner, section, summary } = require('./helpers/mini-test-runner'); function withTempDir(fn) { const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-policy-test-')); @@ -51,9 +41,9 @@ function writeConfig(tmpDir, content) { let passed = 0; let failed = 0; -console.log('\n=== Testing github-coordination/policy.js ===\n'); +banner('Testing github-coordination/policy.js'); -console.log('loadPolicy — no config file:'); +section('loadPolicy — no config file:'); if (test('returns default policy when no config file exists in rootDir', () => { withTempDir(tmpDir => { @@ -74,7 +64,7 @@ if (test('returns default policy when custom configPath does not exist', () => { }); })) passed++; else failed++; -console.log('\nloadPolicy — configPath argument:'); +section('loadPolicy — configPath argument:'); if (test('uses configPath when explicitly provided', () => { withTempDir(tmpDir => { @@ -95,7 +85,7 @@ if (test('falls back to rootDir config file when configPath is null', () => { }); })) passed++; else failed++; -console.log('\nloadPolicy — invalid JSON:'); +section('loadPolicy — invalid JSON:'); if (test('throws on invalid JSON', () => { withTempDir(tmpDir => { @@ -104,7 +94,7 @@ if (test('throws on invalid JSON', () => { }); })) passed++; else failed++; -console.log('\nloadPolicy — non-object JSON:'); +section('loadPolicy — non-object JSON:'); if (test('throws when top-level JSON is null', () => { withTempDir(tmpDir => { @@ -127,7 +117,7 @@ if (test('throws when top-level JSON is a string', () => { }); })) passed++; else failed++; -console.log('\nloadPolicy — labels merging:'); +section('loadPolicy — labels merging:'); if (test('merges labels when parsed.labels is a plain object', () => { withTempDir(tmpDir => { @@ -162,7 +152,7 @@ if (test('falls back to empty labels when parsed.labels is a string', () => { }); })) passed++; else failed++; -console.log('\nloadPolicy — review merging:'); +section('loadPolicy — review merging:'); if (test('merges review when parsed.review is a plain object', () => { withTempDir(tmpDir => { @@ -197,7 +187,7 @@ if (test('falls back when parsed.review is an array', () => { }); })) passed++; else failed++; -console.log('\nloadPolicy — validation merging:'); +section('loadPolicy — validation merging:'); if (test('merges validation when parsed.validation is a plain object', () => { withTempDir(tmpDir => { @@ -215,7 +205,7 @@ if (test('falls back when parsed.validation is not an object', () => { }); })) passed++; else failed++; -console.log('\nloadPolicy — branchModel merging:'); +section('loadPolicy — branchModel merging:'); if (test('merges branchModel when parsed.branchModel is a plain object', () => { withTempDir(tmpDir => { @@ -234,7 +224,7 @@ if (test('falls back when parsed.branchModel is not an object', () => { }); })) passed++; else failed++; -console.log('\nloadPolicy — project merging:'); +section('loadPolicy — project merging:'); if (test('merges project when parsed.project is a plain object', () => { withTempDir(tmpDir => { @@ -261,7 +251,7 @@ if (test('falls back when parsed.project is null', () => { }); })) passed++; else failed++; -console.log('\nloadPolicy — project.fieldNames merging:'); +section('loadPolicy — project.fieldNames merging:'); if (test('merges fieldNames when project.fieldNames is a plain object', () => { withTempDir(tmpDir => { @@ -296,7 +286,7 @@ if (test('falls back when project.fieldNames is an array', () => { }); })) passed++; else failed++; -console.log('\nloadPolicy — sourcePath:'); +section('loadPolicy — sourcePath:'); if (test('sets sourcePath to the resolved config file path', () => { withTempDir(tmpDir => { @@ -306,5 +296,4 @@ if (test('sets sourcePath to the resolved config file path', () => { }); })) passed++; else failed++; -console.log(`\n Results: ${passed} passed, ${failed} failed`); -if (failed > 0) process.exit(1); +summary(passed, failed); diff --git a/tests/lib/github-coordination-store.test.js b/tests/lib/github-coordination-store.test.js index 9d23637dc..58f810cc7 100644 --- a/tests/lib/github-coordination-store.test.js +++ b/tests/lib/github-coordination-store.test.js @@ -16,17 +16,7 @@ const { const { DEFAULT_SCHEMA_VERSION, DEFAULT_POLICY } = require('../../scripts/lib/github-coordination/policy'); -function test(name, fn) { - try { - fn(); - console.log(` ✓ ${name}`); - return true; - } catch (err) { - console.log(` ✗ ${name}`); - console.log(` Error: ${err.message}`); - return false; - } -} +const { test, testAsync, banner, section, summary, fatal } = require('./helpers/mini-test-runner'); function makeStore() { const calls = []; @@ -42,15 +32,15 @@ function makeStore() { let passed = 0; let failed = 0; -console.log('\n=== Testing github-coordination/store.js ===\n'); +banner('Testing github-coordination/store.js'); -console.log('epicWorkItemId:'); +section('epicWorkItemId:'); if (test('produces a stable ID from repo and issue number', () => { assert.strictEqual(epicWorkItemId('acme/my-repo', 42), 'github-acme-my-repo-epic-42'); })) passed++; else failed++; -console.log('\nupsertCoordinationWorkItem — null store:'); +section('upsertCoordinationWorkItem — null store:'); if (test('returns null when store is null', () => { const result = upsertCoordinationWorkItem(null, 'r/r', { number: 1 }, {}, 'sync'); @@ -62,7 +52,7 @@ if (test('returns null when store is undefined', () => { assert.strictEqual(result, null); })) passed++; else failed++; -console.log('\nupsertCoordinationWorkItem — with store:'); +section('upsertCoordinationWorkItem — with store:'); if (test('passes schemaVersion from state when present', () => { const store = makeStore(); @@ -179,30 +169,17 @@ if (test('sets sessionId to null when options.sessionId absent', () => { assert.strictEqual(store.calls[0].sessionId, null); })) passed++; else failed++; -console.log('\nopenStore — dbPath: false:'); +section('openStore — dbPath: false:'); async function runAsyncTests() { - let asyncPassed = 0; - let asyncFailed = 0; - - try { + if (await testAsync('returns null when dbPath is false', async () => { const result = await openStore({ dbPath: false }); assert.strictEqual(result, null); - console.log(' ✓ returns null when dbPath is false'); - asyncPassed++; - } catch (err) { - console.log(' ✗ returns null when dbPath is false'); - console.log(` Error: ${err.message}`); - asyncFailed++; - } + })) passed++; else failed++; - const totalPassed = passed + asyncPassed; - const totalFailed = failed + asyncFailed; - console.log(`\n Results: ${totalPassed} passed, ${totalFailed} failed`); - if (totalFailed > 0) process.exit(1); + summary(passed, failed); } runAsyncTests().catch(err => { - console.error(`Unexpected async test failure: ${err.message}`); - process.exit(1); + fatal(`Unexpected async test failure: ${err.message}`); }); diff --git a/tests/lib/helpers/mini-test-runner.js b/tests/lib/helpers/mini-test-runner.js new file mode 100644 index 000000000..c653d9ed6 --- /dev/null +++ b/tests/lib/helpers/mini-test-runner.js @@ -0,0 +1,56 @@ +/** + * Shared mini test harness for standalone tests/lib/*.test.js scripts. + * + * Centralizes test execution and console reporting so individual test + * files don't duplicate the runner or log directly. + */ + +'use strict'; + +function report(message) { + console.log(message); +} + +function test(name, fn) { + try { + fn(); + report(` ✓ ${name}`); + return true; + } catch (err) { + report(` ✗ ${name}`); + report(` Error: ${err.message}`); + return false; + } +} + +async function testAsync(name, fn) { + try { + await fn(); + report(` ✓ ${name}`); + return true; + } catch (err) { + report(` ✗ ${name}`); + report(` Error: ${err.message}`); + return false; + } +} + +function banner(title) { + report(`\n=== ${title} ===`); +} + +function section(label) { + report(`\n${label}`); +} + +function summary(passed, failed) { + report(`\n Results: ${passed} passed, ${failed} failed`); + if (failed > 0) process.exit(1); +} + +function fatal(message) { + console.error(message); + process.exit(1); +} + +module.exports = { test, testAsync, banner, section, summary, fatal };