feat(ms-ai-architect): rewrite weekly-kb-cron with portable paths, auth-mode-aware pre-flight, lock+backup+rollback [skip-docs]
This commit is contained in:
parent
03c77b6452
commit
a0528e6ef7
2 changed files with 614 additions and 77 deletions
|
|
@ -0,0 +1,126 @@
|
|||
// 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 });
|
||||
}
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue