23f7624596
ADR-166 MCP Bridge Security Lock / Static-source security lock (push) Failing after 0s
ADR-166 MCP Bridge Security Lock / Compose default binds loopback + Mongo has auth (push) Failing after 2s
CodeQL Advanced / Analyze (rust) (push) Failing after 0s
ADR-166 MCP Bridge Security Lock / plugin-agent-federation bindHost default (push) Failing after 1s
ADR-166 MCP Bridge Security Lock / Runtime behavior — 401 + terminal gate + fail-closed (push) Failing after 4s
business-pods-smoke / smoke (push) Failing after 1s
all-plugins-smoke / smoke-all (push) Failing after 2s
CI/CD Pipeline / Security & Code Quality (push) Failing after 1s
CI/CD Pipeline / Test Suite (ubuntu-latest) (push) Failing after 1s
CI/CD Pipeline / Build & Package (macos-latest) (push) Has been skipped
CI/CD Pipeline / Build & Package (ubuntu-latest) (push) Has been skipped
CI/CD Pipeline / Build & Package (windows-latest) (push) Has been skipped
CI/CD Pipeline / Documentation & Examples (push) Failing after 1s
Clone Tracker (14-day rolling) / Snapshot clones for ruflo ecosystem (push) Failing after 1s
CodeQL Advanced / Analyze (actions) (push) Failing after 1s
CodeQL Advanced / Analyze (javascript-typescript) (push) Failing after 1s
federation-peer-rust / stable-noop (push) Failing after 1s
metaharness-ci / score (push) Failing after 1s
metaharness-ci / router-compat (push) Failing after 0s
metaharness-ci / similarity-tests (push) Failing after 0s
no-agentbbs-smoke / smoke-without-agentbbs (push) Failing after 1s
V3 CI/CD Pipeline / Build V3 (windows-latest) (push) Has been skipped
codex-integration-audit / Codex integration audit (push) Failing after 1s
helpers-manifest-guard / guard (push) Failing after 1s
🔗 Cross-Agent Integration Tests / 🤝 Agent Coordination Tests (push) Has been skipped
🔗 Cross-Agent Integration Tests / 🧠 Memory Sharing Integration (push) Has been skipped
🔗 Cross-Agent Integration Tests / 🛡️ Fault Tolerance Tests (push) Has been skipped
🔗 Cross-Agent Integration Tests / ⚡ Performance Integration Tests (push) Has been skipped
metaharness-ci / mcp-scan (push) Failing after 1s
metaharness-ci / eject-dryrun (push) Failing after 1s
metaharness-ci / metaharness-real-data (push) Failing after 0s
no-cli-optdep-bloat-2561 / guard (push) Failing after 1s
no-metaharness-smoke / smoke-without-metaharness (push) Failing after 1s
no-phantom-agentic-flow-subpath / guard (push) Failing after 1s
🔄 Automated Rollback Manager / 🚨 Failure Detection (push) Failing after 1s
V3 CI/CD Pipeline / Plugin hooks smoke / ubuntu-latest / Node 22 (push) Failing after 1s
V3 CI/CD Pipeline / ruflo-graph-intelligence build + test smoke (#2044, ADR-123) (push) Failing after 1s
CVE Audit Gate / Audit root (critical-blocking) (push) Failing after 2s
cost-tracker-smoke / smoke (push) Failing after 3s
oia-audit-weekly / audit (push) Failing after 2s
ruflo-agent-smoke / ruflo-agent structural smoke (push) Failing after 1s
📊 Status Badges Update / 📊 Update Status Badges (push) Failing after 1s
V3 CI/CD Pipeline / Static regression guards (#2267 YAML + (push) Failing after 1s
V3 CI/CD Pipeline / Test V3 Packages (push) Failing after 0s
V3 CI/CD Pipeline / agent_execute provider routing smoke (#2042) (push) Failing after 0s
CVE Audit Gate / Audit v3 (critical-blocking) (push) Failing after 1s
federation-peer-rust / stable-native (push) Failing after 2s
🔗 Cross-Agent Integration Tests / 🚀 Integration Test Setup (push) Failing after 2s
neural-trader-smoke / runtime-smoke (push) Failing after 1s
V3 CI/CD Pipeline / Build V3 (macos-latest) (push) Has been skipped
V3 CI/CD Pipeline / Build V3 (ubuntu-latest) (push) Has been skipped
V3 CI/CD Pipeline / Type Check V3 (push) Failing after 1s
V3 CI/CD Pipeline / Smoke (no better-sqlite3) / ubuntu-latest / Node 24 (push) Failing after 1s
V3 CI/CD Pipeline / Smoke (no better-sqlite3) / ubuntu-latest / Node 22 (push) Failing after 2s
V3 CI/CD Pipeline / browser rvf create flag smoke (#2015) (push) Failing after 0s
V3 CI/CD Pipeline / Dependency review (#2046) (push) Has been skipped
V3 CI/CD Pipeline / Supply-chain audit (#2046) (push) Failing after 0s
V3 CI/CD Pipeline / witness marker drift smoke (#2021) (push) Failing after 1s
V3 CI/CD Pipeline / neural-trader portfolio CG smoke (#2068, ADR-126 Phase 3) (push) Failing after 1s
V3 CI/CD Pipeline / neural-trader backtest signing smoke (#2068, ADR-126 Phase 4) (push) Failing after 1s
V3 CI/CD Pipeline / kg-extract type-import classification smoke (#2049) (push) Failing after 0s
V3 CI/CD Pipeline / witness verify precondition smoke (#1880) (push) Failing after 2s
V3 CI/CD Pipeline / neural-trader pipeline risk-gate smoke (#2068, ADR-126 Phase 5) (push) Failing after 0s
V3 CI/CD Pipeline / neural-trader feature attribution smoke (#2068, ADR-126 Phase 6) (push) Failing after 0s
V3 CI/CD Pipeline / plugin-registry signature verification smoke (#1922, CWE-347) (push) Failing after 4s
V3 CI/CD Pipeline / memory stats legacy-DB smoke (#2120) (push) Failing after 4s
V3 CI/CD Pipeline / github deprecated actions smoke (#2089, ADR-127 Phase 3) (push) Failing after 1s
V3 CI/CD Pipeline / graph query + pathfinder smoke (ADR-130 P2+P5) (push) Has been skipped
V3 CI/CD Pipeline / graph trajectory hooks smoke (ADR-130 P3) (push) Has been skipped
V3 CI/CD Pipeline / graph plugin adapter smoke (ADR-130 P4) (push) Has been skipped
V3 CI/CD Pipeline / graph benchmark (ADR-130 P6) (push) Has been skipped
V3 CI/CD Pipeline / statusline generator delegation smoke (#2195) (push) Failing after 1s
V3 CI/CD Pipeline / wizard init regression guard (#2206 (push) Failing after 1s
V3 CI/CD Pipeline / memory no-stray-db smoke (ADR-125 P7) (push) Failing after 1s
V3 CI/CD Pipeline / github-safe injection smoke (#2089, ADR-127 Phase 1) (push) Failing after 1s
V3 CI/CD Pipeline / github actions pin smoke (#2089, ADR-127 Phase 1) (push) Failing after 1s
V3 CI/CD Pipeline / github attribution opt-in smoke (#2089, ADR-127 Phase 4) (push) Failing after 1s
V3 CI/CD Pipeline / pre-bash hook safety smoke (#2017) (push) Failing after 1s
V3 CI/CD Pipeline / Memory import smoke / ubuntu-latest (push) Failing after 0s
V3 CI/CD Pipeline / MCP protocol smoke / ubuntu-latest (push) Failing after 2s
V3 CI/CD Pipeline / ruvllm WASM auto-init smoke (#2086) (push) Failing after 4s
V3 CI/CD Pipeline / MCP paired-tool round-trip smoke (#1889) (push) Failing after 1s
V3 CI/CD Pipeline / Plugin package install-safety (#1902/#1903/#1904) (push) Failing after 1s
V3 CI/CD Pipeline / Tool description discoverability (ADR-112) (push) Failing after 3s
V3 CI/CD Pipeline / CLI npx-install smoke (#1147 / (22) (push) Failing after 1s
V3 CI/CD Pipeline / CLI npx-install smoke (#1147 / (24) (push) Failing after 1s
V3 CI/CD Pipeline / Windows hook shim smoke (#2132) / ubuntu-latest (push) Failing after 2s
V3 CI/CD Pipeline / Windows hook execution smoke (#2132) / ubuntu-latest (push) Failing after 1s
V3 CI/CD Pipeline / Windows init hooks smoke (#2132) / ubuntu-latest (push) Failing after 1s
V3 CI/CD Pipeline / Vector-index dimension audit (#1947) (push) Failing after 0s
V3 CI/CD Pipeline / Hook-command install safety (#1921) (push) Failing after 1s
V3 CI/CD Pipeline / ToolOutputGuardrail smoke (ADR-131, (push) Failing after 1s
V3 CI/CD Pipeline / init-bundle invariants smoke (#2095, ADR-128 Phase 5) (push) Failing after 1s
V3 CI/CD Pipeline / wasm provider bridge smoke (ADR-129 P1) (push) Failing after 2s
V3 CI/CD Pipeline / wasm gallery CRUD smoke (ADR-129 P3) (push) Failing after 1s
V3 CI/CD Pipeline / wasm plugin bridge smoke (ADR-129 P4) (push) Failing after 0s
V3 CI/CD Pipeline / wasm compose smoke (ADR-129 P2) (push) Failing after 4s
V3 CI/CD Pipeline / graph schema smoke (ADR-130 P1) (push) Failing after 0s
Validate Marketplace / validate (push) Failing after 1s
🔍 Verification Pipeline / 🚀 Setup Verification (push) Failing after 1s
🔍 Verification Pipeline / 🛡️ Security Verification (push) Has been skipped
🔍 Verification Pipeline / 📝 Code Quality (push) Has been skipped
🔍 Verification Pipeline / 🧪 Test Verification (${{ matrix.os }}, Node ${{ matrix.node }}) (push) Has been skipped
🔍 Verification Pipeline / 🏗️ Build Verification (push) Has been skipped
🔍 Verification Pipeline / 📚 Documentation Verification (push) Has been skipped
CVE Audit Gate / High-severity report (warn only) (push) Has been cancelled
🔄 Automated Rollback Manager / 🔄 Execute Rollback (push) Has been cancelled
🔄 Automated Rollback Manager / ✅ Post-Rollback Verification (push) Has been cancelled
🔄 Automated Rollback Manager / 📊 Rollback Monitoring (push) Has been cancelled
V3 CI/CD Pipeline / Windows init hooks smoke (#2132) / windows-latest (push) Has been cancelled
V3 CI/CD Pipeline / Windows hook execution smoke (#2132) / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Windows hook execution smoke (#2132) / windows-latest (push) Has been cancelled
🔄 Automated Rollback Manager / ⏳ Manual Rollback Approval (push) Has been cancelled
V3 CI/CD Pipeline / MCP protocol smoke / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Memory import smoke / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Windows hook shim smoke (#2132) / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Windows hook shim smoke (#2132) / windows-latest (push) Has been cancelled
V3 CI/CD Pipeline / Windows init hooks smoke (#2132) / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Witness verify (signed manifest) / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Witness verify (signed manifest) / ubuntu-latest (push) Has been cancelled
V3 CI/CD Pipeline / Witness verify (signed manifest) / windows-latest (push) Has been cancelled
V3 CI/CD Pipeline / Publish to npm (alpha) (push) Has been cancelled
V3 CI/CD Pipeline / Smoke (no better-sqlite3) / macos-latest / Node 22 (push) Has been cancelled
V3 CI/CD Pipeline / Plugin hooks smoke / macos-latest / Node 22 (push) Has been cancelled
CI/CD Pipeline / Deploy & Release (push) Has been cancelled
CI/CD Pipeline / CI Status (push) Has been cancelled
🔗 Cross-Agent Integration Tests / 📊 Integration Test Report (push) Has been cancelled
🔄 Automated Rollback Manager / 🔍 Pre-Rollback Validation (push) Has been cancelled
🔍 Verification Pipeline / ⚡ Performance Verification (push) Has been cancelled
🔍 Verification Pipeline / 📊 Verification Report (push) Has been cancelled
748 lines
25 KiB
TypeScript
748 lines
25 KiB
TypeScript
/**
|
|
* Tests for Memory Write Gating System
|
|
*/
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import {
|
|
MemoryWriteGate,
|
|
createMemoryWriteGate,
|
|
createMemoryEntry,
|
|
type MemoryAuthority,
|
|
type MemoryEntry,
|
|
} from '../src/memory-gate.js';
|
|
|
|
// ============================================================================
|
|
// Test Helpers
|
|
// ============================================================================
|
|
|
|
function makeAuthority(overrides: Partial<MemoryAuthority> = {}): MemoryAuthority {
|
|
return {
|
|
agentId: 'agent-1',
|
|
role: 'worker',
|
|
namespaces: ['default', 'shared'],
|
|
maxWritesPerMinute: 60,
|
|
canDelete: false,
|
|
canOverwrite: true,
|
|
trustLevel: 0.8,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function makeEntry(overrides: Partial<MemoryEntry> = {}): MemoryEntry {
|
|
const authority = makeAuthority();
|
|
return {
|
|
key: 'test-key',
|
|
namespace: 'default',
|
|
value: 'test value',
|
|
valueHash: 'abc123',
|
|
authority,
|
|
createdAt: Date.now(),
|
|
updatedAt: Date.now(),
|
|
ttlMs: null,
|
|
decayRate: 0,
|
|
confidence: 1,
|
|
lineage: { operation: 'create' },
|
|
contradictions: [],
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
// ============================================================================
|
|
// Authority Check Tests
|
|
// ============================================================================
|
|
|
|
describe('MemoryWriteGate', () => {
|
|
let gate: MemoryWriteGate;
|
|
|
|
beforeEach(() => {
|
|
gate = new MemoryWriteGate();
|
|
});
|
|
|
|
describe('authority checks', () => {
|
|
it('should allow a worker to write to an allowed namespace', () => {
|
|
const authority = makeAuthority({ role: 'worker', namespaces: ['default'] });
|
|
const decision = gate.evaluateWrite(authority, 'key1', 'default', 'value');
|
|
|
|
expect(decision.allowed).toBe(true);
|
|
expect(decision.authorityCheck.passed).toBe(true);
|
|
expect(decision.authorityCheck.actualRole).toBe('worker');
|
|
});
|
|
|
|
it('should block a worker writing to a disallowed namespace', () => {
|
|
const authority = makeAuthority({ role: 'worker', namespaces: ['default'] });
|
|
const decision = gate.evaluateWrite(authority, 'key1', 'restricted', 'value');
|
|
|
|
expect(decision.allowed).toBe(false);
|
|
expect(decision.authorityCheck.passed).toBe(false);
|
|
});
|
|
|
|
it('should block an observer from writing', () => {
|
|
const authority = makeAuthority({ role: 'observer', namespaces: ['default'] });
|
|
const decision = gate.evaluateWrite(authority, 'key1', 'default', 'value');
|
|
|
|
expect(decision.allowed).toBe(false);
|
|
expect(decision.authorityCheck.passed).toBe(false);
|
|
expect(decision.authorityCheck.requiredRole).toBe('worker');
|
|
expect(decision.authorityCheck.actualRole).toBe('observer');
|
|
});
|
|
|
|
it('should allow a queen to write to any namespace', () => {
|
|
const authority = makeAuthority({ role: 'queen', namespaces: [] });
|
|
const decision = gate.evaluateWrite(authority, 'key1', 'any-namespace', 'value');
|
|
|
|
expect(decision.allowed).toBe(true);
|
|
expect(decision.authorityCheck.passed).toBe(true);
|
|
});
|
|
|
|
it('should allow a coordinator to write to allowed namespaces', () => {
|
|
const authority = makeAuthority({
|
|
role: 'coordinator',
|
|
namespaces: ['coordination', 'shared'],
|
|
});
|
|
const decision = gate.evaluateWrite(authority, 'key1', 'coordination', 'value');
|
|
|
|
expect(decision.allowed).toBe(true);
|
|
expect(decision.authorityCheck.passed).toBe(true);
|
|
});
|
|
|
|
it('should block a coordinator writing to disallowed namespace', () => {
|
|
const authority = makeAuthority({
|
|
role: 'coordinator',
|
|
namespaces: ['coordination'],
|
|
});
|
|
const decision = gate.evaluateWrite(authority, 'key1', 'private', 'value');
|
|
|
|
expect(decision.allowed).toBe(false);
|
|
expect(decision.authorityCheck.passed).toBe(false);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Rate Limiting Tests
|
|
// ============================================================================
|
|
|
|
describe('rate limiting', () => {
|
|
it('should allow writes within the rate limit', () => {
|
|
const authority = makeAuthority({ maxWritesPerMinute: 5 });
|
|
|
|
for (let i = 0; i < 5; i++) {
|
|
const decision = gate.evaluateWrite(authority, `key-${i}`, 'default', 'value');
|
|
expect(decision.allowed).toBe(true);
|
|
expect(decision.rateCheck.passed).toBe(true);
|
|
}
|
|
});
|
|
|
|
it('should block writes exceeding the rate limit', () => {
|
|
const authority = makeAuthority({ maxWritesPerMinute: 3 });
|
|
|
|
// Use up the rate limit
|
|
for (let i = 0; i < 3; i++) {
|
|
gate.evaluateWrite(authority, `key-${i}`, 'default', 'value');
|
|
}
|
|
|
|
// This should be blocked
|
|
const decision = gate.evaluateWrite(authority, 'key-extra', 'default', 'value');
|
|
expect(decision.allowed).toBe(false);
|
|
expect(decision.rateCheck.passed).toBe(false);
|
|
expect(decision.rateCheck.writesInWindow).toBe(3);
|
|
expect(decision.rateCheck.limit).toBe(3);
|
|
});
|
|
|
|
it('should track rate limits per agent independently', () => {
|
|
const authority1 = makeAuthority({ agentId: 'agent-1', maxWritesPerMinute: 2 });
|
|
const authority2 = makeAuthority({ agentId: 'agent-2', maxWritesPerMinute: 2 });
|
|
|
|
// Fill up agent-1's limit
|
|
gate.evaluateWrite(authority1, 'key1', 'default', 'value');
|
|
gate.evaluateWrite(authority1, 'key2', 'default', 'value');
|
|
|
|
// agent-2 should still be allowed
|
|
const decision = gate.evaluateWrite(authority2, 'key3', 'default', 'value');
|
|
expect(decision.allowed).toBe(true);
|
|
|
|
// agent-1 should be blocked
|
|
const blocked = gate.evaluateWrite(authority1, 'key4', 'default', 'value');
|
|
expect(blocked.allowed).toBe(false);
|
|
});
|
|
|
|
it('should report rate limit status', () => {
|
|
const authority = makeAuthority({ agentId: 'rate-agent', maxWritesPerMinute: 10 });
|
|
gate.registerAuthority(authority);
|
|
|
|
gate.evaluateWrite(authority, 'k1', 'default', 'v');
|
|
gate.evaluateWrite(authority, 'k2', 'default', 'v');
|
|
|
|
const status = gate.getRateLimitStatus('rate-agent');
|
|
expect(status.writesInWindow).toBe(2);
|
|
expect(status.limit).toBe(10);
|
|
expect(status.resetAt).toBeGreaterThan(Date.now() - 1000);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Overwrite Permission Tests
|
|
// ============================================================================
|
|
|
|
describe('overwrite permissions', () => {
|
|
it('should allow overwrite when authority has canOverwrite', () => {
|
|
const authority = makeAuthority({ canOverwrite: true });
|
|
const existing = makeEntry({ key: 'shared-key', namespace: 'default' });
|
|
|
|
const decision = gate.evaluateWrite(
|
|
authority,
|
|
'shared-key',
|
|
'default',
|
|
'new value',
|
|
[existing]
|
|
);
|
|
|
|
expect(decision.allowed).toBe(true);
|
|
expect(decision.overwriteCheck.isOverwrite).toBe(true);
|
|
expect(decision.overwriteCheck.allowed).toBe(true);
|
|
});
|
|
|
|
it('should block overwrite when authority lacks canOverwrite', () => {
|
|
const authority = makeAuthority({ canOverwrite: false });
|
|
const existing = makeEntry({ key: 'shared-key', namespace: 'default' });
|
|
|
|
const decision = gate.evaluateWrite(
|
|
authority,
|
|
'shared-key',
|
|
'default',
|
|
'new value',
|
|
[existing]
|
|
);
|
|
|
|
expect(decision.allowed).toBe(false);
|
|
expect(decision.overwriteCheck.isOverwrite).toBe(true);
|
|
expect(decision.overwriteCheck.allowed).toBe(false);
|
|
expect(decision.reason).toContain('Overwrite not permitted');
|
|
});
|
|
|
|
it('should not flag as overwrite when key does not exist', () => {
|
|
const authority = makeAuthority({ canOverwrite: false });
|
|
const existing = makeEntry({ key: 'other-key', namespace: 'default' });
|
|
|
|
const decision = gate.evaluateWrite(
|
|
authority,
|
|
'new-key',
|
|
'default',
|
|
'value',
|
|
[existing]
|
|
);
|
|
|
|
expect(decision.overwriteCheck.isOverwrite).toBe(false);
|
|
expect(decision.overwriteCheck.allowed).toBe(true);
|
|
});
|
|
|
|
it('should not flag as overwrite when existing entries is empty', () => {
|
|
const authority = makeAuthority({ canOverwrite: false });
|
|
|
|
const decision = gate.evaluateWrite(authority, 'key', 'default', 'value', []);
|
|
|
|
expect(decision.overwriteCheck.isOverwrite).toBe(false);
|
|
expect(decision.overwriteCheck.allowed).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Contradiction Detection Tests
|
|
// ============================================================================
|
|
|
|
describe('contradiction detection', () => {
|
|
it('should detect "must" vs "never" contradictions', () => {
|
|
const existing = makeEntry({
|
|
key: 'rule-1',
|
|
value: 'You must always validate inputs',
|
|
});
|
|
|
|
const contradictions = gate.detectContradictions(
|
|
'You should never validate inputs',
|
|
[existing]
|
|
);
|
|
|
|
expect(contradictions.length).toBeGreaterThan(0);
|
|
expect(contradictions[0].entryKey).toBe('rule-1');
|
|
});
|
|
|
|
it('should detect "always" vs "never" contradictions', () => {
|
|
const existing = makeEntry({
|
|
key: 'rule-2',
|
|
value: 'Always use encryption for data at rest',
|
|
});
|
|
|
|
const contradictions = gate.detectContradictions(
|
|
'Never use encryption for temporary files',
|
|
[existing]
|
|
);
|
|
|
|
expect(contradictions.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('should detect "require" vs "forbid" contradictions', () => {
|
|
const existing = makeEntry({
|
|
key: 'rule-3',
|
|
value: 'We require two-factor authentication',
|
|
});
|
|
|
|
const contradictions = gate.detectContradictions(
|
|
'We forbid two-factor authentication for API keys',
|
|
[existing]
|
|
);
|
|
|
|
expect(contradictions.length).toBeGreaterThan(0);
|
|
expect(contradictions[0].description).toContain('require');
|
|
});
|
|
|
|
it('should detect "enable" vs "disable" contradictions', () => {
|
|
const existing = makeEntry({
|
|
key: 'config-1',
|
|
value: 'Enable debug logging in production',
|
|
});
|
|
|
|
const contradictions = gate.detectContradictions(
|
|
'Disable debug logging everywhere',
|
|
[existing]
|
|
);
|
|
|
|
expect(contradictions.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('should return empty when no contradictions exist', () => {
|
|
const existing = makeEntry({
|
|
key: 'fact-1',
|
|
value: 'The server runs on port 3000',
|
|
});
|
|
|
|
const contradictions = gate.detectContradictions(
|
|
'The database runs on port 5432',
|
|
[existing]
|
|
);
|
|
|
|
expect(contradictions.length).toBe(0);
|
|
});
|
|
|
|
it('should include contradictions in write decision', () => {
|
|
const authority = makeAuthority();
|
|
const existing = makeEntry({
|
|
key: 'policy-1',
|
|
namespace: 'default',
|
|
value: 'You must use TypeScript for all new code',
|
|
});
|
|
|
|
const decision = gate.evaluateWrite(
|
|
authority,
|
|
'policy-2',
|
|
'default',
|
|
'You should never use TypeScript',
|
|
[existing]
|
|
);
|
|
|
|
expect(decision.contradictions.length).toBeGreaterThan(0);
|
|
expect(decision.contradictions[0].existingKey).toBe('policy-1');
|
|
});
|
|
|
|
it('should not detect contradictions when tracking is disabled', () => {
|
|
const gateNoTracking = new MemoryWriteGate({
|
|
enableContradictionTracking: false,
|
|
});
|
|
const authority = makeAuthority();
|
|
const existing = makeEntry({
|
|
key: 'rule-x',
|
|
namespace: 'default',
|
|
value: 'You must always do X',
|
|
});
|
|
|
|
const decision = gateNoTracking.evaluateWrite(
|
|
authority,
|
|
'rule-y',
|
|
'default',
|
|
'You should never do X',
|
|
[existing]
|
|
);
|
|
|
|
expect(decision.contradictions.length).toBe(0);
|
|
});
|
|
|
|
it('should detect contradictions across multiple existing entries', () => {
|
|
const entries = [
|
|
makeEntry({ key: 'r1', value: 'Always use strict mode' }),
|
|
makeEntry({ key: 'r2', value: 'Require code reviews' }),
|
|
makeEntry({ key: 'r3', value: 'The sky is blue' }),
|
|
];
|
|
|
|
const contradictions = gate.detectContradictions(
|
|
"Don't use strict mode and forbid code reviews",
|
|
entries
|
|
);
|
|
|
|
expect(contradictions.length).toBe(2);
|
|
const keys = contradictions.map((c) => c.entryKey);
|
|
expect(keys).toContain('r1');
|
|
expect(keys).toContain('r2');
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// TTL Expiry Tests
|
|
// ============================================================================
|
|
|
|
describe('TTL expiry', () => {
|
|
it('should identify expired entries', () => {
|
|
const now = Date.now();
|
|
const entries = [
|
|
makeEntry({ key: 'expired', createdAt: now - 10_000, ttlMs: 5_000 }),
|
|
makeEntry({ key: 'still-valid', createdAt: now - 1_000, ttlMs: 5_000 }),
|
|
makeEntry({ key: 'no-ttl', createdAt: now - 100_000, ttlMs: null }),
|
|
];
|
|
|
|
const expired = gate.getExpiredEntries(entries);
|
|
|
|
expect(expired.length).toBe(1);
|
|
expect(expired[0].key).toBe('expired');
|
|
});
|
|
|
|
it('should return empty when no entries are expired', () => {
|
|
const now = Date.now();
|
|
const entries = [
|
|
makeEntry({ key: 'valid-1', createdAt: now, ttlMs: 60_000 }),
|
|
makeEntry({ key: 'valid-2', createdAt: now, ttlMs: null }),
|
|
];
|
|
|
|
const expired = gate.getExpiredEntries(entries);
|
|
expect(expired.length).toBe(0);
|
|
});
|
|
|
|
it('should handle all entries expired', () => {
|
|
const now = Date.now();
|
|
const entries = [
|
|
makeEntry({ key: 'old-1', createdAt: now - 20_000, ttlMs: 1_000 }),
|
|
makeEntry({ key: 'old-2', createdAt: now - 30_000, ttlMs: 1_000 }),
|
|
];
|
|
|
|
const expired = gate.getExpiredEntries(entries);
|
|
expect(expired.length).toBe(2);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Confidence Decay Tests
|
|
// ============================================================================
|
|
|
|
describe('confidence decay', () => {
|
|
it('should return full confidence when decayRate is 0', () => {
|
|
const entry = makeEntry({
|
|
confidence: 0.9,
|
|
decayRate: 0,
|
|
updatedAt: Date.now() - 3_600_000, // 1 hour ago
|
|
});
|
|
|
|
const confidence = gate.computeConfidence(entry);
|
|
expect(confidence).toBe(0.9);
|
|
});
|
|
|
|
it('should return 0 confidence when decayRate is 1', () => {
|
|
const entry = makeEntry({
|
|
confidence: 1,
|
|
decayRate: 1,
|
|
updatedAt: Date.now() - 1_000, // 1 second ago
|
|
});
|
|
|
|
const confidence = gate.computeConfidence(entry);
|
|
expect(confidence).toBe(0);
|
|
});
|
|
|
|
it('should decay confidence over time', () => {
|
|
const oneHourAgo = Date.now() - 3_600_000;
|
|
const entry = makeEntry({
|
|
confidence: 1.0,
|
|
decayRate: 0.5,
|
|
updatedAt: oneHourAgo,
|
|
});
|
|
|
|
const confidence = gate.computeConfidence(entry);
|
|
|
|
// After 1 hour with decayRate 0.5: 1.0 * e^(-0.5 * 1) ~ 0.6065
|
|
expect(confidence).toBeGreaterThan(0.5);
|
|
expect(confidence).toBeLessThan(0.7);
|
|
});
|
|
|
|
it('should return near-full confidence for very recent entries', () => {
|
|
const entry = makeEntry({
|
|
confidence: 1.0,
|
|
decayRate: 0.1,
|
|
updatedAt: Date.now() - 100, // 100ms ago
|
|
});
|
|
|
|
const confidence = gate.computeConfidence(entry);
|
|
expect(confidence).toBeGreaterThan(0.99);
|
|
});
|
|
|
|
it('should identify decayed entries below threshold', () => {
|
|
const twoHoursAgo = Date.now() - 7_200_000;
|
|
const entries = [
|
|
makeEntry({
|
|
key: 'fresh',
|
|
confidence: 1.0,
|
|
decayRate: 0.1,
|
|
updatedAt: Date.now(),
|
|
}),
|
|
makeEntry({
|
|
key: 'decayed',
|
|
confidence: 1.0,
|
|
decayRate: 2.0,
|
|
updatedAt: twoHoursAgo,
|
|
}),
|
|
makeEntry({
|
|
key: 'no-decay',
|
|
confidence: 0.5,
|
|
decayRate: 0,
|
|
updatedAt: twoHoursAgo,
|
|
}),
|
|
];
|
|
|
|
const decayed = gate.getDecayedEntries(entries, 0.3);
|
|
|
|
expect(decayed.length).toBe(1);
|
|
expect(decayed[0].key).toBe('decayed');
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Lineage Tracking Tests
|
|
// ============================================================================
|
|
|
|
describe('lineage tracking', () => {
|
|
it('should create entries with lineage via createMemoryEntry', () => {
|
|
const authority = makeAuthority();
|
|
const entry = createMemoryEntry('derived-key', 'default', 'derived value', authority, {
|
|
lineage: {
|
|
parentKey: 'parent-key',
|
|
derivedFrom: ['source-1', 'source-2'],
|
|
operation: 'merge',
|
|
},
|
|
});
|
|
|
|
expect(entry.lineage.parentKey).toBe('parent-key');
|
|
expect(entry.lineage.derivedFrom).toEqual(['source-1', 'source-2']);
|
|
expect(entry.lineage.operation).toBe('merge');
|
|
});
|
|
|
|
it('should default lineage operation to "create"', () => {
|
|
const authority = makeAuthority();
|
|
const entry = createMemoryEntry('new-key', 'default', 'value', authority);
|
|
|
|
expect(entry.lineage.operation).toBe('create');
|
|
expect(entry.lineage.parentKey).toBeUndefined();
|
|
expect(entry.lineage.derivedFrom).toBeUndefined();
|
|
});
|
|
|
|
it('should preserve lineage through entry creation', () => {
|
|
const authority = makeAuthority();
|
|
const entry = createMemoryEntry('key', 'ns', { data: 123 }, authority, {
|
|
lineage: {
|
|
derivedFrom: ['a', 'b', 'c'],
|
|
operation: 'aggregate',
|
|
},
|
|
});
|
|
|
|
expect(entry.lineage.derivedFrom).toHaveLength(3);
|
|
expect(entry.lineage.operation).toBe('aggregate');
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Factory Function Tests
|
|
// ============================================================================
|
|
|
|
describe('createMemoryWriteGate', () => {
|
|
it('should create a gate with default config', () => {
|
|
const gate = createMemoryWriteGate();
|
|
expect(gate).toBeInstanceOf(MemoryWriteGate);
|
|
expect(gate.isContradictionTrackingEnabled()).toBe(true);
|
|
});
|
|
|
|
it('should create a gate with custom config', () => {
|
|
const authority = makeAuthority({ agentId: 'pre-reg' });
|
|
const gate = createMemoryWriteGate({
|
|
authorities: [authority],
|
|
contradictionThreshold: 0.8,
|
|
defaultTtlMs: 30_000,
|
|
defaultDecayRate: 0.1,
|
|
enableContradictionTracking: false,
|
|
});
|
|
|
|
expect(gate.isContradictionTrackingEnabled()).toBe(false);
|
|
expect(gate.getDefaultTtlMs()).toBe(30_000);
|
|
expect(gate.getDefaultDecayRate()).toBe(0.1);
|
|
expect(gate.getAuthorityFor('pre-reg')).toBeDefined();
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Authority Registration Tests
|
|
// ============================================================================
|
|
|
|
describe('registerAuthority', () => {
|
|
it('should register and retrieve an authority', () => {
|
|
const authority = makeAuthority({ agentId: 'new-agent' });
|
|
gate.registerAuthority(authority);
|
|
|
|
const retrieved = gate.getAuthorityFor('new-agent');
|
|
expect(retrieved).toBeDefined();
|
|
expect(retrieved!.agentId).toBe('new-agent');
|
|
expect(retrieved!.role).toBe('worker');
|
|
});
|
|
|
|
it('should update an existing authority', () => {
|
|
const authority1 = makeAuthority({ agentId: 'agent-x', role: 'worker' });
|
|
gate.registerAuthority(authority1);
|
|
|
|
const authority2 = makeAuthority({ agentId: 'agent-x', role: 'coordinator' });
|
|
gate.registerAuthority(authority2);
|
|
|
|
const retrieved = gate.getAuthorityFor('agent-x');
|
|
expect(retrieved!.role).toBe('coordinator');
|
|
});
|
|
|
|
it('should return undefined for unknown agent', () => {
|
|
const retrieved = gate.getAuthorityFor('nonexistent');
|
|
expect(retrieved).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Contradiction Resolution Tests
|
|
// ============================================================================
|
|
|
|
describe('resolveContradiction', () => {
|
|
it('should store and retrieve a resolution', () => {
|
|
gate.resolveContradiction('entry-1', 'Resolved by promoting entry-1 as authoritative');
|
|
|
|
const resolution = gate.getContradictionResolution('entry-1');
|
|
expect(resolution).toBe('Resolved by promoting entry-1 as authoritative');
|
|
});
|
|
|
|
it('should return undefined for unresolved contradictions', () => {
|
|
const resolution = gate.getContradictionResolution('unresolved-key');
|
|
expect(resolution).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// createMemoryEntry Helper Tests
|
|
// ============================================================================
|
|
|
|
describe('createMemoryEntry', () => {
|
|
it('should create an entry with computed value hash', () => {
|
|
const authority = makeAuthority();
|
|
const entry = createMemoryEntry('k', 'ns', { hello: 'world' }, authority);
|
|
|
|
expect(entry.key).toBe('k');
|
|
expect(entry.namespace).toBe('ns');
|
|
expect(entry.value).toEqual({ hello: 'world' });
|
|
expect(entry.valueHash).toBeTruthy();
|
|
expect(entry.valueHash.length).toBe(64); // SHA-256 hex length
|
|
});
|
|
|
|
it('should apply TTL and decay options', () => {
|
|
const authority = makeAuthority();
|
|
const entry = createMemoryEntry('k', 'ns', 'v', authority, {
|
|
ttlMs: 60_000,
|
|
decayRate: 0.3,
|
|
confidence: 0.75,
|
|
});
|
|
|
|
expect(entry.ttlMs).toBe(60_000);
|
|
expect(entry.decayRate).toBe(0.3);
|
|
expect(entry.confidence).toBe(0.75);
|
|
});
|
|
|
|
it('should default to no TTL, no decay, full confidence', () => {
|
|
const authority = makeAuthority();
|
|
const entry = createMemoryEntry('k', 'ns', 'v', authority);
|
|
|
|
expect(entry.ttlMs).toBeNull();
|
|
expect(entry.decayRate).toBe(0);
|
|
expect(entry.confidence).toBe(1);
|
|
});
|
|
|
|
it('should set timestamps close to now', () => {
|
|
const before = Date.now();
|
|
const authority = makeAuthority();
|
|
const entry = createMemoryEntry('k', 'ns', 'v', authority);
|
|
const after = Date.now();
|
|
|
|
expect(entry.createdAt).toBeGreaterThanOrEqual(before);
|
|
expect(entry.createdAt).toBeLessThanOrEqual(after);
|
|
expect(entry.updatedAt).toBe(entry.createdAt);
|
|
});
|
|
|
|
it('should initialize with empty contradictions', () => {
|
|
const authority = makeAuthority();
|
|
const entry = createMemoryEntry('k', 'ns', 'v', authority);
|
|
|
|
expect(entry.contradictions).toEqual([]);
|
|
});
|
|
|
|
it('should produce different hashes for different values', () => {
|
|
const authority = makeAuthority();
|
|
const entry1 = createMemoryEntry('k', 'ns', 'value-a', authority);
|
|
const entry2 = createMemoryEntry('k', 'ns', 'value-b', authority);
|
|
|
|
expect(entry1.valueHash).not.toBe(entry2.valueHash);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Combined Scenarios
|
|
// ============================================================================
|
|
|
|
describe('combined write evaluation', () => {
|
|
it('should report multiple failure reasons', () => {
|
|
// Observer trying to overwrite in a wrong namespace
|
|
const authority = makeAuthority({
|
|
role: 'observer',
|
|
namespaces: ['other'],
|
|
canOverwrite: false,
|
|
});
|
|
const existing = makeEntry({ key: 'target', namespace: 'default' });
|
|
|
|
const decision = gate.evaluateWrite(
|
|
authority,
|
|
'target',
|
|
'default',
|
|
'new value',
|
|
[existing]
|
|
);
|
|
|
|
expect(decision.allowed).toBe(false);
|
|
expect(decision.authorityCheck.passed).toBe(false);
|
|
// The reason should contain failure info
|
|
expect(decision.reason).toContain('Authority check failed');
|
|
});
|
|
|
|
it('should allow a fully authorized write with no contradictions', () => {
|
|
const authority = makeAuthority({
|
|
role: 'queen',
|
|
canOverwrite: true,
|
|
maxWritesPerMinute: 100,
|
|
});
|
|
|
|
const decision = gate.evaluateWrite(
|
|
authority,
|
|
'new-fact',
|
|
'knowledge',
|
|
'The earth orbits the sun',
|
|
[]
|
|
);
|
|
|
|
expect(decision.allowed).toBe(true);
|
|
expect(decision.reason).toBe('Write allowed');
|
|
expect(decision.contradictions.length).toBe(0);
|
|
expect(decision.authorityCheck.passed).toBe(true);
|
|
expect(decision.rateCheck.passed).toBe(true);
|
|
expect(decision.overwriteCheck.isOverwrite).toBe(false);
|
|
});
|
|
});
|
|
});
|