Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 31 additions & 9 deletions src/lib/heal.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import fs from 'node:fs';
import path from 'node:path';
import { run } from './exec.mjs';
import { rufloRoot, aqeRoot } from './paths.mjs';
import { agentdbLocations, bsq3IsNative, aidefencePresent } from './natives.mjs';
import { agentdbLocations, bsq3IsNative, bsq3Root, aidefencePresent } from './natives.mjs';
import { KIT_PKG } from './versions.mjs';
import { scanRvf, quarantine } from './rvf.mjs';

Expand All @@ -28,6 +28,34 @@ async function npmInstallInto(dir, spec) {
{ cwd: dir, timeout: 300_000 });
}

const failTail = (r) =>
`FAILED (${(r.stderr || `exit ${r.code}`).trim().split('\n').slice(-2).join(' ').slice(0, 200)})`;

/** Deterministic native better-sqlite3 for one location — an escalation
* ladder, verifying after each rung, stopping at the first binding:
* 1. plain install — enough on npm <11.17, or when npm's content store
* already holds a built copy of the exact version (why plain installs
* look like they "work": it depends on cache history, not on scripts).
* 2. npm approve-scripts + rebuild — npm ≥11.17's sanctioned path for the
* blocked install script (approve-scripts pins the exact version into
* the location's package.json; harmless no-op failure on older npm).
* rebuild also recovers a stale half-built build/ dir.
* 3. run the package's own install script directly — explicit `npm run`
* is user-invoked and never gated by allow-scripts. */
export async function ensureNativeBsq3(dir) {
let r = await npmInstallInto(dir, 'better-sqlite3@^12');
if (bsq3IsNative(dir)) return { ok: true, how: 'native installed' };
await run('npm', ['approve-scripts', 'better-sqlite3'], { cwd: dir, timeout: 60_000 });
r = await run('npm', ['rebuild', 'better-sqlite3'], { cwd: dir, timeout: 300_000 });
if (bsq3IsNative(dir)) return { ok: true, how: 'native rebuilt (scripts approved)' };
const pkgRoot = bsq3Root(dir);
if (pkgRoot) {
r = await run('npm', ['run', 'install'], { cwd: pkgRoot, timeout: 300_000 });
if (bsq3IsNative(dir)) return { ok: true, how: 'native built via package install script' };
}
return { ok: false, how: failTail(r) };
}

/** Native better-sqlite3 into every agentdb location that lacks it. */
export async function healNatives() {
const details = [];
Expand All @@ -37,16 +65,10 @@ export async function healNatives() {
// the 3.29.0 tree) between enumeration and heal.
if (!fs.existsSync(dir)) continue;
if (bsq3IsNative(dir)) continue;
const r = await npmInstallInto(dir, 'better-sqlite3@^12');
details.push(`${dir}: ${r.code === 0 && bsq3IsNative(dir)
? 'native installed'
: `FAILED (${(r.stderr || `exit ${r.code}`).trim().split('\n').slice(-2).join(' ').slice(0, 200)})`}`);
details.push(`${dir}: ${(await ensureNativeBsq3(dir)).how}`);
}
if (fs.existsSync(aqeRoot()) && !bsq3IsNative(aqeRoot())) {
const r = await npmInstallInto(aqeRoot(), 'better-sqlite3@^12');
details.push(`agentic-qe: ${r.code === 0 && bsq3IsNative(aqeRoot())
? 'native installed'
: `FAILED (${(r.stderr || `exit ${r.code}`).trim().split('\n').slice(-2).join(' ').slice(0, 200)})`}`);
details.push(`agentic-qe: ${(await ensureNativeBsq3(aqeRoot())).how}`);
}
return { ok: !details.some((d) => d.includes('FAILED')), detail: details.join('; ') || 'already native everywhere' };
}
Expand Down
21 changes: 13 additions & 8 deletions src/lib/natives.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,23 @@ export function agentdbLocations() {
.filter((p) => fs.existsSync(p));
}

/** Does better-sqlite3, as resolved from `fromDir` (real Node resolution),
* have a native binding? Mirrors ruflo-patch-native's check. */
export function bsq3IsNative(fromDir) {
let entry;
/** Package root of better-sqlite3 as resolved from `fromDir` (real Node
* resolution), or null if not resolvable. */
export function bsq3Root(fromDir) {
try {
const req = createRequire(path.join(fromDir, 'noop.js'));
entry = req.resolve('better-sqlite3'); // …/better-sqlite3/lib/index.js
const entry = req.resolve('better-sqlite3'); // …/better-sqlite3/lib/index.js
return path.join(entry.slice(0, entry.lastIndexOf(`${path.sep}better-sqlite3${path.sep}`)), 'better-sqlite3');
} catch {
return false; // not resolvable at all
return null;
}
const pkgRoot = path.join(entry.slice(0, entry.lastIndexOf(`${path.sep}better-sqlite3${path.sep}`)), 'better-sqlite3');
return fs.existsSync(path.join(pkgRoot, 'build', 'Release', 'better_sqlite3.node'));
}

/** Does better-sqlite3, as resolved from `fromDir`, have a native binding?
* Mirrors ruflo-patch-native's check. */
export function bsq3IsNative(fromDir) {
const root = bsq3Root(fromDir);
return !!root && fs.existsSync(path.join(root, 'build', 'Release', 'better_sqlite3.node'));
}

export function nativesStatus() {
Expand Down
47 changes: 47 additions & 0 deletions tests/kit/natives.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// bsq3Root / bsq3IsNative — the resolution + binding check behind the
// natives heal ladder. Uses a synthetic node_modules fixture so the test
// is hermetic (no npm, no network) and runs on the full CI matrix.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { bsq3Root, bsq3IsNative } from '../../src/lib/natives.mjs';

function makeFixture({ withBinding }) {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ak-natives-'));
const pkg = path.join(dir, 'node_modules', 'better-sqlite3');
fs.mkdirSync(path.join(pkg, 'lib'), { recursive: true });
fs.writeFileSync(path.join(pkg, 'package.json'),
JSON.stringify({ name: 'better-sqlite3', version: '12.0.0', main: 'lib/index.js' }));
fs.writeFileSync(path.join(pkg, 'lib', 'index.js'), 'module.exports = {};\n');
if (withBinding) {
fs.mkdirSync(path.join(pkg, 'build', 'Release'), { recursive: true });
fs.writeFileSync(path.join(pkg, 'build', 'Release', 'better_sqlite3.node'), '');
}
return { dir, pkg };
}

test('bsq3Root resolves the package root through node resolution', () => {
const { dir, pkg } = makeFixture({ withBinding: true });
assert.equal(fs.realpathSync(bsq3Root(dir)), fs.realpathSync(pkg));
fs.rmSync(dir, { recursive: true, force: true });
});

test('bsq3Root returns null when better-sqlite3 is not resolvable', () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ak-natives-empty-'));
assert.equal(bsq3Root(dir), null);
fs.rmSync(dir, { recursive: true, force: true });
});

test('bsq3IsNative is true when the compiled binding exists', () => {
const { dir } = makeFixture({ withBinding: true });
assert.equal(bsq3IsNative(dir), true);
fs.rmSync(dir, { recursive: true, force: true });
});

test('bsq3IsNative is false for a WASM-fallback install (no binding file)', () => {
const { dir } = makeFixture({ withBinding: false });
assert.equal(bsq3IsNative(dir), false);
fs.rmSync(dir, { recursive: true, force: true });
});
Loading