This commit is contained in:
283
packages/cli/tests/memory-persistence-hard.test.ts
Normal file
283
packages/cli/tests/memory-persistence-hard.test.ts
Normal file
@@ -0,0 +1,283 @@
|
||||
/**
|
||||
* HARD memory persistence test.
|
||||
*
|
||||
* Simulates 3 real work sessions across a multi-day project,
|
||||
* then verifies a cold-start "session 4" can recall everything
|
||||
* that matters — not trivia, but the kind of context that makes
|
||||
* an assistant feel like it was there the whole time.
|
||||
*
|
||||
* This is the crown jewel test: .mind file = portable brain.
|
||||
*/
|
||||
import { describe, it, expect, afterAll } from 'vitest';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import * as os from 'node:os';
|
||||
import { MindDB } from '@waggle/core';
|
||||
import { Orchestrator } from '@waggle/agent';
|
||||
import { MockEmbedder } from '../../hive-mind-core/tests/mind/helpers/mock-embedder.js';
|
||||
|
||||
const MIND_PATH = path.join(os.tmpdir(), `waggle-hard-memory-${Date.now()}.mind`);
|
||||
let lastDb: MindDB | null = null;
|
||||
|
||||
function cleanup() {
|
||||
lastDb?.close();
|
||||
lastDb = null;
|
||||
for (const f of [MIND_PATH, MIND_PATH + '-wal', MIND_PATH + '-shm']) {
|
||||
try { if (fs.existsSync(f)) fs.unlinkSync(f); } catch { /* ignore */ }
|
||||
}
|
||||
}
|
||||
|
||||
afterAll(cleanup);
|
||||
|
||||
describe('Hard Memory Persistence — Real Work Simulation', () => {
|
||||
const embedder = new MockEmbedder();
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// SESSION 1: Monday morning — project kickoff
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
it('Session 1: Project kickoff — identity, decisions, architecture', async () => {
|
||||
const db = new MindDB(MIND_PATH);
|
||||
const orch = new Orchestrator({ db, embedder });
|
||||
|
||||
// Set up identity
|
||||
orch.getIdentity().create({
|
||||
name: 'Waggle',
|
||||
role: 'Senior Engineering Assistant',
|
||||
department: 'Platform Team',
|
||||
personality: 'Thorough, opinionated, remembers everything',
|
||||
capabilities: 'Code review, architecture, memory, search, knowledge graph',
|
||||
system_prompt: 'You are Waggle, a senior engineering assistant for Marko.',
|
||||
});
|
||||
|
||||
// User context
|
||||
orch.getAwareness().add('flag', 'User is Marko Markovic, prefers direct communication', 10);
|
||||
orch.getAwareness().add('flag', 'Project: Rewrite payment service from Python to Go', 10);
|
||||
orch.getAwareness().add('task', 'Design new payment service architecture', 9);
|
||||
orch.getAwareness().add('pending', 'Waiting for Stripe API credentials from DevOps (asked Alice)', 7);
|
||||
|
||||
// Memories from the kickoff meeting
|
||||
await orch.executeTool('save_memory', {
|
||||
content: 'Architecture decision: Payment service will use Go with chi router, PostgreSQL, and connect to Stripe via their Go SDK. Rejected gRPC in favor of REST for simplicity. Team voted 4-1.',
|
||||
importance: 'critical',
|
||||
});
|
||||
await orch.executeTool('save_memory', {
|
||||
content: 'The current Python payment service handles 3 endpoints: POST /payments/charge, POST /payments/refund, GET /payments/:id. All must be preserved in the rewrite. The charge endpoint also calls an internal fraud-check service at http://fraud.internal:8080/check.',
|
||||
importance: 'critical',
|
||||
});
|
||||
await orch.executeTool('save_memory', {
|
||||
content: 'Alice (DevOps lead) said Stripe credentials will be in Vault at secret/data/stripe/production. She needs 2 business days. ETA: Wednesday.',
|
||||
importance: 'important',
|
||||
});
|
||||
await orch.executeTool('save_memory', {
|
||||
content: 'Marko raised concern about the fraud-check service being a single point of failure. Decision: implement circuit breaker with 5-second timeout, fallback to allowing the charge (business decision: false negatives are worse than false positives for fraud).',
|
||||
importance: 'critical',
|
||||
});
|
||||
await orch.executeTool('save_memory', {
|
||||
content: 'Database schema for payments table: id (uuid), amount_cents (bigint), currency (varchar(3)), stripe_charge_id (text), status (enum: pending/completed/failed/refunded), customer_id (uuid FK), created_at, updated_at. Using bigint for amount to avoid floating point issues.',
|
||||
importance: 'important',
|
||||
});
|
||||
|
||||
// Knowledge graph — people and their roles
|
||||
const kg = orch.getKnowledge();
|
||||
const marko = kg.createEntity('person', 'Marko Markovic', { role: 'Tech Lead', preference: 'direct communication' });
|
||||
const alice = kg.createEntity('person', 'Alice Chen', { role: 'DevOps Lead', team: 'Infrastructure' });
|
||||
const bob = kg.createEntity('person', 'Bob Kumar', { role: 'Backend Engineer', expertise: 'Go' });
|
||||
const paymentSvc = kg.createEntity('service', 'payment-service', { language: 'Go', status: 'in-development', repo: 'github.com/acme/payment-service-go' });
|
||||
const fraudSvc = kg.createEntity('service', 'fraud-check-service', { url: 'http://fraud.internal:8080', owner: 'Risk Team' });
|
||||
const stripe = kg.createEntity('integration', 'Stripe', { sdk: 'stripe-go', env: 'production' });
|
||||
|
||||
kg.createRelation(marko.id, paymentSvc.id, 'leads', 0.95);
|
||||
kg.createRelation(alice.id, paymentSvc.id, 'provides_infra', 0.9);
|
||||
kg.createRelation(bob.id, paymentSvc.id, 'implements', 0.9);
|
||||
kg.createRelation(paymentSvc.id, fraudSvc.id, 'depends_on', 1.0);
|
||||
kg.createRelation(paymentSvc.id, stripe.id, 'integrates', 1.0);
|
||||
|
||||
db.close();
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// SESSION 2: Tuesday — deep implementation work
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
it('Session 2: Implementation day — code decisions, bugs found, PR reviews', async () => {
|
||||
const db = new MindDB(MIND_PATH);
|
||||
const orch = new Orchestrator({ db, embedder });
|
||||
|
||||
await orch.executeTool('save_memory', {
|
||||
content: 'Started implementing the charge endpoint. Using chi router with middleware chain: logging → auth → rate-limit → handler. Bob suggested using errgroup for parallel Stripe + fraud-check calls — good idea, adopted it.',
|
||||
importance: 'important',
|
||||
});
|
||||
await orch.executeTool('save_memory', {
|
||||
content: 'Found a bug in the old Python service: refund endpoint doesn\'t check if payment is already refunded, allowing double refunds. Filed as JIRA PAY-142. Must fix in Go rewrite.',
|
||||
importance: 'critical',
|
||||
});
|
||||
await orch.executeTool('save_memory', {
|
||||
content: 'PR #87 review: Bob\'s implementation of the charge handler looks good but has a subtle race condition — if Stripe returns success but DB write fails, the charge is orphaned. Need to implement idempotency key pattern. Left detailed comment on the PR.',
|
||||
importance: 'important',
|
||||
});
|
||||
await orch.executeTool('save_memory', {
|
||||
content: 'Decided on error handling strategy: all errors return standard JSON { "error": { "code": "...", "message": "...", "request_id": "..." } }. HTTP status codes: 400 for validation, 402 for Stripe declined, 409 for duplicate/already-refunded, 500 for internal, 503 for circuit breaker open.',
|
||||
importance: 'important',
|
||||
});
|
||||
await orch.executeTool('save_memory', {
|
||||
content: 'Performance target from Marko: p99 latency under 200ms for charge endpoint (current Python service is 450ms). Go rewrite should easily beat this. Will add Prometheus metrics from day 1.',
|
||||
importance: 'important',
|
||||
});
|
||||
await orch.executeTool('save_memory', {
|
||||
content: 'Integration test strategy: use Stripe test mode with test API keys (not production). Alice confirmed test keys are already in Vault at secret/data/stripe/test. Docker compose setup with Postgres + test Stripe env.',
|
||||
importance: 'normal',
|
||||
});
|
||||
|
||||
// Update knowledge graph
|
||||
const kg = orch.getKnowledge();
|
||||
kg.createEntity('bug', 'PAY-142: Double refund vulnerability', { severity: 'high', status: 'open', found_in: 'Python payment service' });
|
||||
kg.createEntity('pr', 'PR #87: Charge handler', { author: 'Bob Kumar', status: 'changes-requested', issue: 'race condition on DB write' });
|
||||
|
||||
db.close();
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// SESSION 3: Wednesday — blockers, decisions, progress
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
it('Session 3: Midweek check-in — credentials arrived, new blocker, scope change', async () => {
|
||||
const db = new MindDB(MIND_PATH);
|
||||
const orch = new Orchestrator({ db, embedder });
|
||||
|
||||
await orch.executeTool('save_memory', {
|
||||
content: 'Alice delivered Stripe production credentials to Vault as promised. Verified access works. Removed from pending items.',
|
||||
importance: 'normal',
|
||||
});
|
||||
await orch.executeTool('save_memory', {
|
||||
content: 'NEW BLOCKER: Legal team says we need PCI DSS compliance audit before go-live. This was not in the original scope. Meeting scheduled with compliance team Friday. Could delay launch by 2 weeks.',
|
||||
importance: 'critical',
|
||||
});
|
||||
await orch.executeTool('save_memory', {
|
||||
content: 'Bob fixed the race condition in PR #87 using Stripe idempotency keys. Approved and merged. The charge endpoint is now production-ready pending PCI review.',
|
||||
importance: 'important',
|
||||
});
|
||||
await orch.executeTool('save_memory', {
|
||||
content: 'Marko decided to descope the refund endpoint from the initial launch. Reason: the double-refund bug (PAY-142) needs careful handling and the PCI blocker already delays us. Refund stays in Python service for now, will be migrated in phase 2.',
|
||||
importance: 'critical',
|
||||
});
|
||||
await orch.executeTool('save_memory', {
|
||||
content: 'Updated launch plan: Phase 1 = charge + get payment (Go). Phase 2 = refund migration + PAY-142 fix. Phase 3 = deprecate Python service entirely. Each phase is ~2 weeks.',
|
||||
importance: 'important',
|
||||
});
|
||||
await orch.executeTool('save_memory', {
|
||||
content: 'Circuit breaker implementation complete. Using sony/gobreaker library. Settings: maxRequests=5, interval=60s, timeout=5s, trip after 3 consecutive failures. Tested with fault injection — works correctly.',
|
||||
importance: 'normal',
|
||||
});
|
||||
|
||||
db.close();
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// SESSION 4: Thursday — COLD START. Can the agent pick up where we left off?
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
it('Session 4 (COLD START): Agent must recall project state without being told', async () => {
|
||||
const db = new MindDB(MIND_PATH);
|
||||
lastDb = db; // Track for cleanup
|
||||
const orch = new Orchestrator({ db, embedder });
|
||||
|
||||
// ─── Test A: Identity survives ───
|
||||
const identity = await orch.executeTool('get_identity', {});
|
||||
expect(identity).toContain('Waggle');
|
||||
expect(identity).toContain('Senior Engineering Assistant');
|
||||
|
||||
// ─── Test B: Awareness items survive ───
|
||||
const awareness = await orch.executeTool('get_awareness', {});
|
||||
expect(awareness).toContain('Marko Markovic');
|
||||
expect(awareness).toContain('payment service');
|
||||
|
||||
// ─── Test C: "What are we working on?" ───
|
||||
const projectContext = await orch.executeTool('search_memory', {
|
||||
query: 'payment service architecture Go',
|
||||
});
|
||||
expect(projectContext).toContain('Go');
|
||||
expect(projectContext).toContain('chi router');
|
||||
expect(projectContext).toContain('Stripe');
|
||||
|
||||
// ─── Test D: "What's blocking us?" ───
|
||||
const blockers = await orch.executeTool('search_memory', {
|
||||
query: 'blocker PCI compliance',
|
||||
});
|
||||
expect(blockers).toContain('PCI DSS');
|
||||
expect(blockers).toContain('compliance');
|
||||
|
||||
// ─── Test E: "What happened with the Stripe credentials?" ───
|
||||
const credentials = await orch.executeTool('search_memory', {
|
||||
query: 'Stripe credentials Vault Alice',
|
||||
});
|
||||
expect(credentials).toContain('Vault');
|
||||
expect(credentials).toContain('Alice');
|
||||
|
||||
// ─── Test F: "What's the current scope?" (must know about descoping) ───
|
||||
const scope = await orch.executeTool('search_memory', {
|
||||
query: 'launch plan phase refund descope',
|
||||
});
|
||||
expect(scope).toContain('Phase 1');
|
||||
expect(scope).toContain('refund');
|
||||
|
||||
// ─── Test G: "Tell me about the double-refund bug" ───
|
||||
const bug = await orch.executeTool('search_memory', {
|
||||
query: 'double refund bug PAY-142',
|
||||
});
|
||||
expect(bug).toContain('PAY-142');
|
||||
expect(bug).toContain('refund');
|
||||
|
||||
// ─── Test H: "What's Bob working on?" (knowledge graph) ───
|
||||
const bobInfo = await orch.executeTool('query_knowledge', {
|
||||
query: 'Bob',
|
||||
});
|
||||
expect(bobInfo).toContain('Bob Kumar');
|
||||
expect(bobInfo).toContain('implements');
|
||||
|
||||
// ─── Test I: "What does our service depend on?" ───
|
||||
const deps = await orch.executeTool('query_knowledge', {
|
||||
query: 'payment-service',
|
||||
});
|
||||
expect(deps).toContain('payment-service');
|
||||
expect(deps).toContain('depends_on');
|
||||
expect(deps).toContain('fraud-check');
|
||||
|
||||
// ─── Test J: "What was the error handling decision?" ───
|
||||
const errorHandling = await orch.executeTool('search_memory', {
|
||||
query: 'error handling JSON status codes',
|
||||
});
|
||||
expect(errorHandling).toContain('402');
|
||||
expect(errorHandling).toContain('circuit breaker');
|
||||
|
||||
// ─── Test K: "What are the performance requirements?" ───
|
||||
const perf = await orch.executeTool('search_memory', {
|
||||
query: 'performance latency p99 target',
|
||||
});
|
||||
expect(perf).toContain('200ms');
|
||||
expect(perf).toContain('Prometheus');
|
||||
|
||||
// ─── Test L: "What was decided about the race condition?" ───
|
||||
const raceCondition = await orch.executeTool('search_memory', {
|
||||
query: 'race condition idempotency PR 87',
|
||||
});
|
||||
expect(raceCondition).toContain('idempotency');
|
||||
|
||||
// ─── Test M: "What's the database schema?" ───
|
||||
const schema = await orch.executeTool('search_memory', {
|
||||
query: 'database schema payments table',
|
||||
});
|
||||
expect(schema).toContain('amount_cents');
|
||||
expect(schema).toContain('bigint');
|
||||
|
||||
// ─── Test N: Memory stats show realistic data ───
|
||||
const stats = orch.getMemoryStats();
|
||||
expect(stats.frameCount).toBeGreaterThanOrEqual(15); // We saved ~17 memories
|
||||
expect(stats.sessionCount).toBeGreaterThanOrEqual(1); // At least 1 session (CognifyPipeline reuses active)
|
||||
expect(stats.entityCount).toBeGreaterThanOrEqual(6); // 6+ entities in knowledge graph
|
||||
|
||||
// ─── Test O: System prompt has everything for a cold-start ───
|
||||
const prompt = orch.buildSystemPrompt();
|
||||
expect(prompt).toContain('Waggle');
|
||||
expect(prompt).toContain('payment service');
|
||||
expect(prompt).toContain('search_memory');
|
||||
|
||||
db.close();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user