mirror of
https://github.com/ruvnet/ruflo.git
synced 2026-09-14 14:01:28 +08:00
fix(memory,tests): restore memory search recall and green the CLI suite (69 → 0) (#3252)
* fix(memory,tests): restore `memory search` recall and green the CLI suite (69 → 0) The CLI suite had 69 failing tests across 50 files. Root-causing each rather than adjusting expectations turned up one real product regression, two real bugs, and a set of tests asserting things about the machine instead of the code. PRODUCT REGRESSION — `memory search` recalled nothing (re-break of #2558) `memory search` returned zero results for content that matched word for word, while store/list/retrieve worked. Bisected to the threshold default: * 2026-07-04 #2558 restored recall, designed around a 0.3 threshold. Its fusion scores a full-coverage keyword hit as 0.6*max(0,semantic) + 0.4*lexical, so with a non-positive cosine — routine for a one-word query — a PERFECT keyword match tops out at exactly 0.40. * 2026-07-26 #2790 set the CLI default to 0.7. Above 0.4, keyword recall is mathematically unreachable, so #2558 silently regressed. Instrumented the live path to confirm before changing anything: key=note/alpha terms=["connectivity"] cov=1 bm25=0.029 sem=-0.883 lex=1.000 score=0.400 thr=0.7 → dropped Default restored to 0.3 at both sites, with the ceiling documented so it is not raised past 0.4 again. Verified end to end: a shared keyword recalls all three entries, a unique keyword recalls only its own. This was broken on main too. REAL BUG — anchor containment rejected any project under a symlink containedPath() compared a realpath'd root against a NON-realpath'd candidate, so on macOS (/tmp → /private/tmp) every project looked like an escape. Now compares like with like. Both guarantees re-verified by probe: ../ traversal, deep traversal, absolute-outside and symlink-escape all still rejected; symlinked-root spelling now accepted. REAL BUG — a test that could not fail funnel.test.ts asserted `elapsed < 100ms` as a proxy for "no network call" — its own comment conceded a future fetch() would still pass. It also failed intermittently (118ms) under full-suite CPU contention. Replaced with an actual assertion on fetch/http.request/https.request, patched via the CJS copies (ESM namespaces are non-writable). Negative-controlled: the technique observes a real call and restores cleanly. TESTS THAT MEASURED THE MACHINE, NOT THE CODE * 51 wasm tests asserted `available === true` for optional packages that were simply not installed, gated only on `process.env.CI`. Now also gated on real availability. The 7 tests in those files that need no WASM were lifted into their own blocks so they keep running rather than being skipped along with the rest — no coverage traded away for a green tick. * 4 agenticow tests lacked the `skipIf(!havePkg)` guard their 4 siblings had; in degraded mode the verb is inert and returns before validation. * policy-runtime hashed the raw tmpdir path while the source hashes the CANONICAL one, so it looked for a trust anchor that is never written there — and its cleanup was deleting a nonexistent path, leaking real anchors into ~/.config on every run. * sona's provider allowlist predated the ruvector / wasm-embedder / @claude-flow/embeddings backends. * ruvector/index asserted vi.mock satisfies a *dynamic* import() of a bare specifier. It does not under vitest 4 — the specifier is rewritten to '/@id/@ruvector/core' and fails with ERR_MODULE_NOT_FOUND. Now pins the contract the try/catch actually provides: unloadable → false, never throws. TIMEOUTS — the "flakiness" was a fixed 5s budget Six memory/intelligence files failed with "Test timed out in 5000ms", never an assertion. They initialise a real ONNX embedder and SQLite; alone they are fast, but the full suite saturates every core (~440% CPU). Because it depended on scheduling, a different file failed each run. Raised testTimeout/hookTimeout to 30s. (Process-level isolation was tried first and ruled out — forks changed nothing, which is what identified the cause as time, not shared state.) Verified: 3 consecutive full runs at 0 failed / 3572 passed on the feature base, and 0 failed / 3537 passed on this branch's base. Co-Authored-By: RuFlo <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_013u4pmL9ZUAXb6usVQgNo67 * fix(tests): make ruvector availability assertion environment-independent CI caught what my first pass missed. The rewritten test asserted `isRuvectorAvailable()` resolves to `false`, but whether the dynamic `import('@ruvector/core')` resolves is environment-dependent: - under vitest's module runner a bare-specifier dynamic import is rewritten to '/@id/@ruvector/core' and fails (ERR_MODULE_NOT_FOUND) → false → my `toBe(false)` passed locally - a normal install where the package resolves → true → `toBe(false)` fails, which is exactly what the CI test-ratchet flagged The prior `toBe(true)` had the same defect in the other direction. Pin the invariant that holds in BOTH environments instead — the only thing the try/catch actually promises: it resolves to a boolean and never throws. No specific resolution outcome is asserted. Verified 33/33 locally; the sibling "should return boolean" test already covers the type, so this now documents the no-throw contract distinctly. Co-Authored-By: RuFlo <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_013u4pmL9ZUAXb6usVQgNo67
This commit is contained in:
@@ -118,7 +118,9 @@ describe('agenticow MCP tools — happy path (real package)', () => {
|
||||
expect(st.status.dimension).toBe(8);
|
||||
});
|
||||
|
||||
it('new read/write verbs degrade + validate like the lifecycle verbs', async () => {
|
||||
// Requires the real package: in degraded mode the verb is inert and
|
||||
// returns {degraded:true} before argument validation ever runs.
|
||||
it.skipIf(!havePkg)('new read/write verbs degrade + validate like the lifecycle verbs', async () => {
|
||||
// path-traversal rejection is shared via resolveMemoryPath
|
||||
const query = findTool('agenticow_query');
|
||||
await expect(query.handler({ path: '../../etc/passwd', vector: [1, 2] })).rejects.toThrow(/disallowed/);
|
||||
@@ -169,7 +171,9 @@ describe('agenticow MCP tools — happy path (real package)', () => {
|
||||
expect(rbResult.rolledBack).toBe(true);
|
||||
});
|
||||
|
||||
it('agenticow_branch rejects path traversal in basePath', async () => {
|
||||
// Requires the real package: in degraded mode the verb is inert and
|
||||
// returns {degraded:true} before argument validation ever runs.
|
||||
it.skipIf(!havePkg)('agenticow_branch rejects path traversal in basePath', async () => {
|
||||
const branch = findTool('agenticow_branch');
|
||||
await expect(branch.handler({
|
||||
basePath: '../../../../etc/passwd',
|
||||
@@ -179,7 +183,9 @@ describe('agenticow MCP tools — happy path (real package)', () => {
|
||||
})).rejects.toThrow(/disallowed characters/);
|
||||
});
|
||||
|
||||
it('agenticow_branch rejects a malformed label', async () => {
|
||||
// Requires the real package: in degraded mode the verb is inert and
|
||||
// returns {degraded:true} before argument validation ever runs.
|
||||
it.skipIf(!havePkg)('agenticow_branch rejects a malformed label', async () => {
|
||||
const branch = findTool('agenticow_branch');
|
||||
await expect(branch.handler({
|
||||
basePath,
|
||||
@@ -189,7 +195,9 @@ describe('agenticow MCP tools — happy path (real package)', () => {
|
||||
})).rejects.toThrow(/may only contain/);
|
||||
});
|
||||
|
||||
it('agenticow_branch requires dimension when creating a new memory file', async () => {
|
||||
// Requires the real package: in degraded mode the verb is inert and
|
||||
// returns {degraded:true} before argument validation ever runs.
|
||||
it.skipIf(!havePkg)('agenticow_branch requires dimension when creating a new memory file', async () => {
|
||||
const branch = findTool('agenticow_branch');
|
||||
const freshBase = join(workdir, 'nonexistent.rvf');
|
||||
await expect(branch.handler({
|
||||
|
||||
@@ -718,19 +718,44 @@ describe('attributionUrl (ADR-305 measurement, no runtime network)', () => {
|
||||
expect(new URL(out).searchParams.has('fid')).toBe(false);
|
||||
});
|
||||
|
||||
it('emits no network call — attribution is a pure link builder', () => {
|
||||
// Guard: the function must be synchronous and side-effect-free with
|
||||
// respect to the network. If someone later adds fetch/https here, this
|
||||
// test will still pass but the *design* is documented.
|
||||
const before = Date.now();
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
attributionUrl('https://cognitum.one/ruflo', {
|
||||
medium: 'statusline', campaign: 'disclosure', content: String(i),
|
||||
});
|
||||
it('emits no network call — attribution is a pure link builder', async () => {
|
||||
// This used to assert `elapsed < 100ms` for 1000 builds and treat that as
|
||||
// proof of "no network call". Two problems: it never actually observed the
|
||||
// network (its own comment conceded a future fetch() would still pass), and
|
||||
// wall-clock is not a property of the code under test — under full-suite
|
||||
// CPU contention the loop measured 118ms and failed, intermittently.
|
||||
// Assert the real property instead, by watching the network primitives.
|
||||
// Take the CJS copies: an ESM namespace object's properties are
|
||||
// non-writable ("Cannot redefine property: request"), while module.exports
|
||||
// on the CJS twin can be swapped and restored. Both surface the same
|
||||
// underlying implementation, so patching here observes any real call.
|
||||
const { createRequire } = await import('node:module');
|
||||
const requireCjs = createRequire(import.meta.url);
|
||||
const http = requireCjs('node:http') as { request: unknown };
|
||||
const https = requireCjs('node:https') as { request: unknown };
|
||||
const originalFetch = globalThis.fetch;
|
||||
const fetchCalls: unknown[] = [];
|
||||
const httpReq = http.request;
|
||||
const httpsReq = https.request;
|
||||
const httpCalls: unknown[] = [];
|
||||
const httpsCalls: unknown[] = [];
|
||||
globalThis.fetch = ((...a: unknown[]) => { fetchCalls.push(a); throw new Error('unexpected fetch'); }) as unknown as typeof fetch;
|
||||
(http as { request: unknown }).request = ((...a: unknown[]) => { httpCalls.push(a); throw new Error('unexpected http.request'); }) as unknown;
|
||||
(https as { request: unknown }).request = ((...a: unknown[]) => { httpsCalls.push(a); throw new Error('unexpected https.request'); }) as unknown;
|
||||
try {
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
attributionUrl('https://cognitum.one/ruflo', {
|
||||
medium: 'statusline', campaign: 'disclosure', content: String(i),
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
(http as { request: unknown }).request = httpReq;
|
||||
(https as { request: unknown }).request = httpsReq;
|
||||
}
|
||||
const elapsed = Date.now() - before;
|
||||
// 1000 URL builds must be sub-100ms (network calls would be nowhere near).
|
||||
expect(elapsed).toBeLessThan(100);
|
||||
expect(fetchCalls).toEqual([]);
|
||||
expect(httpCalls).toEqual([]);
|
||||
expect(httpsCalls).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, unlinkSync, writeFileSync } from 'node:fs';
|
||||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, unlinkSync, writeFileSync, realpathSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir, userInfo } from 'node:os';
|
||||
import { createHash, createHmac } from 'node:crypto';
|
||||
@@ -21,7 +21,11 @@ const roots: Array<{ root: string; trust: string }> = [];
|
||||
function project(): string {
|
||||
const root = mkdtempSync(join(tmpdir(), 'ruflo-policy-runtime-'));
|
||||
mkdirSync(join(root, '.claude-flow'), { recursive: true });
|
||||
const projectId = createHash('sha256').update(root).digest('hex');
|
||||
// policy-runtime derives the trust-anchor id from the CANONICAL root
|
||||
// (realpathSync), so hash the same thing here — on macOS tmpdir() sits
|
||||
// behind a symlink, so hashing the raw path pointed this test (and its
|
||||
// cleanup) at a directory that is never created, leaking real anchors.
|
||||
const projectId = createHash('sha256').update(realpathSync(root)).digest('hex');
|
||||
roots.push({
|
||||
root,
|
||||
trust: join(userInfo().homedir, '.config', 'ruflo', 'policy-trust', projectId),
|
||||
|
||||
@@ -135,7 +135,15 @@ import {
|
||||
// the real @ruvector/rvagent-wasm import still happens. Local runs where
|
||||
// the WASM binary is built work fine; CI without postinstall doesn't.
|
||||
// See ruvllm-wasm.test.ts for the same pattern.
|
||||
const __SKIP_WASM_TESTS = process.env.CI === 'true';
|
||||
// Skip when CI (see above) OR when the optional WASM package simply is not
|
||||
// installed — it is an optionalDependency, so a normal dev install may not
|
||||
// have it. Asserting `available === true` in that case tests the developer's
|
||||
// node_modules, not this code.
|
||||
const __WASM_INSTALLED = await (async () => {
|
||||
try { await import('@ruvector/rvagent-wasm'); return true; } catch { return false; }
|
||||
})();
|
||||
const __SKIP_IN_CI = process.env.CI === 'true';
|
||||
const __SKIP_WASM_TESTS = __SKIP_IN_CI || !__WASM_INSTALLED;
|
||||
|
||||
describe.skipIf(__SKIP_WASM_TESTS)('agent-wasm integration', () => {
|
||||
describe('detection and init', () => {
|
||||
@@ -187,9 +195,6 @@ describe.skipIf(__SKIP_WASM_TESTS)('agent-wasm integration', () => {
|
||||
expect(getWasmAgent(agentId)!.id).toBe(agentId);
|
||||
});
|
||||
|
||||
it('returns null for unknown agent', () => {
|
||||
expect(getWasmAgent('nonexistent')).toBeNull();
|
||||
});
|
||||
|
||||
it('terminates agent', async () => {
|
||||
const info = await createWasmAgent();
|
||||
@@ -199,9 +204,6 @@ describe.skipIf(__SKIP_WASM_TESTS)('agent-wasm integration', () => {
|
||||
agentId = ''; // prevent double-terminate
|
||||
});
|
||||
|
||||
it('returns false for terminating nonexistent', () => {
|
||||
expect(terminateWasmAgent('nonexistent')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('prompting', () => {
|
||||
@@ -216,9 +218,6 @@ describe.skipIf(__SKIP_WASM_TESTS)('agent-wasm integration', () => {
|
||||
expect(result).toBe('Hello from WASM agent');
|
||||
});
|
||||
|
||||
it('throws for unknown agent', async () => {
|
||||
await expect(promptWasmAgent('nope', 'test')).rejects.toThrow('WASM agent not found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('tool execution', () => {
|
||||
@@ -233,9 +232,6 @@ describe.skipIf(__SKIP_WASM_TESTS)('agent-wasm integration', () => {
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('throws for unknown agent', async () => {
|
||||
await expect(executeWasmTool('nope', { tool: 'list_files' })).rejects.toThrow('WASM agent not found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('agent state accessors', () => {
|
||||
@@ -268,12 +264,6 @@ describe.skipIf(__SKIP_WASM_TESTS)('agent-wasm integration', () => {
|
||||
expect(parsed).toHaveProperty('info');
|
||||
});
|
||||
|
||||
it('throws for unknown agent', () => {
|
||||
expect(() => getWasmAgentState('nope')).toThrow('WASM agent not found');
|
||||
expect(() => getWasmAgentTools('nope')).toThrow('WASM agent not found');
|
||||
expect(() => getWasmAgentTodos('nope')).toThrow('WASM agent not found');
|
||||
expect(() => exportWasmState('nope')).toThrow('WASM agent not found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('MCP server bridge', () => {
|
||||
@@ -290,9 +280,6 @@ describe.skipIf(__SKIP_WASM_TESTS)('agent-wasm integration', () => {
|
||||
expect(resp).toContain('jsonrpc');
|
||||
});
|
||||
|
||||
it('throws for unknown agent', async () => {
|
||||
await expect(createWasmMcpServer('nope')).rejects.toThrow('WASM agent not found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('gallery templates', () => {
|
||||
@@ -366,3 +353,32 @@ describe.skipIf(__SKIP_WASM_TESTS)('agent-wasm integration', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// These assertions never touch the WASM module — they exercise the
|
||||
// unknown-agent error paths, which resolve from the local registry before any
|
||||
// dynamic import. So they are gated on CI only (exactly as before), NOT on
|
||||
// whether @ruvector/rvagent-wasm is installed: skipping them when the optional
|
||||
// package is absent would test nothing but the developer's node_modules.
|
||||
describe.skipIf(__SKIP_IN_CI)('agent-wasm integration (no WASM module required)', () => {
|
||||
it('returns null for unknown agent', () => {
|
||||
expect(getWasmAgent('nonexistent')).toBeNull();
|
||||
});
|
||||
it('returns false for terminating nonexistent', () => {
|
||||
expect(terminateWasmAgent('nonexistent')).toBe(false);
|
||||
});
|
||||
it('throws for unknown agent', async () => {
|
||||
await expect(promptWasmAgent('nope', 'test')).rejects.toThrow('WASM agent not found');
|
||||
});
|
||||
it('throws for unknown agent', async () => {
|
||||
await expect(executeWasmTool('nope', { tool: 'list_files' })).rejects.toThrow('WASM agent not found');
|
||||
});
|
||||
it('throws for unknown agent', () => {
|
||||
expect(() => getWasmAgentState('nope')).toThrow('WASM agent not found');
|
||||
expect(() => getWasmAgentTools('nope')).toThrow('WASM agent not found');
|
||||
expect(() => getWasmAgentTodos('nope')).toThrow('WASM agent not found');
|
||||
expect(() => exportWasmState('nope')).toThrow('WASM agent not found');
|
||||
});
|
||||
it('throws for unknown agent', async () => {
|
||||
await expect(createWasmMcpServer('nope')).rejects.toThrow('WASM agent not found');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -62,15 +62,18 @@ describe('RuVector Module Exports', () => {
|
||||
expect(typeof result).toBe('boolean');
|
||||
});
|
||||
|
||||
it('returns true when ruvector resolves (mocked at top of file)', async () => {
|
||||
// The top-level vi.mock('@ruvector/core', ...) makes the dynamic
|
||||
// import inside isRuvectorAvailable resolve, so the value must be
|
||||
// true. The previous test used vi.doMock to flip this at runtime,
|
||||
// but vi.doMock is too late: the module graph is already resolved
|
||||
// by the time the test handler runs (vi.mock is hoisted, vi.doMock
|
||||
// is not). Pinning the *real* observable behavior here.
|
||||
it('never rejects — the try/catch swallows module-resolution failure', async () => {
|
||||
// Deliberately does NOT assert a specific boolean. Whether
|
||||
// `import('@ruvector/core')` resolves is environment-dependent: under
|
||||
// vitest's module runner a bare-specifier dynamic import is rewritten to
|
||||
// '/@id/@ruvector/core' and fails (ERR_MODULE_NOT_FOUND), while a normal
|
||||
// node process with the package installed resolves it. The previous
|
||||
// assertion pinned one environment's outcome (`toBe(true)`, then my
|
||||
// `toBe(false)`) and so flipped between the two — green locally, red in
|
||||
// CI. The invariant that holds everywhere is the only thing the try/catch
|
||||
// promises: it resolves to a boolean and never throws.
|
||||
const result = await isRuvectorAvailable();
|
||||
expect(result).toBe(true);
|
||||
expect(typeof result).toBe('boolean');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -220,7 +220,15 @@ vi.mock('node:module', () => ({
|
||||
// cleanly, this skip can come off.
|
||||
//
|
||||
// Skip in CI; run locally where WASM is built.
|
||||
const __SKIP_WASM_TESTS = process.env.CI === 'true';
|
||||
// Skip when CI (see above) OR when the optional WASM package simply is not
|
||||
// installed — it is an optionalDependency, so a normal dev install may not
|
||||
// have it. Asserting `available === true` in that case tests the developer's
|
||||
// node_modules, not this code.
|
||||
const __WASM_INSTALLED = await (async () => {
|
||||
try { await import('@ruvector/ruvllm-wasm'); return true; } catch { return false; }
|
||||
})();
|
||||
const __SKIP_IN_CI = process.env.CI === 'true';
|
||||
const __SKIP_WASM_TESTS = __SKIP_IN_CI || !__WASM_INSTALLED;
|
||||
|
||||
describe.skipIf(__SKIP_WASM_TESTS)('ruvllm-wasm integration', () => {
|
||||
beforeEach(() => {
|
||||
@@ -460,10 +468,13 @@ describe.skipIf(__SKIP_WASM_TESTS)('ruvllm-wasm integration', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('HNSW_MAX_SAFE_PATTERNS', () => {
|
||||
it('should be 1024', async () => {
|
||||
const { HNSW_MAX_SAFE_PATTERNS } = await import('../../src/ruvector/ruvllm-wasm.js');
|
||||
expect(HNSW_MAX_SAFE_PATTERNS).toBe(1024);
|
||||
});
|
||||
});
|
||||
|
||||
// A plain exported-constant assertion — no WASM module involved, so gate it on
|
||||
// CI only rather than on whether the optional package is installed.
|
||||
describe.skipIf(__SKIP_IN_CI)('ruvllm-wasm constants (no WASM module required)', () => {
|
||||
it('HNSW_MAX_SAFE_PATTERNS should be 1024', async () => {
|
||||
const { HNSW_MAX_SAFE_PATTERNS } = await import('../../src/ruvector/ruvllm-wasm.js');
|
||||
expect(HNSW_MAX_SAFE_PATTERNS).toBe(1024);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -100,7 +100,11 @@ describe('Neural Tools (neural-tools)', () => {
|
||||
expect(typeof provider).toBe('string');
|
||||
expect(provider.length).toBeGreaterThan(0);
|
||||
// Must match one of the known provider tiers or fallback
|
||||
const knownProviders = /agentic-flow|onnx|mock|hash|fallback|reasoningbank|none/i;
|
||||
// Every backend neural-tools can actually report. The three added here
|
||||
// (ruvector, wasm-embedder, @claude-flow/embeddings) are real provider
|
||||
// strings the allowlist predated — see neural-tools.ts:115/141/1073.
|
||||
const knownProviders =
|
||||
/agentic-flow|onnx|mock|hash|fallback|reasoningbank|none|ruvector|wasm-embedder|@claude-flow\/embeddings/i;
|
||||
expect(provider).toMatch(knownProviders);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -381,7 +381,14 @@ const searchCommand: Command = {
|
||||
name: 'threshold',
|
||||
description: 'Similarity threshold (0-1)',
|
||||
type: 'number',
|
||||
default: 0.7
|
||||
// MUST stay <= 0.4. The recall fusion in bridgeSearchEntries scores a
|
||||
// full-coverage exact-keyword hit as 0.6*max(0,semantic) + 0.4*lexical,
|
||||
// so when the semantic cosine is <= 0 (routine for a one-word query) a
|
||||
// perfect keyword match tops out at exactly 0.40. #2790 raised this
|
||||
// default to 0.7, which silently re-broke #2558: `memory search` matched
|
||||
// content word-for-word and still returned nothing. Regression guard:
|
||||
// __tests__/memory-search-recall-2558.test.ts.
|
||||
default: 0.3
|
||||
},
|
||||
{
|
||||
name: 'type',
|
||||
@@ -450,7 +457,7 @@ const searchCommand: Command = {
|
||||
// coalescing preserves an explicit zero. Fallback aligned with the
|
||||
// option's declared `default: 0.7` (was `0.3` — the two disagreed
|
||||
// and --help advertised a default the code did not honor).
|
||||
const threshold = ctx.flags.threshold as number ?? 0.7;
|
||||
const threshold = ctx.flags.threshold as number ?? 0.3;
|
||||
const searchType = ctx.flags.type as string || 'semantic';
|
||||
const buildHnsw = (ctx.flags['build-hnsw'] || ctx.flags.buildHnsw) as boolean;
|
||||
const requestedIntent = (ctx.flags.intent as string) || 'mixed';
|
||||
|
||||
@@ -57,14 +57,25 @@ function normalizeHash(value: string): string {
|
||||
}
|
||||
|
||||
function containedPath(projectRoot: string, requested: string): string {
|
||||
const root = realpathSync(resolve(projectRoot));
|
||||
const absolute = isAbsolute(requested) ? resolve(requested) : resolve(root, requested);
|
||||
const lexical = relative(root, absolute);
|
||||
if (lexical === '..' || lexical.startsWith(`..${sep}`) || isAbsolute(lexical)) {
|
||||
// The project root has two equally valid spellings when its path crosses a
|
||||
// symlink — on macOS `/tmp/x` and `/private/tmp/x` name the same directory.
|
||||
// Comparing a realpath'd root against a NON-realpath'd candidate (as this
|
||||
// did) makes every such project look like an escape, so a project anchored
|
||||
// anywhere under a symlink was rejected outright. Compare like with like:
|
||||
// the lexical guard accepts either spelling of the root, and the symlink
|
||||
// guard below still resolves the target and re-checks it physically.
|
||||
const rootLexical = resolve(projectRoot);
|
||||
const rootPhysical = realpathSync(rootLexical);
|
||||
const absolute = isAbsolute(requested) ? resolve(requested) : resolve(rootLexical, requested);
|
||||
const escapes = (base: string): boolean => {
|
||||
const rel = relative(base, absolute);
|
||||
return rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel);
|
||||
};
|
||||
if (escapes(rootLexical) && escapes(rootPhysical)) {
|
||||
throw new Error('flywheel anchor path must stay inside project root');
|
||||
}
|
||||
const actual = realpathSync(absolute);
|
||||
const physical = relative(root, actual);
|
||||
const physical = relative(rootPhysical, actual);
|
||||
if (physical === '..' || physical.startsWith(`..${sep}`) || isAbsolute(physical)) {
|
||||
throw new Error('flywheel anchor symlink escapes project root');
|
||||
}
|
||||
|
||||
@@ -36,6 +36,15 @@ export default defineConfig({
|
||||
environment: 'node',
|
||||
include: ['__tests__/**/*.test.ts'],
|
||||
globals: true,
|
||||
// Vitest's 5s default is unrealistic for this suite: a number of the
|
||||
// memory/intelligence tests initialise a real ONNX embedder and a SQLite
|
||||
// database. In isolation they finish quickly, but the full suite saturates
|
||||
// every core (~440% CPU), and under that contention they exceeded 5s and
|
||||
// failed with "Test timed out in 5000ms" — never an assertion failure.
|
||||
// Because it depended on scheduling, a different file timed out on each
|
||||
// run, which read as flakiness rather than a fixed timeout being too tight.
|
||||
testTimeout: 30_000,
|
||||
hookTimeout: 30_000,
|
||||
coverage: {
|
||||
enabled: false,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user