mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-11-06 12:51:54 +00:00
refactor(lock): Consolidate utilities and fix circular dependencies
Co-authored-by: PR0M3TH3AN <[email protected]>
This commit is contained in:
1 parent
244dbe1511
commit
e7b858d84a
330 files changed
+34090
-3976
No files matched your search
@@ -2,7 +2,8 @@ import { test, after } from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { cmdInit, cmdRemove } from '../src/ops.mjs';
|
||||
import { cmdInit } from '../src/ops.mjs';
|
||||
import { cmdRemove } from '../src/cmd-remove.mjs';
|
||||
|
||||
const MOCK_CONFIG = {
|
||||
installDir: 'torch',
|
||||
@@ -0,0 +1,6 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
test('fail fixture stays red', () => {
|
||||
assert.equal(1, 2);
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
test('pass fixture stays green', () => {
|
||||
assert.equal(2 + 2, 4);
|
||||
});
|
||||
@@ -0,0 +1,111 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { execSync } from 'node:child_process';
|
||||
import os from 'node:os';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
// Calculate paths relative to this file
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = path.resolve(__dirname, '..');
|
||||
const governanceServicePath = path.resolve(repoRoot, 'src/services/governance/index.js');
|
||||
|
||||
async function runTest() {
|
||||
// Create temp dir
|
||||
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'torch-gov-test-'));
|
||||
console.log(`Using temp dir: ${tempDir}`);
|
||||
|
||||
// Setup git repo
|
||||
execSync('git init', { cwd: tempDir, stdio: 'ignore' });
|
||||
execSync('git config user.name "Test User"', { cwd: tempDir, stdio: 'ignore' });
|
||||
execSync('git config user.email "[email protected]"', { cwd: tempDir, stdio: 'ignore' });
|
||||
|
||||
// Create required directory structure
|
||||
await fs.mkdir(path.join(tempDir, 'src/prompts/daily'), { recursive: true });
|
||||
await fs.mkdir(path.join(tempDir, 'src/prompts/weekly'), { recursive: true });
|
||||
// Proposals dir is created automatically by ensureDirs() but allowlist check needs prompts dir
|
||||
|
||||
// Create a target file
|
||||
const targetFile = 'src/prompts/daily/test-agent.md';
|
||||
const absoluteTarget = path.join(tempDir, targetFile);
|
||||
await fs.writeFile(absoluteTarget, 'Original Content');
|
||||
|
||||
// Commit initial state so we have a HEAD
|
||||
execSync(`git add ${targetFile}`, { cwd: tempDir });
|
||||
execSync('git commit -m "Initial commit"', { cwd: tempDir });
|
||||
|
||||
// Switch cwd to tempDir
|
||||
const originalCwd = process.cwd();
|
||||
try {
|
||||
process.chdir(tempDir);
|
||||
console.log(`Changed cwd to: ${process.cwd()}`);
|
||||
|
||||
// Import governance service dynamically so it picks up the new cwd
|
||||
const governance = await import(governanceServicePath);
|
||||
|
||||
// Malicious payload
|
||||
const maliciousPayload = 'attacker"; touch /tmp/pwned_check; echo "';
|
||||
// const proposalId = `test-proposal-${Date.now()}`; // unused
|
||||
|
||||
// Manually create proposal metadata and files because createProposal also uses git diff
|
||||
// We can use createProposal from the service, but we need to pass the malicious agent name there.
|
||||
// Let's try using createProposal first.
|
||||
|
||||
const proposalContent = `Shared contract (required):\nRequired startup + artifacts + memory + issue capture\n\nNew Content`;
|
||||
|
||||
console.log('Creating proposal...');
|
||||
const { id } = await governance.createProposal({
|
||||
agent: maliciousPayload,
|
||||
target: targetFile,
|
||||
newContent: proposalContent,
|
||||
reason: 'Testing security'
|
||||
});
|
||||
|
||||
console.log(`Proposal created with ID: ${id}`);
|
||||
|
||||
// Verify metadata has the malicious author
|
||||
const metaPath = path.join(tempDir, 'src/proposals', id, 'meta.json');
|
||||
const meta = JSON.parse(await fs.readFile(metaPath, 'utf8'));
|
||||
if (meta.author !== maliciousPayload) {
|
||||
throw new Error('Meta author does not match malicious payload');
|
||||
}
|
||||
|
||||
// Apply proposal
|
||||
console.log('Applying proposal...');
|
||||
await governance.applyProposal(id);
|
||||
console.log('Proposal applied.');
|
||||
|
||||
// Verification 1: Check if /tmp/pwned_check exists
|
||||
try {
|
||||
await fs.access('/tmp/pwned_check');
|
||||
console.error('VULNERABILITY DETECTED: /tmp/pwned_check exists!');
|
||||
process.exit(1);
|
||||
} catch {
|
||||
console.log('Security check passed: /tmp/pwned_check does not exist.');
|
||||
}
|
||||
|
||||
// Verification 2: Check git log for commit message
|
||||
const log = execSync('git log -1 --pretty=%B', { cwd: tempDir }).toString();
|
||||
console.log(`Commit message: ${log}`);
|
||||
|
||||
if (!log.includes(maliciousPayload)) {
|
||||
console.error('Verification failed: Commit message does not contain the payload literally.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Check if the commit message looks "broken" (e.g. if newlines were injected)
|
||||
// Here we just want to ensure the whole thing is in the message.
|
||||
console.log('Verification passed: Commit message contains payload.');
|
||||
|
||||
} finally {
|
||||
// Cleanup
|
||||
process.chdir(originalCwd);
|
||||
await fs.rm(tempDir, { recursive: true, force: true });
|
||||
// Also remove /tmp/pwned_check if it exists (it shouldn't)
|
||||
try { await fs.unlink('/tmp/pwned_check'); } catch { /* ignore */ }
|
||||
}
|
||||
}
|
||||
|
||||
runTest().catch(err => {
|
||||
console.error('Test failed:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import { test } from 'node:test';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import assert from 'node:assert';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const implPath = path.join(__dirname, 'governance-security-impl.mjs');
|
||||
|
||||
test('Governance Service Security: Command Injection Prevention', (_t) => {
|
||||
// We run the implementation in a separate process to ensure process.cwd() isolation
|
||||
// and to verify that the dynamic import picks up the correct paths.
|
||||
try {
|
||||
const output = execFileSync(process.execPath, [implPath], { encoding: 'utf8' });
|
||||
// If we reach here, the process exited with code 0 (success)
|
||||
// We can optionally check the output for specific messages if needed,
|
||||
// but the implementation script asserts internally.
|
||||
assert.ok(output.includes('Verification passed: Commit message contains payload.'), 'Output should confirm verification passed');
|
||||
} catch (err) {
|
||||
console.error('Test implementation failed.');
|
||||
if (err.stdout) console.error('STDOUT:', err.stdout);
|
||||
if (err.stderr) console.error('STDERR:', err.stderr);
|
||||
assert.fail(`Security test failed with exit code ${err.status}: ${err.message}`);
|
||||
}
|
||||
});
|
||||
@@ -96,6 +96,24 @@ describe('src/lib.mjs', () => {
|
||||
// unknown agents are not in roster, so available should be full roster (since no roster agent is locked)
|
||||
assert.deepStrictEqual(result.available, mockRoster.sort());
|
||||
});
|
||||
|
||||
it('suppresses relay query loggers when quiet mode is enabled', async () => {
|
||||
let receivedQueryDeps = null;
|
||||
const deps = {
|
||||
...baseDeps,
|
||||
json: true,
|
||||
quiet: true,
|
||||
queryLocks: async (_relays, _cadence, _dateStr, _namespace, queryDeps) => {
|
||||
receivedQueryDeps = queryDeps;
|
||||
return [];
|
||||
},
|
||||
};
|
||||
|
||||
await cmdCheck('daily', deps);
|
||||
|
||||
assert.equal(typeof receivedQueryDeps.errorLogger, 'function');
|
||||
assert.equal(typeof receivedQueryDeps.healthLogger, 'function');
|
||||
});
|
||||
});
|
||||
|
||||
describe('cmdLock', () => {
|
||||
|
||||
+59
-77
@@ -1,84 +1,66 @@
|
||||
import { describe, it, mock } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
import { getCompletedAgents } from '../src/lock-utils.mjs';
|
||||
|
||||
describe('lock-utils', () => {
|
||||
describe('getCompletedAgents', () => {
|
||||
// Helper to create deps
|
||||
const createDeps = (overrides = {}) => ({
|
||||
readdir: async () => [],
|
||||
getDateStr: () => '2023-01-01',
|
||||
getIsoWeek: () => '2023-W01',
|
||||
...overrides,
|
||||
});
|
||||
test('getCompletedAgents', async (t) => {
|
||||
await t.test('returns empty set if directory read fails (non-ENOENT)', async () => {
|
||||
const deps = {
|
||||
readdir: async () => { throw new Error('Permission denied'); },
|
||||
getDateStr: () => '2026-02-24',
|
||||
getIsoWeek: () => '2026-W09'
|
||||
};
|
||||
const completed = await getCompletedAgents('daily', 'logs', deps);
|
||||
assert.strictEqual(completed.size, 0);
|
||||
});
|
||||
|
||||
it('returns completed agents for daily cadence', async () => {
|
||||
const deps = createDeps({
|
||||
readdir: async () => [
|
||||
'2023-01-01T10-00-00Z__agent1__completed.md',
|
||||
'2023-01-01T11-00-00Z__agent2__failed.md',
|
||||
'2023-01-02T10-00-00Z__agent3__completed.md', // different day
|
||||
],
|
||||
getDateStr: () => '2023-01-01',
|
||||
});
|
||||
const result = await getCompletedAgents('daily', 'logs', deps);
|
||||
assert.deepEqual([...result], ['agent1']);
|
||||
});
|
||||
await t.test('returns empty set if directory does not exist (ENOENT)', async () => {
|
||||
const error = new Error('ENOENT');
|
||||
error.code = 'ENOENT';
|
||||
const deps = {
|
||||
readdir: async () => { throw error; },
|
||||
getDateStr: () => '2026-02-24',
|
||||
getIsoWeek: () => '2026-W09'
|
||||
};
|
||||
const completed = await getCompletedAgents('daily', 'logs', deps);
|
||||
assert.strictEqual(completed.size, 0);
|
||||
});
|
||||
|
||||
it('returns completed agents for weekly cadence', async () => {
|
||||
const deps = createDeps({
|
||||
readdir: async () => [
|
||||
'2023-01-01T10-00-00Z__agent1__completed.md', // Week 52 of 2022 (Sunday) - assuming mock logic
|
||||
'2023-01-02T10-00-00Z__agent2__completed.md', // Week 01 of 2023 (Monday)
|
||||
],
|
||||
getIsoWeek: (date) => {
|
||||
if (!date) return '2023-W01'; // current week
|
||||
if (date === '2023-01-01') return '2022-W52';
|
||||
if (date === '2023-01-02') return '2023-W01';
|
||||
return 'UNKNOWN';
|
||||
}
|
||||
});
|
||||
// cadence is weekly
|
||||
const result = await getCompletedAgents('weekly', 'logs', deps);
|
||||
assert.deepEqual([...result], ['agent2']);
|
||||
});
|
||||
await t.test('identifies daily completed agents correctly', async () => {
|
||||
const files = [
|
||||
'2026-02-24T10-00-00Z__agent-a__completed.md',
|
||||
'2026-02-24T11-00-00Z__agent-b__failed.md', // failed, not completed
|
||||
'2026-02-23T10-00-00Z__agent-c__completed.md', // yesterday
|
||||
'invalid-format.md'
|
||||
];
|
||||
const deps = {
|
||||
readdir: async () => files,
|
||||
getDateStr: () => '2026-02-24',
|
||||
getIsoWeek: () => '2026-W09'
|
||||
};
|
||||
const completed = await getCompletedAgents('daily', 'logs', deps);
|
||||
assert.ok(completed.has('agent-a'));
|
||||
assert.ok(!completed.has('agent-b'));
|
||||
assert.ok(!completed.has('agent-c'));
|
||||
assert.strictEqual(completed.size, 1);
|
||||
});
|
||||
|
||||
it('handles ENOENT gracefully', async () => {
|
||||
const error = new Error('No such file or directory');
|
||||
error.code = 'ENOENT';
|
||||
const deps = createDeps({
|
||||
readdir: async () => { throw error; }
|
||||
});
|
||||
|
||||
// Spy on console.error to ensure it is NOT called
|
||||
const consoleError = mock.method(console, 'error');
|
||||
|
||||
const result = await getCompletedAgents('daily', 'logs', deps);
|
||||
|
||||
assert.equal(consoleError.mock.callCount(), 0);
|
||||
assert.deepEqual([...result], []);
|
||||
consoleError.mock.restore();
|
||||
});
|
||||
|
||||
it('logs error for non-ENOENT errors', async () => {
|
||||
const error = new Error('Permission denied');
|
||||
error.code = 'EACCES';
|
||||
const deps = createDeps({
|
||||
readdir: async () => { throw error; }
|
||||
});
|
||||
|
||||
// Spy on console.error to verify it IS called
|
||||
const consoleError = mock.method(console, 'error');
|
||||
|
||||
const result = await getCompletedAgents('daily', 'logs', deps);
|
||||
|
||||
assert.equal(consoleError.mock.callCount(), 1);
|
||||
const call = consoleError.mock.calls[0];
|
||||
assert.match(call.arguments[0], /Warning: Failed to read log dir/);
|
||||
assert.match(call.arguments[0], /Permission denied/);
|
||||
|
||||
assert.deepEqual([...result], []);
|
||||
consoleError.mock.restore();
|
||||
});
|
||||
await t.test('identifies weekly completed agents correctly', async () => {
|
||||
const files = [
|
||||
'2026-02-24T10-00-00Z__agent-a__completed.md', // this week (W09)
|
||||
'2026-02-17T10-00-00Z__agent-b__completed.md', // last week (W08)
|
||||
];
|
||||
const deps = {
|
||||
readdir: async () => files,
|
||||
getDateStr: () => '2026-02-24',
|
||||
getIsoWeek: (date) => {
|
||||
if (date === '2026-02-24') return '2026-W09';
|
||||
if (date === '2026-02-17') return '2026-W08';
|
||||
return '2026-W09'; // Default for current
|
||||
}
|
||||
};
|
||||
const completed = await getCompletedAgents('weekly', 'logs', deps);
|
||||
assert.ok(completed.has('agent-a'));
|
||||
assert.ok(!completed.has('agent-b'));
|
||||
assert.strictEqual(completed.size, 1);
|
||||
});
|
||||
});
|
||||
+50
-31
@@ -2,9 +2,13 @@ import { test, after } from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { execSync } from 'node:child_process';
|
||||
import { exec } from 'node:child_process';
|
||||
import { promisify } from 'node:util';
|
||||
import { WebSocketServer } from 'ws';
|
||||
import { cmdInit, cmdUpdate } from '../src/ops.mjs';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
const MOCK_CONFIG = {
|
||||
installDir: 'torch',
|
||||
namespace: 'test-namespace',
|
||||
@@ -115,45 +119,60 @@ test('cmdUpdate creates backup', async () => {
|
||||
});
|
||||
|
||||
test('torch-lock check respects local roster', async () => {
|
||||
const projectRoot = path.join(tempBase, 'project_roster');
|
||||
fs.mkdirSync(projectRoot, { recursive: true });
|
||||
const wss = new WebSocketServer({ port: 0 });
|
||||
const port = wss.address().port;
|
||||
const mockRelayUrl = `ws://127.0.0.1:${port}`;
|
||||
|
||||
// Init
|
||||
await cmdInit(false, projectRoot, MOCK_CONFIG);
|
||||
|
||||
// Modify roster
|
||||
const rosterPath = path.join(projectRoot, 'torch', 'roster.json');
|
||||
const roster = JSON.parse(fs.readFileSync(rosterPath, 'utf8'));
|
||||
roster.daily.push('custom-agent-007');
|
||||
fs.writeFileSync(rosterPath, JSON.stringify(roster, null, 2));
|
||||
|
||||
// Run CLI
|
||||
const binPath = path.resolve(process.cwd(), 'bin/torch-lock.mjs');
|
||||
|
||||
const output = execSync(`${process.execPath} ${binPath} check --cadence daily`, {
|
||||
cwd: projectRoot,
|
||||
encoding: 'utf8'
|
||||
wss.on('connection', (ws) => {
|
||||
ws.on('message', (message) => {
|
||||
try {
|
||||
const data = JSON.parse(message);
|
||||
if (data[0] === 'REQ') {
|
||||
const subId = data[1];
|
||||
ws.send(JSON.stringify(['EOSE', subId]));
|
||||
}
|
||||
} catch (_err) {
|
||||
// ignore
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const result = JSON.parse(output);
|
||||
assert.ok(result.available.includes('custom-agent-007'), 'Custom agent found in roster');
|
||||
try {
|
||||
const projectRoot = path.join(tempBase, 'project_roster');
|
||||
fs.mkdirSync(projectRoot, { recursive: true });
|
||||
|
||||
// Use local mock relay
|
||||
const localConfig = { ...MOCK_CONFIG, relays: [mockRelayUrl] };
|
||||
|
||||
// Init
|
||||
await cmdInit(false, projectRoot, localConfig);
|
||||
|
||||
// Modify roster
|
||||
const rosterPath = path.join(projectRoot, 'torch', 'roster.json');
|
||||
const roster = JSON.parse(fs.readFileSync(rosterPath, 'utf8'));
|
||||
roster.daily.push('custom-agent-007');
|
||||
fs.writeFileSync(rosterPath, JSON.stringify(roster, null, 2));
|
||||
|
||||
// Run CLI
|
||||
const binPath = path.resolve(process.cwd(), 'bin/torch-lock.mjs');
|
||||
|
||||
const { stdout } = await execAsync(`${process.execPath} ${binPath} check --cadence daily`, {
|
||||
cwd: projectRoot,
|
||||
encoding: 'utf8',
|
||||
env: { ...process.env, NOSTR_LOCK_RELAYS: undefined, NOSTR_LOCK_RELAY_FALLBACKS: '' }
|
||||
});
|
||||
|
||||
const result = JSON.parse(stdout);
|
||||
assert.ok(result.available.includes('custom-agent-007'), 'Custom agent found in roster');
|
||||
} finally {
|
||||
wss.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('cmdInit creates torch-config.json with random namespace', async () => {
|
||||
const projectRoot = path.join(tempBase, 'project_config_init');
|
||||
fs.mkdirSync(projectRoot, { recursive: true });
|
||||
|
||||
// Do NOT pass mockAnswers here to test random namespace generation,
|
||||
// but we know it hangs.
|
||||
// Wait, if I want to test random namespace, I MUST let it call interactiveInit?
|
||||
// No, I should probably change cmdInit to allow optional parts of config or just test the random part separately.
|
||||
|
||||
// Actually, for this test, let's just pass a partial mock if possible?
|
||||
// No, cmdInit takes full mockAnswers.
|
||||
|
||||
// I'll skip this test's random part or just accept it uses the mock.
|
||||
// Actually, if I pass a mock, it WON'T test random namespace.
|
||||
|
||||
await cmdInit(false, projectRoot, MOCK_CONFIG);
|
||||
|
||||
const configPath = path.join(projectRoot, 'torch-config.json');
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
// Prepare logging directory
|
||||
const logDir = 'test_logs';
|
||||
if (!fs.existsSync(logDir)) {
|
||||
fs.mkdirSync(logDir);
|
||||
}
|
||||
const logFile = path.join(logDir, 'playwright_console.log');
|
||||
// Clear previous log
|
||||
fs.writeFileSync(logFile, '');
|
||||
|
||||
test.use({
|
||||
extraHTTPHeaders: {
|
||||
// base64('admin:password') = 'YWRtaW46cGFzc3dvcmQ='
|
||||
'Authorization': 'Basic YWRtaW46cGFzc3dvcmQ='
|
||||
}
|
||||
});
|
||||
|
||||
test.describe.serial('TORCH Dashboard', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
page.on('console', msg => {
|
||||
const logLine = `[${msg.type()}] ${msg.text()}\n`;
|
||||
fs.appendFileSync(logFile, logLine);
|
||||
// Also log to stdout so it appears in the runner output
|
||||
// console.log(`[Browser Console] ${msg.text()}`);
|
||||
});
|
||||
});
|
||||
|
||||
test('should load the dashboard and verify title', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
// Check for title "TORCH Dashboard" or similar. Checking regex /TORCH/ is safer.
|
||||
await expect(page).toHaveTitle(/TORCH/);
|
||||
|
||||
// Check for some basic element presence to ensure rendering happened
|
||||
// Assuming there is a header or main content
|
||||
// We can inspect dashboard/index.html to be sure
|
||||
// But generic check for body visibility is a start
|
||||
await expect(page.locator('body')).toBeVisible();
|
||||
|
||||
// Verify console log capture
|
||||
await page.evaluate(() => console.log('Test log from browser'));
|
||||
});
|
||||
|
||||
test('should fail authentication with wrong credentials', async ({ browser }) => {
|
||||
const context = await browser.newContext({
|
||||
extraHTTPHeaders: {
|
||||
'Authorization': 'Basic YWRtaW46d3Jvbmc=' // admin:wrong
|
||||
}
|
||||
});
|
||||
const page = await context.newPage();
|
||||
const response = await page.goto('/');
|
||||
expect(response.status()).toBe(401);
|
||||
await context.close();
|
||||
});
|
||||
|
||||
test('should serve public config correctly', async ({ request }) => {
|
||||
const response = await request.get('/torch-config.json');
|
||||
expect(response.ok()).toBeTruthy();
|
||||
const config = await response.json();
|
||||
expect(config.nostrLock.namespace).toBe('test-torch');
|
||||
// Security check: Auth should be stripped
|
||||
expect(config.dashboard.auth).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should shutdown gracefully to save coverage', async ({ request }) => {
|
||||
// Attempt to shutdown. It might fail if already down (unlikely in serial).
|
||||
try {
|
||||
// We don't expect a response because the server dies
|
||||
await request.get('/shutdown', { timeout: 1000 }).catch(() => {});
|
||||
} catch (_) {
|
||||
// Ignore
|
||||
}
|
||||
// Wait a bit for file write
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"nostrLock": {
|
||||
"namespace": "test-torch",
|
||||
"relays": ["wss://relay.damus.io"],
|
||||
"relayFallbacks": [],
|
||||
"ttlSeconds": 60,
|
||||
"queryTimeoutMs": 5000,
|
||||
"publishTimeoutMs": 5000,
|
||||
"minSuccessfulRelayPublishes": 1,
|
||||
"minActiveRelayPool": 1
|
||||
},
|
||||
"dashboard": {
|
||||
"auth": "admin:password",
|
||||
"defaultCadenceView": "daily",
|
||||
"defaultStatusView": "active",
|
||||
"relays": ["wss://relay.damus.io"],
|
||||
"namespace": "test-torch",
|
||||
"hashtag": "test-torch-lock"
|
||||
},
|
||||
"scheduler": {
|
||||
"firstPromptByCadence": {
|
||||
"daily": "src/prompts/daily/agent.md",
|
||||
"weekly": "src/prompts/weekly/agent.md"
|
||||
},
|
||||
"paused": {
|
||||
"daily": [],
|
||||
"weekly": []
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,9 @@ async function setupFixture({
|
||||
roster = ['agent-a'],
|
||||
lockHealthPreflight = undefined,
|
||||
schedulerPolicy = {},
|
||||
handoffCommand = 'echo HANDOFF_OK',
|
||||
validationCommands = ['true'],
|
||||
lockCheckOutput = `echo '{"excluded":[]}'`,
|
||||
preflightScript = '#!/usr/bin/env node\nconsole.log(JSON.stringify({ ok: true, relays: ["wss://relay.test"] }));\nprocess.exit(0);\n',
|
||||
}) {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'scheduler-memory-policy-'));
|
||||
@@ -103,8 +106,8 @@ async function setupFixture({
|
||||
JSON.stringify({
|
||||
scheduler: {
|
||||
firstPromptByCadence: { daily: 'agent-a' },
|
||||
handoffCommandByCadence: { daily: 'echo HANDOFF_OK' },
|
||||
validationCommandsByCadence: { daily: ['true'] },
|
||||
handoffCommandByCadence: { daily: handoffCommand },
|
||||
validationCommandsByCadence: { daily: validationCommands },
|
||||
memoryPolicyByCadence: { daily: memoryPolicy },
|
||||
...schedulerPolicy,
|
||||
...(lockHealthPreflight === undefined ? {} : { lockHealthPreflight }),
|
||||
@@ -122,7 +125,7 @@ if [[ "$1" != "run" ]]; then
|
||||
fi
|
||||
case "$2" in
|
||||
lock:check:daily)
|
||||
echo '{"excluded":[]}'
|
||||
${lockCheckOutput}
|
||||
;;
|
||||
lock:health)
|
||||
shift 2
|
||||
@@ -611,3 +614,110 @@ test('does not use backend retry flow for exit code 3 lock conflicts', { concurr
|
||||
assert.equal(result.code, 0, `stdout: ${result.stdout}\nstderr: ${result.stderr}`);
|
||||
assert.doesNotMatch(result.stdout, /"event":"scheduler.lock.retry"/);
|
||||
});
|
||||
|
||||
test('retries handoff command for retryable network failures and succeeds', { concurrency: false }, async () => {
|
||||
const fixture = await setupFixture({
|
||||
memoryPolicy: { mode: 'optional' },
|
||||
handoffCommand: `count_file="$PWD/.handoff-count"
|
||||
count=0
|
||||
if [[ -f "$count_file" ]]; then
|
||||
count="$(cat "$count_file")"
|
||||
fi
|
||||
count=$((count + 1))
|
||||
echo "$count" > "$count_file"
|
||||
if [[ "$count" -lt 2 ]]; then
|
||||
echo "stream disconnected before completion" >&2
|
||||
exit 12
|
||||
fi
|
||||
echo HANDOFF_OK`,
|
||||
schedulerPolicy: {
|
||||
handoffPolicyByCadence: {
|
||||
daily: {
|
||||
maxAttempts: 3,
|
||||
retryBackoffMs: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await runNode(fixture.scriptPath, ['--cadence', 'daily'], {
|
||||
cwd: fixture.root,
|
||||
env: fixture.env,
|
||||
});
|
||||
|
||||
assert.equal(result.code, 0, `stdout: ${result.stdout}\nstderr: ${result.stderr}`);
|
||||
const logs = await fs.readdir(path.join(fixture.root, 'task-logs', 'daily'));
|
||||
assert.ok(logs.some((name) => name.endsWith('__completed.md')));
|
||||
});
|
||||
|
||||
test('uses fallback platform for handoff when primary platform fails', { concurrency: false }, async () => {
|
||||
const fixture = await setupFixture({
|
||||
memoryPolicy: { mode: 'optional' },
|
||||
handoffCommand: `if [[ "$AGENT_PLATFORM" == "linux" ]]; then
|
||||
echo HANDOFF_FALLBACK_OK
|
||||
exit 0
|
||||
fi
|
||||
echo "stream disconnected before completion" >&2
|
||||
exit 13`,
|
||||
schedulerPolicy: {
|
||||
handoffPolicyByCadence: {
|
||||
daily: {
|
||||
maxAttempts: 1,
|
||||
fallbackPlatform: 'linux',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await runNode(fixture.scriptPath, ['--cadence', 'daily', '--platform', 'codex'], {
|
||||
cwd: fixture.root,
|
||||
env: fixture.env,
|
||||
});
|
||||
|
||||
assert.equal(result.code, 0, `stdout: ${result.stdout}\nstderr: ${result.stderr}`);
|
||||
const logs = await fs.readdir(path.join(fixture.root, 'task-logs', 'daily'));
|
||||
assert.ok(logs.some((name) => name.endsWith('__completed.md')));
|
||||
});
|
||||
|
||||
test('fails before lock acquisition when runner health preflight command fails', { concurrency: false }, async () => {
|
||||
const fixture = await setupFixture({
|
||||
memoryPolicy: { mode: 'optional' },
|
||||
schedulerPolicy: {
|
||||
runnerHealthCommandByCadence: {
|
||||
daily: 'echo RUNNER_UNAVAILABLE >&2; exit 9',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await runNode(fixture.scriptPath, ['--cadence', 'daily'], {
|
||||
cwd: fixture.root,
|
||||
env: fixture.env,
|
||||
});
|
||||
|
||||
assert.equal(result.code, 9, `stdout: ${result.stdout}\nstderr: ${result.stderr}`);
|
||||
const logs = await fs.readdir(path.join(fixture.root, 'task-logs', 'daily'));
|
||||
const failedLog = logs.find((name) => name.endsWith('__failed.md'));
|
||||
assert.ok(failedLog);
|
||||
const failedBody = await fs.readFile(path.join(fixture.root, 'task-logs', 'daily', failedLog), 'utf8');
|
||||
assert.match(failedBody, /reason: Runner health preflight failed/);
|
||||
assert.match(failedBody, /failure_class: 'runner_unavailable'/);
|
||||
assert.match(failedBody, /failure_category: 'execution_error'/);
|
||||
assert.match(failedBody, /Prompt not executed\./);
|
||||
});
|
||||
|
||||
test('uses exclusion payload from mixed JSON lock:check output', { concurrency: false }, async () => {
|
||||
const fixture = await setupFixture({
|
||||
memoryPolicy: { mode: 'optional' },
|
||||
roster: ['agent-a', 'agent-b'],
|
||||
lockCheckOutput: `echo '{"event":"relay_health_snapshot"}'
|
||||
echo '{"excluded":["agent-a"]}'`,
|
||||
});
|
||||
|
||||
const result = await runNode(fixture.scriptPath, ['--cadence', 'daily'], {
|
||||
cwd: fixture.root,
|
||||
env: fixture.env,
|
||||
});
|
||||
|
||||
assert.equal(result.code, 0, `stdout: ${result.stdout}\nstderr: ${result.stderr}`);
|
||||
assert.match(result.stdout, /Agent:\s+agent-b/);
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { spawn } from 'node:child_process';
|
||||
|
||||
const SCRIPT = path.resolve('scripts/test-audit/run-flaky-check.mjs');
|
||||
|
||||
function runFlakyCheck(args) {
|
||||
return new Promise((resolve) => {
|
||||
const childEnv = { ...process.env };
|
||||
delete childEnv.NODE_TEST_CONTEXT;
|
||||
delete childEnv.NODE_UNIQUE_ID;
|
||||
|
||||
const child = spawn(process.execPath, [SCRIPT, ...args], {
|
||||
cwd: process.cwd(),
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
env: childEnv,
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
child.stdout.on('data', (chunk) => { stdout += chunk.toString(); });
|
||||
child.stderr.on('data', (chunk) => { stderr += chunk.toString(); });
|
||||
child.on('close', (code) => resolve({ code: code ?? 1, stdout, stderr }));
|
||||
});
|
||||
}
|
||||
|
||||
test('run-flaky-check reports pass/fail counts from TAP output files', async () => {
|
||||
const outputDir = await fs.mkdtemp(path.join(os.tmpdir(), 'flaky-check-report-'));
|
||||
const passFixture = 'test/fixtures/flaky-check/pass.fixture.test.mjs';
|
||||
const failFixture = 'test/fixtures/flaky-check/fail.fixture.test.mjs';
|
||||
|
||||
const result = await runFlakyCheck(['--output-dir', outputDir, passFixture, failFixture]);
|
||||
assert.equal(result.code, 0, result.stderr || result.stdout);
|
||||
|
||||
const matrix = JSON.parse(await fs.readFile(path.join(outputDir, 'flakiness-matrix.json'), 'utf8'));
|
||||
|
||||
// The tool keys results by test name, not filename
|
||||
assert.deepEqual(matrix['pass fixture stays green'], { pass: 5, fail: 0 });
|
||||
assert.deepEqual(matrix['fail fixture stays red'], { pass: 0, fail: 5 });
|
||||
|
||||
const diagnostics = JSON.parse(await fs.readFile(path.join(outputDir, 'flakiness-runs.json'), 'utf8'));
|
||||
assert.equal(diagnostics.length, 5);
|
||||
assert.ok(diagnostics.every((run) => run.observed >= 2));
|
||||
});
|
||||
@@ -2,7 +2,12 @@ import { describe, it, after } from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
<<<<<<< HEAD
|
||||
import { getIsoWeekStr, todayDateStr, nowUnix, ensureDir, detectPlatform, relayListLabel, mergeRelayList } from '../src/utils.mjs';
|
||||
=======
|
||||
import { getIsoWeekStr, todayDateStr, nowUnix, ensureDir, detectPlatform, relayListLabel } from '../src/utils.mjs';
|
||||
import { mergeRelayList } from '../src/lock-utils.mjs';
|
||||
>>>>>>> origin/main
|
||||
|
||||
describe('Date Utilities', () => {
|
||||
describe('todayDateStr', () => {
|
||||
|
||||
Reference in new issue
Block a user