126 lines
4.5 KiB
JavaScript
126 lines
4.5 KiB
JavaScript
// tests/kb-update/test-weekly-kb-cron-flags.test.mjs
|
|
// Subprocess-based flag-parsing tests for scripts/kb-update/weekly-kb-cron.mjs
|
|
// (Step 9). Avoids real Claude spawn by exercising --dry-run + auth-failure
|
|
// fast-path. Full e2e is reserved for Wave 6 live-test.
|
|
//
|
|
// The cron writes its status file to <getCacheDir('ms-ai-architect')>, which
|
|
// on darwin resolves to $HOME/Library/Caches/ms-ai-architect/. Setting HOME
|
|
// in the subprocess env therefore points all path resolution at a tmp dir,
|
|
// keeping the test isolated from the real machine.
|
|
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { mkdtempSync, rmSync, existsSync, readFileSync } from 'node:fs';
|
|
import { tmpdir, platform as osPlatform } from 'node:os';
|
|
import { join, dirname } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const CRON = join(__dirname, '..', '..', 'scripts', 'kb-update', 'weekly-kb-cron.mjs');
|
|
|
|
function mkSandbox() {
|
|
return mkdtempSync(join(tmpdir(), 'cron-test-'));
|
|
}
|
|
|
|
function runCron(extraArgs, env = {}) {
|
|
return spawnSync('node', [CRON, ...extraArgs], {
|
|
env: { PATH: process.env.PATH, ...env },
|
|
encoding: 'utf8',
|
|
timeout: 30_000,
|
|
});
|
|
}
|
|
|
|
function statusFilePath(home) {
|
|
if (osPlatform() === 'darwin') {
|
|
return join(home, 'Library', 'Caches', 'ms-ai-architect', 'kb-update-status.json');
|
|
}
|
|
if (osPlatform() === 'win32') {
|
|
return join(home, 'AppData', 'Local', 'ms-ai-architect', 'Cache', 'kb-update-status.json');
|
|
}
|
|
return join(home, '.cache', 'ms-ai-architect', 'kb-update-status.json');
|
|
}
|
|
|
|
test('--dry-run exits 0 with dry-run status, no Claude spawn', () => {
|
|
const home = mkSandbox();
|
|
try {
|
|
const result = runCron(['--dry-run'], {
|
|
HOME: home,
|
|
ANTHROPIC_API_KEY: '',
|
|
CLAUDE_CODE_OAUTH_TOKEN: '',
|
|
});
|
|
assert.equal(result.status, 0, `stderr: ${result.stderr}\nstdout: ${result.stdout}`);
|
|
assert.match(result.stdout, /DRY RUN/i);
|
|
|
|
const sf = statusFilePath(home);
|
|
assert.equal(existsSync(sf), true, `status file missing at ${sf}`);
|
|
const status = JSON.parse(readFileSync(sf, 'utf8'));
|
|
assert.equal(status.schema_version, 1);
|
|
assert.equal(status.last_run_status, 'dry-run');
|
|
assert.equal(typeof status.last_run_ts, 'string');
|
|
assert.match(status.last_run_ts, /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
|
|
assert.equal(typeof status.auth_mode, 'string');
|
|
assert.equal(typeof status.log_file, 'string');
|
|
} finally {
|
|
rmSync(home, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('missing auth (no --dry-run) fails fast with auth-related error', () => {
|
|
const home = mkSandbox();
|
|
try {
|
|
const result = runCron([], {
|
|
HOME: home,
|
|
ANTHROPIC_API_KEY: '',
|
|
CLAUDE_CODE_OAUTH_TOKEN: '',
|
|
});
|
|
assert.notEqual(result.status, 0, 'cron should exit non-zero on missing auth');
|
|
const combined = (result.stdout || '') + '\n' + (result.stderr || '');
|
|
assert.match(
|
|
combined,
|
|
/not safe for cron|unauthenticated|EAUTHCRON|auth/i,
|
|
`expected auth error in output. stdout: ${result.stdout}\nstderr: ${result.stderr}`
|
|
);
|
|
} finally {
|
|
rmSync(home, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('--budget-usd flag parsed and reflected in dry-run plan', () => {
|
|
const home = mkSandbox();
|
|
try {
|
|
const result = runCron(['--dry-run', '--budget-usd=12.50'], {
|
|
HOME: home,
|
|
ANTHROPIC_API_KEY: '',
|
|
CLAUDE_CODE_OAUTH_TOKEN: '',
|
|
});
|
|
assert.equal(result.status, 0, `stderr: ${result.stderr}`);
|
|
assert.match(
|
|
result.stdout,
|
|
/(budget|Budget)[^\n]*12\.50|12\.5/,
|
|
`expected 12.50 in dry-run output: ${result.stdout}`
|
|
);
|
|
} finally {
|
|
rmSync(home, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('--dry-run writes status file even with no change-report present', () => {
|
|
const home = mkSandbox();
|
|
try {
|
|
const result = runCron(['--dry-run'], {
|
|
HOME: home,
|
|
ANTHROPIC_API_KEY: '',
|
|
CLAUDE_CODE_OAUTH_TOKEN: '',
|
|
});
|
|
assert.equal(result.status, 0);
|
|
const sf = statusFilePath(home);
|
|
const status = JSON.parse(readFileSync(sf, 'utf8'));
|
|
// Required fields per Status File Schema (plan.md L122-153)
|
|
for (const key of ['schema_version', 'last_run_status', 'last_run_ts', 'auth_mode', 'log_file', 'diagnostic']) {
|
|
assert.ok(Object.prototype.hasOwnProperty.call(status, key), `missing required field: ${key}`);
|
|
}
|
|
} finally {
|
|
rmSync(home, { recursive: true, force: true });
|
|
}
|
|
});
|