moving
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled

This commit is contained in:
Oleg Maslov
2026-09-02 10:10:29 +02:00
commit 0c3e2ead3b
3841 changed files with 970576 additions and 0 deletions

View File

@@ -0,0 +1,165 @@
/**
* Wire-format round-trip integration test.
*
* Exercises the cli-bridge against a REAL hive-mind-cli + real
* hive-mind MCP server, in a freshly-init'd tmpdir-isolated mind so
* the test never touches the user's actual ~/.hive-mind.
*
* Acts as a fitness function: if the upstream MCP surface drifts
* (tool renames, schema field changes, importance enum changes, etc.)
* this test fails before any shim ships. Mocked unit tests would
* silently keep passing — that's exactly the gap that produced
* Commit 1.4 in the first place.
*
* Skipped automatically when `hive-mind-cli` is not on PATH (CI
* matrices without the upstream installed). Look for the
* "integration: hive-mind-cli not on PATH" log line.
*/
import { describe, expect, it, beforeAll, afterAll } from 'vitest';
import { mkdtemp, rm } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import { spawnSync } from 'node:child_process';
import { createCliBridge } from '../../src/cli-bridge.js';
import { encodeFrame } from '../../src/frame-encoder.js';
import type { HookEvent } from '../../src/hook-event-types.js';
const PROBE_TIMEOUT_MS = 5000;
const INIT_TIMEOUT_MS = 15000;
const TEST_TIMEOUT_MS = 30000;
const CLI_BIN = 'hive-mind-cli';
/**
* Path resolution priority:
* 1. HIVE_MIND_CLI_JS env override (CI / offline testing)
* 2. In-monorepo package at packages/hive-mind-cli/dist/index.js
* 3. Sibling checkout at ../hive-mind/packages/cli/dist/index.js
* 4. Fall back to 'hive-mind-cli' on PATH (relies on cli-bridge's
* Windows shell:true codepath; acceptable for unit/integration but
* forced JS path keeps things hermetic).
*/
function resolveCliJsPath(): string | undefined {
const envOverride = process.env['HIVE_MIND_CLI_JS'];
if (envOverride && existsSync(envOverride)) return envOverride;
const monorepo = resolve(import.meta.dirname, '..', '..', '..', 'hive-mind-cli', 'dist', 'index.js');
if (existsSync(monorepo)) return monorepo;
const sibling = resolve(process.cwd(), '..', 'hive-mind', 'packages', 'cli', 'dist', 'index.js');
if (existsSync(sibling)) return sibling;
return undefined;
}
const RESOLVED_CLI_JS = resolveCliJsPath();
const SPAWN_CMD = RESOLVED_CLI_JS ?? CLI_BIN;
function probeCli(): boolean {
try {
const probeArgs = RESOLVED_CLI_JS ? [RESOLVED_CLI_JS, '--help'] : ['--help'];
const probeBin = RESOLVED_CLI_JS ? process.execPath : CLI_BIN;
const probe = spawnSync(probeBin, probeArgs, {
stdio: 'pipe',
timeout: PROBE_TIMEOUT_MS,
shell: !RESOLVED_CLI_JS,
});
return probe.status === 0;
} catch {
return false;
}
}
const cliReachable = probeCli();
let tmpHome: string | undefined;
let priorDataDir: string | undefined;
beforeAll(async () => {
if (!cliReachable) {
console.log('[integration] hive-mind-cli not on PATH — round-trip suite will skip.');
return;
}
tmpHome = await mkdtemp(join(tmpdir(), 'hmc-integration-'));
priorDataDir = process.env['HIVE_MIND_DATA_DIR'];
process.env['HIVE_MIND_DATA_DIR'] = tmpHome;
// Initialize an isolated mind file under the tmpdir.
const initBin = RESOLVED_CLI_JS ? process.execPath : CLI_BIN;
const initArgs = RESOLVED_CLI_JS ? [RESOLVED_CLI_JS, 'init'] : ['init'];
const init = spawnSync(initBin, initArgs, {
stdio: 'pipe',
timeout: INIT_TIMEOUT_MS,
shell: !RESOLVED_CLI_JS,
env: process.env,
});
if (init.status !== 0) {
throw new Error(`hive-mind-cli init failed: ${init.stderr?.toString() ?? '(no stderr)'}`);
}
}, INIT_TIMEOUT_MS);
afterAll(async () => {
if (!cliReachable) return;
if (priorDataDir === undefined) {
delete process.env['HIVE_MIND_DATA_DIR'];
} else {
process.env['HIVE_MIND_DATA_DIR'] = priorDataDir;
}
if (tmpHome) {
await rm(tmpHome, { recursive: true, force: true });
}
});
describe.skipIf(!cliReachable)('integration: cli-bridge ↔ hive-mind-cli round-trip', () => {
it('save_memory + recall_memory complete a wire-level round-trip with HookFrame inputs', async () => {
const bridge = createCliBridge({ max_retries: 0, timeout_ms: 15000, cli_path: SPAWN_CMD });
const marker = `roundtrip-${Date.now()}-${Math.random().toString(36).slice(2)}`;
const event: HookEvent = {
eventType: 'user-prompt-submit',
source: 'claude-code',
cwd: '/integration/test',
timestamp_iso: new Date().toISOString(),
payload: {
content: `${marker} this is the integration save body`,
session_id: `int-sess-${marker}`,
},
};
const frame = encodeFrame(event, { importance: 'normal' });
const saveResult = await bridge.saveMemory(frame);
expect(saveResult.success).toBe(true);
expect(saveResult.id).not.toBe('');
expect(Number(saveResult.id)).toBeGreaterThan(0);
// Recall by the unique marker — content prefix carries it through.
const hits = await bridge.recallMemory(marker, { limit: 5 });
expect(hits.length).toBeGreaterThanOrEqual(1);
const hit = hits[0];
expect(hit.content).toContain(marker);
expect(hit.content).toContain('src:claude-code');
expect(hit.importance).toBe('normal');
expect(hit.from).toBe('personal');
}, TEST_TIMEOUT_MS);
it('cleanup_frames responds without error', async () => {
const bridge = createCliBridge({ max_retries: 0, timeout_ms: 15000, cli_path: SPAWN_CMD });
const out = await bridge.cleanupFrames();
expect(typeof out.pruned).toBe('number');
expect(out.pruned).toBeGreaterThanOrEqual(0);
}, TEST_TIMEOUT_MS);
// NOTE: an "empty recall" integration check would be too brittle —
// upstream hybrid search returns very-low-score hits even for
// unrelated queries (semantic vector recall has no zero-score floor).
// The plain-text "No memories found" envelope is exercised by the
// unit test in cli-bridge.test.ts with a mocked spawn instead.
it('save_memory rejects an invalid source (regression: enum mismatch must surface, not be silently swallowed)', async () => {
const bridge = createCliBridge({ max_retries: 0, timeout_ms: 15000, cli_path: SPAWN_CMD });
// Bypass frameToSavePayload — use raw callMcpTool to send a bad source.
await expect(bridge.callMcpTool('save_memory', {
content: 'should fail',
source: 'claude-code', // INVALID — must be one of the four provenance enum values
})).rejects.toThrow(/Invalid enum value/);
}, TEST_TIMEOUT_MS);
});