This commit is contained in:
Oleg Maslov
2026-09-02 10:14:22 +02:00
parent 0c3e2ead3b
commit b20b138fe4
771 changed files with 161561 additions and 9027 deletions

View File

@@ -5,6 +5,7 @@ import path from 'node:path';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import { describe, expect, it } from 'vitest';
import { shutdown } from '../src/core/setup.js';
const ROOT = path.resolve(import.meta.dirname, '..', '..', '..');
const SERVER_ENTRY = path.join(ROOT, 'packages', 'memory-mcp', 'dist', 'index.js');
@@ -20,14 +21,16 @@ interface AsyncRunResult {
stderr: string;
}
function run(command: string, args: string[]): Promise<AsyncRunResult> {
return runInCwd(command, args, ROOT);
}
function runInCwd(command: string, args: string[], cwd: string): Promise<AsyncRunResult> {
function runInCwd(
command: string,
args: string[],
cwd: string,
env: NodeJS.ProcessEnv = process.env,
): Promise<AsyncRunResult> {
return new Promise((resolve, reject) => {
const child = spawn(command, args, {
cwd,
env,
shell: process.platform === 'win32' && command.endsWith('.cmd'),
});
let stdout = '';
@@ -41,6 +44,37 @@ function runInCwd(command: string, args: string[], cwd: string): Promise<AsyncRu
});
}
function runNpm(args: string[], cwd: string = ROOT): Promise<AsyncRunResult> {
const bundledNpmCli = path.join(
path.dirname(process.execPath),
'node_modules',
'waggle-node-runtime',
'node_modules',
'npm',
'bin',
'npm-cli.js',
);
const npmCli = [bundledNpmCli, process.env.npm_execpath]
.find((candidate): candidate is string => (
typeof candidate === 'string' && fs.existsSync(candidate)
));
const npmEnv = { ...process.env };
const inheritedPath = Object.entries(npmEnv)
.find(([key]) => key.toLowerCase() === 'path')?.[1] ?? '';
for (const key of Object.keys(npmEnv)) {
if (key.toLowerCase() === 'path') delete npmEnv[key];
}
npmEnv.PATH = [
...(npmCli ? [path.dirname(npmCli)] : []),
path.dirname(process.execPath),
inheritedPath,
].filter(Boolean).join(path.delimiter);
return npmCli
? runInCwd(process.execPath, [npmCli, ...args], cwd, npmEnv)
: runInCwd(bin('npm'), args, cwd, npmEnv);
}
async function withTimeout<T>(work: Promise<T>, ms: number): Promise<T> {
let timeout: NodeJS.Timeout | undefined;
const timer = new Promise<never>((_, reject) => {
@@ -62,14 +96,19 @@ const WAGGLE_MEMORY_MCP_PACKAGE_CLOSURE = [
] as const;
describe('waggle-memory-mcp built runtime', () => {
it('shuts down safely before initialization and remains idempotent', () => {
expect(() => shutdown()).not.toThrow();
expect(() => shutdown()).not.toThrow();
});
it('completes MCP initialize and lists read-only tools from built JS', async () => {
const coreBuild = await run(bin('npm'), ['run', 'build', '--workspace', '@waggle/core']);
const coreBuild = await runNpm(['run', 'build', '--workspace', '@waggle/core']);
expect(coreBuild.status).toBe(0);
const wikiBuild = await run(bin('npm'), ['run', 'build', '--workspace', '@waggle/wiki-compiler']);
const wikiBuild = await runNpm(['run', 'build', '--workspace', '@waggle/wiki-compiler']);
expect(wikiBuild.status).toBe(0);
const mcpBuild = await run(bin('npm'), ['run', 'build', '--workspace', 'waggle-memory-mcp']);
const mcpBuild = await runNpm(['run', 'build', '--workspace', 'waggle-memory-mcp']);
expect(mcpBuild.status).toBe(0);
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'waggle-memory-mcp-'));
@@ -97,16 +136,16 @@ describe('waggle-memory-mcp built runtime', () => {
await client.close().catch(() => {});
fs.rmSync(dataDir, { recursive: true, force: true });
}
}, 30_000);
}, 60_000);
it('saves and recalls memory through the built write-scope MCP server', async () => {
const coreBuild = await run(bin('npm'), ['run', 'build', '--workspace', '@waggle/core']);
const coreBuild = await runNpm(['run', 'build', '--workspace', '@waggle/core']);
expect(coreBuild.status).toBe(0);
const wikiBuild = await run(bin('npm'), ['run', 'build', '--workspace', '@waggle/wiki-compiler']);
const wikiBuild = await runNpm(['run', 'build', '--workspace', '@waggle/wiki-compiler']);
expect(wikiBuild.status).toBe(0);
const mcpBuild = await run(bin('npm'), ['run', 'build', '--workspace', 'waggle-memory-mcp']);
const mcpBuild = await runNpm(['run', 'build', '--workspace', 'waggle-memory-mcp']);
expect(mcpBuild.status).toBe(0);
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'waggle-memory-mcp-write-'));
@@ -161,7 +200,7 @@ describe('waggle-memory-mcp built runtime', () => {
await client.close().catch(() => {});
fs.rmSync(dataDir, { recursive: true, force: true });
}
}, 30_000);
}, 60_000);
it('installs the local package closure and lists tools from the installed server', async () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'waggle-memory-mcp-installed-'));
@@ -176,11 +215,10 @@ describe('waggle-memory-mcp built runtime', () => {
try {
const dependencies: Record<string, string> = {};
for (const workspace of WAGGLE_MEMORY_MCP_PACKAGE_CLOSURE) {
const build = await run(bin('npm'), ['run', 'build', '--workspace', workspace]);
const build = await runNpm(['run', 'build', '--workspace', workspace]);
expect(build.status).toBe(0);
const pack = await run(
bin('npm'),
const pack = await runNpm(
['pack', '--workspace', workspace, '--pack-destination', packsDir, '--json'],
);
expect(pack.status).toBe(0);
@@ -195,8 +233,7 @@ describe('waggle-memory-mcp built runtime', () => {
JSON.stringify({ private: true, type: 'module', dependencies }, null, 2),
);
const install = await runInCwd(
bin('npm'),
const install = await runNpm(
['install', '--no-audit', '--no-fund', '--prefer-offline'],
projectDir,
);
@@ -220,7 +257,7 @@ describe('waggle-memory-mcp built runtime', () => {
} as Record<string, string>,
});
await withTimeout(client.connect(transport), 10_000);
await withTimeout(client.connect(transport), 30_000);
const tools = await withTimeout(client.listTools(), 10_000);
const names = tools.tools.map((tool) => tool.name);
@@ -231,5 +268,5 @@ describe('waggle-memory-mcp built runtime', () => {
await client.close().catch(() => {});
fs.rmSync(tempDir, { recursive: true, force: true });
}
}, 120_000);
}, 180_000);
});

View File

@@ -1,4 +1,5 @@
import { describe, expect, it } from 'vitest';
import { win32 as pathWin32 } from 'node:path';
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import {
@@ -16,7 +17,17 @@ import { registerIdentityTools } from '../src/tools/identity.js';
import { registerAwarenessTools } from '../src/tools/awareness.js';
import { registerWorkspaceTools } from '../src/tools/workspace.js';
import { registerHarvestTools } from '../src/tools/harvest.js';
import { registerCleanupTools } from '../src/tools/cleanup.js';
import {
buildClaudeLaunch,
registerCleanupTools,
resolveConsolidationGop,
} from '../src/tools/cleanup.js';
import {
collectObservations,
FrameStore,
MindDB,
SessionStore,
} from '@waggle/core';
import { registerEraseTools } from '../src/tools/erase.js';
import { registerIngestTools } from '../src/tools/ingest.js';
import { registerWikiTools } from '../src/tools/wiki.js';
@@ -43,6 +54,122 @@ function registerAll(server: McpServer): void {
registerWikiTools(server);
}
describe('Claude consolidation launch boundary', () => {
it('uses the canonical npm CLI without a shell or ambient secrets on Windows', () => {
const npmRoot = 'C:\\Users\\test\\AppData\\Roaming\\npm';
const shim = pathWin32.join(npmRoot, 'claude.cmd');
const cli = pathWin32.join(npmRoot, 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js');
const files = new Set([shim.toLowerCase(), cli.toLowerCase()]);
const launch = buildClaudeLaunch(['-p', '--output-format=text'], {
platform: 'win32',
env: {
PATH: npmRoot,
USERPROFILE: 'C:\\Users\\test',
APPDATA: 'C:\\Users\\test\\AppData\\Roaming',
CLAUDE_CONFIG_DIR: 'C:\\Users\\test\\.claude-profile',
ANTHROPIC_API_KEY: 'must-not-reach-claude',
OPENAI_API_KEY: 'must-not-reach-claude',
WAGGLE_FUTURE_PROVIDER_SECRET: 'must-also-be-denied',
},
isFile: (candidate) => files.has(pathWin32.normalize(candidate).toLowerCase()),
});
expect(launch.command).toBe(process.execPath);
expect(launch.args).toEqual([cli, '-p', '--output-format=text']);
expect(launch.options.shell).toBe(false);
expect(launch.options.env).toMatchObject({
PATH: npmRoot,
USERPROFILE: 'C:\\Users\\test',
APPDATA: 'C:\\Users\\test\\AppData\\Roaming',
CLAUDE_CONFIG_DIR: 'C:\\Users\\test\\.claude-profile',
HIVE_MIND_NO_SYNTH: '1',
});
expect(launch.options.env.ANTHROPIC_API_KEY).toBeUndefined();
expect(launch.options.env.OPENAI_API_KEY).toBeUndefined();
expect(launch.options.env.WAGGLE_FUTURE_PROVIDER_SECRET).toBeUndefined();
});
});
describe('consolidation GOP anchor', () => {
function fixture(): { db: MindDB; frames: FrameStore; sessions: SessionStore } {
const db = new MindDB(':memory:');
return { db, frames: new FrameStore(db), sessions: new SessionStore(db) };
}
function setCreatedAt(db: MindDB, id: number, createdAt: string): void {
db.getDatabase().prepare('UPDATE memory_frames SET created_at = ? WHERE id = ?')
.run(createdAt, id);
}
it('ignores a newer excluded-source frame', () => {
const { db, frames, sessions } = fixture();
try {
const target = sessions.create();
const decoy = sessions.create();
const first = frames.createIFrame(target.gop_id, 'eligible first', 'normal', 'agent_inferred');
const second = frames.createIFrame(target.gop_id, 'eligible second', 'normal', 'agent_inferred');
const excluded = frames.createIFrame(decoy.gop_id, 'excluded future', 'normal', 'user_stated');
setCreatedAt(db, first.id, '2026-01-01 00:00:00');
setCreatedAt(db, second.id, '2026-01-02 00:00:00');
setCreatedAt(db, excluded.id, '2026-12-01 00:00:00');
const observations = collectObservations(db, { limit: 400 });
expect(resolveConsolidationGop(frames, observations)).toBe(target.gop_id);
} finally {
db.close();
}
});
it('uses canonical offset chronology instead of textual order', () => {
const { db, frames, sessions } = fixture();
try {
const olderSession = sessions.create();
const newerSession = sessions.create();
const older = frames.createIFrame(olderSession.gop_id, 'older instant', 'normal', 'agent_inferred');
const newer = frames.createIFrame(newerSession.gop_id, 'newer instant', 'normal', 'agent_inferred');
setCreatedAt(db, older.id, '2026-01-01 01:00:00+0200');
setCreatedAt(db, newer.id, '2026-01-01 00:30:00+0100');
const observations = collectObservations(db, { limit: 400 });
expect(resolveConsolidationGop(frames, observations)).toBe(newerSession.gop_id);
} finally {
db.close();
}
});
it('breaks equal-instant ties with the higher frame id', () => {
const { db, frames, sessions } = fixture();
try {
const lowerSession = sessions.create();
const higherSession = sessions.create();
const lower = frames.createIFrame(lowerSession.gop_id, 'equal lower id', 'normal', 'agent_inferred');
const higher = frames.createIFrame(higherSession.gop_id, 'equal higher id', 'normal', 'agent_inferred');
setCreatedAt(db, lower.id, '2026-01-01 13:00:00+0100');
setCreatedAt(db, higher.id, '2026-01-01 12:00:00Z');
const observations = collectObservations(db, { limit: 400 });
expect(resolveConsolidationGop(frames, observations)).toBe(higherSession.gop_id);
} finally {
db.close();
}
});
it('returns no anchor when there are no eligible observations', () => {
const { db, frames, sessions } = fixture();
try {
const session = sessions.create();
frames.createIFrame(session.gop_id, 'excluded one', 'normal', 'user_stated');
frames.createIFrame(session.gop_id, 'excluded two', 'normal', 'user_stated');
const observations = collectObservations(db, { limit: 400 });
expect(observations).toEqual([]);
expect(resolveConsolidationGop(frames, observations)).toBeNull();
} finally {
db.close();
}
});
});
describe('parseScopes', () => {
it('defaults to full read+write when unset', () => {
const s = parseScopes(undefined);