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
733 lines
23 KiB
TypeScript
733 lines
23 KiB
TypeScript
/**
|
|
* Tests for the Coherence Scheduler & Economic Governor
|
|
*/
|
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
import {
|
|
CoherenceScheduler,
|
|
EconomicGovernor,
|
|
createCoherenceScheduler,
|
|
createEconomicGovernor,
|
|
} from '../src/coherence.js';
|
|
import type { CoherenceScore } from '../src/coherence.js';
|
|
import type { RunEvent, OptimizationMetrics, TaskIntent } from '../src/types.js';
|
|
|
|
// ============================================================================
|
|
// Test Helpers
|
|
// ============================================================================
|
|
|
|
function createMockEvent(overrides: Partial<RunEvent> = {}): RunEvent {
|
|
return {
|
|
eventId: `evt-${Math.random().toString(36).slice(2)}`,
|
|
taskId: `task-${Math.random().toString(36).slice(2)}`,
|
|
guidanceHash: 'abc123',
|
|
retrievedRuleIds: ['R001', 'R002'],
|
|
toolsUsed: ['Read', 'Edit'],
|
|
filesTouched: ['src/main.ts'],
|
|
diffSummary: { linesAdded: 50, linesRemoved: 10, filesChanged: 1 },
|
|
testResults: { ran: true, passed: 10, failed: 0, skipped: 0 },
|
|
violations: [],
|
|
outcomeAccepted: true,
|
|
reworkLines: 5,
|
|
intent: 'feature',
|
|
timestamp: Date.now(),
|
|
durationMs: 5000,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function createMockMetrics(overrides: Partial<OptimizationMetrics> = {}): OptimizationMetrics {
|
|
return {
|
|
violationRate: 0,
|
|
selfCorrectionRate: 1,
|
|
reworkLines: 0,
|
|
clarifyingQuestions: 0,
|
|
taskCount: 10,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
// ============================================================================
|
|
// CoherenceScheduler Tests
|
|
// ============================================================================
|
|
|
|
describe('CoherenceScheduler', () => {
|
|
let scheduler: CoherenceScheduler;
|
|
|
|
beforeEach(() => {
|
|
scheduler = new CoherenceScheduler();
|
|
});
|
|
|
|
describe('computeCoherence', () => {
|
|
it('should compute a healthy score with clean metrics and consistent events', () => {
|
|
const metrics = createMockMetrics({
|
|
violationRate: 0,
|
|
reworkLines: 0,
|
|
});
|
|
|
|
// All events have the same intent
|
|
const events = Array.from({ length: 10 }, () =>
|
|
createMockEvent({ intent: 'feature' }),
|
|
);
|
|
|
|
const score = scheduler.computeCoherence(metrics, events);
|
|
|
|
expect(score.overall).toBeGreaterThanOrEqual(0.7);
|
|
expect(score.violationComponent).toBe(1);
|
|
expect(score.reworkComponent).toBe(1);
|
|
expect(score.driftComponent).toBe(1);
|
|
expect(score.overall).toBe(1);
|
|
expect(score.windowSize).toBe(10);
|
|
expect(score.timestamp).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('should compute a degraded score with moderate violations and rework', () => {
|
|
const metrics = createMockMetrics({
|
|
violationRate: 5, // 5 violations per 10 tasks
|
|
reworkLines: 50, // 50 rework lines on average
|
|
});
|
|
|
|
const events = Array.from({ length: 10 }, () =>
|
|
createMockEvent({ intent: 'feature' }),
|
|
);
|
|
|
|
const score = scheduler.computeCoherence(metrics, events);
|
|
|
|
// violationComponent: 1 - (5/10) = 0.5
|
|
expect(score.violationComponent).toBe(0.5);
|
|
// reworkComponent: 1 - (50/100) = 0.5
|
|
expect(score.reworkComponent).toBe(0.5);
|
|
// driftComponent: all same intent = 1
|
|
expect(score.driftComponent).toBe(1);
|
|
// overall: 0.4*0.5 + 0.3*0.5 + 0.3*1 = 0.2 + 0.15 + 0.3 = 0.65
|
|
expect(score.overall).toBeCloseTo(0.65, 5);
|
|
});
|
|
|
|
it('should compute a critical score with high violations, rework, and drift', () => {
|
|
const metrics = createMockMetrics({
|
|
violationRate: 10, // max
|
|
reworkLines: 100, // max
|
|
});
|
|
|
|
// Every event has a different intent
|
|
const intents: TaskIntent[] = [
|
|
'bug-fix', 'feature', 'refactor', 'security',
|
|
'performance', 'testing', 'docs', 'deployment',
|
|
'architecture', 'debug',
|
|
];
|
|
const events = intents.map(intent => createMockEvent({ intent }));
|
|
|
|
const score = scheduler.computeCoherence(metrics, events);
|
|
|
|
// violationComponent: 1 - (10/10) = 0, clamped to 0
|
|
expect(score.violationComponent).toBe(0);
|
|
// reworkComponent: 1 - (100/100) = 0, clamped to 0
|
|
expect(score.reworkComponent).toBe(0);
|
|
// driftComponent: 10 unique / 10 events = 1 - (10-1)/(10-1) = 0
|
|
expect(score.driftComponent).toBe(0);
|
|
// overall: 0
|
|
expect(score.overall).toBe(0);
|
|
});
|
|
|
|
it('should clamp violation rate above 10 to 0', () => {
|
|
const metrics = createMockMetrics({ violationRate: 20 });
|
|
const events = [createMockEvent()];
|
|
|
|
const score = scheduler.computeCoherence(metrics, events);
|
|
expect(score.violationComponent).toBe(0);
|
|
});
|
|
|
|
it('should clamp rework lines above 100 to 0', () => {
|
|
const metrics = createMockMetrics({ reworkLines: 200 });
|
|
const events = [createMockEvent()];
|
|
|
|
const score = scheduler.computeCoherence(metrics, events);
|
|
expect(score.reworkComponent).toBe(0);
|
|
});
|
|
|
|
it('should handle empty events with drift component 1', () => {
|
|
const metrics = createMockMetrics();
|
|
const score = scheduler.computeCoherence(metrics, []);
|
|
|
|
expect(score.driftComponent).toBe(1);
|
|
expect(score.windowSize).toBe(0);
|
|
});
|
|
|
|
it('should respect custom window size', () => {
|
|
const customScheduler = new CoherenceScheduler({ windowSize: 5 });
|
|
const metrics = createMockMetrics();
|
|
const events = Array.from({ length: 20 }, () => createMockEvent());
|
|
|
|
const score = customScheduler.computeCoherence(metrics, events);
|
|
expect(score.windowSize).toBe(5);
|
|
});
|
|
|
|
it('should add scores to history', () => {
|
|
const metrics = createMockMetrics();
|
|
const events = [createMockEvent()];
|
|
|
|
scheduler.computeCoherence(metrics, events);
|
|
scheduler.computeCoherence(metrics, events);
|
|
scheduler.computeCoherence(metrics, events);
|
|
|
|
expect(scheduler.getScoreHistory()).toHaveLength(3);
|
|
});
|
|
});
|
|
|
|
describe('getPrivilegeLevel', () => {
|
|
it('should return "full" for healthy scores', () => {
|
|
const score: CoherenceScore = {
|
|
overall: 0.85,
|
|
violationComponent: 0.9,
|
|
reworkComponent: 0.8,
|
|
driftComponent: 0.85,
|
|
timestamp: Date.now(),
|
|
windowSize: 10,
|
|
};
|
|
expect(scheduler.getPrivilegeLevel(score)).toBe('full');
|
|
});
|
|
|
|
it('should return "full" at exactly the healthy threshold', () => {
|
|
const score: CoherenceScore = {
|
|
overall: 0.7,
|
|
violationComponent: 0.7,
|
|
reworkComponent: 0.7,
|
|
driftComponent: 0.7,
|
|
timestamp: Date.now(),
|
|
windowSize: 10,
|
|
};
|
|
expect(scheduler.getPrivilegeLevel(score)).toBe('full');
|
|
});
|
|
|
|
it('should return "restricted" between warning and healthy thresholds', () => {
|
|
const score: CoherenceScore = {
|
|
overall: 0.6,
|
|
violationComponent: 0.6,
|
|
reworkComponent: 0.6,
|
|
driftComponent: 0.6,
|
|
timestamp: Date.now(),
|
|
windowSize: 10,
|
|
};
|
|
expect(scheduler.getPrivilegeLevel(score)).toBe('restricted');
|
|
});
|
|
|
|
it('should return "read-only" between read-only and warning thresholds', () => {
|
|
const score: CoherenceScore = {
|
|
overall: 0.4,
|
|
violationComponent: 0.4,
|
|
reworkComponent: 0.4,
|
|
driftComponent: 0.4,
|
|
timestamp: Date.now(),
|
|
windowSize: 10,
|
|
};
|
|
expect(scheduler.getPrivilegeLevel(score)).toBe('read-only');
|
|
});
|
|
|
|
it('should return "suspended" below read-only threshold', () => {
|
|
const score: CoherenceScore = {
|
|
overall: 0.2,
|
|
violationComponent: 0.2,
|
|
reworkComponent: 0.2,
|
|
driftComponent: 0.2,
|
|
timestamp: Date.now(),
|
|
windowSize: 10,
|
|
};
|
|
expect(scheduler.getPrivilegeLevel(score)).toBe('suspended');
|
|
});
|
|
|
|
it('should use custom thresholds', () => {
|
|
const custom = new CoherenceScheduler({
|
|
thresholds: {
|
|
readOnlyThreshold: 0.2,
|
|
warningThreshold: 0.4,
|
|
healthyThreshold: 0.6,
|
|
},
|
|
});
|
|
|
|
const score: CoherenceScore = {
|
|
overall: 0.5,
|
|
violationComponent: 0.5,
|
|
reworkComponent: 0.5,
|
|
driftComponent: 0.5,
|
|
timestamp: Date.now(),
|
|
windowSize: 10,
|
|
};
|
|
|
|
// 0.5 is between warning (0.4) and healthy (0.6) with custom thresholds
|
|
expect(custom.getPrivilegeLevel(score)).toBe('restricted');
|
|
});
|
|
});
|
|
|
|
describe('drift detection', () => {
|
|
it('should detect drift when score is below warning threshold', () => {
|
|
const driftingScore: CoherenceScore = {
|
|
overall: 0.3,
|
|
violationComponent: 0.2,
|
|
reworkComponent: 0.3,
|
|
driftComponent: 0.4,
|
|
timestamp: Date.now(),
|
|
windowSize: 10,
|
|
};
|
|
|
|
expect(scheduler.isDrifting(driftingScore)).toBe(true);
|
|
});
|
|
|
|
it('should not detect drift when score is healthy', () => {
|
|
const healthyScore: CoherenceScore = {
|
|
overall: 0.8,
|
|
violationComponent: 0.9,
|
|
reworkComponent: 0.8,
|
|
driftComponent: 0.7,
|
|
timestamp: Date.now(),
|
|
windowSize: 10,
|
|
};
|
|
|
|
expect(scheduler.isDrifting(healthyScore)).toBe(false);
|
|
});
|
|
|
|
it('should not detect drift at exactly the warning threshold', () => {
|
|
const borderlineScore: CoherenceScore = {
|
|
overall: 0.5,
|
|
violationComponent: 0.5,
|
|
reworkComponent: 0.5,
|
|
driftComponent: 0.5,
|
|
timestamp: Date.now(),
|
|
windowSize: 10,
|
|
};
|
|
|
|
expect(scheduler.isDrifting(borderlineScore)).toBe(false);
|
|
});
|
|
|
|
it('should measure intent drift from diverse events', () => {
|
|
const metrics = createMockMetrics();
|
|
const intents: TaskIntent[] = [
|
|
'bug-fix', 'feature', 'refactor', 'security', 'performance',
|
|
];
|
|
const events = intents.map(intent => createMockEvent({ intent }));
|
|
|
|
const score = scheduler.computeCoherence(metrics, events);
|
|
|
|
// 5 unique intents out of 5 events: drift = 1 - (5-1)/(5-1) = 0
|
|
expect(score.driftComponent).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('isHealthy', () => {
|
|
it('should return true for scores at or above healthy threshold', () => {
|
|
expect(scheduler.isHealthy({ overall: 0.7 } as CoherenceScore)).toBe(true);
|
|
expect(scheduler.isHealthy({ overall: 1.0 } as CoherenceScore)).toBe(true);
|
|
});
|
|
|
|
it('should return false for scores below healthy threshold', () => {
|
|
expect(scheduler.isHealthy({ overall: 0.69 } as CoherenceScore)).toBe(false);
|
|
expect(scheduler.isHealthy({ overall: 0.0 } as CoherenceScore)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('shouldRestrict', () => {
|
|
it('should restrict when below warning threshold', () => {
|
|
expect(scheduler.shouldRestrict({ overall: 0.49 } as CoherenceScore)).toBe(true);
|
|
});
|
|
|
|
it('should not restrict at or above warning threshold', () => {
|
|
expect(scheduler.shouldRestrict({ overall: 0.5 } as CoherenceScore)).toBe(false);
|
|
expect(scheduler.shouldRestrict({ overall: 0.9 } as CoherenceScore)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('getRecommendation', () => {
|
|
it('should include escalation note for very high scores', () => {
|
|
const score: CoherenceScore = {
|
|
overall: 0.95,
|
|
violationComponent: 0.95,
|
|
reworkComponent: 0.95,
|
|
driftComponent: 0.95,
|
|
timestamp: Date.now(),
|
|
windowSize: 10,
|
|
};
|
|
|
|
const rec = scheduler.getRecommendation(score);
|
|
expect(rec).toContain('healthy');
|
|
expect(rec).toContain('escalation');
|
|
});
|
|
|
|
it('should warn about violations when violation component is low', () => {
|
|
const score: CoherenceScore = {
|
|
overall: 0.65,
|
|
violationComponent: 0.3,
|
|
reworkComponent: 0.8,
|
|
driftComponent: 0.85,
|
|
timestamp: Date.now(),
|
|
windowSize: 10,
|
|
};
|
|
|
|
const rec = scheduler.getRecommendation(score);
|
|
expect(rec).toContain('violation');
|
|
expect(rec).toContain('enforcement');
|
|
});
|
|
|
|
it('should warn about rework when rework component is low', () => {
|
|
const score: CoherenceScore = {
|
|
overall: 0.55,
|
|
violationComponent: 0.8,
|
|
reworkComponent: 0.3,
|
|
driftComponent: 0.55,
|
|
timestamp: Date.now(),
|
|
windowSize: 10,
|
|
};
|
|
|
|
const rec = scheduler.getRecommendation(score);
|
|
expect(rec).toContain('rework');
|
|
});
|
|
|
|
it('should warn about drift when drift component is low', () => {
|
|
const score: CoherenceScore = {
|
|
overall: 0.55,
|
|
violationComponent: 0.8,
|
|
reworkComponent: 0.8,
|
|
driftComponent: 0.3,
|
|
timestamp: Date.now(),
|
|
windowSize: 10,
|
|
};
|
|
|
|
const rec = scheduler.getRecommendation(score);
|
|
expect(rec).toContain('drift');
|
|
});
|
|
|
|
it('should mention suspension for very low scores', () => {
|
|
const score: CoherenceScore = {
|
|
overall: 0.1,
|
|
violationComponent: 0.1,
|
|
reworkComponent: 0.1,
|
|
driftComponent: 0.1,
|
|
timestamp: Date.now(),
|
|
windowSize: 10,
|
|
};
|
|
|
|
const rec = scheduler.getRecommendation(score);
|
|
expect(rec).toContain('suspended');
|
|
});
|
|
});
|
|
|
|
describe('score history', () => {
|
|
it('should cap history at 100 entries', () => {
|
|
const metrics = createMockMetrics();
|
|
const events = [createMockEvent()];
|
|
|
|
for (let i = 0; i < 120; i++) {
|
|
scheduler.computeCoherence(metrics, events);
|
|
}
|
|
|
|
expect(scheduler.getScoreHistory()).toHaveLength(100);
|
|
});
|
|
|
|
it('should preserve most recent scores when capped', () => {
|
|
const events = [createMockEvent()];
|
|
|
|
// First 50 with high violation rate
|
|
for (let i = 0; i < 50; i++) {
|
|
scheduler.computeCoherence(
|
|
createMockMetrics({ violationRate: 8 }),
|
|
events,
|
|
);
|
|
}
|
|
|
|
// Next 70 with zero violation rate
|
|
for (let i = 0; i < 70; i++) {
|
|
scheduler.computeCoherence(
|
|
createMockMetrics({ violationRate: 0 }),
|
|
events,
|
|
);
|
|
}
|
|
|
|
const history = scheduler.getScoreHistory();
|
|
expect(history).toHaveLength(100);
|
|
// The most recent entries should all have violationComponent = 1
|
|
const last = history[history.length - 1];
|
|
expect(last.violationComponent).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('factory function', () => {
|
|
it('should create a scheduler via createCoherenceScheduler', () => {
|
|
const s = createCoherenceScheduler({ windowSize: 50 });
|
|
expect(s).toBeInstanceOf(CoherenceScheduler);
|
|
});
|
|
|
|
it('should create a scheduler with defaults', () => {
|
|
const s = createCoherenceScheduler();
|
|
expect(s).toBeInstanceOf(CoherenceScheduler);
|
|
});
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// EconomicGovernor Tests
|
|
// ============================================================================
|
|
|
|
describe('EconomicGovernor', () => {
|
|
let governor: EconomicGovernor;
|
|
|
|
beforeEach(() => {
|
|
governor = new EconomicGovernor({
|
|
tokenLimit: 10_000,
|
|
toolCallLimit: 100,
|
|
storageLimit: 1_000_000,
|
|
timeLimit: 60_000, // 1 minute
|
|
costPerToken: 0.00001,
|
|
costPerToolCall: 0.001,
|
|
costLimit: 1.0,
|
|
});
|
|
});
|
|
|
|
describe('recordTokenUsage', () => {
|
|
it('should accumulate token usage', () => {
|
|
governor.recordTokenUsage(500);
|
|
governor.recordTokenUsage(300);
|
|
|
|
const usage = governor.getUsageSummary();
|
|
expect(usage.tokens.used).toBe(800);
|
|
expect(usage.tokens.limit).toBe(10_000);
|
|
expect(usage.tokens.percentage).toBeCloseTo(8.0, 1);
|
|
});
|
|
});
|
|
|
|
describe('recordToolCall', () => {
|
|
it('should track tool calls', () => {
|
|
governor.recordToolCall('Edit', 150);
|
|
governor.recordToolCall('Read', 50);
|
|
governor.recordToolCall('Bash', 1200);
|
|
|
|
const usage = governor.getUsageSummary();
|
|
expect(usage.toolCalls.used).toBe(3);
|
|
expect(usage.toolCalls.limit).toBe(100);
|
|
expect(usage.toolCalls.percentage).toBeCloseTo(3.0, 1);
|
|
});
|
|
});
|
|
|
|
describe('recordStorageUsage', () => {
|
|
it('should accumulate storage bytes', () => {
|
|
governor.recordStorageUsage(50_000);
|
|
governor.recordStorageUsage(25_000);
|
|
|
|
const usage = governor.getUsageSummary();
|
|
expect(usage.storage.usedBytes).toBe(75_000);
|
|
expect(usage.storage.limitBytes).toBe(1_000_000);
|
|
expect(usage.storage.percentage).toBeCloseTo(7.5, 1);
|
|
});
|
|
});
|
|
|
|
describe('checkBudget', () => {
|
|
it('should report within budget when usage is low', () => {
|
|
governor.recordTokenUsage(100);
|
|
governor.recordToolCall('Edit', 50);
|
|
|
|
const result = governor.checkBudget();
|
|
expect(result.withinBudget).toBe(true);
|
|
expect(result.alerts).toHaveLength(0);
|
|
});
|
|
|
|
it('should emit NOTICE alert at 75% usage', () => {
|
|
governor.recordTokenUsage(7_500); // 75% of 10,000
|
|
|
|
const result = governor.checkBudget();
|
|
expect(result.withinBudget).toBe(true);
|
|
expect(result.alerts.some(a => a.includes('NOTICE') && a.includes('tokens'))).toBe(true);
|
|
});
|
|
|
|
it('should emit WARNING alert at 90% usage', () => {
|
|
governor.recordTokenUsage(9_000); // 90%
|
|
|
|
const result = governor.checkBudget();
|
|
expect(result.withinBudget).toBe(true);
|
|
expect(result.alerts.some(a => a.includes('WARNING') && a.includes('tokens'))).toBe(true);
|
|
});
|
|
|
|
it('should emit CRITICAL alert at 95% usage', () => {
|
|
governor.recordTokenUsage(9_500); // 95%
|
|
|
|
const result = governor.checkBudget();
|
|
expect(result.withinBudget).toBe(true);
|
|
expect(result.alerts.some(a => a.includes('CRITICAL') && a.includes('tokens'))).toBe(true);
|
|
});
|
|
|
|
it('should report budget exceeded at 100%', () => {
|
|
governor.recordTokenUsage(10_001); // Over limit
|
|
|
|
const result = governor.checkBudget();
|
|
expect(result.withinBudget).toBe(false);
|
|
expect(result.alerts.some(a => a.includes('BUDGET EXCEEDED') && a.includes('tokens'))).toBe(true);
|
|
});
|
|
|
|
it('should generate alerts for multiple dimensions simultaneously', () => {
|
|
governor.recordTokenUsage(9_000); // 90% tokens
|
|
for (let i = 0; i < 95; i++) {
|
|
governor.recordToolCall('Edit', 10); // 95% tool calls
|
|
}
|
|
|
|
const result = governor.checkBudget();
|
|
expect(result.alerts.length).toBeGreaterThanOrEqual(2);
|
|
});
|
|
});
|
|
|
|
describe('budget alerts', () => {
|
|
it('should include cost alerts when cost exceeds thresholds', () => {
|
|
// costPerToken = 0.00001, costLimit = 1.0
|
|
// Need 75,000 tokens for 75% cost threshold ($0.75)
|
|
governor.recordTokenUsage(75_000);
|
|
|
|
const result = governor.checkBudget();
|
|
expect(result.alerts.some(a => a.includes('cost'))).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('getCostEstimate', () => {
|
|
it('should compute cost from token usage', () => {
|
|
governor.recordTokenUsage(1_000); // 1000 * 0.00001 = $0.01
|
|
|
|
const estimate = governor.getCostEstimate();
|
|
expect(estimate.totalCost).toBeCloseTo(0.01, 5);
|
|
expect(estimate.breakdown.tokens).toBeCloseTo(0.01, 5);
|
|
});
|
|
|
|
it('should compute cost from tool call usage', () => {
|
|
governor.recordToolCall('Edit', 100); // 1 * 0.001 = $0.001
|
|
|
|
const estimate = governor.getCostEstimate();
|
|
expect(estimate.breakdown.toolCalls).toBeCloseTo(0.001, 5);
|
|
});
|
|
|
|
it('should sum token and tool call costs', () => {
|
|
governor.recordTokenUsage(1_000); // $0.01
|
|
governor.recordToolCall('Edit', 100); // $0.001
|
|
|
|
const estimate = governor.getCostEstimate();
|
|
expect(estimate.totalCost).toBeCloseTo(0.011, 5);
|
|
});
|
|
|
|
it('should report zero cost with no usage', () => {
|
|
const estimate = governor.getCostEstimate();
|
|
expect(estimate.totalCost).toBe(0);
|
|
expect(estimate.breakdown.tokens).toBe(0);
|
|
expect(estimate.breakdown.toolCalls).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('estimateRemainingCapacity', () => {
|
|
it('should show full capacity at start', () => {
|
|
const remaining = governor.estimateRemainingCapacity();
|
|
expect(remaining.tokensRemaining).toBe(10_000);
|
|
expect(remaining.callsRemaining).toBe(100);
|
|
expect(remaining.timeRemainingMs).toBeLessThanOrEqual(60_000);
|
|
expect(remaining.timeRemainingMs).toBeGreaterThan(59_000);
|
|
});
|
|
|
|
it('should decrease remaining tokens after usage', () => {
|
|
governor.recordTokenUsage(4_000);
|
|
|
|
const remaining = governor.estimateRemainingCapacity();
|
|
expect(remaining.tokensRemaining).toBe(6_000);
|
|
});
|
|
|
|
it('should decrease remaining calls after usage', () => {
|
|
for (let i = 0; i < 30; i++) {
|
|
governor.recordToolCall('Edit', 10);
|
|
}
|
|
|
|
const remaining = governor.estimateRemainingCapacity();
|
|
expect(remaining.callsRemaining).toBe(70);
|
|
});
|
|
|
|
it('should not go below zero', () => {
|
|
governor.recordTokenUsage(20_000); // way over limit
|
|
|
|
const remaining = governor.estimateRemainingCapacity();
|
|
expect(remaining.tokensRemaining).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('resetPeriod', () => {
|
|
it('should reset all counters', () => {
|
|
governor.recordTokenUsage(5_000);
|
|
governor.recordToolCall('Edit', 100);
|
|
governor.recordStorageUsage(500_000);
|
|
|
|
governor.resetPeriod();
|
|
|
|
const usage = governor.getUsageSummary();
|
|
expect(usage.tokens.used).toBe(0);
|
|
expect(usage.toolCalls.used).toBe(0);
|
|
expect(usage.storage.usedBytes).toBe(0);
|
|
});
|
|
|
|
it('should reset cost estimate to zero', () => {
|
|
governor.recordTokenUsage(5_000);
|
|
governor.recordToolCall('Edit', 100);
|
|
|
|
governor.resetPeriod();
|
|
|
|
const estimate = governor.getCostEstimate();
|
|
expect(estimate.totalCost).toBe(0);
|
|
});
|
|
|
|
it('should reset time tracking', () => {
|
|
governor.resetPeriod();
|
|
|
|
const usage = governor.getUsageSummary();
|
|
// Time should be near zero since we just reset
|
|
expect(usage.time.usedMs).toBeLessThan(100);
|
|
});
|
|
|
|
it('should allow budget to pass again after reset', () => {
|
|
governor.recordTokenUsage(10_001); // Over limit
|
|
expect(governor.checkBudget().withinBudget).toBe(false);
|
|
|
|
governor.resetPeriod();
|
|
expect(governor.checkBudget().withinBudget).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('factory function', () => {
|
|
it('should create a governor via createEconomicGovernor', () => {
|
|
const g = createEconomicGovernor({ tokenLimit: 50_000 });
|
|
expect(g).toBeInstanceOf(EconomicGovernor);
|
|
});
|
|
|
|
it('should create a governor with defaults', () => {
|
|
const g = createEconomicGovernor();
|
|
expect(g).toBeInstanceOf(EconomicGovernor);
|
|
|
|
// Default token limit is 1,000,000
|
|
const usage = g.getUsageSummary();
|
|
expect(usage.tokens.limit).toBe(1_000_000);
|
|
});
|
|
});
|
|
|
|
describe('getUsageSummary', () => {
|
|
it('should return complete usage breakdown', () => {
|
|
governor.recordTokenUsage(2_000);
|
|
governor.recordToolCall('Edit', 50);
|
|
governor.recordStorageUsage(100_000);
|
|
|
|
const usage = governor.getUsageSummary();
|
|
|
|
expect(usage.tokens.used).toBe(2_000);
|
|
expect(usage.tokens.limit).toBe(10_000);
|
|
expect(usage.tokens.percentage).toBeCloseTo(20.0, 1);
|
|
|
|
expect(usage.toolCalls.used).toBe(1);
|
|
expect(usage.toolCalls.limit).toBe(100);
|
|
expect(usage.toolCalls.percentage).toBeCloseTo(1.0, 1);
|
|
|
|
expect(usage.storage.usedBytes).toBe(100_000);
|
|
expect(usage.storage.limitBytes).toBe(1_000_000);
|
|
expect(usage.storage.percentage).toBeCloseTo(10.0, 1);
|
|
|
|
expect(usage.time.limitMs).toBe(60_000);
|
|
expect(usage.time.usedMs).toBeGreaterThanOrEqual(0);
|
|
|
|
expect(usage.cost.totalUsd).toBeGreaterThan(0);
|
|
expect(usage.cost.limitUsd).toBe(1.0);
|
|
});
|
|
});
|
|
});
|