This commit is contained in:
86
packages/server/tests/local/memory-stats-isolation.test.ts
Normal file
86
packages/server/tests/local/memory-stats-isolation.test.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Mind-isolation contract for GET /api/memory/stats (founder directive
|
||||
* 2026-06-12): workspace minds are SEPARATE stores — no leakage between
|
||||
* users. The default response counts the personal mind only; the cross-mind
|
||||
* aggregate is explicit opt-in (?scope=all-minds), counts-only, and exists
|
||||
* solely for the single-user loopback sidecar.
|
||||
*/
|
||||
import { describe, it, expect, afterEach } from 'vitest';
|
||||
import Fastify from 'fastify';
|
||||
import { MindDB, FrameStore, SessionStore } from '@waggle/core';
|
||||
import { memoryRoutes } from '../../src/local/routes/memory.js';
|
||||
|
||||
function seedFrames(db: MindDB, contents: string[]) {
|
||||
const sessions = new SessionStore(db);
|
||||
const gop = sessions.create().gop_id;
|
||||
const frames = new FrameStore(db);
|
||||
for (const c of contents) frames.createIFrame(gop, c);
|
||||
}
|
||||
|
||||
describe('GET /api/memory/stats — mind isolation', () => {
|
||||
let dbs: MindDB[] = [];
|
||||
let server: ReturnType<typeof Fastify>;
|
||||
|
||||
afterEach(async () => {
|
||||
await server.close();
|
||||
for (const db of dbs) db.close();
|
||||
dbs = [];
|
||||
});
|
||||
|
||||
function boot() {
|
||||
const personal = new MindDB(':memory:');
|
||||
const wsA = new MindDB(':memory:');
|
||||
const wsB = new MindDB(':memory:');
|
||||
dbs = [personal, wsA, wsB];
|
||||
seedFrames(personal, ['A personal memory that is long enough to count.']);
|
||||
seedFrames(wsA, ['Workspace A memory one is long enough.', 'Workspace A memory two is long enough.']);
|
||||
seedFrames(wsB, ['Workspace B memory one is long enough.']);
|
||||
|
||||
server = Fastify({ logger: false });
|
||||
server.decorate('multiMind', {
|
||||
personal,
|
||||
workspace: undefined,
|
||||
setWorkspace: () => {},
|
||||
getFrameStore: (scope: string) => (scope === 'personal' ? new FrameStore(personal) : undefined),
|
||||
search: () => [],
|
||||
});
|
||||
server.decorate('agentState', {
|
||||
getWorkspaceMindDb: (id: string) => (id === 'ws-a' ? wsA : id === 'ws-b' ? wsB : undefined),
|
||||
activateWorkspaceMind: () => true,
|
||||
});
|
||||
server.decorate('workspaceManager', {
|
||||
list: () => [
|
||||
{ id: 'ws-a', name: 'A', group: 'Personal' },
|
||||
{ id: 'ws-b', name: 'B', group: 'Personal' },
|
||||
],
|
||||
get: () => undefined,
|
||||
});
|
||||
server.register(memoryRoutes);
|
||||
}
|
||||
|
||||
it('default response counts the personal mind ONLY — no silent cross-mind mixing', async () => {
|
||||
boot();
|
||||
const res = await server.inject({ method: 'GET', url: '/api/memory/stats' });
|
||||
expect(res.statusCode).toBe(200);
|
||||
const body = res.json();
|
||||
expect(body.personal.frameCount).toBe(1);
|
||||
expect(body.total.frameCount).toBe(1);
|
||||
expect(body.workspace).toBeNull();
|
||||
});
|
||||
|
||||
it('?scope=all-minds aggregates counts across the local user’s minds, opt-in only', async () => {
|
||||
boot();
|
||||
const res = await server.inject({ method: 'GET', url: '/api/memory/stats?scope=all-minds' });
|
||||
const body = res.json();
|
||||
expect(body.personal.frameCount).toBe(1);
|
||||
expect(body.total.frameCount).toBe(4); // 1 personal + 2 wsA + 1 wsB
|
||||
});
|
||||
|
||||
it('an explicit workspaceId scopes to that single workspace mind', async () => {
|
||||
boot();
|
||||
const res = await server.inject({ method: 'GET', url: '/api/memory/stats?workspaceId=ws-a' });
|
||||
const body = res.json();
|
||||
expect(body.workspace.frameCount).toBe(2);
|
||||
expect(body.total.frameCount).toBe(3); // personal + ws-a only
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user