diff --git a/.opencode/tools/format-code.ts b/.opencode/tools/format-code.ts index b9e5244c1..5c2d96d54 100644 --- a/.opencode/tools/format-code.ts +++ b/.opencode/tools/format-code.ts @@ -107,7 +107,7 @@ function buildFormatterCommand(formatter: Formatter, filePath: string, cwd?: str // Normalize to forward slashes so the emitted command is identical on every // platform. `path.normalize` yields backslashes on Windows, which broke the // command string (and Windows CI); all formatter CLIs accept `/` on Windows. - const normalizedPath = path.normalize(filePath).split(path.sep).join("/") + const normalizedPath = path.normalize(filePath).replace(/\\/g, "/") // Build command based on formatter and platform const commands: Record = { diff --git a/tests/lib/github-coordination-branches.test.js b/tests/lib/github-coordination-branches.test.js new file mode 100644 index 000000000..5dbae1c43 --- /dev/null +++ b/tests/lib/github-coordination-branches.test.js @@ -0,0 +1,268 @@ +/** + * Targeted branch coverage tests for uncovered paths in: + * scripts/lib/github-coordination/parsing.js + * scripts/lib/github-coordination/state.js + * + * Run with: node tests/lib/github-coordination-branches.test.js + */ + +'use strict'; + +const assert = require('assert'); + +const { + normalizeBodyForComparison, + parseStringList, + mergeIssueBody, +} = require('../../scripts/lib/github-coordination/parsing'); + +const { + assertIssueClaimable, + buildIssueStateFromAction, + defaultCoordinationState, + desiredLabelsForState, + mapStateToWorkItemStatus, + 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; + } +} + +let passed = 0; +let failed = 0; + +console.log('\n=== parsing.js — uncovered branches ===\n'); + +console.log('normalizeBodyForComparison:'); + +if (test('handles null body (uses empty string fallback)', () => { + const result = normalizeBodyForComparison(null); + assert.strictEqual(result, ''); +})) passed++; else failed++; + +if (test('handles undefined body', () => { + const result = normalizeBodyForComparison(undefined); + assert.strictEqual(result, ''); +})) passed++; else failed++; + +if (test('normalizes lastSyncAt timestamps in body text', () => { + const body = 'before "lastSyncAt": "2024-01-01T00:00:00.000Z", after'; + const result = normalizeBodyForComparison(body); + assert.ok(result.includes('"lastSyncAt": NORMALIZED')); + assert.ok(!result.includes('2024-01-01')); +})) passed++; else failed++; + +console.log('\nparseStringList:'); + +if (test('returns empty array for null', () => { + assert.deepStrictEqual(parseStringList(null), []); +})) passed++; else failed++; + +if (test('returns empty array for undefined', () => { + assert.deepStrictEqual(parseStringList(undefined), []); +})) passed++; else failed++; + +if (test('returns empty array for empty string', () => { + assert.deepStrictEqual(parseStringList(''), []); +})) passed++; else failed++; + +if (test('splits a comma-separated string into trimmed parts', () => { + assert.deepStrictEqual(parseStringList('a, b , c'), ['a', 'b', 'c']); +})) passed++; else failed++; + +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:'); + +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' }; + const result = mergeIssueBody({ body: '' }, state); + assert.ok(result.includes('ecc-coordination:start')); +})) passed++; else failed++; + +if (test('returns rendered state when issue body is null', () => { + 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' }; + const result = mergeIssueBody({ body: null }, state); + assert.ok(result.includes('ecc-coordination:start')); +})) passed++; else failed++; + +console.log('\n=== state.js — uncovered branches ===\n'); + +console.log('buildIssueStateFromAction — options absent (false branches):'); + +const baseIssue = { number: 1, labels: [], body: '' }; +const baseState = { + schemaVersion: 'v1', kind: 'epic', status: 'available', owner: null, + branch: null, validation: 'pending', review: 'not-requested', + project: { state: 'backlog', fields: {} }, dependencies: [], tasks: [], + labels: [], lastAction: 'sync', lastActionAt: null, lastSyncAt: null, notes: null +}; + +if (test('buildIssueStateFromAction with no options — does not set owner/branch/etc', () => { + const result = buildIssueStateFromAction(baseIssue, baseState, 'sync'); + assert.strictEqual(result.lastAction, 'sync'); + assert.strictEqual(result.owner, null); + assert.strictEqual(result.branch, null); +})) passed++; else failed++; + +if (test('buildIssueStateFromAction with empty options — all conditional branches skip', () => { + const result = buildIssueStateFromAction(baseIssue, { ...baseState }, 'sync', {}); + assert.ok(typeof result.lastAction === 'string'); +})) passed++; else failed++; + +if (test('buildIssueStateFromAction — currentState.dependencies not array → re-extracted', () => { + const issue = { number: 1, labels: [], body: 'Depends on #5 and #6' }; + const result = buildIssueStateFromAction(issue, { ...baseState, dependencies: 'not-array' }, 'sync'); + assert.ok(Array.isArray(result.dependencies)); +})) passed++; else failed++; + +if (test('buildIssueStateFromAction — currentState.tasks not array → re-extracted', () => { + const issue = { number: 1, labels: [], body: '## Tasks\n- [ ] Step 1\n- [x] Step 2' }; + const result = buildIssueStateFromAction(issue, { ...baseState, tasks: 'not-array' }, 'sync'); + assert.ok(Array.isArray(result.tasks)); +})) passed++; else failed++; + +console.log('\ndesiredLabelsForState — uncovered status/review/validation branches:'); + +if (test('includes published label for status "published"', () => { + const labels = desiredLabelsForState({ status: 'published' }); + assert.ok(labels.includes('coordination:published')); +})) passed++; else failed++; + +if (test('includes validated label for validation "passed"', () => { + const labels = desiredLabelsForState({ status: 'available', validation: 'passed' }); + assert.ok(labels.includes('coordination:validated')); +})) passed++; else failed++; + +if (test('includes review-requested label for review "requested"', () => { + const labels = desiredLabelsForState({ status: 'available', review: 'requested' }); + assert.ok(labels.includes('coordination:review-requested')); +})) passed++; else failed++; + +if (test('includes review-approved label for review "approved"', () => { + const labels = desiredLabelsForState({ status: 'available', review: 'approved' }); + assert.ok(labels.includes('coordination:review-approved')); +})) passed++; else failed++; + +if (test('includes review-changes-requested label for review "changes-requested"', () => { + const labels = desiredLabelsForState({ status: 'available', review: 'changes-requested' }); + assert.ok(labels.includes('coordination:review-changes-requested')); +})) passed++; else failed++; + +console.log('\nmapStateToWorkItemStatus — uncovered switch cases:'); + +if (test('"validated" → "in-progress"', () => { + assert.strictEqual(mapStateToWorkItemStatus('validated'), 'in-progress'); +})) passed++; else failed++; + +if (test('"reviewing" → "in-progress"', () => { + assert.strictEqual(mapStateToWorkItemStatus('reviewing'), 'in-progress'); +})) passed++; else failed++; + +if (test('"changes-requested" → "needs-review"', () => { + assert.strictEqual(mapStateToWorkItemStatus('changes-requested'), 'needs-review'); +})) passed++; else failed++; + +if (test('"published" → "done"', () => { + assert.strictEqual(mapStateToWorkItemStatus('published'), 'done'); +})) passed++; else failed++; + +if (test('"unknown-state" → "open" (default)', () => { + assert.strictEqual(mapStateToWorkItemStatus('unknown-state'), 'open'); +})) passed++; else failed++; + +console.log('\nassertIssueClaimable:'); + +if (test('throws when issue is not open', () => { + assert.throws( + () => assertIssueClaimable({ number: 1, state: 'closed' }, { status: 'available' }), + /is not open/ + ); +})) passed++; else failed++; + +if (test('throws when issue is already claimed', () => { + assert.throws( + () => assertIssueClaimable({ number: 1, state: 'open' }, { status: 'claimed', owner: 'alice' }), + /already claimed/ + ); +})) passed++; else failed++; + +if (test('does not throw for open, unclaimed issue', () => { + assert.doesNotThrow(() => { + assertIssueClaimable({ number: 1, state: 'open' }, { status: 'available' }); + }); +})) passed++; else failed++; + +console.log('\nverifyDependenciesClosed:'); + +if (test('returns empty array when dependencyNumbers is not an array', () => { + const result = verifyDependenciesClosed('r/r', null, {}, []); + assert.deepStrictEqual(result, []); +})) passed++; else failed++; + +if (test('returns empty array when dependencyNumbers is empty', () => { + const result = verifyDependenciesClosed('r/r', [], {}, []); + assert.deepStrictEqual(result, []); +})) passed++; else failed++; + +if (test('returns closed issues when dependency is in closed state', () => { + const issues = [{ number: 5, state: 'closed' }, { number: 6, state: 'open' }]; + const result = verifyDependenciesClosed('r/r', [5, 6], {}, issues); + assert.deepStrictEqual(result, [5]); +})) passed++; else failed++; + +if (test('warns via stderr and skips when dependency issue is not in allIssues list', () => { + const issues = [{ number: 99, state: 'closed' }]; + const originalWrite = process.stderr.write; + let stderrOutput = ''; + process.stderr.write = (chunk) => { + stderrOutput += chunk; + return true; + }; + let result; + try { + result = verifyDependenciesClosed('r/r', [5], {}, issues); + } finally { + process.stderr.write = originalWrite; + } + assert.deepStrictEqual(result, []); + assert.ok(stderrOutput.includes('dependency issue #5 not found'), `expected stderr warning, got: ${stderrOutput}`); +})) passed++; else failed++; + +console.log('\ndefaultCoordinationState — edge branches:'); + +if (test('owner is null when issue has no author', () => { + const result = defaultCoordinationState({ number: 1, labels: [] }); + assert.strictEqual(result.owner, null); +})) passed++; else failed++; + +if (test('owner is null when issue.author has no login', () => { + const result = defaultCoordinationState({ number: 1, labels: [], author: {} }); + assert.strictEqual(result.owner, null); +})) passed++; else failed++; + +if (test('owner is set from issue.author.login', () => { + const result = defaultCoordinationState({ number: 1, labels: [], author: { login: 'alice' } }); + assert.strictEqual(result.owner, 'alice'); +})) passed++; else failed++; + +if (test('handles null issue', () => { + const result = defaultCoordinationState(null); + assert.strictEqual(result.owner, null); + assert.deepStrictEqual(result.dependencies, []); + assert.deepStrictEqual(result.tasks, []); +})) passed++; else failed++; + +console.log(`\n Results: ${passed} passed, ${failed} failed`); +if (failed > 0) process.exit(1); diff --git a/tests/lib/github-coordination-policy.test.js b/tests/lib/github-coordination-policy.test.js new file mode 100644 index 000000000..2143548be --- /dev/null +++ b/tests/lib/github-coordination-policy.test.js @@ -0,0 +1,310 @@ +/** + * Tests for scripts/lib/github-coordination/policy.js — loadPolicy branch coverage + * + * Run with: node tests/lib/github-coordination-policy.test.js + */ + +'use strict'; + +const assert = require('assert'); +const fs = require('fs'); +const os = require('os'); +const path = require('path'); + +const { + loadPolicy, + DEFAULT_POLICY, + DEFAULT_LABELS, + DEFAULT_SCHEMA_VERSION, + 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; + } +} + +function withTempDir(fn) { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-policy-test-')); + try { + fn(tmpDir); + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } +} + +function writeConfig(tmpDir, content) { + const configDir = path.join(tmpDir, 'config'); + fs.mkdirSync(configDir, { recursive: true }); + const configPath = path.join(configDir, 'github-native-coordination.json'); + fs.writeFileSync(configPath, typeof content === 'string' ? content : JSON.stringify(content)); + return configPath; +} + +let passed = 0; +let failed = 0; + +console.log('\n=== Testing github-coordination/policy.js ===\n'); + +console.log('loadPolicy — no config file:'); + +if (test('returns default policy when no config file exists in rootDir', () => { + withTempDir(tmpDir => { + const result = loadPolicy(tmpDir); + assert.strictEqual(result.sourcePath, null); + assert.strictEqual(result.schemaVersion, DEFAULT_SCHEMA_VERSION); + assert.strictEqual(result.sectionMarker, DEFAULT_SECTION_MARKER); + assert.deepStrictEqual(result.labels, DEFAULT_LABELS); + assert.deepStrictEqual(result.review, DEFAULT_POLICY.review); + }); +})) passed++; else failed++; + +if (test('returns default policy when custom configPath does not exist', () => { + withTempDir(tmpDir => { + const result = loadPolicy(tmpDir, path.join(tmpDir, 'nonexistent.json')); + assert.strictEqual(result.sourcePath, null); + assert.deepStrictEqual(result.review, DEFAULT_POLICY.review); + }); +})) passed++; else failed++; + +console.log('\nloadPolicy — configPath argument:'); + +if (test('uses configPath when explicitly provided', () => { + withTempDir(tmpDir => { + const configPath = path.join(tmpDir, 'my-policy.json'); + fs.writeFileSync(configPath, JSON.stringify({ schemaVersion: 'custom-v1' })); + const result = loadPolicy(tmpDir, configPath); + assert.strictEqual(result.sourcePath, configPath); + assert.strictEqual(result.schemaVersion, 'custom-v1'); + }); +})) passed++; else failed++; + +if (test('falls back to rootDir config file when configPath is null', () => { + withTempDir(tmpDir => { + writeConfig(tmpDir, { schemaVersion: 'root-v1' }); + const result = loadPolicy(tmpDir, null); + assert.strictEqual(result.schemaVersion, 'root-v1'); + assert.ok(result.sourcePath !== null); + }); +})) passed++; else failed++; + +console.log('\nloadPolicy — invalid JSON:'); + +if (test('throws on invalid JSON', () => { + withTempDir(tmpDir => { + writeConfig(tmpDir, '{ bad json !!!! }'); + assert.throws(() => loadPolicy(tmpDir), /Failed to load policy/); + }); +})) passed++; else failed++; + +console.log('\nloadPolicy — non-object JSON:'); + +if (test('throws when top-level JSON is null', () => { + withTempDir(tmpDir => { + writeConfig(tmpDir, 'null'); + assert.throws(() => loadPolicy(tmpDir), /must contain a JSON object/); + }); +})) passed++; else failed++; + +if (test('throws when top-level JSON is an array', () => { + withTempDir(tmpDir => { + writeConfig(tmpDir, '[]'); + assert.throws(() => loadPolicy(tmpDir), /must contain a JSON object/); + }); +})) passed++; else failed++; + +if (test('throws when top-level JSON is a string', () => { + withTempDir(tmpDir => { + writeConfig(tmpDir, '"just a string"'); + assert.throws(() => loadPolicy(tmpDir), /must contain a JSON object/); + }); +})) passed++; else failed++; + +console.log('\nloadPolicy — labels merging:'); + +if (test('merges labels when parsed.labels is a plain object', () => { + withTempDir(tmpDir => { + writeConfig(tmpDir, { labels: { epic: 'my-epic' } }); + const result = loadPolicy(tmpDir); + assert.strictEqual(result.labels.epic, 'my-epic'); + assert.strictEqual(result.labels.available, DEFAULT_LABELS.available); + }); +})) passed++; else failed++; + +if (test('falls back to empty labels when parsed.labels is null', () => { + withTempDir(tmpDir => { + writeConfig(tmpDir, { labels: null }); + const result = loadPolicy(tmpDir); + assert.deepStrictEqual(result.labels, DEFAULT_LABELS); + }); +})) passed++; else failed++; + +if (test('falls back to empty labels when parsed.labels is an array', () => { + withTempDir(tmpDir => { + writeConfig(tmpDir, { labels: ['a', 'b'] }); + const result = loadPolicy(tmpDir); + assert.deepStrictEqual(result.labels, DEFAULT_LABELS); + }); +})) passed++; else failed++; + +if (test('falls back to empty labels when parsed.labels is a string', () => { + withTempDir(tmpDir => { + writeConfig(tmpDir, { labels: 'bad' }); + const result = loadPolicy(tmpDir); + assert.deepStrictEqual(result.labels, DEFAULT_LABELS); + }); +})) passed++; else failed++; + +console.log('\nloadPolicy — review merging:'); + +if (test('merges review when parsed.review is a plain object', () => { + withTempDir(tmpDir => { + writeConfig(tmpDir, { review: { required: false } }); + const result = loadPolicy(tmpDir); + assert.strictEqual(result.review.required, false); + assert.strictEqual(result.review.defaultMode, DEFAULT_POLICY.review.defaultMode); + }); +})) passed++; else failed++; + +if (test('falls back when parsed.review is not an object', () => { + withTempDir(tmpDir => { + writeConfig(tmpDir, { review: 'string' }); + const result = loadPolicy(tmpDir); + assert.deepStrictEqual(result.review, DEFAULT_POLICY.review); + }); +})) passed++; else failed++; + +if (test('falls back when parsed.review is null', () => { + withTempDir(tmpDir => { + writeConfig(tmpDir, { review: null }); + const result = loadPolicy(tmpDir); + assert.deepStrictEqual(result.review, DEFAULT_POLICY.review); + }); +})) passed++; else failed++; + +if (test('falls back when parsed.review is an array', () => { + withTempDir(tmpDir => { + writeConfig(tmpDir, { review: [] }); + const result = loadPolicy(tmpDir); + assert.deepStrictEqual(result.review, DEFAULT_POLICY.review); + }); +})) passed++; else failed++; + +console.log('\nloadPolicy — validation merging:'); + +if (test('merges validation when parsed.validation is a plain object', () => { + withTempDir(tmpDir => { + writeConfig(tmpDir, { validation: { required: false } }); + const result = loadPolicy(tmpDir); + assert.strictEqual(result.validation.required, false); + }); +})) passed++; else failed++; + +if (test('falls back when parsed.validation is not an object', () => { + withTempDir(tmpDir => { + writeConfig(tmpDir, { validation: 42 }); + const result = loadPolicy(tmpDir); + assert.deepStrictEqual(result.validation, DEFAULT_POLICY.validation); + }); +})) passed++; else failed++; + +console.log('\nloadPolicy — branchModel merging:'); + +if (test('merges branchModel when parsed.branchModel is a plain object', () => { + withTempDir(tmpDir => { + writeConfig(tmpDir, { branchModel: { epicOnly: false, taskBranches: true } }); + const result = loadPolicy(tmpDir); + assert.strictEqual(result.branchModel.epicOnly, false); + assert.strictEqual(result.branchModel.taskBranches, true); + }); +})) passed++; else failed++; + +if (test('falls back when parsed.branchModel is not an object', () => { + withTempDir(tmpDir => { + writeConfig(tmpDir, { branchModel: true }); + const result = loadPolicy(tmpDir); + assert.deepStrictEqual(result.branchModel, DEFAULT_POLICY.branchModel); + }); +})) passed++; else failed++; + +console.log('\nloadPolicy — project merging:'); + +if (test('merges project when parsed.project is a plain object', () => { + withTempDir(tmpDir => { + writeConfig(tmpDir, { project: { enabled: true } }); + const result = loadPolicy(tmpDir); + assert.strictEqual(result.project.enabled, true); + assert.deepStrictEqual(result.project.fieldNames, DEFAULT_POLICY.project.fieldNames); + }); +})) passed++; else failed++; + +if (test('falls back when parsed.project is not an object', () => { + withTempDir(tmpDir => { + writeConfig(tmpDir, { project: 'invalid' }); + const result = loadPolicy(tmpDir); + assert.deepStrictEqual(result.project, DEFAULT_POLICY.project); + }); +})) passed++; else failed++; + +if (test('falls back when parsed.project is null', () => { + withTempDir(tmpDir => { + writeConfig(tmpDir, { project: null }); + const result = loadPolicy(tmpDir); + assert.deepStrictEqual(result.project, DEFAULT_POLICY.project); + }); +})) passed++; else failed++; + +console.log('\nloadPolicy — project.fieldNames merging:'); + +if (test('merges fieldNames when project.fieldNames is a plain object', () => { + withTempDir(tmpDir => { + writeConfig(tmpDir, { project: { enabled: true, fieldNames: { status: 'MyStatus' } } }); + const result = loadPolicy(tmpDir); + assert.strictEqual(result.project.fieldNames.status, 'MyStatus'); + assert.strictEqual(result.project.fieldNames.owner, DEFAULT_POLICY.project.fieldNames.owner); + }); +})) passed++; else failed++; + +if (test('falls back when project.fieldNames is not an object', () => { + withTempDir(tmpDir => { + writeConfig(tmpDir, { project: { fieldNames: 'bad' } }); + const result = loadPolicy(tmpDir); + assert.deepStrictEqual(result.project.fieldNames, DEFAULT_POLICY.project.fieldNames); + }); +})) passed++; else failed++; + +if (test('falls back when project.fieldNames is null', () => { + withTempDir(tmpDir => { + writeConfig(tmpDir, { project: { fieldNames: null } }); + const result = loadPolicy(tmpDir); + assert.deepStrictEqual(result.project.fieldNames, DEFAULT_POLICY.project.fieldNames); + }); +})) passed++; else failed++; + +if (test('falls back when project.fieldNames is an array', () => { + withTempDir(tmpDir => { + writeConfig(tmpDir, { project: { fieldNames: [] } }); + const result = loadPolicy(tmpDir); + assert.deepStrictEqual(result.project.fieldNames, DEFAULT_POLICY.project.fieldNames); + }); +})) passed++; else failed++; + +console.log('\nloadPolicy — sourcePath:'); + +if (test('sets sourcePath to the resolved config file path', () => { + withTempDir(tmpDir => { + const configPath = writeConfig(tmpDir, {}); + const result = loadPolicy(tmpDir); + assert.strictEqual(result.sourcePath, configPath); + }); +})) passed++; else failed++; + +console.log(`\n Results: ${passed} passed, ${failed} failed`); +if (failed > 0) process.exit(1); diff --git a/tests/lib/github-coordination-store.test.js b/tests/lib/github-coordination-store.test.js new file mode 100644 index 000000000..9d23637dc --- /dev/null +++ b/tests/lib/github-coordination-store.test.js @@ -0,0 +1,208 @@ +/** + * Tests for scripts/lib/github-coordination/store.js — branch coverage + * + * Run with: node tests/lib/github-coordination-store.test.js + */ + +'use strict'; + +const assert = require('assert'); + +const { + epicWorkItemId, + upsertCoordinationWorkItem, + openStore, +} = require('../../scripts/lib/github-coordination/store'); + +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; + } +} + +function makeStore() { + const calls = []; + return { + calls, + upsertWorkItem(item) { + calls.push(item); + return item; + }, + }; +} + +let passed = 0; +let failed = 0; + +console.log('\n=== Testing github-coordination/store.js ===\n'); + +console.log('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:'); + +if (test('returns null when store is null', () => { + const result = upsertCoordinationWorkItem(null, 'r/r', { number: 1 }, {}, 'sync'); + assert.strictEqual(result, null); +})) passed++; else failed++; + +if (test('returns null when store is undefined', () => { + const result = upsertCoordinationWorkItem(undefined, 'r/r', { number: 1 }, {}, 'sync'); + assert.strictEqual(result, null); +})) passed++; else failed++; + +console.log('\nupsertCoordinationWorkItem — with store:'); + +if (test('passes schemaVersion from state when present', () => { + const store = makeStore(); + upsertCoordinationWorkItem(store, 'a/b', { number: 1, labels: [] }, { schemaVersion: 'v99', status: 'available' }, 'sync'); + assert.strictEqual(store.calls[0].metadata.schemaVersion, 'v99'); +})) passed++; else failed++; + +if (test('uses DEFAULT_SCHEMA_VERSION when state.schemaVersion is absent', () => { + const store = makeStore(); + upsertCoordinationWorkItem(store, 'a/b', { number: 1, labels: [] }, { status: 'available' }, 'sync'); + assert.strictEqual(store.calls[0].metadata.schemaVersion, DEFAULT_SCHEMA_VERSION); +})) passed++; else failed++; + +if (test('sets issueUrl from issue.url when present', () => { + const store = makeStore(); + upsertCoordinationWorkItem(store, 'a/b', { number: 1, url: 'https://example.com/1', labels: [] }, { status: 'available' }, 'sync'); + assert.strictEqual(store.calls[0].metadata.issueUrl, 'https://example.com/1'); +})) passed++; else failed++; + +if (test('sets issueUrl to null when issue.url is absent', () => { + const store = makeStore(); + upsertCoordinationWorkItem(store, 'a/b', { number: 1, labels: [] }, { status: 'available' }, 'sync'); + assert.strictEqual(store.calls[0].metadata.issueUrl, null); +})) passed++; else failed++; + +if (test('sets issueTitle from issue.title when present', () => { + const store = makeStore(); + upsertCoordinationWorkItem(store, 'a/b', { number: 1, title: 'My Epic', labels: [] }, { status: 'available' }, 'sync'); + assert.strictEqual(store.calls[0].metadata.issueTitle, 'My Epic'); +})) passed++; else failed++; + +if (test('sets issueTitle to null when issue.title is absent', () => { + const store = makeStore(); + upsertCoordinationWorkItem(store, 'a/b', { number: 1, labels: [] }, { status: 'available' }, 'sync'); + assert.strictEqual(store.calls[0].metadata.issueTitle, null); +})) passed++; else failed++; + +if (test('uses custom policy from options.policy', () => { + const store = makeStore(); + const customPolicy = { schemaVersion: 'custom', labels: {}, review: {}, validation: {}, branchModel: {}, project: { enabled: true, fieldNames: {} } }; + upsertCoordinationWorkItem(store, 'a/b', { number: 1, labels: [] }, { status: 'available' }, 'sync', { policy: customPolicy }); + assert.strictEqual(store.calls[0].metadata.projectProjection.enabled, true); +})) passed++; else failed++; + +if (test('falls back to DEFAULT_POLICY when options.policy is absent', () => { + const store = makeStore(); + upsertCoordinationWorkItem(store, 'a/b', { number: 1, labels: [] }, { status: 'available' }, 'sync', {}); + assert.strictEqual(store.calls[0].metadata.projectProjection.enabled, DEFAULT_POLICY.project.enabled); +})) passed++; else failed++; + +if (test('sets priority high when state.status is blocked', () => { + const store = makeStore(); + upsertCoordinationWorkItem(store, 'a/b', { number: 1, labels: [] }, { status: 'blocked' }, 'sync'); + assert.strictEqual(store.calls[0].priority, 'high'); +})) passed++; else failed++; + +if (test('sets priority normal when state.status is not blocked', () => { + const store = makeStore(); + upsertCoordinationWorkItem(store, 'a/b', { number: 1, labels: [] }, { status: 'available' }, 'sync'); + assert.strictEqual(store.calls[0].priority, 'normal'); +})) passed++; else failed++; + +if (test('sets url from issue.url in upsertWorkItem call', () => { + const store = makeStore(); + upsertCoordinationWorkItem(store, 'a/b', { number: 1, url: 'https://gh/1', labels: [] }, { status: 'available' }, 'sync'); + assert.strictEqual(store.calls[0].url, 'https://gh/1'); +})) passed++; else failed++; + +if (test('sets url to null when issue.url absent in upsertWorkItem call', () => { + const store = makeStore(); + upsertCoordinationWorkItem(store, 'a/b', { number: 1, labels: [] }, { status: 'available' }, 'sync'); + assert.strictEqual(store.calls[0].url, null); +})) passed++; else failed++; + +if (test('uses state.owner when present', () => { + const store = makeStore(); + upsertCoordinationWorkItem(store, 'a/b', { number: 1, labels: [] }, { status: 'available', owner: 'alice' }, 'sync'); + assert.strictEqual(store.calls[0].owner, 'alice'); +})) passed++; else failed++; + +if (test('falls back to issue.author.login when state.owner absent', () => { + const store = makeStore(); + upsertCoordinationWorkItem(store, 'a/b', { number: 1, labels: [], author: { login: 'bob' } }, { status: 'available' }, 'sync'); + assert.strictEqual(store.calls[0].owner, 'bob'); +})) passed++; else failed++; + +if (test('sets owner to null when neither state.owner nor author.login present', () => { + const store = makeStore(); + upsertCoordinationWorkItem(store, 'a/b', { number: 1, labels: [] }, { status: 'available' }, 'sync'); + assert.strictEqual(store.calls[0].owner, null); +})) passed++; else failed++; + +if (test('uses options.repoRoot when present', () => { + const store = makeStore(); + upsertCoordinationWorkItem(store, 'a/b', { number: 1, labels: [] }, { status: 'available' }, 'sync', { repoRoot: '/my/repo' }); + assert.strictEqual(store.calls[0].repoRoot, '/my/repo'); +})) passed++; else failed++; + +if (test('falls back to process.cwd() when options.repoRoot absent', () => { + const store = makeStore(); + upsertCoordinationWorkItem(store, 'a/b', { number: 1, labels: [] }, { status: 'available' }, 'sync'); + assert.strictEqual(store.calls[0].repoRoot, process.cwd()); +})) passed++; else failed++; + +if (test('uses options.sessionId when present', () => { + const store = makeStore(); + upsertCoordinationWorkItem(store, 'a/b', { number: 1, labels: [] }, { status: 'available' }, 'sync', { sessionId: 'sess-1' }); + assert.strictEqual(store.calls[0].sessionId, 'sess-1'); +})) passed++; else failed++; + +if (test('sets sessionId to null when options.sessionId absent', () => { + const store = makeStore(); + upsertCoordinationWorkItem(store, 'a/b', { number: 1, labels: [] }, { status: 'available' }, 'sync'); + assert.strictEqual(store.calls[0].sessionId, null); +})) passed++; else failed++; + +console.log('\nopenStore — dbPath: false:'); + +async function runAsyncTests() { + let asyncPassed = 0; + let asyncFailed = 0; + + try { + 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++; + } + + const totalPassed = passed + asyncPassed; + const totalFailed = failed + asyncFailed; + console.log(`\n Results: ${totalPassed} passed, ${totalFailed} failed`); + if (totalFailed > 0) process.exit(1); +} + +runAsyncTests().catch(err => { + console.error(`Unexpected async test failure: ${err.message}`); + process.exit(1); +}); diff --git a/tests/opencode-tools.test.js b/tests/opencode-tools.test.js index 297def9ff..a09f067dc 100644 --- a/tests/opencode-tools.test.js +++ b/tests/opencode-tools.test.js @@ -108,6 +108,24 @@ async function main() { ), ]) + tests.push([ + "format-code: normalizes Windows backslash paths to forward slashes", + async () => withTempProject( + ["tsconfig.json", "src/index.ts"], + async (projectDir) => { + const context = createMockContext(projectDir) + const result = await tools.formatcode.execute( + { filePath: "src\\index.ts" }, + context + ) + const parsed = JSON.parse(result) + assert.strictEqual(parsed.success, true) + assert.ok(parsed.command.includes("src/index.ts"), `expected forward slashes in command: ${parsed.command}`) + assert.ok(!parsed.command.includes("src\\index.ts"), `unexpected backslashes in command: ${parsed.command}`) + } + ), + ]) + tests.push([ "format-code: detects Python formatter", async () => withTempProject(