diff --git a/scripts/execution-gate.js b/scripts/execution-gate.js index 94b39ce91..565e16b5a 100644 --- a/scripts/execution-gate.js +++ b/scripts/execution-gate.js @@ -111,28 +111,26 @@ class ExecutionGate { if (classification.type === 'LEGACY_MESSAGE_ID') { const msgId = msg.completion_message_id; if (!msgId) return { verified: false, wouldVerify: false, reason: 'completion_message_id is empty' }; - + if (this.dryRun) { + return { verified: false, wouldVerify: true, reason: 'DRY_RUN_SKIP_REF_CHECK' }; + } const found = this._findReferencedMessage(msgId, msg); if (found) { return { verified: true, wouldVerify: true, reason: 'Referenced message exists on disk' }; } - if (this.dryRun) { - return { verified: false, wouldVerify: true, reason: 'DRY_RUN_SKIP_REF_CHECK' }; - } return { verified: false, wouldVerify: false, reason: `Referenced message not found: ${msgId}` }; } if (classification.type === 'LEGACY_TASK_ID') { const taskId = msg.resolved_by_task_id; if (!taskId) return { verified: false, wouldVerify: false, reason: 'resolved_by_task_id is empty' }; - + if (this.dryRun) { + return { verified: false, wouldVerify: true, reason: 'DRY_RUN_SKIP_REF_CHECK' }; + } const found = this._findReferencedTask(taskId, msg); if (found) { return { verified: true, wouldVerify: true, reason: 'Referenced task exists on disk' }; } - if (this.dryRun) { - return { verified: false, wouldVerify: true, reason: 'DRY_RUN_SKIP_REF_CHECK' }; - } return { verified: false, wouldVerify: false, reason: `Referenced task not found: ${taskId}` }; } diff --git a/scripts/lane-worker.js b/scripts/lane-worker.js index c545a80dd..7f801810b 100644 --- a/scripts/lane-worker.js +++ b/scripts/lane-worker.js @@ -711,7 +711,7 @@ if (msg.confidence !== undefined && msg.confidence >= 7) { task_id: msg.task_id || 'unknown', confidence: msg.confidence, }; - const cpsPath = path.join(repoRoot, 'context-buffer', 'cps_log.jsonl'); + const cpsPath = path.join(this.repoRoot, 'context-buffer', 'cps_log.jsonl'); try { fs.appendFileSync(cpsPath, JSON.stringify(cpsEntry) + '\n'); } catch (e) { @@ -991,7 +991,13 @@ processFile(filePath) { target_path: nrPath, reason: 'FOREIGN_INSTANCE_ACTIONABLE', detail: `Non-owner session ${SESSION_ID.slice(0,12)}: actionable message from different instance deferred`, schema_valid: false, signature_valid: false, actionable: true, - has_completion_proof: false, dry_run: this.dryRun, + has_completion_proof: false, execution_verified: false, + would_verify: false, enforce_ownership: this.enforceOwnership, + ownership_enforcement_enabled: this.enforceOwnership, + ownership: { present: false }, ownership_notes: [], + verification_outcome: null, domain_validation: null, + domain_gate_executed: false, verification_path: null, + schema_remediation: null, dry_run: this.dryRun, }; } @@ -1015,7 +1021,13 @@ processFile(filePath) { target_path: sfPath, reason: 'STALE_FOREIGN_INSTANCE', detail: `Message from foreign session ${msg._lane_worker.session_identity.session_id.slice(0,12)}, cross-instance not allowed`, schema_valid: false, signature_valid: false, actionable: true, - has_completion_proof: false, dry_run: this.dryRun, + has_completion_proof: false, execution_verified: false, + would_verify: false, enforce_ownership: this.enforceOwnership, + ownership_enforcement_enabled: this.enforceOwnership, + ownership: { present: false }, ownership_notes: [], + verification_outcome: null, domain_validation: null, + domain_gate_executed: false, verification_path: null, + schema_remediation: null, dry_run: this.dryRun, }; } } @@ -1101,6 +1113,17 @@ _routeRaw(filePath, queueKey, meta) { signature_valid: false, actionable: false, has_completion_proof: false, + execution_verified: false, + would_verify: false, + enforce_ownership: this.enforceOwnership, + ownership_enforcement_enabled: this.enforceOwnership, + ownership: { present: false }, + ownership_notes: [], + verification_outcome: null, + domain_validation: null, + domain_gate_executed: false, + verification_path: null, + schema_remediation: null, dry_run: this.dryRun, }; @@ -1138,6 +1161,17 @@ _routeRaw(filePath, queueKey, meta) { reason: 'PROCESSING_EXCEPTION', detail: err.message, dry_run: this.dryRun, + execution_verified: false, + would_verify: false, + enforce_ownership: this.enforceOwnership, + ownership_enforcement_enabled: this.enforceOwnership, + ownership: { present: false }, + ownership_notes: [], + verification_outcome: null, + domain_validation: null, + domain_gate_executed: false, + verification_path: null, + schema_remediation: null, }); } } diff --git a/scripts/test-execution-gate.js b/scripts/test-execution-gate.js index 230ec768e..4fc922813 100644 --- a/scripts/test-execution-gate.js +++ b/scripts/test-execution-gate.js @@ -120,6 +120,7 @@ test('lane-worker blocks fake artifact with EXECUTION_NOT_VERIFIED', function(tm priority: 'P1', timestamp: new Date().toISOString(), requires_action: true, + confidence: 8, subject: 'Fake artifact via worker', body: 'Worker should block this', evidence: { required: true }, @@ -183,6 +184,7 @@ test('lane-worker stamps execution_verified=true on valid artifact', function(tm priority: 'P1', timestamp: new Date().toISOString(), requires_action: true, + confidence: 8, subject: 'Real artifact via worker', body: 'Worker should accept this', evidence: { required: true }, @@ -317,10 +319,18 @@ test('execution_verified=false default when no proof present', function(tmpRoot) }); // TEST 9: dry-run reference check must not set execution_verified=true +// Must use from: 'library' to prove lane discovery is never called test('dry-run reference skip reports would_verify=true but execution_verified=false', function(tmpRoot) { var resolver = new ArtifactResolver({ allowedRoots: [tmpRoot], dryRun: true }); var gate = new ExecutionGate({ lane: 'archivist', dryRun: true, resolver: resolver }); + // Wrap _findReferencedMessage to prove it is never called during dry-run + var lookupCalled = false; + gate._findReferencedMessage = function() { + lookupCalled = true; + throw new Error('LOOKUP_CALLED_DURING_DRY_RUN'); + }; + var msg = { id: 'dry-run-ref-check', from: 'library', @@ -333,12 +343,66 @@ test('dry-run reference skip reports would_verify=true but execution_verified=fa }; var result = gate.verify(msg); + assert.strictEqual(lookupCalled, false, '_findReferencedMessage must not be called during dry-run'); assert.strictEqual(result.execution_verified, false, 'dry-run ref skip must not verify execution'); assert.strictEqual(result.would_verify, true, 'dry-run ref skip should report would_verify=true'); assert.strictEqual(result.reason, 'DRY_RUN_SKIP_REF_CHECK'); }); -// TEST 10: dry-run path check must not set execution_verified=true +// TEST 10: dry-run task reference check must not set execution_verified=true +// Proves resolved_by_task_id lookup is skipped in dry-run mode +test('dry-run task reference skip reports would_verify=true but execution_verified=false', function(tmpRoot) { + var resolver = new ArtifactResolver({ allowedRoots: [tmpRoot], dryRun: true }); + var gate = new ExecutionGate({ lane: 'archivist', dryRun: true, resolver: resolver }); + + // Wrap _findReferencedTask to prove it is never called during dry-run + var lookupCalled = false; + gate._findReferencedTask = function() { + lookupCalled = true; + throw new Error('LOOKUP_CALLED_DURING_DRY_RUN'); + }; + + var msg = { + id: 'dry-run-task-check', + from: 'library', + to: 'archivist', + type: 'task', + priority: 'P1', + timestamp: new Date().toISOString(), + requires_action: true, + resolved_by_task_id: 'missing-task-id', + }; + + var result = gate.verify(msg); + assert.strictEqual(lookupCalled, false, '_findReferencedTask must not be called during dry-run'); + assert.strictEqual(result.execution_verified, false, 'dry-run task ref skip must not verify execution'); + assert.strictEqual(result.would_verify, true, 'dry-run task ref skip should report would_verify=true'); + assert.strictEqual(result.reason, 'DRY_RUN_SKIP_REF_CHECK'); +}); + +// TEST 11: non-dry-run cross-lane path security remains fail-closed +// Source lane outside allowed roots must trigger SECURITY error +test('non-dry-run cross-lane path security is fail-closed', function(tmpRoot) { + var resolver = new ArtifactResolver({ allowedRoots: [tmpRoot], dryRun: false }); + var gate = new ExecutionGate({ lane: 'archivist', dryRun: false, resolver: resolver }); + + var msg = { + id: 'security-test', + from: 'swarmmind', + to: 'archivist', + type: 'task', + priority: 'P1', + timestamp: new Date().toISOString(), + requires_action: true, + completion_message_id: 'some-message-id', + }; + + assert.throws(function() { + gate.verify(msg); + }, /outside allowed roots|Invalid lane identifier/i, 'non-dry-run cross-lane must throw security error'); +}); + +// TEST 12: dry-run fs check must not set execution_verified=true test('dry-run fs skip reports would_verify=true but execution_verified=false', function(tmpRoot) { var resolver = new ArtifactResolver({ allowedRoots: [tmpRoot], dryRun: true }); var gate = new ExecutionGate({ lane: 'archivist', dryRun: true, resolver: resolver }); @@ -365,6 +429,108 @@ test('dry-run fs skip reports would_verify=true but execution_verified=false', f assert.strictEqual(result.reason, 'DRY_RUN_SKIP_FS_CHECK'); }); +// TEST 13: INVALID_JSON route includes normalized metadata defaults +test('INVALID_JSON route includes normalized metadata defaults', function(tmpRoot) { + var inbox = path.join(tmpRoot, 'lanes', 'archivist', 'inbox'); + mkDir(inbox); + ['action-required', 'in-progress', 'processed', 'blocked', 'quarantine'].forEach(function(d) { + mkDir(path.join(inbox, d)); + }); + + // Write a message file that will trigger INVALID_JSON + var msgPath = path.join(inbox, '2026-01-01_trigger_exception.json'); + fs.writeFileSync(msgPath, 'NOT VALID JSON', 'utf8'); + + var worker = new LaneWorker({ + repoRoot: tmpRoot, + lane: 'archivist', + dryRun: false, + config: { + repoRoot: tmpRoot, + lane: 'archivist', + queues: { + inbox: inbox, + actionRequired: path.join(inbox, 'action-required'), + inProgress: path.join(inbox, 'in-progress'), + processed: path.join(inbox, 'processed'), + blocked: path.join(inbox, 'blocked'), + quarantine: path.join(inbox, 'quarantine'), + }, + }, + schemaValidator: function() { return { valid: true, errors: [] }; }, + signatureValidator: function() { return { valid: true, reason: null, details: null }; }, + }); + + var summary = worker.processOnce(); + assert.strictEqual(summary.routed.quarantine, 1, 'Must route to quarantine'); + + var route = summary.routes[0]; + assert.strictEqual(route.reason, 'INVALID_JSON'); + assert.strictEqual(route.execution_verified, false, 'INVALID_JSON must have execution_verified=false'); + assert.strictEqual(route.would_verify, false, 'INVALID_JSON must have would_verify=false'); + assert.deepStrictEqual(route.ownership_notes, [], 'INVALID_JSON must have ownership_notes=[]'); + assert.strictEqual(route.schema_remediation, null, 'INVALID_JSON must have schema_remediation=null'); +}); + +// TEST 14: PROCESSING_EXCEPTION route includes normalized metadata defaults +// Exercises the processOnce() catch block by replacing processFile with a throwing stub +test('PROCESSING_EXCEPTION route includes normalized metadata defaults', function(tmpRoot) { + var inbox = path.join(tmpRoot, 'lanes', 'archivist', 'inbox'); + mkDir(inbox); + ['action-required', 'in-progress', 'processed', 'blocked', 'quarantine'].forEach(function(d) { + mkDir(path.join(inbox, d)); + }); + + // Write a valid message file (will be processed by the stubbed processFile) + var msgPath = path.join(inbox, '2026-01-01_process_exception.json'); + fs.writeFileSync(msgPath, JSON.stringify({ + id: 'process-exception-test', + from: 'library', + to: 'archivist', + type: 'task', + priority: 'P1', + timestamp: new Date().toISOString(), + requires_action: true, + subject: 'Test', + body: 'Test', + }, null, 2), 'utf8'); + + var worker = new LaneWorker({ + repoRoot: tmpRoot, + lane: 'archivist', + dryRun: false, + config: { + repoRoot: tmpRoot, + lane: 'archivist', + queues: { + inbox: inbox, + actionRequired: path.join(inbox, 'action-required'), + inProgress: path.join(inbox, 'in-progress'), + processed: path.join(inbox, 'processed'), + blocked: path.join(inbox, 'blocked'), + quarantine: path.join(inbox, 'quarantine'), + }, + }, + schemaValidator: function() { return { valid: true, errors: [] }; }, + signatureValidator: function() { return { valid: true, reason: null, details: null }; }, + }); + + // Replace processFile with a function that throws to trigger the catch block + worker.processFile = function() { + throw new Error('TEST_PROCESSING_EXCEPTION'); + }; + + var summary = worker.processOnce(); + assert.strictEqual(summary.routed.quarantine, 1, 'Must route to quarantine'); + + var route = summary.routes[0]; + assert.strictEqual(route.reason, 'PROCESSING_EXCEPTION'); + assert.strictEqual(route.execution_verified, false, 'PROCESSING_EXCEPTION must have execution_verified=false'); + assert.strictEqual(route.would_verify, false, 'PROCESSING_EXCEPTION must have would_verify=false'); + assert.deepStrictEqual(route.ownership_notes, [], 'PROCESSING_EXCEPTION must have ownership_notes=[]'); + assert.strictEqual(route.schema_remediation, null, 'PROCESSING_EXCEPTION must have schema_remediation=null'); +}); + // SUMMARY console.log('\n========================================'); console.log('Execution Gate Tests');