feat: add ECC2 local control pane (#2131)

* feat: add ECC2 local control pane

* fix: refresh control pane package locks

* test: harden control pane coverage

* test: allow portable control pane shutdown

* test: retry local control pane fetches

* fix: harden control pane error handling

* fix: wrap control pane metadata
This commit is contained in:
Affaan Mustafa
2026-06-03 21:54:30 +08:00
committed by GitHub
parent 99baa82500
commit 0f84c0e279
14 changed files with 2746 additions and 0 deletions

View File

@@ -0,0 +1,100 @@
/**
* Tests for allowlisted ECC2 control-pane actions.
*/
const assert = require('assert');
const path = require('path');
const {
buildControlPaneActions,
buildControlPaneAction,
shellQuote,
} = require('../../scripts/lib/control-pane/actions');
function test(name, fn) {
try {
fn();
console.log(` PASS ${name}`);
return true;
} catch (error) {
console.log(` FAIL ${name}`);
console.log(` Error: ${error.message}`);
return false;
}
}
function runTests() {
console.log('\n=== Testing control-pane actions ===\n');
let passed = 0;
let failed = 0;
if (test('builds copyable and executable allowlisted ECC2 actions', () => {
const repoRoot = path.join(__dirname, '..', '..');
const actions = buildControlPaneActions({
repoRoot,
query: 'Hermes Desktop Zellij',
limit: 25,
});
assert.ok(actions.some(action => action.id === 'sync-knowledge'));
assert.ok(actions.some(action => action.id === 'recall-knowledge'));
assert.ok(actions.some(action => action.id === 'open-dashboard'));
const sync = actions.find(action => action.id === 'sync-knowledge');
assert.strictEqual(sync.executable, true);
assert.strictEqual(sync.command, 'cargo');
assert.deepStrictEqual(sync.args, [
'run',
'--quiet',
'--',
'graph',
'connector-sync',
'--all',
'--json',
'--limit',
'25',
]);
assert.strictEqual(sync.cwd, path.join(repoRoot, 'ecc2'));
assert.ok(sync.commandLine.includes('connector-sync'));
})) passed++; else failed++;
if (test('preserves recall query as a single argument instead of shell text', () => {
const action = buildControlPaneAction('recall-knowledge', {
repoRoot: '/repo/ecc',
query: 'Hermes "Desktop"; rm -rf ~',
limit: 7,
});
assert.deepStrictEqual(action.args, [
'run',
'--quiet',
'--',
'graph',
'recall',
'Hermes "Desktop"; rm -rf ~',
'--json',
'--limit',
'7',
]);
assert.ok(action.commandLine.includes("'Hermes \"Desktop\"; rm -rf ~'"));
})) passed++; else failed++;
if (test('rejects unknown action identifiers', () => {
assert.throws(
() => buildControlPaneAction('rm -rf', { repoRoot: '/repo/ecc' }),
/Unknown control-pane action/
);
})) passed++; else failed++;
if (test('shellQuote handles empty strings and single quotes', () => {
assert.strictEqual(shellQuote(''), "''");
assert.strictEqual(shellQuote("can't"), "'can'\\''t'");
assert.strictEqual(shellQuote('simple'), 'simple');
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();

View File

@@ -0,0 +1,663 @@
/**
* Tests for the local ECC2 control-pane state projection.
*/
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const initSqlJs = require('sql.js');
const {
buildControlPaneSnapshot,
recallKnowledgeEntries,
resolveControlPaneConfig,
} = require('../../scripts/lib/control-pane/state');
async function test(name, fn) {
try {
await fn();
console.log(` PASS ${name}`);
return true;
} catch (error) {
console.log(` FAIL ${name}`);
console.log(` Error: ${error.message}`);
return false;
}
}
async function writeSampleEcc2Database(dbPath) {
const SQL = await initSqlJs();
const db = new SQL.Database();
db.run(`
CREATE TABLE sessions (
id TEXT PRIMARY KEY,
task TEXT NOT NULL,
project TEXT NOT NULL DEFAULT '',
task_group TEXT NOT NULL DEFAULT '',
agent_type TEXT NOT NULL,
harness TEXT NOT NULL DEFAULT 'unknown',
detected_harnesses_json TEXT NOT NULL DEFAULT '[]',
working_dir TEXT NOT NULL DEFAULT '.',
state TEXT NOT NULL DEFAULT 'pending',
pid INTEGER,
worktree_path TEXT,
worktree_branch TEXT,
worktree_base TEXT,
input_tokens INTEGER DEFAULT 0,
output_tokens INTEGER DEFAULT 0,
tokens_used INTEGER DEFAULT 0,
tool_calls INTEGER DEFAULT 0,
files_changed INTEGER DEFAULT 0,
duration_secs INTEGER DEFAULT 0,
cost_usd REAL DEFAULT 0.0,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
last_heartbeat_at TEXT NOT NULL
);
CREATE TABLE messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
from_session TEXT NOT NULL,
to_session TEXT NOT NULL,
content TEXT NOT NULL,
msg_type TEXT NOT NULL DEFAULT 'info',
read INTEGER DEFAULT 0,
timestamp TEXT NOT NULL
);
CREATE TABLE context_graph_entities (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT,
entity_key TEXT NOT NULL UNIQUE,
entity_type TEXT NOT NULL,
name TEXT NOT NULL,
path TEXT,
summary TEXT NOT NULL DEFAULT '',
metadata_json TEXT NOT NULL DEFAULT '{}',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE context_graph_observations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT,
entity_id INTEGER NOT NULL,
observation_type TEXT NOT NULL,
priority INTEGER NOT NULL DEFAULT 1,
pinned INTEGER NOT NULL DEFAULT 0,
summary TEXT NOT NULL,
details_json TEXT NOT NULL DEFAULT '{}',
created_at TEXT NOT NULL
);
CREATE TABLE context_graph_relations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT,
from_entity_id INTEGER NOT NULL,
to_entity_id INTEGER NOT NULL,
relation_type TEXT NOT NULL,
summary TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL
);
CREATE TABLE context_graph_connector_checkpoints (
connector_name TEXT NOT NULL,
source_path TEXT NOT NULL,
source_signature TEXT NOT NULL,
updated_at TEXT NOT NULL,
PRIMARY KEY (connector_name, source_path)
);
`);
const insertSession = db.prepare(`
INSERT INTO sessions (
id, task, project, task_group, agent_type, harness, detected_harnesses_json,
working_dir, state, pid, worktree_path, worktree_branch, worktree_base,
input_tokens, output_tokens, tokens_used, tool_calls, files_changed,
duration_secs, cost_usd, created_at, updated_at, last_heartbeat_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
insertSession.run([
'lead-hermes',
'Coordinate Hermes desktop and ECC release work',
'ECC',
'2.0-control-pane',
'claude',
'claude',
JSON.stringify(['claude', 'codex']),
'/repo/ecc',
'running',
4242,
'/tmp/ecc-worktrees/hermes',
'ecc/hermes-control-pane',
'main',
1200,
800,
2000,
19,
6,
540,
0.42,
'2026-06-03T10:00:00Z',
'2026-06-03T10:15:00Z',
'2026-06-03T10:15:00Z',
]);
insertSession.run([
'worker-kb',
'Index operator memory',
'ECC',
'knowledge',
'codex',
'codex',
JSON.stringify(['codex']),
'/repo/ecc',
'idle',
null,
null,
null,
null,
300,
200,
500,
4,
2,
120,
0.07,
'2026-06-03T10:05:00Z',
'2026-06-03T10:14:00Z',
'2026-06-03T10:14:00Z',
]);
insertSession.free();
db.run(
'INSERT INTO messages (from_session, to_session, content, msg_type, read, timestamp) VALUES (?, ?, ?, ?, ?, ?)',
['worker-kb', 'lead-hermes', 'Need approval for connector sync', 'approval_request', 0, '2026-06-03T10:16:00Z']
);
const insertEntity = db.prepare(`
INSERT INTO context_graph_entities (
session_id, entity_key, entity_type, name, path, summary, metadata_json, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
insertEntity.run([
'lead-hermes',
'runbook:Hermes revenue runbook:/notes/hermes.md',
'runbook',
'Hermes revenue runbook',
'/notes/hermes.md',
'How Affaan routes Hermes Desktop, Zellij panes, Devin-style delegation, and ECC release control work.',
JSON.stringify({ source: 'hermes_workspace', platform: 'desktop' }),
'2026-06-03T10:10:00Z',
'2026-06-03T10:10:00Z',
]);
insertEntity.run([
null,
'concept:gbrain memory:/notes/gbrain.md',
'concept',
'gbrain memory',
'/notes/gbrain.md',
'Operator knowledge base pattern for cross-platform agent memory.',
JSON.stringify({ source: 'workspace_notes' }),
'2026-06-03T10:11:00Z',
'2026-06-03T10:11:00Z',
]);
insertEntity.free();
db.run(
'INSERT INTO context_graph_observations (session_id, entity_id, observation_type, priority, pinned, summary, details_json, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
[
'lead-hermes',
1,
'operator_memory',
3,
1,
'Hermes Desktop and ECC should share recall before dispatching work.',
JSON.stringify({ note: 'safe public summary only' }),
'2026-06-03T10:12:00Z',
]
);
db.run(
'INSERT INTO context_graph_relations (session_id, from_entity_id, to_entity_id, relation_type, summary, created_at) VALUES (?, ?, ?, ?, ?, ?)',
['lead-hermes', 1, 2, 'depends_on', 'Runbook uses durable memory concepts.', '2026-06-03T10:13:00Z']
);
db.run(
'INSERT INTO context_graph_connector_checkpoints (connector_name, source_path, source_signature, updated_at) VALUES (?, ?, ?, ?)',
['hermes_workspace', '/notes/hermes.md', 'sig-1', '2026-06-03T10:12:00Z']
);
fs.writeFileSync(dbPath, Buffer.from(db.export()));
db.close();
}
async function mutateSqlDatabase(dbPath, mutator) {
const SQL = await initSqlJs();
const buffer = fs.readFileSync(dbPath);
const db = new SQL.Database(buffer);
try {
await mutator(db);
fs.writeFileSync(dbPath, Buffer.from(db.export()));
} finally {
db.close();
}
}
async function runTests() {
console.log('\n=== Testing control-pane state ===\n');
let passed = 0;
let failed = 0;
if (await test('builds an operator snapshot from ECC2 SQLite and configured connectors', async () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-control-pane-state-'));
const dbPath = path.join(tempDir, 'ecc2.db');
try {
await writeSampleEcc2Database(dbPath);
const snapshot = await buildControlPaneSnapshot({
dbPath,
repoRoot: path.join(__dirname, '..', '..'),
query: 'Hermes Desktop Zellij gbrain',
config: {
memoryConnectors: {
hermes_workspace: {
kind: 'markdown_directory',
path: '/notes',
recurse: true,
},
safe_env: {
kind: 'dotenv_file',
path: '/notes/.env',
includeSafeValues: false,
},
},
},
});
assert.strictEqual(snapshot.schemaVersion, 'ecc.control-pane.snapshot.v1');
assert.strictEqual(snapshot.summary.totalSessions, 2);
assert.strictEqual(snapshot.summary.runningSessions, 1);
assert.strictEqual(snapshot.summary.unreadMessages, 1);
assert.strictEqual(snapshot.sessions[0].id, 'lead-hermes');
assert.deepStrictEqual(snapshot.sessions[0].detectedHarnesses, ['claude', 'codex']);
assert.strictEqual(snapshot.knowledge.query, 'Hermes Desktop Zellij gbrain');
assert.strictEqual(snapshot.knowledge.results[0].entity.name, 'Hermes revenue runbook');
assert.ok(snapshot.knowledge.results[0].matchedTerms.includes('hermes'));
assert.strictEqual(snapshot.knowledge.results[0].hasPinnedObservation, true);
assert.strictEqual(snapshot.connectors.length, 2);
assert.strictEqual(snapshot.connectors[0].name, 'hermes_workspace');
assert.strictEqual(snapshot.connectors[0].syncedSources, 1);
assert.strictEqual(snapshot.connectors[1].syncedSources, 0);
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (await test('resolves config from explicit db path and TOML connector file', async () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-control-pane-config-'));
const dbPath = path.join(tempDir, 'state.db');
const configPath = path.join(tempDir, 'ecc2.toml');
try {
fs.writeFileSync(
configPath,
[
`db_path = "${dbPath.replace(/\\/g, '\\\\')}"`,
'',
'[memory_connectors.hermes_workspace]',
'kind = "markdown_directory"',
'path = "/tmp/hermes"',
'recurse = true',
'default_entity_type = "operator_note"',
].join('\n'),
'utf8'
);
const config = resolveControlPaneConfig({
cwd: tempDir,
configPath,
});
assert.strictEqual(config.dbPath, dbPath);
assert.strictEqual(config.memoryConnectors.hermes_workspace.kind, 'markdown_directory');
assert.strictEqual(config.memoryConnectors.hermes_workspace.path, '/tmp/hermes');
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (await test('prefers the operator home config over stale app-support config', async () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-control-pane-precedence-'));
const homeDir = path.join(tempDir, 'home');
const homeConfigDir = path.join(homeDir, '.claude');
const appConfigDir = path.join(homeDir, 'Library', 'Application Support', 'ecc2');
const homeDbPath = path.join(tempDir, 'operator.db');
const staleDbPath = path.join(tempDir, 'stale-smoke.db');
try {
fs.mkdirSync(homeConfigDir, { recursive: true });
fs.mkdirSync(appConfigDir, { recursive: true });
fs.writeFileSync(
path.join(appConfigDir, 'config.toml'),
`db_path = "${staleDbPath.replace(/\\/g, '\\\\')}"\n`,
'utf8'
);
fs.writeFileSync(
path.join(homeConfigDir, 'ecc2.toml'),
`db_path = "${homeDbPath.replace(/\\/g, '\\\\')}"\n`,
'utf8'
);
const config = resolveControlPaneConfig({
cwd: tempDir,
env: { HOME: homeDir },
});
assert.strictEqual(config.dbPath, homeDbPath);
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (await test('shows configured connectors even when the SQLite database is missing', async () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-control-pane-missing-db-'));
try {
const snapshot = await buildControlPaneSnapshot({
repoRoot: path.join(__dirname, '..', '..'),
dbPath: path.join(tempDir, 'missing.db'),
config: {
memoryConnectors: {
hermes_workspace: {
kind: 'markdown_directory',
path: '/notes/hermes',
recurse: true,
},
},
},
});
assert.strictEqual(snapshot.database.exists, false);
assert.strictEqual(snapshot.connectors.length, 1);
assert.strictEqual(snapshot.connectors[0].name, 'hermes_workspace');
assert.strictEqual(snapshot.connectors[0].syncedSources, 0);
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (await test('handles an existing SQLite database before ECC2 tables are created', async () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-control-pane-empty-db-'));
const dbPath = path.join(tempDir, 'empty.db');
try {
const SQL = await initSqlJs();
const db = new SQL.Database();
fs.writeFileSync(dbPath, Buffer.from(db.export()));
db.close();
const snapshot = await buildControlPaneSnapshot({
repoRoot: path.join(__dirname, '..', '..'),
dbPath,
config: {
memoryConnectors: {
workspace_notes: {
kind: 'markdown_directory',
path: '/notes',
includeSafeValues: false,
},
},
},
});
assert.strictEqual(snapshot.database.exists, true);
assert.strictEqual(snapshot.summary.totalSessions, 0);
assert.strictEqual(snapshot.knowledge.entityCount, 0);
assert.strictEqual(snapshot.knowledge.observationCount, 0);
assert.strictEqual(snapshot.connectors[0].name, 'workspace_notes');
assert.strictEqual(snapshot.connectors[0].lastSyncedAt, null);
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (await test('recalls pinned knowledge when no query is provided', async () => {
const results = recallKnowledgeEntries({
entities: [
{
id: 1,
entityType: 'runbook',
name: 'Pinned runbook',
path: '/notes/pinned.md',
summary: 'Pinned operator context',
metadata: {},
updatedAt: '2026-06-03T10:00:00Z',
},
{
id: 2,
entityType: 'concept',
name: 'Unpinned concept',
path: null,
summary: 'Secondary context',
metadata: {},
updatedAt: '2026-06-03T11:00:00Z',
},
],
observations: [
{
entityId: 1,
priority: 4,
pinned: true,
summary: 'Pinned detail',
},
{
entityId: 2,
priority: 2,
pinned: false,
summary: 'Other detail',
},
],
relationCounts: new Map([[1, 3]]),
query: '',
limit: 0,
});
assert.strictEqual(results.length, 2);
assert.strictEqual(results[0].entity.name, 'Pinned runbook');
assert.strictEqual(results[0].hasPinnedObservation, true);
assert.strictEqual(results[0].relationCount, 3);
assert.strictEqual(results[1].entity.name, 'Unpinned concept');
})) passed++; else failed++;
if (await test('handles malformed JSON rows and all session state counters', async () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-control-pane-edge-db-'));
const dbPath = path.join(tempDir, 'ecc2.db');
try {
await writeSampleEcc2Database(dbPath);
await mutateSqlDatabase(dbPath, db => {
const insertSession = db.prepare(`
INSERT INTO sessions (
id, task, project, task_group, agent_type, harness, detected_harnesses_json,
working_dir, state, pid, worktree_path, worktree_branch, worktree_base,
input_tokens, output_tokens, tokens_used, tool_calls, files_changed,
duration_secs, cost_usd, created_at, updated_at, last_heartbeat_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
for (const state of ['pending', 'failed', 'stopped', 'completed']) {
insertSession.run([
`session-${state}`,
`Exercise ${state}`,
'ECC',
'coverage',
'codex',
'',
state === 'failed' ? '{bad json' : '[]',
'',
state,
state === 'pending' ? 'not-a-pid' : null,
state === 'completed' ? '/tmp/worktree' : null,
null,
null,
'not-input-tokens',
null,
state === 'pending' ? 'not-tokens' : 10,
null,
null,
null,
state === 'failed' ? 'not-cost' : 0.1,
'2026-06-03T11:00:00Z',
`2026-06-03T11:0${state.length % 10}:00Z`,
'',
]);
}
insertSession.free();
db.run(
`INSERT INTO context_graph_entities (
session_id, entity_key, entity_type, name, path, summary, metadata_json, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
'session-failed',
'bad:json',
'note',
'Malformed JSON knowledge',
'/notes/malformed.md',
'This record should still be searchable.',
'{bad json',
'2026-06-03T11:20:00Z',
'2026-06-03T11:20:00Z',
]
);
db.run(
'INSERT INTO context_graph_observations (session_id, entity_id, observation_type, priority, pinned, summary, details_json, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
[
'session-failed',
3,
'',
'not-a-priority',
0,
'Malformed details should fall back safely.',
'{bad json',
'2026-06-03T11:21:00Z',
]
);
});
const snapshot = await buildControlPaneSnapshot({
repoRoot: path.join(__dirname, '..', '..'),
dbPath,
query: 'Malformed',
config: {
memoryConnectors: {
malformed_notes: {
kind: 'markdown_directory',
path: '/notes/malformed',
recurse: false,
defaultEntityType: 'note',
defaultObservationType: 'operator_memory',
includeSafeValues: true,
},
},
},
});
assert.strictEqual(snapshot.summary.pendingSessions, 1);
assert.strictEqual(snapshot.summary.failedSessions, 1);
assert.strictEqual(snapshot.summary.stoppedSessions, 1);
assert.strictEqual(snapshot.summary.completedSessions, 1);
assert.strictEqual(snapshot.summary.runningSessions, 1);
assert.strictEqual(snapshot.summary.idleSessions, 1);
assert.strictEqual(snapshot.summary.totalSessions, 6);
const failedSession = snapshot.sessions.find(session => session.id === 'session-failed');
assert.deepStrictEqual(failedSession.detectedHarnesses, []);
assert.strictEqual(failedSession.metrics.costUsd, 0);
const pendingSession = snapshot.sessions.find(session => session.id === 'session-pending');
assert.strictEqual(pendingSession.pid, 0);
assert.strictEqual(pendingSession.metrics.tokensUsed, 0);
assert.strictEqual(snapshot.knowledge.results[0].entity.name, 'Malformed JSON knowledge');
assert.deepStrictEqual(snapshot.knowledge.results[0].entity.metadata, {});
assert.deepStrictEqual(snapshot.knowledge.results[0].latestObservation.details, {});
assert.strictEqual(snapshot.connectors[0].defaultEntityType, 'note');
assert.strictEqual(snapshot.connectors[0].defaultObservationType, 'operator_memory');
assert.strictEqual(snapshot.connectors[0].includeSafeValues, true);
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (await test('recall search covers metadata, relation caps, no matches, and tie ordering', async () => {
const baseEntities = [
{
id: 1,
entityType: 'note',
name: 'First shared memory',
path: '/notes/shared-a.md',
summary: 'Platform context',
metadata: { source: 'workspace' },
updatedAt: '2026-06-03T10:00:00Z',
},
{
id: 2,
entityType: 'note',
name: 'Second shared memory',
path: '/notes/shared-b.md',
summary: 'Platform context',
metadata: { source: 'workspace' },
updatedAt: '2026-06-03T12:00:00Z',
},
{
id: 3,
entityType: 'concept',
name: 'Markets graph',
path: null,
summary: 'Correlation graph visualization',
metadata: { flow: 'friction-flow' },
updatedAt: '2026-06-03T09:00:00Z',
},
];
const observations = [
{
entityId: 3,
priority: 1,
pinned: false,
summary: 'Ito should expose market backtesting through ECC tools.',
},
];
const tied = recallKnowledgeEntries({
entities: baseEntities,
observations: [],
relationCounts: new Map(),
query: 'shared',
limit: 50,
});
assert.deepStrictEqual(tied.map(entry => entry.entity.id), [2, 1]);
const metadataHit = recallKnowledgeEntries({
entities: baseEntities,
observations,
relationCounts: new Map([[3, 20]]),
query: 'friction-flow backtesting',
limit: -5,
});
assert.strictEqual(metadataHit.length, 1);
assert.strictEqual(metadataHit[0].entity.id, 3);
assert.strictEqual(metadataHit[0].relationCount, 20);
assert.ok(metadataHit[0].score >= 18);
const noHits = recallKnowledgeEntries({
entities: baseEntities,
observations,
relationCounts: new Map(),
query: 'unmatched',
limit: 'wat',
});
assert.deepStrictEqual(noHits, []);
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();

View File

@@ -0,0 +1,423 @@
/**
* Tests for scripts/control-pane.js and its local HTTP API.
*/
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { spawn, spawnSync } = require('child_process');
const initSqlJs = require('sql.js');
const {
createControlPaneServer,
parseArgs,
runAction,
} = require('../../scripts/lib/control-pane/server');
const {
main: runControlPaneCli,
} = require('../../scripts/control-pane');
const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'control-pane.js');
const REPO_ROOT = path.join(__dirname, '..', '..');
async function test(name, fn) {
try {
await fn();
console.log(` PASS ${name}`);
return true;
} catch (error) {
console.log(` FAIL ${name}`);
console.log(` Error: ${error.message}`);
return false;
}
}
async function writeMinimalDatabase(dbPath) {
const SQL = await initSqlJs();
const db = new SQL.Database();
db.run(`
CREATE TABLE sessions (
id TEXT PRIMARY KEY,
task TEXT NOT NULL,
project TEXT NOT NULL DEFAULT '',
task_group TEXT NOT NULL DEFAULT '',
agent_type TEXT NOT NULL,
harness TEXT NOT NULL DEFAULT 'unknown',
detected_harnesses_json TEXT NOT NULL DEFAULT '[]',
working_dir TEXT NOT NULL DEFAULT '.',
state TEXT NOT NULL DEFAULT 'pending',
pid INTEGER,
worktree_path TEXT,
worktree_branch TEXT,
worktree_base TEXT,
input_tokens INTEGER DEFAULT 0,
output_tokens INTEGER DEFAULT 0,
tokens_used INTEGER DEFAULT 0,
tool_calls INTEGER DEFAULT 0,
files_changed INTEGER DEFAULT 0,
duration_secs INTEGER DEFAULT 0,
cost_usd REAL DEFAULT 0.0,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
last_heartbeat_at TEXT NOT NULL
);
INSERT INTO sessions (
id, task, agent_type, harness, detected_harnesses_json, working_dir, state,
created_at, updated_at, last_heartbeat_at
) VALUES (
'session-a', 'Build the control pane', 'codex', 'codex', '["codex"]', '/repo/ecc',
'running', '2026-06-03T10:00:00Z', '2026-06-03T10:05:00Z', '2026-06-03T10:05:00Z'
);
`);
fs.writeFileSync(dbPath, Buffer.from(db.export()));
db.close();
}
function waitForCliReady(child) {
return new Promise((resolve, reject) => {
let stdout = '';
let stderr = '';
let settled = false;
const timer = setTimeout(() => {
if (settled) return;
settled = true;
child.kill('SIGTERM');
reject(new Error(`Timed out waiting for control pane CLI.\nstdout:\n${stdout}\nstderr:\n${stderr}`));
}, 5000);
child.stdout.on('data', chunk => {
stdout += chunk.toString('utf8');
if (!settled && stdout.includes('ECC Control Pane:') && stdout.includes('Actions:')) {
settled = true;
clearTimeout(timer);
resolve({ stdout, stderr });
}
});
child.stderr.on('data', chunk => {
stderr += chunk.toString('utf8');
});
child.on('error', error => {
if (settled) return;
settled = true;
clearTimeout(timer);
reject(error);
});
child.on('exit', code => {
if (settled) return;
settled = true;
clearTimeout(timer);
reject(new Error(`control pane CLI exited early with ${code}.\nstdout:\n${stdout}\nstderr:\n${stderr}`));
});
});
}
function waitForExit(child) {
return new Promise(resolve => {
child.once('exit', (code, signal) => resolve({ code, signal }));
});
}
async function fetchLocal(url, options) {
let lastError;
for (let attempt = 0; attempt < 5; attempt += 1) {
try {
return await fetch(url, options);
} catch (error) {
lastError = error;
await new Promise(resolve => setTimeout(resolve, 25 * (attempt + 1)));
}
}
throw lastError;
}
async function runTests() {
console.log('\n=== Testing control-pane server ===\n');
let passed = 0;
let failed = 0;
if (await test('parses CLI arguments for local-only serving', async () => {
const parsed = parseArgs([
'node',
'scripts/control-pane.js',
'--host',
'127.0.0.1',
'--port',
'8788',
'--db',
'/tmp/ecc2.db',
'--query',
'Hermes memory',
'--no-open',
]);
assert.strictEqual(parsed.host, '127.0.0.1');
assert.strictEqual(parsed.port, 8788);
assert.strictEqual(parsed.dbPath, '/tmp/ecc2.db');
assert.strictEqual(parsed.query, 'Hermes memory');
assert.strictEqual(parsed.openBrowser, false);
})) passed++; else failed++;
if (await test('rejects invalid CLI port values', async () => {
assert.throws(
() => parseArgs(['node', 'scripts/control-pane.js', '--port', '70000']),
/Invalid --port value/
);
assert.throws(
() => parseArgs(['node', 'scripts/control-pane.js', '--port', 'wat']),
/Invalid --port value/
);
})) passed++; else failed++;
if (await test('serves HTML and snapshot JSON from a temp ECC2 database', async () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-control-pane-server-'));
const dbPath = path.join(tempDir, 'ecc2.db');
try {
await writeMinimalDatabase(dbPath);
const app = await createControlPaneServer({
host: '127.0.0.1',
port: 0,
dbPath,
repoRoot: REPO_ROOT,
query: 'control pane',
allowActions: false,
});
await app.listen();
try {
const html = await fetchLocal(`${app.url}/`).then(response => response.text());
assert.ok(html.includes('ECC Control Pane'));
assert.ok(html.includes('id="app"'));
assert.ok(html.includes('function showError'));
assert.ok(html.includes('response.ok'));
const snapshot = await fetchLocal(`${app.url}/api/snapshot?query=control`).then(response => response.json());
assert.strictEqual(snapshot.schemaVersion, 'ecc.control-pane.snapshot.v1');
assert.strictEqual(snapshot.summary.totalSessions, 1);
assert.strictEqual(snapshot.sessions[0].id, 'session-a');
} finally {
await app.close();
}
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (await test('serves health, asset, not-found, invalid body, and read-only action responses', async () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-control-pane-routes-'));
try {
const app = await createControlPaneServer({
host: '127.0.0.1',
port: 0,
dbPath: path.join(tempDir, 'missing.db'),
repoRoot: tempDir,
allowActions: false,
});
await app.listen();
try {
const health = await fetchLocal(`${app.url}/api/health`).then(response => response.json());
assert.strictEqual(health.ok, true);
assert.strictEqual(health.allowActions, false);
const realAssetApp = await createControlPaneServer({
host: '127.0.0.1',
port: 0,
dbPath: path.join(tempDir, 'missing.db'),
repoRoot: REPO_ROOT,
allowActions: false,
});
await realAssetApp.listen();
try {
const realAsset = await fetchLocal(`${realAssetApp.url}/assets/ecc-icon.svg`);
assert.strictEqual(realAsset.status, 200);
assert.match(await realAsset.text(), /<svg/);
} finally {
await realAssetApp.close();
}
const missingAsset = await fetchLocal(`${app.url}/assets/ecc-icon.svg`);
assert.strictEqual(missingAsset.status, 404);
assert.strictEqual(await missingAsset.text(), 'not found');
const missing = await fetchLocal(`${app.url}/not-here`).then(response => response.json());
assert.strictEqual(missing.ok, false);
assert.strictEqual(missing.error, 'not found');
const blocked = await fetchLocal(`${app.url}/api/actions/sync-knowledge`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ query: 'memory' }),
}).then(async response => ({ status: response.status, body: await response.json() }));
assert.strictEqual(blocked.status, 403);
assert.match(blocked.body.error, /disabled/);
const invalidBody = await fetchLocal(`${app.url}/api/actions/sync-knowledge`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: '{bad json',
}).then(async response => ({ status: response.status, body: await response.json() }));
assert.strictEqual(invalidBody.status, 403);
} finally {
await app.close();
}
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (await test('guards copy-only and unknown action requests', async () => {
const app = await createControlPaneServer({
host: '127.0.0.1',
port: 0,
repoRoot: REPO_ROOT,
allowActions: true,
});
await app.listen();
try {
const copyOnly = await fetchLocal(`${app.url}/api/actions/open-dashboard`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: '{}',
}).then(async response => ({ status: response.status, body: await response.json() }));
assert.strictEqual(copyOnly.status, 400);
assert.strictEqual(copyOnly.body.action, 'open-dashboard');
assert.match(copyOnly.body.error, /copy-only/);
const unknown = await fetchLocal(`${app.url}/api/actions/nope`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: '{}',
}).then(async response => ({ status: response.status, body: await response.json() }));
assert.strictEqual(unknown.status, 500);
assert.match(unknown.body.error, /Unknown control-pane action/);
const invalidBody = await fetchLocal(`${app.url}/api/actions/sync-knowledge`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: '{bad json',
}).then(async response => ({ status: response.status, body: await response.json() }));
assert.strictEqual(invalidBody.status, 500);
assert.match(invalidBody.body.error, /JSON/);
} finally {
await app.close();
}
})) passed++; else failed++;
if (await test('runAction captures success, failure, and bounded output', async () => {
const repoRoot = REPO_ROOT;
const success = await runAction({
id: 'node-success',
command: process.execPath,
args: ['-e', 'process.stdout.write("x".repeat(21010))'],
cwd: repoRoot,
});
assert.strictEqual(success.ok, true);
assert.strictEqual(success.code, 0);
assert.ok(success.stdout.includes('[truncated '));
const failure = await runAction({
id: 'node-failure',
command: process.execPath,
args: ['-e', 'process.stderr.write("bad"); process.exit(7)'],
cwd: repoRoot,
});
assert.strictEqual(failure.ok, false);
assert.strictEqual(failure.code, 7);
assert.strictEqual(failure.stderr, 'bad');
const spawnError = await runAction({
id: 'spawn-error',
command: 'definitely-not-ecc-control-pane-command',
args: [],
cwd: repoRoot,
});
assert.strictEqual(spawnError.ok, false);
assert.strictEqual(spawnError.code, null);
assert.match(spawnError.error, /ENOENT/);
})) passed++; else failed++;
if (await test('runAction terminates commands that exceed the local timeout', async () => {
const timedOut = await runAction(
{
id: 'node-timeout',
command: process.execPath,
args: ['-e', 'setTimeout(() => {}, 5000)'],
cwd: REPO_ROOT,
},
{ timeoutMs: 25 }
);
assert.strictEqual(timedOut.ok, false);
assert.strictEqual(timedOut.signal, 'SIGTERM');
})) passed++; else failed++;
if (await test('CLI prints help', async () => {
const result = spawnSync('node', [SCRIPT, '--help'], {
encoding: 'utf8',
cwd: REPO_ROOT,
});
assert.strictEqual(result.status, 0, result.stderr);
assert.ok(result.stdout.includes('Usage:'));
assert.ok(result.stdout.includes('control-pane'));
})) passed++; else failed++;
if (await test('CLI browser opener handles spawn errors', async () => {
const source = fs.readFileSync(SCRIPT, 'utf8');
assert.match(source, /child\.on\('error'/);
assert.match(source, /child\.unref\(\)/);
})) passed++; else failed++;
if (await test('CLI main handles help without starting a server', async () => {
const originalLog = console.log;
const lines = [];
console.log = line => {
lines.push(String(line));
};
try {
await runControlPaneCli(['node', 'scripts/control-pane.js', '--help']);
} finally {
console.log = originalLog;
}
assert.match(lines.join('\n'), /Usage:/);
assert.match(lines.join('\n'), /--read-only/);
})) passed++; else failed++;
if (await test('CLI starts a read-only local server and shuts down on SIGTERM', async () => {
const child = spawn(process.execPath, [SCRIPT, '--host', '127.0.0.1', '--port', '0', '--read-only', '--no-open'], {
cwd: REPO_ROOT,
stdio: ['ignore', 'pipe', 'pipe'],
env: {
...process.env,
ECC2_DB_PATH: path.join(os.tmpdir(), 'missing-ecc2-cli.db'),
},
});
const exitPromise = waitForExit(child);
try {
const ready = await waitForCliReady(child);
assert.match(ready.stdout, /ECC Control Pane: http:\/\/127\.0\.0\.1:\d+/);
assert.match(ready.stdout, /Actions: read-only/);
} finally {
if (child.exitCode === null && child.signalCode === null) child.kill('SIGTERM');
const result = await exitPromise;
assert.ok(
result.code === 0 || result.signal === 'SIGTERM',
`expected graceful shutdown or SIGTERM, got code=${result.code} signal=${result.signal}`
);
}
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();

View File

@@ -70,6 +70,7 @@ function main() {
assert.match(result.stdout, /doctor/);
assert.match(result.stdout, /auto-update/);
assert.match(result.stdout, /consult/);
assert.match(result.stdout, /control-pane/);
assert.match(result.stdout, /loop-status/);
assert.match(result.stdout, /work-items/);
assert.match(result.stdout, /platform-audit/);
@@ -114,6 +115,12 @@ function main() {
assert.strictEqual(payload.schemaVersion, 'ecc.consult.v1');
assert.strictEqual(payload.matches[0].componentId, 'capability:security');
}],
['supports help for the control-pane subcommand', () => {
const result = runCli(['help', 'control-pane']);
assert.strictEqual(result.status, 0, result.stderr);
assert.match(result.stdout, /Usage:/);
assert.match(result.stdout, /control-pane/);
}],
['delegates lifecycle commands', () => {
const homeDir = createTempDir('ecc-cli-home-');
const projectRoot = createTempDir('ecc-cli-project-');

View File

@@ -46,6 +46,7 @@ function buildExpectedPublishPaths(repoRoot) {
"scripts/ci/scan-supply-chain-iocs.js",
"scripts/ci/supply-chain-advisory-sources.js",
"scripts/consult.js",
"scripts/control-pane.js",
"scripts/claw.js",
"scripts/discussion-audit.js",
"scripts/doctor.js",
@@ -132,6 +133,7 @@ function main() {
"scripts/ci/scan-supply-chain-iocs.js",
"scripts/ci/supply-chain-advisory-sources.js",
"scripts/consult.js",
"scripts/control-pane.js",
"scripts/discussion-audit.js",
"scripts/operator-readiness-dashboard.js",
"scripts/preview-pack-smoke.js",