@remnic/coding-graph 9.3.759
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 +130 -0
- package/dist/chunk-5I2DBHOQ.js +1042 -0
- package/dist/chunk-5I2DBHOQ.js.map +1 -0
- package/dist/chunk-CPYJACC5.js +1838 -0
- package/dist/chunk-CPYJACC5.js.map +1 -0
- package/dist/chunk-ZVCMIM4T.js +216 -0
- package/dist/chunk-ZVCMIM4T.js.map +1 -0
- package/dist/cypher/query-parser.d.ts +253 -0
- package/dist/cypher/query-parser.js +17 -0
- package/dist/cypher/query-parser.js.map +1 -0
- package/dist/graph-schema.d.ts +84 -0
- package/dist/graph-schema.js +17 -0
- package/dist/graph-schema.js.map +1 -0
- package/dist/graph-store.d.ts +938 -0
- package/dist/graph-store.js +16 -0
- package/dist/graph-store.js.map +1 -0
- package/dist/index.d.ts +1953 -0
- package/dist/index.js +3509 -0
- package/dist/index.js.map +1 -0
- package/grammars/tree-sitter-bash.wasm +0 -0
- package/grammars/tree-sitter-c.wasm +0 -0
- package/grammars/tree-sitter-c_sharp.wasm +0 -0
- package/grammars/tree-sitter-cpp.wasm +0 -0
- package/grammars/tree-sitter-go.wasm +0 -0
- package/grammars/tree-sitter-java.wasm +0 -0
- package/grammars/tree-sitter-javascript.wasm +0 -0
- package/grammars/tree-sitter-kotlin.wasm +0 -0
- package/grammars/tree-sitter-php.wasm +0 -0
- package/grammars/tree-sitter-python.wasm +0 -0
- package/grammars/tree-sitter-ruby.wasm +0 -0
- package/grammars/tree-sitter-rust.wasm +0 -0
- package/grammars/tree-sitter-swift.wasm +0 -0
- package/grammars/tree-sitter-tsx.wasm +0 -0
- package/grammars/tree-sitter-typescript.wasm +0 -0
- package/package.json +79 -0
- package/src/co-change.test.ts +175 -0
- package/src/co-change.ts +167 -0
- package/src/cypher/query-parser.test.ts +1107 -0
- package/src/cypher/query-parser.ts +1692 -0
- package/src/detect-changes.test.ts +533 -0
- package/src/detect-changes.ts +367 -0
- package/src/engine/emit.ts +556 -0
- package/src/engine/engine.test.ts +1417 -0
- package/src/engine/engine.ts +182 -0
- package/src/engine/extractors.ts +486 -0
- package/src/engine/fixtures.ts +364 -0
- package/src/engine/language-sniff.ts +56 -0
- package/src/engine/parser-backend.ts +206 -0
- package/src/engine/utf16-offsets.ts +68 -0
- package/src/git-invoker.test.ts +116 -0
- package/src/git-invoker.ts +426 -0
- package/src/graph-schema.test.ts +541 -0
- package/src/graph-schema.ts +383 -0
- package/src/graph-store-pr2.test.ts +1879 -0
- package/src/graph-store.test.ts +1420 -0
- package/src/graph-store.ts +3489 -0
- package/src/index-status.test.ts +303 -0
- package/src/index-status.ts +135 -0
- package/src/index.ts +384 -0
- package/src/lsp/byte-position.ts +173 -0
- package/src/lsp/characterization.test.ts +174 -0
- package/src/lsp/client.test.ts +275 -0
- package/src/lsp/client.ts +484 -0
- package/src/lsp/config.ts +219 -0
- package/src/lsp/degradation.ts +86 -0
- package/src/lsp/fixtures/fake-server.mjs +198 -0
- package/src/lsp/framing.test.ts +180 -0
- package/src/lsp/framing.ts +177 -0
- package/src/lsp/resolution.test.ts +497 -0
- package/src/lsp/resolution.ts +483 -0
- package/src/lsp/status.ts +140 -0
- package/src/lsp/types.ts +167 -0
- package/src/reindex.test.ts +1038 -0
- package/src/reindex.ts +908 -0
- package/src/row-types.ts +45 -0
- package/src/semantic/canonical-text.test.ts +150 -0
- package/src/semantic/canonical-text.ts +219 -0
- package/src/semantic/config.ts +235 -0
- package/src/semantic/index.ts +78 -0
- package/src/semantic/minhash.test.ts +197 -0
- package/src/semantic/minhash.ts +261 -0
- package/src/semantic/semantic-query.ts +173 -0
- package/src/semantic/semantic.test.ts +1315 -0
- package/src/semantic/similarity.ts +268 -0
- package/src/semantic/types.ts +145 -0
- package/src/semantic/vectors.ts +235 -0
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LSP resolution pass tests (issue #1555 step 4 — prove-fail-before).
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - Planner: basic planning, budget enforcement, deterministic ordering
|
|
6
|
+
* - Executor: happy path (location maps → upgrade applied)
|
|
7
|
+
* - Executor: location doesn't map → unresolved
|
|
8
|
+
* - Executor: mid-batch applyUpgrades failure → caught, counted as unresolved
|
|
9
|
+
* - Executor: server crash mid-run → degradation, remaining sites unresolved
|
|
10
|
+
* - Executor: fatal error after upgrade in same batch → NOT committed (rule 25)
|
|
11
|
+
* - Executor: didOpen sent before definition requests (LSP 3.17)
|
|
12
|
+
* - Executor: workspaceRoot resolves repo-relative paths to absolute URIs
|
|
13
|
+
* - mapLocationToNode: same-file, cross-file with resolver, fallback
|
|
14
|
+
* - Characterization: with lsp.enabled=false, resolution is a no-op
|
|
15
|
+
*/
|
|
16
|
+
import assert from "node:assert/strict";
|
|
17
|
+
import test from "node:test";
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
planLspUpgrades,
|
|
21
|
+
executeLspResolution,
|
|
22
|
+
mapLocationToNode,
|
|
23
|
+
type EdgeUpgrade,
|
|
24
|
+
type NodeLocator,
|
|
25
|
+
type UnresolvedCallSite,
|
|
26
|
+
} from "./resolution.js";
|
|
27
|
+
import type { LspLocation } from "./types.js";
|
|
28
|
+
import type { LspClient } from "./client.js";
|
|
29
|
+
|
|
30
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
31
|
+
// Planner tests (pure — no I/O)
|
|
32
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
function makeCallSite(
|
|
35
|
+
filePath: string,
|
|
36
|
+
calleeName: string,
|
|
37
|
+
calleeByteOffset: number,
|
|
38
|
+
srcQualifiedName: string,
|
|
39
|
+
content = "line0\nline1\nline2",
|
|
40
|
+
): UnresolvedCallSite {
|
|
41
|
+
return {
|
|
42
|
+
filePath,
|
|
43
|
+
language: "typescript",
|
|
44
|
+
content,
|
|
45
|
+
calleeByteOffset,
|
|
46
|
+
calleeName,
|
|
47
|
+
srcQualifiedName,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
test("planner: basic — plans one request per call site", () => {
|
|
52
|
+
const sites = [
|
|
53
|
+
makeCallSite("src/a.ts", "foo", 0, "a.caller"),
|
|
54
|
+
makeCallSite("src/b.ts", "bar", 6, "b.caller"),
|
|
55
|
+
];
|
|
56
|
+
const result = planLspUpgrades(sites, { maxRequests: 100 });
|
|
57
|
+
assert.equal(result.requests.length, 2);
|
|
58
|
+
assert.equal(result.budgetExhausted, 0);
|
|
59
|
+
// Sorted by file path.
|
|
60
|
+
assert.equal(result.requests[0].filePath, "src/a.ts");
|
|
61
|
+
assert.equal(result.requests[1].filePath, "src/b.ts");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("planner: budget enforcement — excess counted as budgetExhausted", () => {
|
|
65
|
+
const sites = [
|
|
66
|
+
makeCallSite("src/a.ts", "foo", 0, "a.x"),
|
|
67
|
+
makeCallSite("src/b.ts", "bar", 0, "b.x"),
|
|
68
|
+
makeCallSite("src/c.ts", "baz", 0, "c.x"),
|
|
69
|
+
];
|
|
70
|
+
const result = planLspUpgrades(sites, { maxRequests: 2 });
|
|
71
|
+
assert.equal(result.requests.length, 2);
|
|
72
|
+
assert.equal(result.budgetExhausted, 1);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("planner: zero budget → all exhausted", () => {
|
|
76
|
+
const sites = [makeCallSite("a.ts", "x", 0, "a.x")];
|
|
77
|
+
const result = planLspUpgrades(sites, { maxRequests: 0 });
|
|
78
|
+
assert.equal(result.requests.length, 0);
|
|
79
|
+
assert.equal(result.budgetExhausted, 1);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("planner: deterministic ordering — same input always same output", () => {
|
|
83
|
+
const sites = [
|
|
84
|
+
makeCallSite("src/z.ts", "z", 10, "z.x"),
|
|
85
|
+
makeCallSite("src/a.ts", "a", 5, "a.x"),
|
|
86
|
+
makeCallSite("src/a.ts", "b", 3, "a.y"),
|
|
87
|
+
];
|
|
88
|
+
const r1 = planLspUpgrades(sites, { maxRequests: 100 });
|
|
89
|
+
const r2 = planLspUpgrades([...sites].reverse(), { maxRequests: 100 });
|
|
90
|
+
assert.deepEqual(
|
|
91
|
+
r1.requests.map((r) => `${r.filePath}:${r.position.line}:${r.position.character}`),
|
|
92
|
+
r2.requests.map((r) => `${r.filePath}:${r.position.line}:${r.position.character}`),
|
|
93
|
+
);
|
|
94
|
+
// Sorted: a.ts byte 3, a.ts byte 5, z.ts byte 10.
|
|
95
|
+
assert.equal(r1.requests[0].calleeName, "b");
|
|
96
|
+
assert.equal(r1.requests[1].calleeName, "a");
|
|
97
|
+
assert.equal(r1.requests[2].calleeName, "z");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("planner: position conversion — byte offset → line/character", () => {
|
|
101
|
+
// content = "line0\nline1\nline2"
|
|
102
|
+
// ^ byte 6 = start of "line1"
|
|
103
|
+
const site = makeCallSite("a.ts", "foo", 6, "a.x", "line0\nline1\nline2");
|
|
104
|
+
const result = planLspUpgrades([site], { maxRequests: 100 });
|
|
105
|
+
assert.equal(result.requests.length, 1);
|
|
106
|
+
assert.equal(result.requests[0].position.line, 1);
|
|
107
|
+
assert.equal(result.requests[0].position.character, 0);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
111
|
+
// Location → node mapping tests (rule 35 — half-open containment)
|
|
112
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
test("mapLocationToNode: location in span → returns qualified name", () => {
|
|
115
|
+
const locator: NodeLocator = (_path, byteOffset) => {
|
|
116
|
+
// Node spans bytes [0, 10).
|
|
117
|
+
return byteOffset >= 0 && byteOffset < 10 ? "mod.target" : null;
|
|
118
|
+
};
|
|
119
|
+
const locations: LspLocation[] = [
|
|
120
|
+
{
|
|
121
|
+
uri: "file:///src/target.ts",
|
|
122
|
+
range: { start: { line: 0, character: 5 }, end: { line: 0, character: 8 } },
|
|
123
|
+
},
|
|
124
|
+
];
|
|
125
|
+
// callerContent is used for position→byte conversion.
|
|
126
|
+
// character 5 on line 0 = byte 5 (ASCII content).
|
|
127
|
+
const result = mapLocationToNode(
|
|
128
|
+
locations,
|
|
129
|
+
{ callerFilePath: "src/target.ts", callerContent: "abcdefghij" },
|
|
130
|
+
locator,
|
|
131
|
+
);
|
|
132
|
+
assert.equal(result, "mod.target");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("mapLocationToNode: location outside span → null", () => {
|
|
136
|
+
const locator: NodeLocator = () => null;
|
|
137
|
+
const locations: LspLocation[] = [
|
|
138
|
+
{
|
|
139
|
+
uri: "file:///src/target.ts",
|
|
140
|
+
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 5 } },
|
|
141
|
+
},
|
|
142
|
+
];
|
|
143
|
+
const result = mapLocationToNode(
|
|
144
|
+
locations,
|
|
145
|
+
{ callerFilePath: "src/target.ts", callerContent: "abcdef" },
|
|
146
|
+
locator,
|
|
147
|
+
);
|
|
148
|
+
assert.equal(result, null);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("mapLocationToNode: multiple locations — first match wins", () => {
|
|
152
|
+
let callCount = 0;
|
|
153
|
+
const locator: NodeLocator = () => {
|
|
154
|
+
callCount++;
|
|
155
|
+
return callCount === 2 ? "mod.second" : null;
|
|
156
|
+
};
|
|
157
|
+
const locations: LspLocation[] = [
|
|
158
|
+
{ uri: "file:///a.ts", range: { start: { line: 0, character: 0 }, end: { line: 0, character: 3 } } },
|
|
159
|
+
{ uri: "file:///b.ts", range: { start: { line: 0, character: 0 }, end: { line: 0, character: 3 } } },
|
|
160
|
+
];
|
|
161
|
+
const result = mapLocationToNode(
|
|
162
|
+
locations,
|
|
163
|
+
{ callerFilePath: "src/a.ts", callerContent: "abc" },
|
|
164
|
+
locator,
|
|
165
|
+
);
|
|
166
|
+
assert.equal(result, "mod.second");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("mapLocationToNode: empty locations → null", () => {
|
|
170
|
+
const locator: NodeLocator = () => "should-not-be-called";
|
|
171
|
+
const result = mapLocationToNode(
|
|
172
|
+
[],
|
|
173
|
+
{ callerFilePath: "src/a.ts", callerContent: "abc" },
|
|
174
|
+
locator,
|
|
175
|
+
);
|
|
176
|
+
assert.equal(result, null);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("mapLocationToNode: cross-file definition uses resolveContent", () => {
|
|
180
|
+
// Target file has different content than the caller. The correct byte
|
|
181
|
+
// offset can only be computed from the target file's content.
|
|
182
|
+
// Target: "ab\ncdefgh\n" — line 1 starts at byte 3, character 2 = byte 5.
|
|
183
|
+
const locator: NodeLocator = (filePath, byteOffset) => {
|
|
184
|
+
if (filePath === "src/target.ts" && byteOffset === 5) return "mod.target";
|
|
185
|
+
return null;
|
|
186
|
+
};
|
|
187
|
+
const locations: LspLocation[] = [
|
|
188
|
+
{
|
|
189
|
+
uri: "file:///workspace/src/target.ts",
|
|
190
|
+
range: { start: { line: 1, character: 2 }, end: { line: 1, character: 5 } },
|
|
191
|
+
},
|
|
192
|
+
];
|
|
193
|
+
const result = mapLocationToNode(
|
|
194
|
+
locations,
|
|
195
|
+
{
|
|
196
|
+
callerFilePath: "src/caller.ts",
|
|
197
|
+
callerContent: "XXXXXXXXXX", // wrong content — must NOT be used
|
|
198
|
+
workspaceRoot: "/workspace",
|
|
199
|
+
resolveContent: (fp) => (fp === "src/target.ts" ? "ab\ncdefgh\n" : null),
|
|
200
|
+
},
|
|
201
|
+
locator,
|
|
202
|
+
);
|
|
203
|
+
assert.equal(result, "mod.target");
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("mapLocationToNode: cross-file without resolveContent → callerContent fallback", () => {
|
|
207
|
+
// Without a content resolver, cross-file definitions fall back to the
|
|
208
|
+
// caller's content (best-effort, documented behavior).
|
|
209
|
+
const locator: NodeLocator = () => "mod.fallback";
|
|
210
|
+
const locations: LspLocation[] = [
|
|
211
|
+
{ uri: "file:///src/other.ts", range: { start: { line: 0, character: 0 }, end: { line: 0, character: 3 } } },
|
|
212
|
+
];
|
|
213
|
+
const result = mapLocationToNode(
|
|
214
|
+
locations,
|
|
215
|
+
{ callerFilePath: "src/caller.ts", callerContent: "abc" },
|
|
216
|
+
locator,
|
|
217
|
+
);
|
|
218
|
+
assert.equal(result, "mod.fallback");
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
222
|
+
// Executor tests (mock client)
|
|
223
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Minimal mock LspClient — only implements the methods the executor calls.
|
|
227
|
+
*/
|
|
228
|
+
function makeMockClient(
|
|
229
|
+
definitionResults: Map<string, { ok: true; locations: LspLocation[] } | { ok: false; code: string }>,
|
|
230
|
+
): LspClient {
|
|
231
|
+
return {
|
|
232
|
+
definition: async (params: { textDocument: { uri: string }; position: { line: number; character: number } }) => {
|
|
233
|
+
// Key by uri+line+character for deterministic lookup.
|
|
234
|
+
const key = `${params.textDocument.uri}:${params.position.line}:${params.position.character}`;
|
|
235
|
+
const result = definitionResults.get(key);
|
|
236
|
+
if (!result) return { ok: true, locations: [] };
|
|
237
|
+
if (result.ok) return result;
|
|
238
|
+
return {
|
|
239
|
+
ok: false,
|
|
240
|
+
degradation: { backend: "lsp" as const, code: result.code as "server_crashed" },
|
|
241
|
+
};
|
|
242
|
+
},
|
|
243
|
+
didOpen: () => {},
|
|
244
|
+
dispose: async () => {},
|
|
245
|
+
supportsDefinition: true,
|
|
246
|
+
pid: undefined,
|
|
247
|
+
} as unknown as LspClient;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
test("executor: happy path — definition maps to node, upgrade applied", async () => {
|
|
251
|
+
const requests = planLspUpgrades(
|
|
252
|
+
[makeCallSite("src/a.ts", "target", 0, "a.caller")],
|
|
253
|
+
{ maxRequests: 100 },
|
|
254
|
+
).requests;
|
|
255
|
+
|
|
256
|
+
// Mock: definition at position {0, 0} returns a location at byte 0
|
|
257
|
+
// in src/target.ts. The nodeLocator finds "mod.target" at byte 0.
|
|
258
|
+
const mockClient = makeMockClient(new Map([
|
|
259
|
+
["file:///src/a.ts:0:0", {
|
|
260
|
+
ok: true as const,
|
|
261
|
+
locations: [{
|
|
262
|
+
uri: "file:///src/target.ts",
|
|
263
|
+
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 6 } },
|
|
264
|
+
}],
|
|
265
|
+
}],
|
|
266
|
+
]));
|
|
267
|
+
|
|
268
|
+
const locator: NodeLocator = () => "mod.target";
|
|
269
|
+
const applied: EdgeUpgrade[] = [];
|
|
270
|
+
const result = await executeLspResolution(requests, {
|
|
271
|
+
client: mockClient,
|
|
272
|
+
nodeLocator: locator,
|
|
273
|
+
applyUpgrades: async (upgrades) => {
|
|
274
|
+
applied.push(...upgrades);
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
assert.equal(result.upgraded, 1);
|
|
279
|
+
assert.equal(result.unresolved, 0);
|
|
280
|
+
assert.equal(applied.length, 1);
|
|
281
|
+
assert.equal(applied[0].provenance, "lsp");
|
|
282
|
+
assert.equal(applied[0].confidence, 0.9);
|
|
283
|
+
assert.equal(applied[0].dstQualifiedName, "mod.target");
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test("executor: definition returns empty → unresolved", async () => {
|
|
287
|
+
const requests = planLspUpgrades(
|
|
288
|
+
[makeCallSite("src/a.ts", "missing", 0, "a.caller")],
|
|
289
|
+
{ maxRequests: 100 },
|
|
290
|
+
).requests;
|
|
291
|
+
|
|
292
|
+
const mockClient = makeMockClient(new Map());
|
|
293
|
+
const locator: NodeLocator = () => null;
|
|
294
|
+
const result = await executeLspResolution(requests, {
|
|
295
|
+
client: mockClient,
|
|
296
|
+
nodeLocator: locator,
|
|
297
|
+
applyUpgrades: async () => {},
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
assert.equal(result.upgraded, 0);
|
|
301
|
+
assert.equal(result.unresolved, 1);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test("executor: mid-batch applyUpgrades failure → caught, counted as unresolved", async () => {
|
|
305
|
+
const requests = planLspUpgrades(
|
|
306
|
+
[makeCallSite("src/a.ts", "target", 0, "a.caller")],
|
|
307
|
+
{ maxRequests: 100 },
|
|
308
|
+
).requests;
|
|
309
|
+
|
|
310
|
+
const mockClient = makeMockClient(new Map([
|
|
311
|
+
["file:///src/a.ts:0:0", {
|
|
312
|
+
ok: true as const,
|
|
313
|
+
locations: [{
|
|
314
|
+
uri: "file:///src/target.ts",
|
|
315
|
+
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 6 } },
|
|
316
|
+
}],
|
|
317
|
+
}],
|
|
318
|
+
]));
|
|
319
|
+
|
|
320
|
+
const locator: NodeLocator = () => "mod.target";
|
|
321
|
+
const result = await executeLspResolution(requests, {
|
|
322
|
+
client: mockClient,
|
|
323
|
+
nodeLocator: locator,
|
|
324
|
+
applyUpgrades: async () => {
|
|
325
|
+
throw new Error("simulated transaction failure");
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// The upgrade was found but the apply failed → counted as unresolved.
|
|
330
|
+
assert.equal(result.upgraded, 0);
|
|
331
|
+
assert.equal(result.unresolved, 1);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test("executor: server crash mid-run → degradation, remaining unresolved", async () => {
|
|
335
|
+
const sites = [
|
|
336
|
+
makeCallSite("src/a.ts", "first", 0, "a.x"),
|
|
337
|
+
makeCallSite("src/b.ts", "second", 0, "b.x"),
|
|
338
|
+
];
|
|
339
|
+
const requests = planLspUpgrades(sites, { maxRequests: 100 }).requests;
|
|
340
|
+
|
|
341
|
+
// First file batch: definition request gets server_crashed.
|
|
342
|
+
const mockClient = makeMockClient(new Map([
|
|
343
|
+
["file:///src/a.ts:0:0", { ok: false as const, code: "server_crashed" }],
|
|
344
|
+
]));
|
|
345
|
+
|
|
346
|
+
const result = await executeLspResolution(requests, {
|
|
347
|
+
client: mockClient,
|
|
348
|
+
nodeLocator: () => null,
|
|
349
|
+
applyUpgrades: async () => {},
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
assert.ok(result.degradation, "should have a degradation");
|
|
353
|
+
assert.equal(result.degradation.code, "server_crashed");
|
|
354
|
+
// The first request was interrupted by the crash; subsequent file
|
|
355
|
+
// batches are NOT processed because the degradation short-circuits.
|
|
356
|
+
assert.equal(result.upgraded, 0);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
test("executor: fatal error after upgrade in same batch → NOT committed (rule 25)", async () => {
|
|
360
|
+
// Two call sites in the SAME file. First succeeds, second crashes.
|
|
361
|
+
// The first upgrade must NOT be committed — per-batch atomicity.
|
|
362
|
+
const sites = [
|
|
363
|
+
makeCallSite("src/a.ts", "first", 0, "a.x"),
|
|
364
|
+
makeCallSite("src/a.ts", "second", 6, "a.y"),
|
|
365
|
+
];
|
|
366
|
+
const requests = planLspUpgrades(sites, { maxRequests: 100 }).requests;
|
|
367
|
+
|
|
368
|
+
const mockClient = makeMockClient(new Map([
|
|
369
|
+
// byte 0 → line 0, char 0 — succeeds.
|
|
370
|
+
["file:///src/a.ts:0:0", {
|
|
371
|
+
ok: true as const,
|
|
372
|
+
locations: [{
|
|
373
|
+
uri: "file:///src/a.ts",
|
|
374
|
+
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 3 } },
|
|
375
|
+
}],
|
|
376
|
+
}],
|
|
377
|
+
// byte 6 → line 1, char 0 — server crashes.
|
|
378
|
+
["file:///src/a.ts:1:0", { ok: false as const, code: "server_crashed" }],
|
|
379
|
+
]));
|
|
380
|
+
|
|
381
|
+
let applyCalled = false;
|
|
382
|
+
const result = await executeLspResolution(requests, {
|
|
383
|
+
client: mockClient,
|
|
384
|
+
nodeLocator: () => "mod.target",
|
|
385
|
+
applyUpgrades: async () => { applyCalled = true; },
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// Per-batch atomicity: the upgrade from the first request is NOT
|
|
389
|
+
// committed because the batch was poisoned by the fatal error.
|
|
390
|
+
assert.equal(result.upgraded, 0);
|
|
391
|
+
assert.equal(applyCalled, false, "applyUpgrades must NOT be called for a poisoned batch");
|
|
392
|
+
assert.ok(result.degradation);
|
|
393
|
+
assert.equal(result.degradation.code, "server_crashed");
|
|
394
|
+
// The crashed request (1) + the lost upgrade (1) = 2 unresolved.
|
|
395
|
+
assert.equal(result.unresolved, 2);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
test("executor: request_timeout counts as unresolved, pass continues", async () => {
|
|
399
|
+
const sites = [
|
|
400
|
+
makeCallSite("src/a.ts", "slow", 0, "a.x"),
|
|
401
|
+
makeCallSite("src/b.ts", "fast", 0, "b.x"),
|
|
402
|
+
];
|
|
403
|
+
const requests = planLspUpgrades(sites, { maxRequests: 100 }).requests;
|
|
404
|
+
|
|
405
|
+
// First request times out; second succeeds.
|
|
406
|
+
const mockClient = makeMockClient(new Map([
|
|
407
|
+
["file:///src/a.ts:0:0", { ok: false as const, code: "request_timeout" }],
|
|
408
|
+
["file:///src/b.ts:0:0", {
|
|
409
|
+
ok: true as const,
|
|
410
|
+
locations: [{
|
|
411
|
+
uri: "file:///src/b.ts",
|
|
412
|
+
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 3 } },
|
|
413
|
+
}],
|
|
414
|
+
}],
|
|
415
|
+
]));
|
|
416
|
+
|
|
417
|
+
const applied: EdgeUpgrade[] = [];
|
|
418
|
+
const result = await executeLspResolution(requests, {
|
|
419
|
+
client: mockClient,
|
|
420
|
+
nodeLocator: () => "mod.resolved",
|
|
421
|
+
applyUpgrades: async (u) => { applied.push(...u); },
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
assert.equal(result.upgraded, 1, "second request should succeed");
|
|
425
|
+
assert.equal(result.unresolved, 1, "first request timed out");
|
|
426
|
+
assert.equal(applied.length, 1);
|
|
427
|
+
assert.equal(applied[0].srcQualifiedName, "b.x");
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
test("executor: empty requests → zero upgraded, zero unresolved", async () => {
|
|
431
|
+
const mockClient = makeMockClient(new Map());
|
|
432
|
+
const result = await executeLspResolution([], {
|
|
433
|
+
client: mockClient,
|
|
434
|
+
nodeLocator: () => null,
|
|
435
|
+
applyUpgrades: async () => {},
|
|
436
|
+
});
|
|
437
|
+
assert.equal(result.upgraded, 0);
|
|
438
|
+
assert.equal(result.unresolved, 0);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
test("executor: didOpen sent before definition request (LSP 3.17)", async () => {
|
|
442
|
+
const requests = planLspUpgrades(
|
|
443
|
+
[makeCallSite("src/a.ts", "target", 0, "a.caller")],
|
|
444
|
+
{ maxRequests: 100 },
|
|
445
|
+
).requests;
|
|
446
|
+
|
|
447
|
+
const didOpenCalls: { uri: string; languageId: string; text: string }[] = [];
|
|
448
|
+
const mockClient = {
|
|
449
|
+
definition: async () => ({ ok: true as const, locations: [] }),
|
|
450
|
+
didOpen: (item: { uri: string; languageId: string; text: string }) => {
|
|
451
|
+
didOpenCalls.push({ uri: item.uri, languageId: item.languageId, text: item.text });
|
|
452
|
+
},
|
|
453
|
+
dispose: async () => {},
|
|
454
|
+
} as unknown as LspClient;
|
|
455
|
+
|
|
456
|
+
await executeLspResolution(requests, {
|
|
457
|
+
client: mockClient,
|
|
458
|
+
nodeLocator: () => null,
|
|
459
|
+
applyUpgrades: async () => {},
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
assert.equal(didOpenCalls.length, 1, "didOpen must be called once per file batch");
|
|
463
|
+
assert.equal(didOpenCalls[0].uri, "file:///src/a.ts");
|
|
464
|
+
assert.equal(didOpenCalls[0].languageId, "typescript");
|
|
465
|
+
assert.equal(didOpenCalls[0].text, "line0\nline1\nline2");
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
test("executor: workspaceRoot resolves repo-relative paths to absolute URIs", async () => {
|
|
469
|
+
const requests = planLspUpgrades(
|
|
470
|
+
[makeCallSite("src/a.ts", "target", 0, "a.caller")],
|
|
471
|
+
{ maxRequests: 100 },
|
|
472
|
+
).requests;
|
|
473
|
+
|
|
474
|
+
// With workspaceRoot, the URI sent to the server is absolute.
|
|
475
|
+
const definitionUris: string[] = [];
|
|
476
|
+
const didOpenUris: string[] = [];
|
|
477
|
+
const mockClient = {
|
|
478
|
+
definition: async (params: { textDocument: { uri: string } }) => {
|
|
479
|
+
definitionUris.push(params.textDocument.uri);
|
|
480
|
+
return { ok: true as const, locations: [] };
|
|
481
|
+
},
|
|
482
|
+
didOpen: (item: { uri: string }) => { didOpenUris.push(item.uri); },
|
|
483
|
+
dispose: async () => {},
|
|
484
|
+
} as unknown as LspClient;
|
|
485
|
+
|
|
486
|
+
await executeLspResolution(requests, {
|
|
487
|
+
client: mockClient,
|
|
488
|
+
nodeLocator: () => null,
|
|
489
|
+
applyUpgrades: async () => {},
|
|
490
|
+
workspaceRoot: "/workspace",
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
assert.equal(didOpenUris.length, 1);
|
|
494
|
+
assert.equal(didOpenUris[0], "file:///workspace/src/a.ts");
|
|
495
|
+
assert.equal(definitionUris.length, 1);
|
|
496
|
+
assert.equal(definitionUris[0], "file:///workspace/src/a.ts");
|
|
497
|
+
});
|