Files
everything-claude-code/tests/scripts/discussion-audit.test.js
2026-05-15 16:26:57 -04:00

259 lines
7.7 KiB
JavaScript

/**
* Tests for scripts/discussion-audit.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', 'discussion-audit.js');
const { DISCUSSION_QUERY } = require(path.join(__dirname, '..', '..', 'scripts', 'lib', 'github-discussions'));
function createTempDir(prefix) {
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
}
function cleanup(dirPath) {
fs.rmSync(dirPath, { recursive: true, force: true });
}
function discussionGhKey(owner, name, first = 100) {
return `api graphql -f owner=${owner} -f name=${name} -F first=${first} -f query=${DISCUSSION_QUERY}`;
}
function writeGhShim(rootDir, responses) {
const shimPath = path.join(rootDir, 'gh-shim.js');
fs.writeFileSync(shimPath, `
const responses = ${JSON.stringify(responses)};
const args = process.argv.slice(2);
const key = args.join(' ');
if (process.env.GITHUB_TOKEN) {
console.error('GITHUB_TOKEN should be unset by default');
process.exit(42);
}
if (!Object.prototype.hasOwnProperty.call(responses, key)) {
console.error('Unexpected gh args: ' + key);
process.exit(3);
}
process.stdout.write(JSON.stringify(responses[key]));
`);
return shimPath;
}
function run(args = [], options = {}) {
const env = {
...process.env,
...(options.env || {})
};
return execFileSync('node', [SCRIPT, ...args], {
cwd: options.cwd || path.join(__dirname, '..', '..'),
env,
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 10000
});
}
function runProcess(args = [], options = {}) {
const env = {
...process.env,
...(options.env || {})
};
return spawnSync('node', [SCRIPT, ...args], {
cwd: options.cwd || path.join(__dirname, '..', '..'),
env,
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 discussion-audit.js ===\n');
let passed = 0;
let failed = 0;
if (test('passes when discussions have maintainer touch and accepted answers', () => {
const rootDir = createTempDir('discussion-audit-pass-');
try {
const shimPath = writeGhShim(rootDir, {
[discussionGhKey('affaan-m', 'everything-claude-code')]: {
data: {
repository: {
hasDiscussionsEnabled: true,
discussions: {
totalCount: 2,
nodes: [
{
number: 1923,
title: 'Does Continuous Learning v2 work with VS Code Claude Code?',
url: 'https://github.com/example/discussions/1923',
updatedAt: '2026-05-15T19:08:52Z',
authorAssociation: 'NONE',
category: { name: 'Q&A', isAnswerable: true },
answer: { url: 'https://github.com/example/discussions/1923#discussioncomment-1', authorAssociation: 'OWNER' },
comments: { nodes: [] }
},
{
number: 73,
title: 'Compacting during workflow',
url: 'https://github.com/example/discussions/73',
updatedAt: '2026-05-15T00:00:00Z',
authorAssociation: 'NONE',
category: { name: 'General', isAnswerable: false },
answer: null,
comments: { nodes: [{ authorAssociation: 'MEMBER' }] }
}
]
}
}
}
}
});
const parsed = JSON.parse(run([
'--json',
'--repo',
'affaan-m/everything-claude-code'
], {
cwd: rootDir,
env: {
ECC_GH_SHIM: shimPath,
GITHUB_TOKEN: 'must-be-removed'
}
}));
assert.strictEqual(parsed.ready, true);
assert.strictEqual(parsed.totals.needingMaintainerTouch, 0);
assert.strictEqual(parsed.totals.missingAcceptedAnswer, 0);
assert.ok(parsed.checks.some(check => check.id === 'discussion-accepted-answers' && check.status === 'pass'));
} finally {
cleanup(rootDir);
}
})) passed++; else failed++;
if (test('fails when Q&A lacks accepted answer and maintainer touch', () => {
const rootDir = createTempDir('discussion-audit-fail-');
try {
const shimPath = writeGhShim(rootDir, {
[discussionGhKey('affaan-m', 'everything-claude-code')]: {
data: {
repository: {
hasDiscussionsEnabled: true,
discussions: {
totalCount: 1,
nodes: [
{
number: 1239,
title: 'Losing context',
url: 'https://github.com/example/discussions/1239',
updatedAt: '2026-05-15T00:00:00Z',
authorAssociation: 'NONE',
category: { name: 'Q&A', isAnswerable: true },
answer: null,
comments: { nodes: [] }
}
]
}
}
}
}
});
const result = runProcess([
'--json',
'--repo',
'affaan-m/everything-claude-code',
'--exit-code'
], {
cwd: rootDir,
env: { ECC_GH_SHIM: shimPath }
});
const parsed = JSON.parse(result.stdout);
assert.strictEqual(result.status, 2);
assert.strictEqual(parsed.ready, false);
assert.strictEqual(parsed.totals.needingMaintainerTouch, 1);
assert.strictEqual(parsed.totals.missingAcceptedAnswer, 1);
assert.ok(parsed.top_actions.some(action => action.id === 'discussion-maintainer-touch'));
assert.ok(parsed.top_actions.some(action => action.id === 'discussion-accepted-answers'));
} finally {
cleanup(rootDir);
}
})) passed++; else failed++;
if (test('writes markdown output as a durable operator artifact', () => {
const rootDir = createTempDir('discussion-audit-markdown-');
const outputPath = path.join(rootDir, 'artifacts', 'discussion-audit.md');
try {
const shimPath = writeGhShim(rootDir, {
[discussionGhKey('affaan-m', 'everything-claude-code')]: {
data: {
repository: {
hasDiscussionsEnabled: true,
discussions: { totalCount: 0, nodes: [] }
}
}
}
});
const stdout = run([
'--markdown',
'--write',
outputPath,
'--repo',
'affaan-m/everything-claude-code'
], {
cwd: rootDir,
env: { ECC_GH_SHIM: shimPath }
});
const written = fs.readFileSync(outputPath, 'utf8');
assert.strictEqual(stdout, written);
assert.ok(written.includes('# ECC Discussion Audit'));
assert.ok(written.includes('Answerable discussions missing accepted answer'));
assert.ok(written.includes('- none'));
} finally {
cleanup(rootDir);
}
})) passed++; else failed++;
if (test('cli help and invalid args exit cleanly', () => {
const help = runProcess(['--help']);
assert.strictEqual(help.status, 0);
assert.ok(help.stdout.includes('Usage: node scripts/discussion-audit.js'));
const invalid = runProcess(['--format', 'xml']);
assert.strictEqual(invalid.status, 1);
assert.ok(invalid.stderr.includes('Invalid format'));
})) passed++; else failed++;
console.log(`\nPassed: ${passed}`);
console.log(`Failed: ${failed}`);
if (failed > 0) {
process.exit(1);
}
}
runTests();