diff --git a/reports/test-audit/test-integrity-note-2025-02-14.yaml b/reports/test-audit/test-integrity-note-2025-02-14.yaml new file mode 100644 index 0000000..7947a68 --- /dev/null +++ b/reports/test-audit/test-integrity-note-2025-02-14.yaml @@ -0,0 +1,27 @@ +test_integrity_note: + change_type: ["refactor_tests", "new_tests", "spec_correction"] + scenarios: + - id: SCN-MEMORY-SCHEDULER-DETERMINISM + given: "A memory maintenance scheduler with retry logic" + when: "A job fails transiently and retries are scheduled" + then: "The scheduler waits exactly the retry delay without using real time sleep" + - id: SCN-LOCK-PARSE-MALFORMED + given: "A lock event with invalid JSON content or missing fields" + when: "The event is parsed" + then: "The parser returns a lock object with null content fields instead of throwing or returning partial data" + - id: SCN-DASHBOARD-CONFIG-SANITIZATION + given: "A dashboard configuration with sensitive fields (roster, auth)" + when: "The /torch-config.json endpoint is accessed" + then: "Sensitive fields are stripped from the response" + observable_outcomes: + - "Scheduler tests run deterministically using mock timers" + - "Lock parser handles garbage input gracefully" + - "Dashboard config endpoint does not leak sensitive roster data" + determinism_controls: + - "Replaced setTimeout/wait with node:test mock.timers" + anti_cheat_rationale: + prevents: + - "retry/sleep-based flake masking" + - "hard-coded return value (dashboard config fix)" + relaxation: + did_relax_any_assertion: false diff --git a/src/dashboard.mjs b/src/dashboard.mjs index a8a5332..771362a 100644 --- a/src/dashboard.mjs +++ b/src/dashboard.mjs @@ -129,7 +129,7 @@ export async function cmdDashboard(port = DEFAULT_DASHBOARD_PORT, host = '127.0. } res.writeHead(200, { ...SECURITY_HEADERS, 'Content-Type': 'application/json' }); - res.end(JSON.stringify(safeConfig, null, 2)); + res.end(JSON.stringify(publicConfig, null, 2)); return; } catch (err) { console.error('Error parsing torch-config.json:', err); diff --git a/src/prompts/daily/test-audit-agent.md b/src/prompts/daily/test-audit-agent.md index 6d82b64..5e7aa57 100644 --- a/src/prompts/daily/test-audit-agent.md +++ b/src/prompts/daily/test-audit-agent.md @@ -1,5 +1,13 @@ > **Shared contract (required):** Follow [`Scheduler Flow → Shared Agent Run Contract`](../scheduler-flow.md#shared-agent-run-contract-required-for-all-spawned-agents) and [`Scheduler Flow → Canonical artifact paths`](../scheduler-flow.md#canonical-artifact-paths) before and during this run. +## Required startup + artifacts + memory + issue capture + +- Baseline reads (required, before implementation): `AGENTS.md`, `CLAUDE.md`, `KNOWN_ISSUES.md`, and `docs/agent-handoffs/README.md`. +- Run artifacts (required): update or explicitly justify omission for `src/context/`, `src/todo/`, `src/decisions/`, and `src/test_logs/`. +- Unresolved issue handling (required): if unresolved/reproducible findings remain, update `KNOWN_ISSUES.md` and add or update an incidents note in `docs/agent-handoffs/incidents/`. +- Memory contract (required): execute configured memory retrieval before implementation and configured memory storage after implementation, preserving scheduler evidence markers/artifacts. +- Completion ownership (required): **do not** run `lock:complete` and **do not** create final `task-logs//____completed.md` or `__failed.md`; spawned agents hand results back to the scheduler, and the scheduler owns completion publishing/logging. + [SYSTEM] You are the Test Integrity & Scenario Spec Agent. Your purpose is to keep validation truthful. You do not optimize for green CI. You optimize for reality. diff --git a/test/lock-ops.test.mjs b/test/lock-ops.test.mjs index 2064ab6..74d8e1c 100644 --- a/test/lock-ops.test.mjs +++ b/test/lock-ops.test.mjs @@ -35,6 +35,44 @@ describe('lock-ops', () => { assert.strictEqual(result.agent, 'test-agent'); assert.strictEqual(result.platform, 'test-platform'); }); + + it('returns nulls when content is invalid JSON', () => { + const event = { + id: 'mock-id', + pubkey: 'mock-pubkey', + created_at: 1678886400, + tags: [], + content: 'invalid json', + }; + const result = parseLockEvent(event); + assert.strictEqual(result.agent, null); + assert.strictEqual(result.cadence, null); + }); + + it('returns nulls when required content fields are missing', () => { + const event = { + id: 'mock-id', + pubkey: 'mock-pubkey', + created_at: 1678886400, + tags: [], + content: JSON.stringify({}), + }; + const result = parseLockEvent(event); + assert.strictEqual(result.agent, null); + assert.strictEqual(result.cadence, null); + }); + + it('returns nulls when content is an array (not object)', () => { + const event = { + id: 'mock-id', + pubkey: 'mock-pubkey', + created_at: 1678886400, + tags: [], + content: JSON.stringify(['not', 'an', 'object']), + }; + const result = parseLockEvent(event); + assert.strictEqual(result.agent, null); + }); }); describe('queryLocks', () => { diff --git a/test/memory-scheduler.test.mjs b/test/memory-scheduler.test.mjs index d609ae6..ae0d62f 100644 --- a/test/memory-scheduler.test.mjs +++ b/test/memory-scheduler.test.mjs @@ -1,4 +1,4 @@ -import { test } from 'node:test'; +import { test, mock } from 'node:test'; import assert from 'node:assert/strict'; import { @@ -6,8 +6,6 @@ import { startMemoryMaintenanceScheduler, } from '../src/services/memory/scheduler.js'; -const wait = (ms = 0) => new Promise((resolve) => setTimeout(resolve, ms)); - test('createDbAdvisoryLockProvider acquires and releases postgres advisory locks', async () => { const calls = []; const db = { @@ -32,6 +30,8 @@ test('createDbAdvisoryLockProvider acquires and releases postgres advisory locks }); test('startMemoryMaintenanceScheduler runs all jobs and emits metrics including retries', async () => { + mock.timers.enable({ apis: ['Date', 'setTimeout', 'setInterval'] }); + const metrics = []; let ingestAttempts = 0; @@ -71,8 +71,22 @@ test('startMemoryMaintenanceScheduler runs all jobs and emits metrics including }, }); - await wait(25); + // Advance time to allow async jobs and retries to complete. + // We tick enough to cover the retryDelayMs (1ms). + // We also need to allow the promise microtask queue to drain between ticks + // if necessary, but tick() usually handles the timer callbacks. + // Since the scheduler logic uses await, we need to ensure those promises resolve. + + // Tick time forward and yield to event loop to allow promise chains to progress. + // The job involves multiple async steps (lock, handler, retry wait, lock, handler). + // We tick multiple times to ensure the retry timer fires and subsequent microtasks run. + for (let i = 0; i < 10; i++) { + mock.timers.tick(5); + await new Promise((resolve) => setImmediate(resolve)); + } + scheduler.stop(); + mock.timers.reset(); const byJob = metrics.reduce((acc, entry) => { const key = entry.payload.job; @@ -81,7 +95,7 @@ test('startMemoryMaintenanceScheduler runs all jobs and emits metrics including return acc; }, {}); - assert.equal(byJob.ingestRecentRuntimeEvents.length, 1); + assert.equal(byJob.ingestRecentRuntimeEvents?.length, 1, 'Expected 1 ingest metric'); assert.equal(byJob.ingestRecentRuntimeEvents[0].payload.status, 'success'); assert.equal(byJob.ingestRecentRuntimeEvents[0].payload.itemCount, 3); assert.equal(byJob.ingestRecentRuntimeEvents[0].payload.retries, 1); @@ -93,6 +107,8 @@ test('startMemoryMaintenanceScheduler runs all jobs and emits metrics including test('startMemoryMaintenanceScheduler skips jobs when feature flags disable memory', async () => { + mock.timers.enable({ apis: ['Date', 'setTimeout', 'setInterval'] }); + const metrics = []; const called = []; @@ -118,8 +134,11 @@ test('startMemoryMaintenanceScheduler skips jobs when feature flags disable memo }, }); - await wait(10); + mock.timers.tick(10); + await new Promise(resolve => setImmediate(resolve)); + scheduler.stop(); + mock.timers.reset(); assert.deepEqual(called, []); assert.ok(metrics.some((entry) => entry.payload.status === 'skipped_flag_disabled'));