mirror of
https://github.com/giancarloerra/socraticode.git
synced 2026-07-03 14:05:21 +02:00
e075d6ffa4
Resolves #43. On Linux/Node combinations where one ast-grep grammar package's prebuilt parser binary is missing for the host architecture (reporter: Ubuntu 24.04 + WSL2 + Node 24, missing the cpp prebuild), the existing loader silently failed to register every dynamic grammar, not just the broken one. Mechanism: registerDynamicLanguage iterates the modules it receives and accesses the lazy libraryPath getter on each. If that getter throws (because the prebuild lookup fails), the entire batch call aborts atomically — none of the 13 dynamic grammars get registered. The empty inner `catch {}` in ensureDynamicLanguages discarded the underlying reason, and the failure surfaced only at WARN as a generic "Failed to register dynamic ast-grep languages", with no indication of which grammar was the culprit. Empirically verified against @ast-grep/napi@0.40.5 in a clean Node environment: - Sequential register({A}); register({B}) calls REPLACE rather than accumulate; only the last call's grammar survives. So the obvious alternative of "register one at a time" is actively broken — it would silently leave only the last grammar in langPackages registered (php, since it's last in our list). On a polyglot codebase that's a regression. - Batch register({A, B, C}) with one entry whose libraryPath getter throws aborts atomically — none of A/B/C end up registered. The fix is to pre-validate each grammar's libraryPath getter inside our own per-grammar try/catch, exclude any that throw, then make ONE batch registerDynamicLanguage call with only the survivors: - src/services/code-graph.ts: ensureDynamicLanguages now touches mod.libraryPath inside the inner try/catch so a missing-prebuild failure is contained to that one grammar. Adds module-level loadedDynamicLanguages and failedDynamicLanguages state plus a public getDynamicLanguageStatus() introspection API. The empty `catch {}` becomes `catch (err) { ... }` capturing the actual reason. Per-grammar failures and the (now-rare) catch-all log at warn level with the underlying message. - src/tools/graph-tools.ts: codebase_graph_status now appends an "AST grammars: Loaded / Failed" block listing both sets, with the failure reason for each unloaded grammar. Users see the loader state without needing to enable debug logging. - src/services/graph-symbols.ts: extractSymbolsAndCalls bumps its failure log from DEBUG to WARN with one-shot dedupe per language — one warn per language for the lifetime of the process, not one warn per file (potentially hundreds). - src/services/graph-imports.ts: extractImports gets the same one-shot dedupe per language treatment for consistency. - tests/unit/dynamic-language-loader.test.ts: nine new tests covering the sync contract, idempotence, loaded/failed sort order, disjoint sets, and a sanity check that at least one expected grammar registers in the test environment. The loader stays sync. createRequire stays. No call-site signature changes, no test fixture rewrites. Existing 721 tests pass; with the new file, 730 tests pass. typecheck and biome clean. CodeRabbit review of the diff returned no findings. Co-authored-by: X-Adam <X-Adam@users.noreply.github.com>
110 lines
4.1 KiB
TypeScript
110 lines
4.1 KiB
TypeScript
// SPDX-License-Identifier: AGPL-3.0-only
|
|
// Copyright (C) 2026 Giancarlo Erra - Altaire Limited
|
|
//
|
|
// Tests for the dynamic ast-grep grammar loader.
|
|
//
|
|
// Background: in 1.8.3 the loader called `registerDynamicLanguage` once with
|
|
// every loaded grammar in a single batch. Per `@ast-grep/napi`, that call is
|
|
// atomic: a single throwing `libraryPath` getter aborts the whole batch and
|
|
// no grammar gets registered. On environments where one prebuilt binary is
|
|
// missing for the host architecture, this manifested as all dynamic
|
|
// languages being silently broken (issue #43).
|
|
//
|
|
// 1.8.4 fixes this by pre-validating each grammar's `libraryPath` getter in
|
|
// the inner per-grammar try/catch so a missing prebuild is contained to that
|
|
// one grammar. The `getDynamicLanguageStatus` API exposes which grammars
|
|
// registered successfully and which failed (with the underlying reason),
|
|
// surfaced in `codebase_graph_status`.
|
|
|
|
import { beforeAll, describe, expect, it } from "vitest";
|
|
import {
|
|
ensureDynamicLanguages,
|
|
getDynamicLanguageStatus,
|
|
} from "../../src/services/code-graph.js";
|
|
|
|
beforeAll(() => {
|
|
ensureDynamicLanguages();
|
|
});
|
|
|
|
describe("dynamic-language-loader", () => {
|
|
describe("ensureDynamicLanguages", () => {
|
|
it("is synchronous (returns void, not a Promise)", () => {
|
|
const result = ensureDynamicLanguages();
|
|
expect(result).toBeUndefined();
|
|
});
|
|
|
|
it("is idempotent — calling repeatedly does not change registration state", () => {
|
|
const before = getDynamicLanguageStatus();
|
|
ensureDynamicLanguages();
|
|
ensureDynamicLanguages();
|
|
const after = getDynamicLanguageStatus();
|
|
|
|
expect(after.loaded).toEqual(before.loaded);
|
|
expect(after.failed.map((f) => f.name)).toEqual(before.failed.map((f) => f.name));
|
|
});
|
|
});
|
|
|
|
describe("getDynamicLanguageStatus", () => {
|
|
it("returns loaded and failed arrays", () => {
|
|
const status = getDynamicLanguageStatus();
|
|
expect(status).toHaveProperty("loaded");
|
|
expect(status).toHaveProperty("failed");
|
|
expect(Array.isArray(status.loaded)).toBe(true);
|
|
expect(Array.isArray(status.failed)).toBe(true);
|
|
});
|
|
|
|
it("loaded entries are unique strings", () => {
|
|
const status = getDynamicLanguageStatus();
|
|
const seen = new Set<string>();
|
|
for (const name of status.loaded) {
|
|
expect(typeof name).toBe("string");
|
|
expect(seen.has(name)).toBe(false);
|
|
seen.add(name);
|
|
}
|
|
});
|
|
|
|
it("failed entries each include name and error", () => {
|
|
const status = getDynamicLanguageStatus();
|
|
for (const f of status.failed) {
|
|
expect(typeof f.name).toBe("string");
|
|
expect(typeof f.error).toBe("string");
|
|
expect(f.error.length).toBeGreaterThan(0);
|
|
}
|
|
});
|
|
|
|
it("loaded list is sorted alphabetically", () => {
|
|
const status = getDynamicLanguageStatus();
|
|
const sorted = [...status.loaded].sort();
|
|
expect(status.loaded).toEqual(sorted);
|
|
});
|
|
|
|
it("failed list is sorted alphabetically by name", () => {
|
|
const status = getDynamicLanguageStatus();
|
|
const names = status.failed.map((f) => f.name);
|
|
const sorted = [...names].sort();
|
|
expect(names).toEqual(sorted);
|
|
});
|
|
|
|
it("loaded and failed sets are disjoint", () => {
|
|
const status = getDynamicLanguageStatus();
|
|
const failedNames = new Set(status.failed.map((f) => f.name));
|
|
for (const loaded of status.loaded) {
|
|
expect(failedNames.has(loaded)).toBe(false);
|
|
}
|
|
});
|
|
|
|
it("at least one expected dynamic grammar registers in this environment", () => {
|
|
// On every supported dev environment for this repo (macOS, common
|
|
// Linux distros), the pre-validation step should let at least a few
|
|
// of these load successfully. If this fails, either the environment
|
|
// is unusual (and we want to know) or there is a real regression in
|
|
// the loader.
|
|
const status = getDynamicLanguageStatus();
|
|
const loaded = new Set(status.loaded);
|
|
const expected = ["python", "go", "java", "php", "ruby"];
|
|
const found = expected.filter((n) => loaded.has(n));
|
|
expect(found.length).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
});
|