@mrciphersmith/keryx 0.2.83 → 0.2.88
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli.js +29861 -20525
- package/dist/core.js +25871 -0
- package/package.json +16 -2
- package/src/gdgraph/dangling.ts +204 -0
- package/src/gdgraph/find.ts +529 -37
- package/src/gdgraph/repomap.ts +140 -12
- package/src/gdgraph/staleness.ts +22 -9
- package/src/gdgraph/symbol.ts +45 -6
- package/src/gdgraph/treesitter/extract.ts +153 -5
- package/src/gdgraph/wiki-layer.ts +32 -1
- package/src/gdskills/bundled/skills/core/reviewer-skill-creator/SKILL.md +29 -0
- package/src/gdskills/bundled/skills/orchestration/code-verifier/SKILL.codex.md +1 -1
- package/src/gdskills/bundled/skills/orchestration/code-verifier/SKILL.cursor.md +1 -1
- package/src/gdskills/bundled/skills/orchestration/code-verifier/SKILL.md +1 -1
- package/src/gdskills/bundled/skills/orchestration/code-verifier/SKILL.opencode.md +1 -1
- package/src/gdskills/bundled/skills/orchestration/code-verifier/SKILL.zed.md +1 -1
- package/src/gdskills/bundled/skills/orchestration/flow-orchestrator/SKILL.md +50 -13
- package/src/gdskills/bundled/skills/orchestration/job-orchestrator/SKILL.codex.md +1 -1
- package/src/gdskills/bundled/skills/orchestration/job-orchestrator/SKILL.cursor.md +1 -1
- package/src/gdskills/bundled/skills/orchestration/job-orchestrator/SKILL.md +1 -1
- package/src/gdskills/bundled/skills/orchestration/job-orchestrator/SKILL.opencode.md +1 -1
- package/src/gdskills/bundled/skills/orchestration/job-orchestrator/SKILL.zed.md +1 -1
- package/src/gdskills/bundled/skills/quality/security-audit/SKILL.codex.md +52 -5
- package/src/gdskills/bundled/skills/quality/security-audit/SKILL.cursor.md +52 -5
- package/src/gdskills/bundled/skills/quality/security-audit/SKILL.md +52 -5
- package/src/gdskills/bundled/skills/review/review-pr-feedback/SKILL.md +12 -0
- package/src/gdgraph/affected.test.ts +0 -133
- package/src/gdgraph/build-integrity.test.ts +0 -193
- package/src/gdgraph/build-lang.test.ts +0 -406
- package/src/gdgraph/build.test.ts +0 -120
- package/src/gdgraph/config.test.ts +0 -47
- package/src/gdgraph/core-sources.test.ts +0 -99
- package/src/gdgraph/fallback.test.ts +0 -153
- package/src/gdgraph/find.test.ts +0 -78
- package/src/gdgraph/import-kind.test.ts +0 -525
- package/src/gdgraph/path.test.ts +0 -56
- package/src/gdgraph/repomap.test.ts +0 -110
- package/src/gdgraph/service.test.ts +0 -89
- package/src/gdgraph/staleness.test.ts +0 -208
- package/src/gdgraph/symbol.test.ts +0 -89
- package/src/gdgraph/symbols-capability.test.ts +0 -138
- package/src/gdgraph/treesitter/adapter.test.ts +0 -496
- package/src/gdgraph/treesitter/extract.test.ts +0 -278
- package/src/gdgraph/treesitter/no-treesitter-import.test.ts +0 -51
- package/src/gdgraph/treesitter/real-grammar-fixture.test.ts +0 -71
- package/src/gdgraph/treesitter/resolve-calls.test.ts +0 -38
- package/src/gdgraph/wiki-layer-no-git.test.ts +0 -73
- package/src/gdgraph/wiki-layer.test.ts +0 -211
|
@@ -1,496 +0,0 @@
|
|
|
1
|
-
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import { beforeEach, expect, mock, test } from "bun:test";
|
|
6
|
-
import { createTreesitterSpec, resolveTreesitterCapability, type BuildInput } from "./adapter";
|
|
7
|
-
import type { TsNode } from "./extract";
|
|
8
|
-
import { enrichBuildWithSymbols } from "../enrich";
|
|
9
|
-
import { hasWarned, resetWarnOnce } from "../../capability/warn-once";
|
|
10
|
-
import { setTreesitterEnabled } from "../symbols-capability";
|
|
11
|
-
import type { CallEdge, SymbolLayer, SymbolNode } from "../types";
|
|
12
|
-
|
|
13
|
-
// Registered at MODULE TOP LEVEL, before any `test()` body runs: Bun loads
|
|
14
|
-
// every test file's static imports/module body in a resolution pass before
|
|
15
|
-
// running any test's body, so a `mock.module` call placed here reliably wins
|
|
16
|
-
// the specifier UNTIL some test anywhere in the same `bun test` invocation
|
|
17
|
-
// performs a real `await import("web-tree-sitter")` first (e.g.
|
|
18
|
-
// `fallback.test.ts`'s AC4.3 test does exactly that with the capability
|
|
19
|
-
// enabled) — once that happens, Bun's module cache is warmed with the real
|
|
20
|
-
// module and this mock's factory is no longer invoked for later imports in
|
|
21
|
-
// the same process. That makes `webTreeSitterImportAttempts` a reliable
|
|
22
|
-
// "definitely zero attempts" signal (nothing increments it unless OUR mock's
|
|
23
|
-
// factory ran) but NOT a reliable "definitely attempted" signal when run
|
|
24
|
-
// alongside the rest of the suite — the enabled-path test below therefore
|
|
25
|
-
// proves "gate 1 passed and we reached the real isAvailable() probe" via the
|
|
26
|
-
// process-scoped warn-once tracker instead, which is correct regardless of
|
|
27
|
-
// which physical module served the import.
|
|
28
|
-
let webTreeSitterImportAttempts = 0;
|
|
29
|
-
mock.module("web-tree-sitter", () => {
|
|
30
|
-
webTreeSitterImportAttempts += 1;
|
|
31
|
-
function MockParser(this: unknown): void {}
|
|
32
|
-
(MockParser as unknown as { init: () => Promise<void> }).init = async () => {};
|
|
33
|
-
(MockParser as unknown as { Language: { load: (p: string) => Promise<unknown> } }).Language = {
|
|
34
|
-
load: async () => ({}),
|
|
35
|
-
};
|
|
36
|
-
return { default: MockParser };
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
// --- tiny structural mock tree: one top-level function `boot` that calls `tick` ---
|
|
40
|
-
function mk(o: {
|
|
41
|
-
type: string;
|
|
42
|
-
line?: number;
|
|
43
|
-
endLine?: number;
|
|
44
|
-
text?: string;
|
|
45
|
-
fields?: Record<string, TsNode | null>;
|
|
46
|
-
namedChildren?: TsNode[];
|
|
47
|
-
}): TsNode {
|
|
48
|
-
const line = o.line ?? 1;
|
|
49
|
-
const named = o.namedChildren ?? [];
|
|
50
|
-
return {
|
|
51
|
-
type: o.type,
|
|
52
|
-
text: o.text ?? "",
|
|
53
|
-
startPosition: { row: line - 1, column: 0 },
|
|
54
|
-
endPosition: { row: (o.endLine ?? line) - 1, column: 0 },
|
|
55
|
-
childForFieldName: (field: string) => o.fields?.[field] ?? null,
|
|
56
|
-
namedChildren: named,
|
|
57
|
-
children: named,
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function bootTree(): TsNode {
|
|
62
|
-
const id = (t: string) => mk({ type: "identifier", text: t });
|
|
63
|
-
const body = mk({
|
|
64
|
-
type: "statement_block",
|
|
65
|
-
line: 1,
|
|
66
|
-
namedChildren: [mk({ type: "call_expression", line: 1, fields: { function: id("tick") }, text: "tick()" })],
|
|
67
|
-
});
|
|
68
|
-
const boot = mk({
|
|
69
|
-
type: "function_declaration",
|
|
70
|
-
line: 1,
|
|
71
|
-
endLine: 2,
|
|
72
|
-
fields: { name: id("boot"), parameters: mk({ type: "formal_parameters", text: "()" }) },
|
|
73
|
-
namedChildren: [body],
|
|
74
|
-
});
|
|
75
|
-
const tick = mk({
|
|
76
|
-
type: "function_declaration",
|
|
77
|
-
line: 3,
|
|
78
|
-
endLine: 3,
|
|
79
|
-
fields: { name: id("tick"), parameters: mk({ type: "formal_parameters", text: "()" }) },
|
|
80
|
-
});
|
|
81
|
-
return mk({ type: "program", line: 1, endLine: 3, namedChildren: [boot, tick] });
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// A structural mock of the `web-tree-sitter` module (a Parser constructor with
|
|
85
|
-
// static `init` + `Language.load`). Parse always returns the bootTree.
|
|
86
|
-
function mockParserModule(): unknown {
|
|
87
|
-
function MockParser(this: unknown) {}
|
|
88
|
-
(MockParser as unknown as { init: () => Promise<void> }).init = async () => {};
|
|
89
|
-
(MockParser as unknown as { Language: { load: (p: string) => Promise<unknown> } }).Language = {
|
|
90
|
-
load: async () => ({}),
|
|
91
|
-
};
|
|
92
|
-
MockParser.prototype.setLanguage = function setLanguage(): void {};
|
|
93
|
-
MockParser.prototype.parse = function parse(): { rootNode: TsNode } {
|
|
94
|
-
return { rootNode: bootTree() };
|
|
95
|
-
};
|
|
96
|
-
return MockParser;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Create a temp workspace whose lockfile pins a real on-disk grammar file so the
|
|
100
|
-
// Asset Resolver verifies it (availability-true), with a config grammarsPath (T1).
|
|
101
|
-
async function makeWorkspaceWithGrammar(): Promise<{ root: string; grammarsDir: string }> {
|
|
102
|
-
const root = await mkdtemp(path.join(tmpdir(), "keryx-ts-"));
|
|
103
|
-
const grammarsDir = path.join(root, "grammars");
|
|
104
|
-
await mkdir(path.join(root, ".metaproject"), { recursive: true });
|
|
105
|
-
await mkdir(grammarsDir, { recursive: true });
|
|
106
|
-
const wasmPath = path.join(grammarsDir, "tree-sitter-typescript.wasm");
|
|
107
|
-
const bytes = Buffer.from("fake-grammar-bytes");
|
|
108
|
-
await writeFile(wasmPath, bytes);
|
|
109
|
-
const sha = createHash("sha256").update(bytes).digest("hex");
|
|
110
|
-
await writeFile(
|
|
111
|
-
path.join(root, ".metaproject", "assets.lock.json"),
|
|
112
|
-
JSON.stringify({
|
|
113
|
-
schemaVersion: 1,
|
|
114
|
-
assets: {
|
|
115
|
-
"tree-sitter-typescript": {
|
|
116
|
-
version: "0.22.0",
|
|
117
|
-
url: "https://example.dev/tree-sitter-typescript.wasm",
|
|
118
|
-
sha256: sha,
|
|
119
|
-
size: bytes.length,
|
|
120
|
-
},
|
|
121
|
-
},
|
|
122
|
-
}),
|
|
123
|
-
);
|
|
124
|
-
return { root, grammarsDir };
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
test("AC5.2 availability-true — adapter isAvailable + run() yields the symbol layer", async () => {
|
|
128
|
-
const { root, grammarsDir } = await makeWorkspaceWithGrammar();
|
|
129
|
-
try {
|
|
130
|
-
const spec = createTreesitterSpec(root, { languages: ["typescript"], grammarsPath: grammarsDir });
|
|
131
|
-
const adapter = spec.load({ dep: mockParserModule(), asset: null });
|
|
132
|
-
|
|
133
|
-
expect(await adapter.isAvailable()).toBe(true);
|
|
134
|
-
|
|
135
|
-
const layer = await adapter.run({ files: [{ path: "src/boot.ts", content: "ignored-by-mock" }] });
|
|
136
|
-
const symbolIds = layer.symbols.map((symbol: SymbolNode) => symbol.id).sort();
|
|
137
|
-
expect(symbolIds).toEqual(["src/boot.ts#boot", "src/boot.ts#tick"]);
|
|
138
|
-
const callKinds = layer.calls.map((call: CallEdge) => `${call.kind}:${call.from}=>${call.to}`);
|
|
139
|
-
expect(callKinds).toContain("calls:src/boot.ts#boot=>src/boot.ts#tick");
|
|
140
|
-
} finally {
|
|
141
|
-
await rm(root, { recursive: true, force: true });
|
|
142
|
-
}
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
test("AC5.2 availability-false — no grammar ⇒ isAvailable false", async () => {
|
|
146
|
-
const root = await mkdtemp(path.join(tmpdir(), "keryx-ts-none-"));
|
|
147
|
-
try {
|
|
148
|
-
await mkdir(path.join(root, ".metaproject"), { recursive: true });
|
|
149
|
-
const spec = createTreesitterSpec(root, { languages: ["typescript"], grammarsPath: null });
|
|
150
|
-
const adapter = spec.load({ dep: mockParserModule(), asset: null });
|
|
151
|
-
expect(await adapter.isAvailable()).toBe(false);
|
|
152
|
-
} finally {
|
|
153
|
-
await rm(root, { recursive: true, force: true });
|
|
154
|
-
}
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
test("availability-false — missing dep ⇒ isAvailable false", async () => {
|
|
158
|
-
const { root, grammarsDir } = await makeWorkspaceWithGrammar();
|
|
159
|
-
try {
|
|
160
|
-
const spec = createTreesitterSpec(root, { languages: ["typescript"], grammarsPath: grammarsDir });
|
|
161
|
-
const adapter = spec.load({ dep: undefined, asset: null });
|
|
162
|
-
expect(await adapter.isAvailable()).toBe(false);
|
|
163
|
-
} finally {
|
|
164
|
-
await rm(root, { recursive: true, force: true });
|
|
165
|
-
}
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
// --- AFC-13 / AC5 requirement 3: "the grammar is not installed" and "the
|
|
169
|
-
// grammar is installed but its ABI version does not match this runtime" must
|
|
170
|
-
// be distinguishable outcomes, not one collapsed "unavailable". Neither
|
|
171
|
-
// failure mode can be produced on a real machine without either uninstalling
|
|
172
|
-
// `web-tree-sitter` (not available as a seam) or installing an ABI-mismatched
|
|
173
|
-
// grammar build (forbidden by this task). Both are exercised instead through
|
|
174
|
-
// the module's own seam: `spec.load({ dep, asset })` accepts an injected
|
|
175
|
-
// `dep` exactly like the existing tests above, and a `Language.load` that
|
|
176
|
-
// rejects is a faithful simulation of a real ABI/version-mismatch error from
|
|
177
|
-
// `web-tree-sitter` (the real runtime rejects `Language.load` the same way
|
|
178
|
-
// for a grammar built against an incompatible ABI).
|
|
179
|
-
function brokenLanguageParserModule(rejection: Error): unknown {
|
|
180
|
-
function MockParser(this: unknown) {}
|
|
181
|
-
(MockParser as unknown as { init: () => Promise<void> }).init = async () => {};
|
|
182
|
-
(MockParser as unknown as { Language: { load: (p: string) => Promise<unknown> } }).Language = {
|
|
183
|
-
load: async () => {
|
|
184
|
-
throw rejection;
|
|
185
|
-
},
|
|
186
|
-
};
|
|
187
|
-
MockParser.prototype.setLanguage = function setLanguage(): void {};
|
|
188
|
-
MockParser.prototype.parse = function parse(): { rootNode: TsNode } {
|
|
189
|
-
return { rootNode: bootTree() };
|
|
190
|
-
};
|
|
191
|
-
return MockParser;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
test("AC5.req3 — grammar asset never resolved ⇒ diagnosis status 'missing', not 'incompatible'", async () => {
|
|
195
|
-
const root = await mkdtemp(path.join(tmpdir(), "keryx-ts-diag-missing-"));
|
|
196
|
-
try {
|
|
197
|
-
await mkdir(path.join(root, ".metaproject"), { recursive: true });
|
|
198
|
-
// No assets.lock.json entry at all ⇒ resolveGrammar can never find the asset.
|
|
199
|
-
const spec = createTreesitterSpec(root, { languages: ["typescript"], grammarsPath: null });
|
|
200
|
-
const adapter = spec.load({ dep: mockParserModule(), asset: null }) as unknown as {
|
|
201
|
-
isAvailable(): Promise<boolean>;
|
|
202
|
-
getDiagnoses(): { language: string; status: string; reason: string }[];
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
expect(await adapter.isAvailable()).toBe(false);
|
|
206
|
-
const diagnoses = adapter.getDiagnoses();
|
|
207
|
-
expect(diagnoses).toHaveLength(1);
|
|
208
|
-
expect(diagnoses[0]?.status).toBe("missing");
|
|
209
|
-
expect(diagnoses[0]?.reason).not.toContain("ABI");
|
|
210
|
-
expect(diagnoses[0]?.reason).toContain("not resolved");
|
|
211
|
-
} finally {
|
|
212
|
-
await rm(root, { recursive: true, force: true });
|
|
213
|
-
}
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
test("AC5.req3 — grammar asset resolves and verifies, but the runtime refuses to load it ⇒ diagnosis status 'incompatible', not 'missing'", async () => {
|
|
217
|
-
const { root, grammarsDir } = await makeWorkspaceWithGrammar();
|
|
218
|
-
try {
|
|
219
|
-
const spec = createTreesitterSpec(root, { languages: ["typescript"], grammarsPath: grammarsDir });
|
|
220
|
-
const abiError = new Error("Incompatible language version 13. Expected minimum 14, maximum 15");
|
|
221
|
-
const adapter = spec.load({ dep: brokenLanguageParserModule(abiError), asset: null }) as unknown as {
|
|
222
|
-
isAvailable(): Promise<boolean>;
|
|
223
|
-
getDiagnoses(): { language: string; status: string; reason: string }[];
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
// The grammar resolved (it is on disk with a verified checksum) — this is
|
|
227
|
-
// the crucial difference from the "missing" case above — but the runtime
|
|
228
|
-
// could not load it, so overall availability is still false.
|
|
229
|
-
expect(await adapter.isAvailable()).toBe(false);
|
|
230
|
-
const diagnoses = adapter.getDiagnoses();
|
|
231
|
-
expect(diagnoses).toHaveLength(1);
|
|
232
|
-
expect(diagnoses[0]?.status).toBe("incompatible");
|
|
233
|
-
expect(diagnoses[0]?.reason).toContain("Incompatible language version 13");
|
|
234
|
-
expect(diagnoses[0]?.reason).not.toContain("not resolved");
|
|
235
|
-
} finally {
|
|
236
|
-
await rm(root, { recursive: true, force: true });
|
|
237
|
-
}
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
test("AC5.req3 — missing and incompatible are genuinely different values end to end (adapter diagnosis -> warn reason -> requireSymbols code), never the same outcome", async () => {
|
|
241
|
-
const missingRoot = await mkdtemp(path.join(tmpdir(), "keryx-ts-diag-a-"));
|
|
242
|
-
const incompatibleWorkspace = await makeWorkspaceWithGrammar();
|
|
243
|
-
try {
|
|
244
|
-
await mkdir(path.join(missingRoot, ".metaproject"), { recursive: true });
|
|
245
|
-
const missingSpec = createTreesitterSpec(missingRoot, { languages: ["typescript"], grammarsPath: null });
|
|
246
|
-
const missingAdapter = missingSpec.load({ dep: mockParserModule(), asset: null }) as unknown as {
|
|
247
|
-
isAvailable(): Promise<boolean>;
|
|
248
|
-
getDiagnoses(): { language: string; status: string; reason: string }[];
|
|
249
|
-
};
|
|
250
|
-
await missingAdapter.isAvailable();
|
|
251
|
-
|
|
252
|
-
const incompatibleSpec = createTreesitterSpec(incompatibleWorkspace.root, {
|
|
253
|
-
languages: ["typescript"],
|
|
254
|
-
grammarsPath: incompatibleWorkspace.grammarsDir,
|
|
255
|
-
});
|
|
256
|
-
const incompatibleAdapter = incompatibleSpec.load({
|
|
257
|
-
dep: brokenLanguageParserModule(new Error("bad wasm magic number")),
|
|
258
|
-
asset: null,
|
|
259
|
-
}) as unknown as {
|
|
260
|
-
isAvailable(): Promise<boolean>;
|
|
261
|
-
getDiagnoses(): { language: string; status: string; reason: string }[];
|
|
262
|
-
};
|
|
263
|
-
await incompatibleAdapter.isAvailable();
|
|
264
|
-
|
|
265
|
-
expect(missingAdapter.getDiagnoses()[0]?.status).not.toBe(incompatibleAdapter.getDiagnoses()[0]?.status);
|
|
266
|
-
} finally {
|
|
267
|
-
await rm(missingRoot, { recursive: true, force: true });
|
|
268
|
-
await rm(incompatibleWorkspace.root, { recursive: true, force: true });
|
|
269
|
-
}
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
test("AC1.1 additive write path — enrich writes symbols.jsonl + calls.jsonl via a mock adapter", async () => {
|
|
273
|
-
const root = await mkdtemp(path.join(tmpdir(), "keryx-enrich-"));
|
|
274
|
-
try {
|
|
275
|
-
await mkdir(path.join(root, ".metaproject", "data", "gdgraph", "storage"), { recursive: true });
|
|
276
|
-
const expectedLayer: SymbolLayer = {
|
|
277
|
-
symbols: [
|
|
278
|
-
{
|
|
279
|
-
id: "src/x.ts#run",
|
|
280
|
-
kind: "function",
|
|
281
|
-
path: "src/x.ts",
|
|
282
|
-
name: "run",
|
|
283
|
-
container: null,
|
|
284
|
-
startLine: 1,
|
|
285
|
-
endLine: 1,
|
|
286
|
-
language: "typescript",
|
|
287
|
-
signature: "run()",
|
|
288
|
-
},
|
|
289
|
-
],
|
|
290
|
-
calls: [{ id: "defines:src/x.ts=>src/x.ts#run", from: "src/x.ts", to: "src/x.ts#run", kind: "defines", resolved: true }],
|
|
291
|
-
};
|
|
292
|
-
|
|
293
|
-
const injected = async () => ({
|
|
294
|
-
id: "gdgraph.treesitter",
|
|
295
|
-
isAvailable: async () => true,
|
|
296
|
-
run: async (_input: BuildInput) => expectedLayer,
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
const result = await enrichBuildWithSymbols(root, [{ path: "src/x.ts", content: "" }], injected);
|
|
300
|
-
expect(result.enriched).toBe(true);
|
|
301
|
-
expect(result.symbols).toBe(1);
|
|
302
|
-
|
|
303
|
-
const storage = path.join(root, ".metaproject", "data", "gdgraph", "storage");
|
|
304
|
-
const symbolsFile = await readFile(path.join(storage, "symbols.jsonl"), "utf8");
|
|
305
|
-
const callsFile = await readFile(path.join(storage, "calls.jsonl"), "utf8");
|
|
306
|
-
expect(symbolsFile.trim()).toBe(JSON.stringify(expectedLayer.symbols[0]));
|
|
307
|
-
expect(callsFile.trim()).toBe(JSON.stringify(expectedLayer.calls[0]));
|
|
308
|
-
} finally {
|
|
309
|
-
await rm(root, { recursive: true, force: true });
|
|
310
|
-
}
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
test("Java grammar resolution — grammarForFile selects java for .java files", async () => {
|
|
314
|
-
const root = await mkdtemp(path.join(tmpdir(), "keryx-java-"));
|
|
315
|
-
try {
|
|
316
|
-
const grammarsDir = path.join(root, "grammars");
|
|
317
|
-
await mkdir(path.join(root, ".metaproject"), { recursive: true });
|
|
318
|
-
await mkdir(grammarsDir, { recursive: true });
|
|
319
|
-
|
|
320
|
-
const wasmPath = path.join(grammarsDir, "tree-sitter-java.wasm");
|
|
321
|
-
const bytes = Buffer.from("fake-java-grammar");
|
|
322
|
-
await writeFile(wasmPath, bytes);
|
|
323
|
-
const sha = createHash("sha256").update(bytes).digest("hex");
|
|
324
|
-
|
|
325
|
-
await writeFile(
|
|
326
|
-
path.join(root, ".metaproject", "assets.lock.json"),
|
|
327
|
-
JSON.stringify({
|
|
328
|
-
schemaVersion: 1,
|
|
329
|
-
assets: {
|
|
330
|
-
"tree-sitter-java": {
|
|
331
|
-
version: "0.1.13",
|
|
332
|
-
url: "https://example.dev/tree-sitter-java.wasm",
|
|
333
|
-
sha256: sha,
|
|
334
|
-
size: bytes.length,
|
|
335
|
-
},
|
|
336
|
-
},
|
|
337
|
-
}),
|
|
338
|
-
);
|
|
339
|
-
|
|
340
|
-
const spec = createTreesitterSpec(root, { languages: ["java"], grammarsPath: grammarsDir });
|
|
341
|
-
const adapter = spec.load({ dep: mockParserModule(), asset: null });
|
|
342
|
-
expect(await adapter.isAvailable()).toBe(true);
|
|
343
|
-
} finally {
|
|
344
|
-
await rm(root, { recursive: true, force: true });
|
|
345
|
-
}
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
test("Python grammar resolution — grammarForFile selects python for .py files", async () => {
|
|
349
|
-
const root = await mkdtemp(path.join(tmpdir(), "keryx-python-"));
|
|
350
|
-
try {
|
|
351
|
-
const grammarsDir = path.join(root, "grammars");
|
|
352
|
-
await mkdir(path.join(root, ".metaproject"), { recursive: true });
|
|
353
|
-
await mkdir(grammarsDir, { recursive: true });
|
|
354
|
-
|
|
355
|
-
const wasmPath = path.join(grammarsDir, "tree-sitter-python.wasm");
|
|
356
|
-
const bytes = Buffer.from("fake-python-grammar");
|
|
357
|
-
await writeFile(wasmPath, bytes);
|
|
358
|
-
const sha = createHash("sha256").update(bytes).digest("hex");
|
|
359
|
-
|
|
360
|
-
await writeFile(
|
|
361
|
-
path.join(root, ".metaproject", "assets.lock.json"),
|
|
362
|
-
JSON.stringify({
|
|
363
|
-
schemaVersion: 1,
|
|
364
|
-
assets: {
|
|
365
|
-
"tree-sitter-python": {
|
|
366
|
-
version: "0.1.13",
|
|
367
|
-
url: "https://example.dev/tree-sitter-python.wasm",
|
|
368
|
-
sha256: sha,
|
|
369
|
-
size: bytes.length,
|
|
370
|
-
},
|
|
371
|
-
},
|
|
372
|
-
}),
|
|
373
|
-
);
|
|
374
|
-
|
|
375
|
-
const spec = createTreesitterSpec(root, { languages: ["python"], grammarsPath: grammarsDir });
|
|
376
|
-
const adapter = spec.load({ dep: mockParserModule(), asset: null });
|
|
377
|
-
expect(await adapter.isAvailable()).toBe(true);
|
|
378
|
-
} finally {
|
|
379
|
-
await rm(root, { recursive: true, force: true });
|
|
380
|
-
}
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
// --- Real default (no-injected-resolver) production path (T6 review fix) ---
|
|
384
|
-
//
|
|
385
|
-
// Every test above injects an explicit `CapabilityResolver`/`dep`, bypassing
|
|
386
|
-
// `resolveTreesitterCapability` entirely. The REAL call site
|
|
387
|
-
// (`build.ts` → `enrichBuildWithSymbols(projectRoot, fileRecords)`, no
|
|
388
|
-
// resolver argument) goes through `resolveTreesitterCapability`'s literal
|
|
389
|
-
// `await import("web-tree-sitter")` fast path with NO injected resolver at
|
|
390
|
-
// all. These tests exercise that exact default path and, via the
|
|
391
|
-
// module-top-level `mock.module` above, directly observe whether the literal
|
|
392
|
-
// import was attempted — proving gate 1 (manifest-enabled) runs BEFORE the
|
|
393
|
-
// import, not after (the bug this ordering fixes).
|
|
394
|
-
|
|
395
|
-
beforeEach(() => {
|
|
396
|
-
webTreeSitterImportAttempts = 0;
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
async function makeDisabledOrAbsentWorkspace(enabled: "absent" | false): Promise<string> {
|
|
400
|
-
const root = await mkdtemp(path.join(tmpdir(), "keryx-ts-default-"));
|
|
401
|
-
if (enabled === false) {
|
|
402
|
-
await mkdir(path.join(root, ".metaproject"), { recursive: true });
|
|
403
|
-
const manifest = setTreesitterEnabled({}, false);
|
|
404
|
-
await writeFile(
|
|
405
|
-
path.join(root, ".metaproject", "metaproject.json"),
|
|
406
|
-
JSON.stringify(manifest),
|
|
407
|
-
"utf8",
|
|
408
|
-
);
|
|
409
|
-
}
|
|
410
|
-
// enabled === "absent" ⇒ no .metaproject/metaproject.json at all (missing
|
|
411
|
-
// manifest = off, per seam.ts's own contract).
|
|
412
|
-
return root;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
test("default path, capability DISABLED (missing manifest) — resolveTreesitterCapability never attempts the web-tree-sitter import", async () => {
|
|
416
|
-
const root = await makeDisabledOrAbsentWorkspace("absent");
|
|
417
|
-
try {
|
|
418
|
-
const adapter = await resolveTreesitterCapability(root, {
|
|
419
|
-
languages: ["typescript"],
|
|
420
|
-
grammarsPath: null,
|
|
421
|
-
});
|
|
422
|
-
expect(adapter).toBeNull();
|
|
423
|
-
// The core assertion (Fix 1): a disabled/missing-manifest ceiling must
|
|
424
|
-
// resolve to null WITHOUT ever attempting the literal dep import.
|
|
425
|
-
expect(webTreeSitterImportAttempts).toBe(0);
|
|
426
|
-
} finally {
|
|
427
|
-
await rm(root, { recursive: true, force: true });
|
|
428
|
-
}
|
|
429
|
-
});
|
|
430
|
-
|
|
431
|
-
test("default path, capability DISABLED (enabled: false) — resolveTreesitterCapability never attempts the web-tree-sitter import", async () => {
|
|
432
|
-
const root = await makeDisabledOrAbsentWorkspace(false);
|
|
433
|
-
try {
|
|
434
|
-
const adapter = await resolveTreesitterCapability(root, {
|
|
435
|
-
languages: ["typescript"],
|
|
436
|
-
grammarsPath: null,
|
|
437
|
-
});
|
|
438
|
-
expect(adapter).toBeNull();
|
|
439
|
-
expect(webTreeSitterImportAttempts).toBe(0);
|
|
440
|
-
} finally {
|
|
441
|
-
await rm(root, { recursive: true, force: true });
|
|
442
|
-
}
|
|
443
|
-
});
|
|
444
|
-
|
|
445
|
-
test("default path, capability DISABLED — enrichBuildWithSymbols (no injected resolver) degrades cleanly with no writes and no dep-load attempt", async () => {
|
|
446
|
-
const root = await makeDisabledOrAbsentWorkspace("absent");
|
|
447
|
-
try {
|
|
448
|
-
// No third argument ⇒ the real production default path (build.ts calls
|
|
449
|
-
// enrichBuildWithSymbols with exactly two arguments).
|
|
450
|
-
const result = await enrichBuildWithSymbols(root, [{ path: "src/x.ts", content: "" }]);
|
|
451
|
-
expect(result).toEqual({ enriched: false, symbols: 0, calls: 0 });
|
|
452
|
-
expect(webTreeSitterImportAttempts).toBe(0);
|
|
453
|
-
|
|
454
|
-
const storage = path.join(root, ".metaproject", "data", "gdgraph", "storage");
|
|
455
|
-
await expect(readFile(path.join(storage, "symbols.jsonl"), "utf8")).rejects.toThrow();
|
|
456
|
-
} finally {
|
|
457
|
-
await rm(root, { recursive: true, force: true });
|
|
458
|
-
}
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
test("default path, capability ENABLED — resolveTreesitterCapability gets past gate 1 and reaches the real isAvailable() probe", async () => {
|
|
462
|
-
resetWarnOnce();
|
|
463
|
-
const root = await mkdtemp(path.join(tmpdir(), "keryx-ts-default-enabled-"));
|
|
464
|
-
try {
|
|
465
|
-
await mkdir(path.join(root, ".metaproject"), { recursive: true });
|
|
466
|
-
const manifest = setTreesitterEnabled({}, true);
|
|
467
|
-
await writeFile(
|
|
468
|
-
path.join(root, ".metaproject", "metaproject.json"),
|
|
469
|
-
JSON.stringify(manifest),
|
|
470
|
-
"utf8",
|
|
471
|
-
);
|
|
472
|
-
|
|
473
|
-
// No grammarsPath and no lockfile entries ⇒ resolveGrammars deterministically
|
|
474
|
-
// resolves zero grammars ⇒ isAvailable() is false, so this proves the
|
|
475
|
-
// manifest gate short-circuits correctly on the enabled side too, without
|
|
476
|
-
// requiring a real compiled binary or a real WASM grammar asset.
|
|
477
|
-
const adapter = await resolveTreesitterCapability(root, {
|
|
478
|
-
languages: ["typescript"],
|
|
479
|
-
grammarsPath: null,
|
|
480
|
-
});
|
|
481
|
-
|
|
482
|
-
// No grammar resolves ⇒ isAvailable() is false ⇒ degrades to null, same
|
|
483
|
-
// AC0-8 catch-and-degrade contract as the seam.
|
|
484
|
-
expect(adapter).toBeNull();
|
|
485
|
-
// Gate 1 passed (unlike the disabled cases above) and execution reached
|
|
486
|
-
// the real `isAvailable()` probe, which reported unavailable and warned
|
|
487
|
-
// once — the process-scoped warn-once tracker is a reliable,
|
|
488
|
-
// ordering-independent witness of this (unlike counting the mock
|
|
489
|
-
// factory's own invocations, which only fires when THIS file's mock wins
|
|
490
|
-
// the module-cache race against any real `web-tree-sitter` import
|
|
491
|
-
// elsewhere in a full-suite run — see the top-of-file comment).
|
|
492
|
-
expect(hasWarned("gdgraph.treesitter")).toBe(true);
|
|
493
|
-
} finally {
|
|
494
|
-
await rm(root, { recursive: true, force: true });
|
|
495
|
-
}
|
|
496
|
-
});
|