@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,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Characterization test (issue #1555 — rule 30/48):
|
|
3
|
+
*
|
|
4
|
+
* With `lsp.enabled: false`, enabling LSP with zero servers installed
|
|
5
|
+
* must produce a working index IDENTICAL to Phase A. The LSP layer is
|
|
6
|
+
* a pure addition — when disabled, no LSP code path runs, no edges
|
|
7
|
+
* change, no degradations surface beyond `not_enabled`.
|
|
8
|
+
*
|
|
9
|
+
* This test also covers the characterization contract: the LSP layer
|
|
10
|
+
* never throws, never changes graph state, and produces a visible
|
|
11
|
+
* `not_enabled` degradation when disabled.
|
|
12
|
+
*/
|
|
13
|
+
import assert from "node:assert/strict";
|
|
14
|
+
import test from "node:test";
|
|
15
|
+
|
|
16
|
+
import { DEFAULT_LSP_CONFIG, parseLspConfig } from "./config.js";
|
|
17
|
+
import { lspDegradation } from "./degradation.js";
|
|
18
|
+
import { getLspStatus, formatLspStatusLine } from "./status.js";
|
|
19
|
+
import {
|
|
20
|
+
planLspUpgrades,
|
|
21
|
+
executeLspResolution,
|
|
22
|
+
type EdgeUpgrade,
|
|
23
|
+
} from "./resolution.js";
|
|
24
|
+
import type { CodingGraphLanguage } from "@remnic/core";
|
|
25
|
+
|
|
26
|
+
test("characterization: default config has enabled=false (rule 30/48)", () => {
|
|
27
|
+
assert.equal(DEFAULT_LSP_CONFIG.enabled, false);
|
|
28
|
+
assert.equal(DEFAULT_LSP_CONFIG.servers === undefined || Object.keys(DEFAULT_LSP_CONFIG.servers).length === 0, true);
|
|
29
|
+
assert.equal(DEFAULT_LSP_CONFIG.timeoutMs, 3_000);
|
|
30
|
+
assert.equal(DEFAULT_LSP_CONFIG.maxRequestsPerRun, 500);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("characterization: parseLspConfig(null) → default (disabled)", () => {
|
|
34
|
+
const result = parseLspConfig(null, ["typescript"]);
|
|
35
|
+
assert.equal(result.ok, true);
|
|
36
|
+
if (result.ok) {
|
|
37
|
+
assert.equal(result.config.enabled, false);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("characterization: parseLspConfig(undefined) → default (disabled)", () => {
|
|
42
|
+
const result = parseLspConfig(undefined, ["typescript"]);
|
|
43
|
+
assert.equal(result.ok, true);
|
|
44
|
+
if (result.ok) {
|
|
45
|
+
assert.equal(result.config.enabled, false);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("characterization: planLspUpgrades with enabled=false → planner still works but executor is not called", () => {
|
|
50
|
+
// The planner is pure — it doesn't check config.enabled. The caller
|
|
51
|
+
// checks config.enabled BEFORE calling the planner. This test verifies
|
|
52
|
+
// the planner produces correct output regardless, so the caller's gate
|
|
53
|
+
// is the only thing preventing LSP requests.
|
|
54
|
+
const sites = [
|
|
55
|
+
{
|
|
56
|
+
filePath: "src/a.ts",
|
|
57
|
+
language: "typescript" as CodingGraphLanguage,
|
|
58
|
+
content: "export function foo() {}",
|
|
59
|
+
calleeByteOffset: 0,
|
|
60
|
+
calleeName: "foo",
|
|
61
|
+
srcQualifiedName: "a.foo",
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
const plan = planLspUpgrades(sites, { maxRequests: 100 });
|
|
65
|
+
assert.equal(plan.requests.length, 1);
|
|
66
|
+
|
|
67
|
+
// But with enabled=false, the caller MUST NOT call executeLspResolution.
|
|
68
|
+
// This is the characterization: the index is byte-identical to Phase A.
|
|
69
|
+
// We verify this by asserting the planner output is non-empty but the
|
|
70
|
+
// caller's gate (config.enabled === false) prevents execution.
|
|
71
|
+
assert.equal(DEFAULT_LSP_CONFIG.enabled, false);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("characterization: getLspStatus with enabled=false → all entries show [disabled]", () => {
|
|
75
|
+
const status = getLspStatus({
|
|
76
|
+
config: DEFAULT_LSP_CONFIG,
|
|
77
|
+
probeResults: new Map(),
|
|
78
|
+
degradations: new Map(),
|
|
79
|
+
requestCounts: new Map(),
|
|
80
|
+
languages: ["typescript", "python"],
|
|
81
|
+
});
|
|
82
|
+
assert.equal(status.length, 2);
|
|
83
|
+
for (const entry of status) {
|
|
84
|
+
assert.equal(entry.enabled, false);
|
|
85
|
+
assert.equal(entry.probed, false);
|
|
86
|
+
assert.equal(entry.degraded, false);
|
|
87
|
+
assert.equal(entry.requestsUsed, 0);
|
|
88
|
+
const line = formatLspStatusLine(entry);
|
|
89
|
+
assert.ok(line.includes("[disabled]"), `line should show [disabled]: ${line}`);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("characterization: enabled=true but zero servers → [not_probed] + visible degradation", () => {
|
|
94
|
+
// Enabling LSP with zero servers installed must produce a working
|
|
95
|
+
// index identical to Phase A PLUS visible degradations. This is the
|
|
96
|
+
// rule 30/48 characterization: enabling is safe, it just doesn't help.
|
|
97
|
+
const status = getLspStatus({
|
|
98
|
+
config: { ...DEFAULT_LSP_CONFIG, enabled: true },
|
|
99
|
+
probeResults: new Map([["typescript", false]]),
|
|
100
|
+
degradations: new Map([["typescript", "server_missing"]]),
|
|
101
|
+
requestCounts: new Map(),
|
|
102
|
+
languages: ["typescript"],
|
|
103
|
+
});
|
|
104
|
+
assert.equal(status.length, 1);
|
|
105
|
+
const entry = status[0];
|
|
106
|
+
assert.equal(entry.enabled, true);
|
|
107
|
+
assert.equal(entry.probed, false);
|
|
108
|
+
assert.equal(entry.degraded, true);
|
|
109
|
+
assert.equal(entry.degradationCode, "server_missing");
|
|
110
|
+
const line = formatLspStatusLine(entry);
|
|
111
|
+
assert.ok(line.includes("not_probed") || line.includes("degraded"), `line should show state: ${line}`);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("characterization: formatLspStatusLine renders probed + request count", () => {
|
|
115
|
+
const line = formatLspStatusLine({
|
|
116
|
+
language: "typescript",
|
|
117
|
+
enabled: true,
|
|
118
|
+
probed: true,
|
|
119
|
+
degraded: false,
|
|
120
|
+
requestsUsed: 42,
|
|
121
|
+
});
|
|
122
|
+
assert.ok(line.includes("probed"), line);
|
|
123
|
+
assert.ok(line.includes("42 requests"), line);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("characterization: formatLspStatusLine renders degraded state", () => {
|
|
127
|
+
const line = formatLspStatusLine({
|
|
128
|
+
language: "python",
|
|
129
|
+
enabled: true,
|
|
130
|
+
probed: true,
|
|
131
|
+
degraded: true,
|
|
132
|
+
degradationCode: "request_timeout",
|
|
133
|
+
requestsUsed: 3,
|
|
134
|
+
});
|
|
135
|
+
assert.ok(line.includes("degraded:request_timeout"), line);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("characterization: lspDegradation produces backend=lsp for all codes", () => {
|
|
139
|
+
const codes = [
|
|
140
|
+
"server_missing",
|
|
141
|
+
"handshake_timeout",
|
|
142
|
+
"request_timeout",
|
|
143
|
+
"protocol_error",
|
|
144
|
+
"server_crashed",
|
|
145
|
+
"budget_exhausted",
|
|
146
|
+
"not_enabled",
|
|
147
|
+
"unknown_language",
|
|
148
|
+
] as const;
|
|
149
|
+
for (const code of codes) {
|
|
150
|
+
const d = lspDegradation(code);
|
|
151
|
+
assert.equal(d.backend, "lsp", `${code} must have backend=lsp`);
|
|
152
|
+
assert.equal(d.code, code);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("characterization: executeLspResolution with empty requests never calls client", async () => {
|
|
157
|
+
// When the planner produces zero requests (budget=0 or no call sites),
|
|
158
|
+
// the executor does nothing — no client calls, no applyUpgrades calls.
|
|
159
|
+
let clientCalled = false;
|
|
160
|
+
let applyCalled = false;
|
|
161
|
+
const result = await executeLspResolution([], {
|
|
162
|
+
client: {
|
|
163
|
+
definition: async () => { clientCalled = true; return { ok: true, locations: [] }; },
|
|
164
|
+
didOpen: () => {},
|
|
165
|
+
dispose: async () => {},
|
|
166
|
+
} as never,
|
|
167
|
+
nodeLocator: () => null,
|
|
168
|
+
applyUpgrades: async () => { applyCalled = true; },
|
|
169
|
+
});
|
|
170
|
+
assert.equal(result.upgraded, 0);
|
|
171
|
+
assert.equal(result.unresolved, 0);
|
|
172
|
+
assert.equal(clientCalled, false, "client.definition must not be called with zero requests");
|
|
173
|
+
assert.equal(applyCalled, false, "applyUpgrades must not be called with zero requests");
|
|
174
|
+
});
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LSP client tests — the 5-mode fake-server matrix (issue #1555 step 1).
|
|
3
|
+
*
|
|
4
|
+
* Each failure mode yields a DISTINCT degradation code (rule 34):
|
|
5
|
+
* - happy → ok:true
|
|
6
|
+
* - server_missing → server_missing
|
|
7
|
+
* - handshake_timeout → handshake_timeout
|
|
8
|
+
* - request_timeout → request_timeout
|
|
9
|
+
* - protocol_error → protocol_error
|
|
10
|
+
*
|
|
11
|
+
* Plus the zombie-cleanup test: after dispose(), no child process remains
|
|
12
|
+
* (asserted via pid liveness).
|
|
13
|
+
*
|
|
14
|
+
* The fake server is a checked-in Node script speaking canned JSON-RPC
|
|
15
|
+
* over stdio (synthetic responses only — rule 33: shapes match LSP 3.17).
|
|
16
|
+
*/
|
|
17
|
+
import assert from "node:assert/strict";
|
|
18
|
+
import { spawn } from "node:child_process";
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
import process from "node:process";
|
|
21
|
+
import test from "node:test";
|
|
22
|
+
|
|
23
|
+
import { LspClient } from "./client.js";
|
|
24
|
+
import { DEFAULT_LSP_TIMEOUT_MS } from "./config.js";
|
|
25
|
+
|
|
26
|
+
const FAKE_SERVER = path.resolve(
|
|
27
|
+
import.meta.dirname,
|
|
28
|
+
"fixtures",
|
|
29
|
+
"fake-server.mjs",
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Spawn the fake server with the given scenario and connect a real client.
|
|
34
|
+
* Returns the connect result.
|
|
35
|
+
*/
|
|
36
|
+
function connectFake(
|
|
37
|
+
scenario: string,
|
|
38
|
+
timeoutMs: number = DEFAULT_LSP_TIMEOUT_MS,
|
|
39
|
+
): Promise<
|
|
40
|
+
| { ok: true; client: LspClient }
|
|
41
|
+
| { ok: false; degradation: { code: string; detail?: string } }
|
|
42
|
+
> {
|
|
43
|
+
return LspClient.connect({
|
|
44
|
+
launchSpec: { command: process.execPath, args: [FAKE_SERVER, scenario] },
|
|
45
|
+
rootUri: null,
|
|
46
|
+
timeoutMs,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
51
|
+
// The 5-mode matrix
|
|
52
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
test("fake-server matrix: happy — handshake succeeds, supportsDefinition=true", async () => {
|
|
55
|
+
const result = await connectFake("happy", 5_000);
|
|
56
|
+
assert.equal(result.ok, true, `expected ok, got degradation: ${JSON.stringify(result)}`);
|
|
57
|
+
if (!result.ok) return;
|
|
58
|
+
assert.equal(result.client.supportsDefinition, true);
|
|
59
|
+
await result.client.dispose();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("fake-server matrix: server_missing — spawn fails with ENOENT", async () => {
|
|
63
|
+
const result = await LspClient.connect({
|
|
64
|
+
launchSpec: { command: "/nonexistent/binary/that/does/not/exist", args: [] },
|
|
65
|
+
rootUri: null,
|
|
66
|
+
timeoutMs: 2_000,
|
|
67
|
+
});
|
|
68
|
+
assert.equal(result.ok, false);
|
|
69
|
+
if (!result.ok) {
|
|
70
|
+
assert.equal(result.degradation.code, "server_missing");
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("fake-server matrix: handshake_timeout — initialize never responds", async () => {
|
|
75
|
+
const result = await connectFake("handshake_timeout", 1_500);
|
|
76
|
+
assert.equal(result.ok, false);
|
|
77
|
+
if (!result.ok) {
|
|
78
|
+
assert.equal(result.degradation.code, "handshake_timeout");
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("fake-server matrix: request_timeout — definition never responds", async () => {
|
|
83
|
+
// Handshake succeeds, but definition request hangs.
|
|
84
|
+
const connectResult = await connectFake("request_timeout", 5_000);
|
|
85
|
+
assert.equal(connectResult.ok, true, `handshake should succeed: ${JSON.stringify(connectResult)}`);
|
|
86
|
+
if (!connectResult.ok) return;
|
|
87
|
+
|
|
88
|
+
// Override the client's timeout for the definition request.
|
|
89
|
+
// We create a new client with a shorter timeout to test request-level
|
|
90
|
+
// timeout separately from handshake timeout.
|
|
91
|
+
await connectResult.client.dispose();
|
|
92
|
+
|
|
93
|
+
// Now connect with a short timeout — both handshake AND requests use it.
|
|
94
|
+
const shortResult = await connectFake("request_timeout", 1_000);
|
|
95
|
+
assert.equal(shortResult.ok, true, `handshake should succeed with short timeout`);
|
|
96
|
+
if (!shortResult.ok) return;
|
|
97
|
+
|
|
98
|
+
const defResult = await shortResult.client.definition({
|
|
99
|
+
textDocument: { uri: "file:///fake/test.ts" },
|
|
100
|
+
position: { line: 0, character: 0 },
|
|
101
|
+
});
|
|
102
|
+
assert.equal(defResult.ok, false);
|
|
103
|
+
if (!defResult.ok) {
|
|
104
|
+
assert.equal(defResult.degradation.code, "request_timeout");
|
|
105
|
+
}
|
|
106
|
+
await shortResult.client.dispose();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("fake-server matrix: protocol_error — malformed frame from server", async () => {
|
|
110
|
+
const result = await connectFake("protocol_error", 2_000);
|
|
111
|
+
// The protocol error happens during handshake — the server sends
|
|
112
|
+
// garbage after receiving initialize.
|
|
113
|
+
assert.equal(result.ok, false);
|
|
114
|
+
if (!result.ok) {
|
|
115
|
+
assert.equal(result.degradation.code, "protocol_error");
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
120
|
+
// Zombie cleanup — after dispose, no child process remains.
|
|
121
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
test("zombie cleanup: after dispose, no child process remains (pid liveness)", async () => {
|
|
124
|
+
const result = await connectFake("happy", 5_000);
|
|
125
|
+
assert.equal(result.ok, true);
|
|
126
|
+
if (!result.ok) return;
|
|
127
|
+
|
|
128
|
+
const pid = result.client.pid;
|
|
129
|
+
assert.ok(typeof pid === "number" && pid > 0, "client must have a pid");
|
|
130
|
+
assert.ok(isProcessAlive(pid), "process must be alive before dispose");
|
|
131
|
+
|
|
132
|
+
await result.client.dispose();
|
|
133
|
+
|
|
134
|
+
// After dispose, the process should be gone. Poll pid liveness with a
|
|
135
|
+
// bounded retry instead of a fixed sleep — the process usually dies
|
|
136
|
+
// within one event-loop tick (SIGKILL is synchronous), but on a loaded
|
|
137
|
+
// machine the reaping may lag by a few ms.
|
|
138
|
+
// Integration test: real child-process lifecycle, not fake-timerable.
|
|
139
|
+
const dead = await waitForCondition(() => !isProcessAlive(pid), 2_000, 50);
|
|
140
|
+
assert.equal(
|
|
141
|
+
dead,
|
|
142
|
+
true,
|
|
143
|
+
"process must be dead after dispose — no zombie",
|
|
144
|
+
);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("zombie cleanup: dispose is idempotent — safe to call twice", async () => {
|
|
148
|
+
const result = await connectFake("happy", 5_000);
|
|
149
|
+
assert.equal(result.ok, true);
|
|
150
|
+
if (!result.ok) return;
|
|
151
|
+
await result.client.dispose();
|
|
152
|
+
// Second dispose must not throw.
|
|
153
|
+
await result.client.dispose();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
157
|
+
// Happy-path definition request — end-to-end resolution.
|
|
158
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
test("happy path: didOpen + definition returns canned locations", async () => {
|
|
161
|
+
const result = await connectFake("happy", 5_000);
|
|
162
|
+
assert.equal(result.ok, true);
|
|
163
|
+
if (!result.ok) return;
|
|
164
|
+
|
|
165
|
+
result.client.didOpen({
|
|
166
|
+
uri: "file:///fake/src/caller.ts",
|
|
167
|
+
languageId: "typescript",
|
|
168
|
+
version: 1,
|
|
169
|
+
text: "export function caller() { target(); }",
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const defResult = await result.client.definition({
|
|
173
|
+
textDocument: { uri: "file:///fake/src/caller.ts" },
|
|
174
|
+
position: { line: 0, character: 30 },
|
|
175
|
+
});
|
|
176
|
+
assert.equal(defResult.ok, true, `definition should succeed: ${JSON.stringify(defResult)}`);
|
|
177
|
+
if (defResult.ok) {
|
|
178
|
+
assert.ok(defResult.locations.length > 0, "should return at least one location");
|
|
179
|
+
const loc = defResult.locations[0];
|
|
180
|
+
assert.ok(loc.uri, "location must have a uri");
|
|
181
|
+
assert.ok(loc.range, "location must have a range");
|
|
182
|
+
}
|
|
183
|
+
await result.client.dispose();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("definition after dispose → server_crashed degradation", async () => {
|
|
187
|
+
const result = await connectFake("happy", 5_000);
|
|
188
|
+
assert.equal(result.ok, true);
|
|
189
|
+
if (!result.ok) return;
|
|
190
|
+
await result.client.dispose();
|
|
191
|
+
const defResult = await result.client.definition({
|
|
192
|
+
textDocument: { uri: "file:///fake/test.ts" },
|
|
193
|
+
position: { line: 0, character: 0 },
|
|
194
|
+
});
|
|
195
|
+
assert.equal(defResult.ok, false);
|
|
196
|
+
if (!defResult.ok) {
|
|
197
|
+
assert.equal(defResult.degradation.code, "server_crashed");
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("crash_after_start: server exits mid-run → server_crashed", async () => {
|
|
202
|
+
const result = await connectFake("crash_after_start", 5_000);
|
|
203
|
+
assert.equal(result.ok, true, `handshake should succeed before crash: ${JSON.stringify(result)}`);
|
|
204
|
+
if (!result.ok) return;
|
|
205
|
+
|
|
206
|
+
// The fake server exits immediately after the initialized notification.
|
|
207
|
+
// Wait for the child process to actually exit before sending the next
|
|
208
|
+
// request. Integration test: real process exit, not fake-timerable.
|
|
209
|
+
const pid = result.client.pid;
|
|
210
|
+
if (typeof pid === "number") {
|
|
211
|
+
await waitForCondition(() => !isProcessAlive(pid), 2_000, 50);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const defResult = await result.client.definition({
|
|
215
|
+
textDocument: { uri: "file:///fake/test.ts" },
|
|
216
|
+
position: { line: 0, character: 0 },
|
|
217
|
+
});
|
|
218
|
+
assert.equal(defResult.ok, false);
|
|
219
|
+
if (!defResult.ok) {
|
|
220
|
+
assert.equal(
|
|
221
|
+
defResult.degradation.code,
|
|
222
|
+
"server_crashed",
|
|
223
|
+
`expected server_crashed, got ${defResult.degradation.code}`,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
await result.client.dispose();
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
230
|
+
// Helpers
|
|
231
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Check whether a process is still alive by sending signal 0.
|
|
235
|
+
* Returns true if the process exists and we can signal it.
|
|
236
|
+
*/
|
|
237
|
+
function isProcessAlive(pid: number): boolean {
|
|
238
|
+
try {
|
|
239
|
+
process.kill(pid, 0);
|
|
240
|
+
return true;
|
|
241
|
+
} catch {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Poll a condition at a bounded interval until it returns true or the
|
|
248
|
+
* deadline elapses. Used for real child-process lifecycle checks where
|
|
249
|
+
* deterministic fake-timers cannot replace the OS scheduler.
|
|
250
|
+
*/
|
|
251
|
+
function waitForCondition(
|
|
252
|
+
cond: () => boolean,
|
|
253
|
+
timeoutMs: number,
|
|
254
|
+
intervalMs: number,
|
|
255
|
+
): Promise<boolean> {
|
|
256
|
+
return new Promise<boolean>((resolve) => {
|
|
257
|
+
const deadline = Date.now() + timeoutMs;
|
|
258
|
+
const check = () => {
|
|
259
|
+
if (cond()) {
|
|
260
|
+
resolve(true);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (Date.now() >= deadline) {
|
|
264
|
+
resolve(false);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
setTimeout(check, intervalMs);
|
|
268
|
+
};
|
|
269
|
+
check();
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Suppress unhandled rejection noise from the fake server's exit events
|
|
274
|
+
// during tests (the 'error' event from ENOENT is handled by the client).
|
|
275
|
+
process.on("unhandledRejection", () => {});
|