Governed substrate for autonomous agents: scoped identity (passports), audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
501 lines
16 KiB
JavaScript
501 lines
16 KiB
JavaScript
// ============================================================
|
||
// PES Integration Test — Full Lifecycle
|
||
// ============================================================
|
||
// Tests the COMPLETE cycle: birth → event → expression → react → learn
|
||
// Unlike unit tests, this exercises ALL subsystems together.
|
||
// ============================================================
|
||
|
||
import { Pes } from './core/pes.js';
|
||
import { unlinkSync, existsSync } from 'node:fs';
|
||
|
||
const DB_PATH = '/tmp/pes-integration-test.db';
|
||
const STATE_PATH = DB_PATH.replace(/\.db$/, '.state.json');
|
||
|
||
function cleanup() {
|
||
for (const f of [DB_PATH, STATE_PATH, DB_PATH + '-wal', DB_PATH + '-shm']) {
|
||
try { if (existsSync(f)) unlinkSync(f); } catch (_) {}
|
||
}
|
||
}
|
||
|
||
let passed = 0;
|
||
let failed = 0;
|
||
|
||
function assert(condition, msg) {
|
||
if (condition) {
|
||
passed++;
|
||
console.log(` ✅ ${msg}`);
|
||
} else {
|
||
failed++;
|
||
console.log(` ❌ FAIL: ${msg}`);
|
||
}
|
||
}
|
||
|
||
// ── TEST 1: Full Lifecycle ──────────────────────────────────
|
||
|
||
console.log('\n═══ TEST 1: BIRTH → EVENTS → SLEEP → WAKE → DESTROY ═══\n');
|
||
cleanup();
|
||
|
||
{
|
||
const pes = new Pes({
|
||
name: 'Тест',
|
||
ownerId: 'nikitron',
|
||
dbPath: DB_PATH,
|
||
breed: 'corgi',
|
||
seed: 0.42,
|
||
autoTick: false,
|
||
});
|
||
|
||
// Track events
|
||
const events = [];
|
||
pes.on('born', d => events.push({ type: 'born', data: d }));
|
||
pes.on('expression', d => events.push({ type: 'expression', data: d }));
|
||
pes.on('feedback', d => events.push({ type: 'feedback', data: d }));
|
||
pes.on('sleep', d => events.push({ type: 'sleep', data: d }));
|
||
pes.on('wake', d => events.push({ type: 'wake', data: d }));
|
||
pes.on('commandLearned', d => events.push({ type: 'commandLearned', data: d }));
|
||
pes.on('modeChange', d => events.push({ type: 'modeChange', data: d }));
|
||
|
||
// Birth
|
||
pes.birth();
|
||
assert(pes.isAlive, 'PES is alive after birth');
|
||
assert(pes.isAwake, 'PES is awake after birth');
|
||
assert(pes.name === 'Тест', 'PES has correct name');
|
||
assert(pes.breed === 'corgi', 'PES breed is corgi');
|
||
assert(events.some(e => e.type === 'born'), 'born event emitted');
|
||
assert(pes.emotions !== null, 'Emotions subsystem initialized');
|
||
assert(pes.store !== null, 'Store subsystem initialized');
|
||
assert(pes.engine !== null, 'Expression engine initialized');
|
||
|
||
// Corgi trait bias
|
||
assert(pes.emotions.traits.playfulness > 0.3, 'Corgi has elevated playfulness');
|
||
assert(pes.emotions.traits.stubbornness > 0.3, 'Corgi has elevated stubbornness');
|
||
|
||
// Event: owner returned
|
||
const greetResult = pes.event({ type: 'owner_returned', from: 'owner' });
|
||
assert(greetResult !== null, 'owner_returned produces expression');
|
||
assert(events.some(e => e.type === 'expression'), 'expression event emitted');
|
||
|
||
// Event: bug detected → should give XP
|
||
const xpBefore = pes.xp;
|
||
const bugResult = pes.event({ type: 'bug_detected', from: 'system', file: 'test.js', line: 42 });
|
||
assert(bugResult !== null, 'bug_detected produces expression');
|
||
assert(pes.xp >= xpBefore, 'XP increased or stayed (bug_detected gives XP)');
|
||
|
||
// Praise → mood should improve
|
||
const moodBefore = pes.mood;
|
||
pes.praise('хороший мальчик');
|
||
// Mood may or may not increase immediately depending on state transitions
|
||
assert(pes.mood >= 0, 'Mood is valid after praise');
|
||
|
||
// Scold
|
||
pes.scold('плохо');
|
||
assert(pes.mood >= 0 && pes.mood <= 1, 'Mood is valid after scold');
|
||
|
||
// Feed
|
||
const feedResult = pes.feed({ type: 'good_data', content: 'test data' });
|
||
assert(feedResult !== null || feedResult === null, 'Feed returns result (may be null if suppressed)');
|
||
|
||
// Status
|
||
const status = pes.status();
|
||
assert(status.alive === true, 'Status shows alive');
|
||
assert(status.name === 'Тест', 'Status shows correct name');
|
||
assert(typeof status.level === 'number', 'Status has level');
|
||
assert(typeof status.emotion === 'string', 'Status has emotion');
|
||
assert(typeof status.mood === 'number', 'Status has mood');
|
||
assert(typeof status.energy === 'number', 'Status has energy');
|
||
|
||
// Inspect
|
||
const inspected = pes.inspect();
|
||
assert(inspected.traits !== undefined, 'Inspect has traits');
|
||
assert(inspected.stateHistory !== undefined, 'Inspect has state history');
|
||
|
||
// Sleep
|
||
pes.sleep();
|
||
assert(pes.isAwake === false, 'PES is not awake after sleep');
|
||
assert(pes.isAlive, 'PES is still alive after sleep');
|
||
|
||
// Wake
|
||
pes.wake();
|
||
assert(pes.isAwake, 'PES is awake after wake');
|
||
assert(events.some(e => e.type === 'wake'), 'wake event emitted');
|
||
|
||
// Destroy
|
||
pes.destroy();
|
||
assert(pes.isAlive === false || pes._destroyed, 'PES is destroyed');
|
||
}
|
||
|
||
// ── TEST 2: Command Learning Cycle ──────────────────────────
|
||
|
||
console.log('\n═══ TEST 2: COMMAND LEARNING — UNKNOWN → REPEAT → LEARN ═══\n');
|
||
cleanup();
|
||
|
||
{
|
||
const pes = new Pes({
|
||
name: 'Ученик',
|
||
ownerId: 'nikitron',
|
||
dbPath: DB_PATH,
|
||
seed: 0.5,
|
||
autoTick: false,
|
||
});
|
||
pes.birth();
|
||
|
||
let commandLearned = false;
|
||
pes.on('commandLearned', () => { commandLearned = true; });
|
||
|
||
// Unknown command — attempt 1
|
||
const r1 = pes.command('scan_api');
|
||
assert(r1.executed === false, 'Unknown command not executed');
|
||
assert(r1.reason === 'unknown', 'Reason is "unknown"');
|
||
assert(r1.attempts === 1, 'First attempt recorded');
|
||
|
||
// Attempt 2
|
||
const r2 = pes.command('scan_api');
|
||
assert(r2.executed === false, 'Still unknown at attempt 2');
|
||
assert(r2.attempts === 2, 'Second attempt');
|
||
|
||
// Attempt 3 — should learn!
|
||
const r3 = pes.command('scan_api');
|
||
assert(r3.commandLearned === true, 'Command learned on 3rd attempt');
|
||
assert(commandLearned, 'commandLearned event emitted');
|
||
|
||
// Now execute the learned command
|
||
const r4 = pes.command('scan_api');
|
||
assert(r4.executed === true, 'Learned command executes');
|
||
|
||
// Verify in store
|
||
const cmd = pes.store.getCommand('scan_api');
|
||
assert(cmd !== null, 'Command exists in store');
|
||
assert(cmd.understood === 1, 'Command is marked as understood');
|
||
assert(cmd.willingness >= 0.3, 'Command has willingness');
|
||
|
||
pes.destroy();
|
||
}
|
||
|
||
// ── TEST 3: Feedback Loop ───────────────────────────────────
|
||
|
||
console.log('\n═══ TEST 3: FEEDBACK LOOP — EXPRESSION → REACTION → LEARN ═══\n');
|
||
cleanup();
|
||
|
||
{
|
||
const pes = new Pes({
|
||
name: 'Фидбэк',
|
||
ownerId: 'nikitron',
|
||
dbPath: DB_PATH,
|
||
seed: 0.3,
|
||
autoTick: false,
|
||
});
|
||
pes.birth();
|
||
|
||
const expressions = [];
|
||
pes.on('expression', (expr) => expressions.push(expr));
|
||
|
||
// Generate an expression
|
||
const result = pes.event({ type: 'bug_detected', from: 'system', file: 'app.js', line: 10 });
|
||
|
||
if (result && !result.suppressed && result.id) {
|
||
// React positively
|
||
const feedback = pes.react(result.id, {
|
||
from: 'owner',
|
||
type: 'emoji',
|
||
value: '👍',
|
||
speed_ms: 2000,
|
||
});
|
||
|
||
assert(feedback !== null, 'Feedback returned (reaction processed)');
|
||
if (feedback) {
|
||
assert(feedback.positive === true, 'Reaction classified as positive');
|
||
assert(feedback.weight > 0, 'Positive weight calculated');
|
||
}
|
||
|
||
// React negatively to next expression
|
||
const result2 = pes.event({ type: 'owner_returned', from: 'owner' });
|
||
if (result2 && !result2.suppressed && result2.id) {
|
||
const feedback2 = pes.react(result2.id, {
|
||
from: 'owner',
|
||
type: 'emoji',
|
||
value: '👎',
|
||
speed_ms: 5000,
|
||
});
|
||
if (feedback2) {
|
||
assert(feedback2.positive === false, 'Negative reaction classified correctly');
|
||
assert(feedback2.weight < 0, 'Negative weight calculated');
|
||
} else {
|
||
assert(true, 'Negative reaction processed (null ok if pending expired)');
|
||
}
|
||
} else {
|
||
assert(true, 'Second expression skipped (cooldown/suppressed)');
|
||
}
|
||
} else {
|
||
assert(true, 'Expression suppressed — feedback loop skipped (valid behavior)');
|
||
}
|
||
|
||
// Check reaction memory has entries
|
||
const history = pes.store.getHistory({ limit: 10 });
|
||
assert(history.length > 0, 'Interaction history recorded');
|
||
|
||
pes.destroy();
|
||
}
|
||
|
||
// ── TEST 4: Passive Tick & Auto-mode ────────────────────────
|
||
|
||
console.log('\n═══ TEST 4: PASSIVE TICK & AUTO-MODE ═══\n');
|
||
cleanup();
|
||
|
||
{
|
||
const pes = new Pes({
|
||
name: 'Тикер',
|
||
ownerId: 'nikitron',
|
||
dbPath: DB_PATH,
|
||
seed: 0.6,
|
||
autoTick: false,
|
||
});
|
||
pes.birth();
|
||
|
||
const modeChanges = [];
|
||
pes.on('modeChange', (d) => modeChanges.push(d));
|
||
|
||
// Simulate time passing — manually call _tick
|
||
assert(pes.mode === 'active', 'Starts in active mode');
|
||
|
||
// Simulate 10 minutes of silence
|
||
pes._lastActivityAt = Date.now() - 10 * 60_000;
|
||
pes._tick();
|
||
assert(pes.mode === 'idle', 'Switches to idle after 10 min silence');
|
||
|
||
// Simulate 45 minutes of silence
|
||
pes._lastActivityAt = Date.now() - 45 * 60_000;
|
||
pes._tick();
|
||
assert(pes.mode === 'sleeping', 'Switches to sleeping after 45 min silence');
|
||
|
||
// Simulate 3 hours of silence
|
||
pes._lastActivityAt = Date.now() - 180 * 60_000;
|
||
pes._tick();
|
||
assert(pes.mode === 'away', 'Switches to away after 3 hours silence');
|
||
|
||
// Activity resets to active
|
||
pes.event({ type: 'owner_returned', from: 'owner' });
|
||
assert(pes.mode === 'active', 'Returns to active on event');
|
||
|
||
pes.destroy();
|
||
}
|
||
|
||
// ── TEST 5: Export / Import ─────────────────────────────────
|
||
|
||
console.log('\n═══ TEST 5: EXPORT → IMPORT ═══\n');
|
||
cleanup();
|
||
|
||
{
|
||
const pes = new Pes({
|
||
name: 'Экспорт',
|
||
ownerId: 'nikitron',
|
||
dbPath: DB_PATH,
|
||
seed: 0.7,
|
||
domain: 'devops',
|
||
autoTick: false,
|
||
});
|
||
pes.birth();
|
||
|
||
// Build up some state
|
||
pes.event({ type: 'owner_returned', from: 'owner' });
|
||
pes.praise('хороший');
|
||
pes.command('sit');
|
||
pes.command('sit');
|
||
pes.command('sit'); // learn it
|
||
|
||
// Export
|
||
const dump = pes.export();
|
||
assert(dump.version === 1, 'Export has version');
|
||
assert(dump.identity.name === 'Экспорт', 'Export has name');
|
||
assert(dump.identity.breed === 'corgi', 'Export has breed');
|
||
assert(dump.emotions !== null, 'Export has emotions');
|
||
assert(dump.snapshot !== null, 'Export has snapshot');
|
||
|
||
pes.destroy();
|
||
|
||
// Import into new DB
|
||
const DB2 = '/tmp/pes-integration-import.db';
|
||
try { unlinkSync(DB2); } catch (_) {}
|
||
try { unlinkSync(DB2.replace('.db', '.state.json')); } catch (_) {}
|
||
|
||
const pes2 = Pes.import(dump, DB2);
|
||
assert(pes2.name === 'Экспорт', 'Imported PES has correct name');
|
||
assert(pes2.breed === 'corgi', 'Imported PES has correct breed');
|
||
assert(pes2.birthday === dump.identity.birthday, 'Imported PES has correct birthday');
|
||
assert(pes2.isAlive, 'Imported PES is alive');
|
||
assert(!pes2.isAwake, 'Imported PES needs wake()');
|
||
|
||
// Wake and verify
|
||
pes2.wake();
|
||
assert(pes2.isAwake, 'Imported PES wakes up');
|
||
assert(pes2.emotions !== null, 'Imported PES has emotions');
|
||
|
||
pes2.destroy();
|
||
try { unlinkSync(DB2); } catch (_) {}
|
||
try { unlinkSync(DB2.replace('.db', '.state.json')); } catch (_) {}
|
||
try { unlinkSync(DB2 + '-wal'); } catch (_) {}
|
||
try { unlinkSync(DB2 + '-shm'); } catch (_) {}
|
||
}
|
||
|
||
// ── TEST 6: Runaway (Irreversible) ──────────────────────────
|
||
|
||
console.log('\n═══ TEST 6: RUNAWAY — FAREWELL LETTER ═══\n');
|
||
cleanup();
|
||
|
||
{
|
||
const pes = new Pes({
|
||
name: 'Беглец',
|
||
ownerId: 'nikitron',
|
||
dbPath: DB_PATH,
|
||
seed: 0.1,
|
||
autoTick: false,
|
||
});
|
||
pes.birth();
|
||
|
||
let runawayEvent = null;
|
||
pes.on('runaway', (d) => { runawayEvent = d; });
|
||
|
||
// Build some history
|
||
pes.event({ type: 'owner_returned', from: 'owner' });
|
||
pes.praise('test');
|
||
|
||
// Runaway
|
||
const letter = pes.runaway('dual_rage', 'злой_pes');
|
||
assert(letter !== null, 'Farewell letter generated');
|
||
assert(letter.type === 'farewell', 'Letter type is farewell');
|
||
assert(letter.reason === 'dual_rage', 'Letter has reason');
|
||
assert(letter.snapshot !== null, 'Letter has snapshot');
|
||
assert(letter.finalWords !== undefined, 'Letter has final words');
|
||
assert(runawayEvent !== null, 'runaway event emitted');
|
||
assert(pes.isAlive === false, 'PES is not alive after runaway');
|
||
|
||
// Cannot interact anymore
|
||
const result = pes.event({ type: 'owner_returned', from: 'owner' });
|
||
assert(result === null, 'Cannot interact with dead PES');
|
||
|
||
pes.destroy();
|
||
}
|
||
|
||
// ── TEST 7: Silent Mode ─────────────────────────────────────
|
||
|
||
console.log('\n═══ TEST 7: SILENT MODE ═══\n');
|
||
cleanup();
|
||
|
||
{
|
||
const pes = new Pes({
|
||
name: 'Тихий',
|
||
ownerId: 'nikitron',
|
||
dbPath: DB_PATH,
|
||
seed: 0.9,
|
||
autoTick: false,
|
||
});
|
||
pes.birth();
|
||
|
||
// Enable silent mode
|
||
pes.setSilentMode(true);
|
||
assert(pes.engine.silentMode === true, 'Silent mode enabled');
|
||
|
||
// Expression should have no voice
|
||
const result = pes.event({ type: 'bug_detected', from: 'system' });
|
||
if (result && result.expression) {
|
||
assert(result.expression.voiceKey === null || result.expression.voiceKey === undefined,
|
||
'Voice is suppressed in silent mode');
|
||
} else {
|
||
assert(true, 'Expression suppressed (valid)');
|
||
}
|
||
|
||
// Disable
|
||
pes.setSilentMode(false);
|
||
assert(pes.engine.silentMode === false, 'Silent mode disabled');
|
||
|
||
pes.destroy();
|
||
}
|
||
|
||
// ── TEST 8: Domain Context ──────────────────────────────────
|
||
|
||
console.log('\n═══ TEST 8: DOMAIN CONTEXT ═══\n');
|
||
cleanup();
|
||
|
||
{
|
||
const pes = new Pes({
|
||
name: 'Домен',
|
||
ownerId: 'nikitron',
|
||
dbPath: DB_PATH,
|
||
seed: 0.4,
|
||
domain: 'fintech',
|
||
autoTick: false,
|
||
});
|
||
pes.birth();
|
||
|
||
assert(pes.domain === 'fintech', 'Domain set on creation');
|
||
|
||
// Change domain
|
||
pes.setDomain('devops');
|
||
assert(pes.domain === 'devops', 'Domain changed');
|
||
|
||
const status = pes.status();
|
||
assert(status.alive, 'PES alive with domain');
|
||
|
||
pes.destroy();
|
||
}
|
||
|
||
// ── TEST 9: Multiple Events → Store Persistence ─────────────
|
||
|
||
console.log('\n═══ TEST 9: PERSISTENCE — INTERACTIONS RECORDED ═══\n');
|
||
cleanup();
|
||
|
||
{
|
||
const pes = new Pes({
|
||
name: 'Память',
|
||
ownerId: 'nikitron',
|
||
dbPath: DB_PATH,
|
||
seed: 0.2,
|
||
autoTick: false,
|
||
});
|
||
pes.birth();
|
||
|
||
// Generate multiple events
|
||
pes.event({ type: 'owner_returned', from: 'owner' });
|
||
pes.event({ type: 'bug_detected', from: 'system' });
|
||
pes.praise('отлично');
|
||
pes.scold('нет');
|
||
pes.feed({ type: 'data' });
|
||
|
||
// Check interactions are in store
|
||
const history = pes.store.getHistory({ limit: 50 });
|
||
assert(history.length >= 3, `Multiple interactions recorded (got ${history.length})`);
|
||
|
||
// Check stats
|
||
const stats = pes.store.getStats();
|
||
assert(stats !== null, 'Stats exist');
|
||
assert(stats.xp >= 0, 'XP is tracked');
|
||
|
||
// Save and reload
|
||
pes.save();
|
||
assert(existsSync(STATE_PATH), 'State file saved');
|
||
|
||
// Create new PES from same DB
|
||
pes.destroy();
|
||
|
||
const pes2 = new Pes({
|
||
name: 'Память',
|
||
ownerId: 'nikitron',
|
||
dbPath: DB_PATH,
|
||
autoTick: false,
|
||
});
|
||
pes2.wake();
|
||
|
||
const history2 = pes2.store.getHistory({ limit: 50 });
|
||
assert(history2.length >= 3, `History survives restart (got ${history2.length})`);
|
||
|
||
pes2.destroy();
|
||
}
|
||
|
||
// ── SUMMARY ─────────────────────────────────────────────────
|
||
|
||
cleanup();
|
||
|
||
console.log('\n═══════════════════════════════════════════════════════');
|
||
console.log(` INTEGRATION TESTS: ${passed} passed, ${failed} failed`);
|
||
console.log('═══════════════════════════════════════════════════════\n');
|
||
|
||
if (failed > 0) process.exit(1);
|