@drkmttr/adhere 0.15.0 → 0.18.0
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 +313 -131
- package/bin/adhere.js +19 -4
- package/package.json +34 -12
- package/skills/adhere/SKILL.md +180 -31
- package/skills/adhere-setup/SKILL.md +142 -16
- package/src/cli.ts +56 -23
- package/src/config.ts +62 -16
- package/src/index.ts +14 -0
- package/src/markdown.ts +9 -5
- package/src/providers/cloudflare.ts +84 -0
- package/src/providers/jev.ts +66 -0
- package/src/providers/openai.ts +89 -0
- package/src/providers/post.ts +53 -0
- package/src/providers/provider.ts +131 -0
- package/src/providers/providers.test.ts +276 -0
- package/src/providers/systemOne.ts +95 -0
- package/src/services/AdhereConfig.ts +2 -4
- package/src/services/Credentials.ts +101 -51
- package/src/services/Jev.http.ts +184 -171
- package/src/services/Jev.provider.test.ts +483 -0
- package/src/services/Jev.reads.test.ts +516 -0
- package/src/services/Jev.ts +276 -120
- package/src/services/NativeLsp.ts +289 -0
- package/src/services/SourceReads.test.ts +1289 -0
- package/src/services/SourceReads.ts +460 -0
- package/src/services/SourceWalker.ts +26 -59
- package/src/workflows/audit.ts +513 -267
- package/src/workflows/format.ts +74 -17
- package/src/workspaces.test.ts +73 -0
- package/src/workspaces.ts +91 -0
|
@@ -0,0 +1,1289 @@
|
|
|
1
|
+
import { isNote, withoutComments } from "#comments.ts";
|
|
2
|
+
import { DEFAULT_MAX_SYMBOL_DOCUMENTATION_LENGTH, DEFAULT_MAX_SYMBOL_LENGTH } from "#config.ts";
|
|
3
|
+
import { suppressionsOf } from "#suppress.ts";
|
|
4
|
+
import { Effect, Fiber, Layer, Path } from "effect";
|
|
5
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
6
|
+
import { createRequire } from "node:module";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import { dirname, join } from "node:path";
|
|
9
|
+
import { API, Checker, LanguageService, Program, Snapshot } from "typescript/async";
|
|
10
|
+
import { afterEach, describe, expect, it, vi } from "vite-plus/test";
|
|
11
|
+
import packageJson from "../../package.json";
|
|
12
|
+
import { NativeLsp } from "./NativeLsp.ts";
|
|
13
|
+
import {
|
|
14
|
+
SourceReads,
|
|
15
|
+
SourceReadsLive,
|
|
16
|
+
type SourceReadRequest,
|
|
17
|
+
type SourceReadState,
|
|
18
|
+
} from "./SourceReads.ts";
|
|
19
|
+
|
|
20
|
+
const library = [
|
|
21
|
+
"export const start = 0;",
|
|
22
|
+
"// ordinary comment",
|
|
23
|
+
"// @adhere: keep this note",
|
|
24
|
+
"// adhere-ignore sample -- never send this directive",
|
|
25
|
+
"/** The original documented value. */",
|
|
26
|
+
"export const original = 42;",
|
|
27
|
+
" export const neighbor = 2;",
|
|
28
|
+
"\texport const another = 3;",
|
|
29
|
+
"export const lastNeighbor = 4;",
|
|
30
|
+
"export const outsideWindow = 5;",
|
|
31
|
+
"export const unused = 6;",
|
|
32
|
+
...Array<string>(4).fill(""),
|
|
33
|
+
"export interface Shape { first: number }",
|
|
34
|
+
"export const between = 7;",
|
|
35
|
+
"export interface Shape { second: string }",
|
|
36
|
+
...Array<string>(12).fill(""),
|
|
37
|
+
"export function pair(value: number): number;",
|
|
38
|
+
...Array<string>(10).fill(""),
|
|
39
|
+
"export function pair(value: string): string;",
|
|
40
|
+
...Array<string>(10).fill(""),
|
|
41
|
+
"export function pair(value: number | string) { return value; }",
|
|
42
|
+
"",
|
|
43
|
+
].join("\n");
|
|
44
|
+
const consumer = [
|
|
45
|
+
'import { original as local, Shape, pair, unused } from "./lib";',
|
|
46
|
+
'import { external } from "./types";',
|
|
47
|
+
'import { secret as dependency } from "private";',
|
|
48
|
+
"export const result = local;",
|
|
49
|
+
"export const object = { local };",
|
|
50
|
+
"export function shadow(local: string) { return local.trim(); }",
|
|
51
|
+
"export function narrow(value: string | undefined) {",
|
|
52
|
+
' if (typeof value === "string") return value.trim();',
|
|
53
|
+
" return value;",
|
|
54
|
+
"}",
|
|
55
|
+
'export const shape: Shape = { first: 1, second: "ok" };',
|
|
56
|
+
"export const first = pair(1);",
|
|
57
|
+
'export const second = pair("text");',
|
|
58
|
+
"export const externalValue = external;",
|
|
59
|
+
"export const privateValue = dependency;",
|
|
60
|
+
"",
|
|
61
|
+
].join("\n");
|
|
62
|
+
const sources = {
|
|
63
|
+
"lib.ts": library,
|
|
64
|
+
"use.ts": consumer,
|
|
65
|
+
"types.d.ts": "export declare const external: number;",
|
|
66
|
+
"node_modules/private/package.json": JSON.stringify({ name: "private", types: "index.d.ts" }),
|
|
67
|
+
"node_modules/private/index.d.ts": "export declare const secret: number;",
|
|
68
|
+
};
|
|
69
|
+
const both: SourceReadRequest = {
|
|
70
|
+
symbols: true,
|
|
71
|
+
references: true,
|
|
72
|
+
includeComments: false,
|
|
73
|
+
maxSymbolDocumentationLength: DEFAULT_MAX_SYMBOL_DOCUMENTATION_LENGTH,
|
|
74
|
+
maxSymbolLength: DEFAULT_MAX_SYMBOL_LENGTH,
|
|
75
|
+
};
|
|
76
|
+
const roots: string[] = [];
|
|
77
|
+
const within = async <A>(promise: Promise<A>, milliseconds: number): Promise<A> => {
|
|
78
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
79
|
+
try {
|
|
80
|
+
return await Promise.race([
|
|
81
|
+
promise,
|
|
82
|
+
new Promise<never>((_, reject) => {
|
|
83
|
+
timer = setTimeout(
|
|
84
|
+
() => reject(new Error("Native lifecycle did not settle in time")),
|
|
85
|
+
milliseconds,
|
|
86
|
+
);
|
|
87
|
+
}),
|
|
88
|
+
]);
|
|
89
|
+
} finally {
|
|
90
|
+
clearTimeout(timer);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
const normalized = (text: string, includeComments = false) => {
|
|
94
|
+
const directed = suppressionsOf(text.split("\n")).lines;
|
|
95
|
+
return includeComments ? directed : withoutComments(directed, isNote);
|
|
96
|
+
};
|
|
97
|
+
const run = <A, E>(root: string, work: Effect.Effect<A, E, SourceReads>) =>
|
|
98
|
+
Effect.runPromise(
|
|
99
|
+
work.pipe(Effect.provide(SourceReadsLive(root).pipe(Layer.provide(Path.layer)))),
|
|
100
|
+
);
|
|
101
|
+
async function fixture(
|
|
102
|
+
files: Readonly<Record<string, string>> = sources,
|
|
103
|
+
config: object | false = {
|
|
104
|
+
compilerOptions: { strict: true, types: [], module: "Preserve", moduleResolution: "Bundler" },
|
|
105
|
+
files: ["lib.ts", "use.ts", "types.d.ts"],
|
|
106
|
+
},
|
|
107
|
+
) {
|
|
108
|
+
const root = await mkdtemp(join(tmpdir(), "adhere-source-reads-"));
|
|
109
|
+
roots.push(root);
|
|
110
|
+
await Promise.all(
|
|
111
|
+
Object.entries(files).map(async ([name, text]) => {
|
|
112
|
+
const path = join(root, name);
|
|
113
|
+
await mkdir(dirname(path), { recursive: true });
|
|
114
|
+
await writeFile(path, text);
|
|
115
|
+
}),
|
|
116
|
+
);
|
|
117
|
+
if (config !== false) await writeFile(join(root, "tsconfig.json"), JSON.stringify(config));
|
|
118
|
+
const prepare = (names: readonly string[]) =>
|
|
119
|
+
Effect.flatMap(SourceReads, (reads) => reads.prepare(names.map((name) => join(root, name))));
|
|
120
|
+
const read = (name: string, requested: SourceReadRequest = both) =>
|
|
121
|
+
Effect.flatMap(SourceReads, (reads) =>
|
|
122
|
+
reads.read(
|
|
123
|
+
{ path: join(root, name), contents: files[name]! },
|
|
124
|
+
normalized(files[name]!, requested.includeComments),
|
|
125
|
+
requested,
|
|
126
|
+
),
|
|
127
|
+
).pipe(Effect.map((state) => state as SourceReadState));
|
|
128
|
+
return { root, read, prepare };
|
|
129
|
+
}
|
|
130
|
+
afterEach(async () => {
|
|
131
|
+
vi.useRealTimers();
|
|
132
|
+
vi.restoreAllMocks();
|
|
133
|
+
vi.unstubAllGlobals();
|
|
134
|
+
vi.unstubAllEnvs();
|
|
135
|
+
await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe("native TypeScript compiler and declaration reads", () => {
|
|
139
|
+
it("registers thousands of paths without native work and opens only the file actually read", async () => {
|
|
140
|
+
const { root, read, prepare } = await fixture();
|
|
141
|
+
const open = vi.spyOn(NativeLsp, "open");
|
|
142
|
+
const attach = vi.spyOn(API, "fromLSPConnection");
|
|
143
|
+
const update = vi.spyOn(API.prototype, "getCurrentLanguageServerSnapshot");
|
|
144
|
+
const openDocument = vi.spyOn(NativeLsp.prototype, "openDocument");
|
|
145
|
+
const closeDocument = vi.spyOn(NativeLsp.prototype, "closeDocument");
|
|
146
|
+
const dispose = vi.spyOn(Snapshot.prototype, "dispose");
|
|
147
|
+
const close = vi.spyOn(API.prototype, "close");
|
|
148
|
+
const result = await run(
|
|
149
|
+
root,
|
|
150
|
+
Effect.gen(function* () {
|
|
151
|
+
yield* prepare(Array.from({ length: 12_222 }, (_, index) => `not-read/${index}.ts`));
|
|
152
|
+
const unprepared = yield* read("use.ts").pipe(Effect.flip);
|
|
153
|
+
expect(unprepared.message).toContain("was not prepared");
|
|
154
|
+
expect(unprepared.message).not.toMatch(/reinstall/i);
|
|
155
|
+
expect(yield* read("use.ts", { ...both, symbols: false, references: false })).toEqual({});
|
|
156
|
+
yield* prepare(["use.ts"]);
|
|
157
|
+
expect(open).not.toHaveBeenCalled();
|
|
158
|
+
expect(attach).not.toHaveBeenCalled();
|
|
159
|
+
expect(update).not.toHaveBeenCalled();
|
|
160
|
+
expect(openDocument).not.toHaveBeenCalled();
|
|
161
|
+
const state = yield* read("use.ts");
|
|
162
|
+
expect(closeDocument).toHaveBeenCalledExactlyOnceWith(join(root, "use.ts"));
|
|
163
|
+
expect(dispose).toHaveBeenCalledOnce();
|
|
164
|
+
expect(close).not.toHaveBeenCalled();
|
|
165
|
+
return state;
|
|
166
|
+
}),
|
|
167
|
+
);
|
|
168
|
+
expect(result.symbols!.local).toContain("The original documented value.");
|
|
169
|
+
expect(open).toHaveBeenCalledOnce();
|
|
170
|
+
expect(attach).toHaveBeenCalledOnce();
|
|
171
|
+
expect(openDocument).toHaveBeenCalledExactlyOnceWith(join(root, "use.ts"), consumer);
|
|
172
|
+
expect(update.mock.calls).toEqual([[]]);
|
|
173
|
+
expect(close).toHaveBeenCalledOnce();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("keeps compiler types/docs, shadowed bindings, narrowing and overloads without reference searches", async () => {
|
|
177
|
+
const { root, read } = await fixture();
|
|
178
|
+
const search = vi
|
|
179
|
+
.spyOn(LanguageService.prototype, "getReferencedSymbolsForNode")
|
|
180
|
+
.mockRejectedValue(new Error("Reference searches are forbidden"));
|
|
181
|
+
const result = await run(root, read("use.ts"));
|
|
182
|
+
const local = result.symbols!.local!;
|
|
183
|
+
expect(local).toContain("```typescript\n42\n```");
|
|
184
|
+
expect(local).toContain("```typescript\nstring\n```");
|
|
185
|
+
expect(local.match(/The original documented value\./g)).toHaveLength(1);
|
|
186
|
+
expect(result.symbols!.value).toContain("```typescript\nstring | undefined\n```");
|
|
187
|
+
expect(result.symbols!.value).toContain("```typescript\nstring\n```");
|
|
188
|
+
expect(result.symbols!.value).toContain("```typescript\nundefined\n```");
|
|
189
|
+
expect(result.symbols!.pair).toContain("(value: number): number");
|
|
190
|
+
expect(result.symbols!.pair).toContain("(value: string): string");
|
|
191
|
+
expect(Object.values(result.symbols!).every((value) => typeof value === "string")).toBe(true);
|
|
192
|
+
expect(search).not.toHaveBeenCalled();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("shares documentation across instantiated methods while retaining types, tags and separate bindings", async () => {
|
|
196
|
+
const files = {
|
|
197
|
+
"lib.ts": [
|
|
198
|
+
"export interface Box<T> {",
|
|
199
|
+
" /**",
|
|
200
|
+
" * Read the stored value.",
|
|
201
|
+
" * @returns The stored value.",
|
|
202
|
+
" * @example box.get();",
|
|
203
|
+
" */",
|
|
204
|
+
" get(): T;",
|
|
205
|
+
"}",
|
|
206
|
+
"export const first = {} as Box<string>;",
|
|
207
|
+
"export const second = {} as Box<number>;",
|
|
208
|
+
"export const mapped: Readonly<Box<string>> = first;",
|
|
209
|
+
].join("\n"),
|
|
210
|
+
"other.ts": "/** An unrelated reader. */\nexport const other = { get: () => true };",
|
|
211
|
+
"use.ts": [
|
|
212
|
+
'import { first, second, mapped } from "./lib";',
|
|
213
|
+
'import { other } from "./other";',
|
|
214
|
+
"export const results = [first.get(), second.get(), mapped.get(), other.get()];",
|
|
215
|
+
].join("\n"),
|
|
216
|
+
"types.d.ts": "",
|
|
217
|
+
};
|
|
218
|
+
const { root, read } = await fixture(files);
|
|
219
|
+
const result = await run(root, read("use.ts"));
|
|
220
|
+
const get = result.symbols!.get!;
|
|
221
|
+
expect(get).toContain("() => string");
|
|
222
|
+
expect(get).toContain("() => number");
|
|
223
|
+
expect(get).toContain("() => boolean");
|
|
224
|
+
expect(get.match(/Read the stored value\./g)).toHaveLength(1);
|
|
225
|
+
expect(get.match(/\*\*@returns\*\*/g)).toHaveLength(1);
|
|
226
|
+
expect(get).toContain("The stored value.");
|
|
227
|
+
expect(get).toContain("**@example** box.get();");
|
|
228
|
+
expect(result.references!["get:lib.ts"]).toContain(" get(): T;");
|
|
229
|
+
expect(result.references!["get:other.ts"]).toBe("export const other = { get: () => true };");
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("caps documentation independently when the whole-symbol limit allows long signatures", async () => {
|
|
233
|
+
const documentation = `Documentation: ${"x".repeat(600)}`;
|
|
234
|
+
const example = "y".repeat(600);
|
|
235
|
+
const parameters = Array.from({ length: 24 }, (_, index) => `argument${index}: number`).join(
|
|
236
|
+
", ",
|
|
237
|
+
);
|
|
238
|
+
const { root, read } = await fixture({
|
|
239
|
+
"lib.ts": [
|
|
240
|
+
`/** ${documentation} */`,
|
|
241
|
+
`export function original(${parameters}) { return argument0; }`,
|
|
242
|
+
`/** A value.\n * @example ${example}\n */`,
|
|
243
|
+
"export const tagged = 42;",
|
|
244
|
+
].join("\n"),
|
|
245
|
+
"use.ts":
|
|
246
|
+
'import { original, tagged } from "./lib";\nexport const result = [original, tagged];',
|
|
247
|
+
"types.d.ts": "",
|
|
248
|
+
});
|
|
249
|
+
const results = await run(
|
|
250
|
+
root,
|
|
251
|
+
Effect.forEach([256, 32, 0, 1024], (maxSymbolDocumentationLength) =>
|
|
252
|
+
read("use.ts", { ...both, maxSymbolDocumentationLength, maxSymbolLength: 4096 }),
|
|
253
|
+
),
|
|
254
|
+
);
|
|
255
|
+
for (const [index, limit] of [256, 32, 0, 1024].entries()) {
|
|
256
|
+
const result = results[index];
|
|
257
|
+
const content = result?.symbols?.original ?? "";
|
|
258
|
+
expect(content).toMatch(/^```typescript\n[\s\S]+\n```/);
|
|
259
|
+
const [, signature = "", docs = ""] =
|
|
260
|
+
content.match(/^```typescript\n([\s\S]*?)\n```(?:\n\n([\s\S]*))?$/) ?? [];
|
|
261
|
+
expect(signature.length).toBeGreaterThan(256);
|
|
262
|
+
expect(signature).toContain("argument23: number");
|
|
263
|
+
expect(docs).toBe(documentation.slice(0, limit));
|
|
264
|
+
const tags = `A value.\n\n**@example** ${example}`.slice(0, limit);
|
|
265
|
+
expect(result?.symbols?.tagged).toBe(
|
|
266
|
+
`\`\`\`typescript\n42\n\`\`\`${tags ? `\n\n${tags}` : ""}`,
|
|
267
|
+
);
|
|
268
|
+
expect(result?.references).toEqual(results[0]?.references);
|
|
269
|
+
expect(result?.references?.["original:lib.ts"]).toContain(parameters);
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("caps each final symbol across bindings and documentation without shortening references", async () => {
|
|
274
|
+
const { root, read } = await fixture({
|
|
275
|
+
...sources,
|
|
276
|
+
"lib.ts": library.replace("The original documented value.", "x".repeat(600)),
|
|
277
|
+
});
|
|
278
|
+
const { full, capped } = await run(
|
|
279
|
+
root,
|
|
280
|
+
Effect.gen(function* () {
|
|
281
|
+
const full = yield* read("use.ts", { ...both, maxSymbolLength: 4096 });
|
|
282
|
+
const capped = yield* Effect.forEach([256, 32, 0, 1024], (maxSymbolLength) =>
|
|
283
|
+
read("use.ts", { ...both, maxSymbolLength }),
|
|
284
|
+
);
|
|
285
|
+
return { full, capped };
|
|
286
|
+
}),
|
|
287
|
+
);
|
|
288
|
+
expect(full.symbols?.local?.length).toBeGreaterThan(256);
|
|
289
|
+
expect(full.symbols?.local).toContain("```typescript\nstring\n```");
|
|
290
|
+
for (const [index, limit] of [256, 32, 0, 1024].entries()) {
|
|
291
|
+
expect(capped[index]?.symbols).toEqual(
|
|
292
|
+
Object.fromEntries(
|
|
293
|
+
Object.entries(full.symbols ?? {}).map(([name, content]) => [
|
|
294
|
+
name,
|
|
295
|
+
content.slice(0, limit),
|
|
296
|
+
]),
|
|
297
|
+
),
|
|
298
|
+
);
|
|
299
|
+
expect(capped[index]?.references).toEqual(full.references);
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("reads const-asserted empty tuple properties and their normal usages without losing surrounding types or references", async () => {
|
|
304
|
+
const files = {
|
|
305
|
+
"lib.ts": "export const original = 42;",
|
|
306
|
+
"use.ts": [
|
|
307
|
+
'import { original } from "./lib";',
|
|
308
|
+
'export const value = { action: "update", stables: [] } as const;',
|
|
309
|
+
"export const used = value.stables;",
|
|
310
|
+
"export const linked = original;",
|
|
311
|
+
].join("\n"),
|
|
312
|
+
"types.d.ts": "",
|
|
313
|
+
};
|
|
314
|
+
const { root, read } = await fixture(files);
|
|
315
|
+
const result = await run(root, read("use.ts"));
|
|
316
|
+
expect(result.symbols!.stables).toBe("```typescript\nreadonly []\n```");
|
|
317
|
+
expect(result.symbols!.used).toBe("```typescript\nreadonly []\n```");
|
|
318
|
+
expect(result.symbols!.value).toContain(
|
|
319
|
+
'{ readonly action: "update"; readonly stables: readonly []; }',
|
|
320
|
+
);
|
|
321
|
+
expect(result.symbols!.action).toBe('```typescript\n"update"\n```');
|
|
322
|
+
expect(result.references).toEqual({ "original:lib.ts": "export const original = 42;" });
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it("keeps error types unavailable without hiding explicitly declared any types", async () => {
|
|
326
|
+
const files = {
|
|
327
|
+
"lib.ts": "",
|
|
328
|
+
"use.ts": [
|
|
329
|
+
"/** The unresolved value's documentation. */",
|
|
330
|
+
"export declare const unavailable: MissingType;",
|
|
331
|
+
"export declare const explicit: any;",
|
|
332
|
+
].join("\n"),
|
|
333
|
+
"types.d.ts": "",
|
|
334
|
+
};
|
|
335
|
+
const { root, read } = await fixture(files);
|
|
336
|
+
const result = await run(root, read("use.ts"));
|
|
337
|
+
expect(result.symbols!.unavailable).toBe("The unresolved value's documentation.");
|
|
338
|
+
expect(result.symbols!.explicit).toBe("```typescript\nany\n```");
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it("omits standard built-ins without changing audited code or meaningful user signatures", async () => {
|
|
342
|
+
const declaration = [
|
|
343
|
+
"export function collect(values: Array<string>): Promise<Array<string>> {",
|
|
344
|
+
" return Promise.resolve(values);",
|
|
345
|
+
"}",
|
|
346
|
+
].join("\n");
|
|
347
|
+
const contents = [
|
|
348
|
+
'import { collect } from "./lib";',
|
|
349
|
+
'export const values: Array<string> = [" value "];',
|
|
350
|
+
"export const copied = Array.from(values).map(value => value.trim());",
|
|
351
|
+
"export const objectKeys = Object.keys({ copied });",
|
|
352
|
+
"export const pending: Promise<string[]> = collect(copied);",
|
|
353
|
+
'pending.then(value => console.log(value.join(","))).catch((error: unknown) => String(error));',
|
|
354
|
+
"export function inspect(flag: boolean, count: number): unknown {",
|
|
355
|
+
" if (flag && count > 0) {",
|
|
356
|
+
" try { return arguments[0] ?? undefined; } catch { return undefined; }",
|
|
357
|
+
" }",
|
|
358
|
+
" return undefined;",
|
|
359
|
+
"}",
|
|
360
|
+
].join("\n");
|
|
361
|
+
const { root } = await fixture(
|
|
362
|
+
{
|
|
363
|
+
"lib.ts": "/** Collects typed values. */\n" + declaration,
|
|
364
|
+
"use.ts": contents,
|
|
365
|
+
"types.d.ts": "",
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
compilerOptions: {
|
|
369
|
+
strict: true,
|
|
370
|
+
target: "ES2022",
|
|
371
|
+
types: [],
|
|
372
|
+
module: "Preserve",
|
|
373
|
+
moduleResolution: "Bundler",
|
|
374
|
+
},
|
|
375
|
+
files: ["lib.ts", "use.ts", "types.d.ts"],
|
|
376
|
+
},
|
|
377
|
+
);
|
|
378
|
+
const file = { path: join(root, "use.ts"), contents };
|
|
379
|
+
const lines = normalized(contents);
|
|
380
|
+
const result = (await run(
|
|
381
|
+
root,
|
|
382
|
+
Effect.flatMap(SourceReads, (reads) => reads.read(file, lines, both)),
|
|
383
|
+
)) as SourceReadState;
|
|
384
|
+
for (const name of [
|
|
385
|
+
"Array",
|
|
386
|
+
"console",
|
|
387
|
+
"Object",
|
|
388
|
+
"Promise",
|
|
389
|
+
"String",
|
|
390
|
+
"from",
|
|
391
|
+
"map",
|
|
392
|
+
"trim",
|
|
393
|
+
"keys",
|
|
394
|
+
"then",
|
|
395
|
+
"log",
|
|
396
|
+
"join",
|
|
397
|
+
"catch",
|
|
398
|
+
"undefined",
|
|
399
|
+
"arguments",
|
|
400
|
+
"string",
|
|
401
|
+
"number",
|
|
402
|
+
"boolean",
|
|
403
|
+
"unknown",
|
|
404
|
+
"if",
|
|
405
|
+
"try",
|
|
406
|
+
]) {
|
|
407
|
+
expect(result.symbols).not.toHaveProperty(name);
|
|
408
|
+
expect(Object.keys(result.references!).some((key) => key.startsWith(`${name}:`))).toBe(false);
|
|
409
|
+
}
|
|
410
|
+
expect(result.symbols!.collect).toContain("(values: Array<string>) => Promise<Array<string>>");
|
|
411
|
+
expect(result.symbols!.collect).toContain("Collects typed values.");
|
|
412
|
+
expect(result.symbols!.inspect).toContain("(flag: boolean, count: number) => unknown");
|
|
413
|
+
expect(result.references!["collect:lib.ts"]).toBe(declaration);
|
|
414
|
+
expect(file.contents).toBe(contents);
|
|
415
|
+
expect(lines).toEqual(normalized(contents));
|
|
416
|
+
expect(await readFile(file.path, "utf8")).toBe(contents);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it("omits aliased runtime built-ins while preserving third-party declarations and augmentations", async () => {
|
|
420
|
+
const files = {
|
|
421
|
+
"lib.ts": "",
|
|
422
|
+
"use.ts": [
|
|
423
|
+
'import { readFile as readText } from "node:fs";',
|
|
424
|
+
'import { hash as hashText } from "bun";',
|
|
425
|
+
'import { Database as SqliteDatabase } from "bun:sqlite";',
|
|
426
|
+
'import { readFile as vendorRead } from "third-party";',
|
|
427
|
+
'export const runtimeText = readText("fixture.txt");',
|
|
428
|
+
"export const runtimeHash = hashText(runtimeText);",
|
|
429
|
+
'export const database = new SqliteDatabase(":memory:");',
|
|
430
|
+
'export const vendorText = vendorRead("fixture.txt");',
|
|
431
|
+
"export const vendorValues = Array.from([vendorText]);",
|
|
432
|
+
"export const extended = vendorValues.vendorFeature();",
|
|
433
|
+
].join("\n"),
|
|
434
|
+
"types.d.ts": [
|
|
435
|
+
'/// <reference path="./node_modules/@types/node/index.d.ts" />',
|
|
436
|
+
'/// <reference path="./node_modules/@types/bun/index.d.ts" />',
|
|
437
|
+
'/// <reference path="./node_modules/bun-types/index.d.ts" />',
|
|
438
|
+
].join("\n"),
|
|
439
|
+
"node_modules/@types/node/index.d.ts":
|
|
440
|
+
'declare module "node:fs" { /** Reads a runtime file. */ export function readFile(path: string): string; }',
|
|
441
|
+
"node_modules/@types/bun/index.d.ts":
|
|
442
|
+
'declare module "bun" { /** Hashes a runtime value. */ export function hash(value: string): number; }',
|
|
443
|
+
"node_modules/bun-types/index.d.ts":
|
|
444
|
+
'declare module "bun:sqlite" { /** Opens a runtime database. */ export class Database { constructor(path: string); } }',
|
|
445
|
+
"node_modules/third-party/package.json": JSON.stringify({
|
|
446
|
+
name: "third-party",
|
|
447
|
+
types: "index.d.ts",
|
|
448
|
+
}),
|
|
449
|
+
"node_modules/third-party/index.d.ts": [
|
|
450
|
+
"/** Third-party file reader. */",
|
|
451
|
+
"export declare function readFile(path: string): string;",
|
|
452
|
+
"declare global {",
|
|
453
|
+
" interface Array<T> {",
|
|
454
|
+
" /** Third-party array extension. */",
|
|
455
|
+
" vendorFeature(): string;",
|
|
456
|
+
" }",
|
|
457
|
+
"}",
|
|
458
|
+
].join("\n"),
|
|
459
|
+
};
|
|
460
|
+
const { root, read } = await fixture(files);
|
|
461
|
+
const result = await run(root, read("use.ts"));
|
|
462
|
+
for (const name of ["readText", "hashText", "SqliteDatabase"]) {
|
|
463
|
+
expect(result.symbols).not.toHaveProperty(name);
|
|
464
|
+
expect(Object.keys(result.references!).some((key) => key.startsWith(`${name}:`))).toBe(false);
|
|
465
|
+
}
|
|
466
|
+
expect(result.symbols!.vendorRead).toContain("(path: string) => string");
|
|
467
|
+
expect(result.symbols!.vendorRead).toContain("Third-party file reader.");
|
|
468
|
+
expect(result.symbols!.vendorFeature).toContain("Third-party array extension.");
|
|
469
|
+
expect(result.symbols!.Array).toEqual(expect.any(String));
|
|
470
|
+
expect(result.symbols!.database).toEqual(expect.any(String));
|
|
471
|
+
expect(result.references).toEqual({});
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it("preserves project-owned built-in names, intrinsic-name properties and imported aliases", async () => {
|
|
475
|
+
const declarations = {
|
|
476
|
+
Array: "export const Array = (value: string) => value;",
|
|
477
|
+
Promise: "export const Promise = (value: number) => value;",
|
|
478
|
+
console: "export const console = { log(value: string) { return value; } };",
|
|
479
|
+
Object: "export const Object = { catch(value: string) { return value; } };",
|
|
480
|
+
tokens:
|
|
481
|
+
'export const tokens = { undefined: "project marker", arguments: "project arguments" };',
|
|
482
|
+
};
|
|
483
|
+
const files = {
|
|
484
|
+
"lib.ts": [
|
|
485
|
+
"/** Project array builder. */",
|
|
486
|
+
declarations.Array,
|
|
487
|
+
"/** Project promise maker. */",
|
|
488
|
+
declarations.Promise,
|
|
489
|
+
declarations.console,
|
|
490
|
+
declarations.Object,
|
|
491
|
+
declarations.tokens,
|
|
492
|
+
].join("\n"),
|
|
493
|
+
"use.ts": [
|
|
494
|
+
'import { Array, Promise as createPromise, console, Object, tokens } from "./lib";',
|
|
495
|
+
'export const result = createPromise(Array("value").length);',
|
|
496
|
+
'console.log(Object.catch("value"));',
|
|
497
|
+
"export const markers = [tokens.undefined, tokens.arguments];",
|
|
498
|
+
].join("\n"),
|
|
499
|
+
"types.d.ts": "",
|
|
500
|
+
};
|
|
501
|
+
const { root, read } = await fixture(files);
|
|
502
|
+
const result = await run(root, read("use.ts"));
|
|
503
|
+
for (const name of [
|
|
504
|
+
"Array",
|
|
505
|
+
"createPromise",
|
|
506
|
+
"console",
|
|
507
|
+
"Object",
|
|
508
|
+
"log",
|
|
509
|
+
"catch",
|
|
510
|
+
"tokens",
|
|
511
|
+
"undefined",
|
|
512
|
+
"arguments",
|
|
513
|
+
]) {
|
|
514
|
+
expect(result.symbols![name]).toEqual(expect.any(String));
|
|
515
|
+
expect(result.references).toHaveProperty([`${name}:lib.ts`]);
|
|
516
|
+
}
|
|
517
|
+
expect(result.symbols!.Array).toContain("Project array builder.");
|
|
518
|
+
expect(result.symbols!.createPromise).toContain("Project promise maker.");
|
|
519
|
+
expect(result.references!["Array:lib.ts"]).toBe(declarations.Array);
|
|
520
|
+
expect(result.references!["createPromise:lib.ts"]).toBe(declarations.Promise);
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
it("keeps declaration references and filename casing under case-insensitive native paths", async () => {
|
|
524
|
+
const declaration = [
|
|
525
|
+
"export function twice(value: number): number {",
|
|
526
|
+
"\treturn value * 2;",
|
|
527
|
+
"}",
|
|
528
|
+
].join("\n");
|
|
529
|
+
const files = {
|
|
530
|
+
"MixedCaseRoot/Library.ts": [
|
|
531
|
+
"export const outsideBefore = 0;",
|
|
532
|
+
"/** Double a number. */",
|
|
533
|
+
declaration,
|
|
534
|
+
"export const outsideAfter = 0;",
|
|
535
|
+
].join("\n"),
|
|
536
|
+
"MixedCaseRoot/caller.ts":
|
|
537
|
+
'import { twice as scale } from "./Library";\nexport const result = scale(2);',
|
|
538
|
+
"MixedCaseRoot/tsconfig.json": JSON.stringify({
|
|
539
|
+
compilerOptions: {
|
|
540
|
+
strict: true,
|
|
541
|
+
types: [],
|
|
542
|
+
module: "Preserve",
|
|
543
|
+
moduleResolution: "Bundler",
|
|
544
|
+
},
|
|
545
|
+
files: ["Library.ts", "caller.ts"],
|
|
546
|
+
}),
|
|
547
|
+
};
|
|
548
|
+
const { root: temporary, read } = await fixture(files, false);
|
|
549
|
+
const root = join(temporary, "MixedCaseRoot");
|
|
550
|
+
const api = new API({ cwd: root, useCaseSensitiveFileNames: false });
|
|
551
|
+
// The standalone SDK exposes the native host's case policy; adapt only the connection boundary.
|
|
552
|
+
vi.spyOn(NativeLsp, "open").mockResolvedValue({
|
|
553
|
+
api,
|
|
554
|
+
openDocument: () => Promise.resolve(),
|
|
555
|
+
closeDocument: () => Promise.resolve(),
|
|
556
|
+
snapshot: () => api.createSnapshot({ openFiles: [join(root, "caller.ts")] }),
|
|
557
|
+
sdk: <A>(_phase: string, work: () => Promise<A>) => work(),
|
|
558
|
+
close: () => api.close(),
|
|
559
|
+
} as unknown as NativeLsp);
|
|
560
|
+
try {
|
|
561
|
+
const result = await run(root, read("MixedCaseRoot/caller.ts"));
|
|
562
|
+
expect(api.getCanonicalFileName(root)).not.toBe(root);
|
|
563
|
+
expect(result.symbols!.scale).toContain("(value: number) => number");
|
|
564
|
+
expect(result.symbols!.scale).toContain("Double a number.");
|
|
565
|
+
expect(result.references).toEqual({ "scale:Library.ts": declaration });
|
|
566
|
+
} finally {
|
|
567
|
+
await api.close();
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
it("returns complete declarations, separates distinct scopes and omits unused/self/external targets", async () => {
|
|
572
|
+
const { root, read } = await fixture();
|
|
573
|
+
const result = await run(root, read("use.ts"));
|
|
574
|
+
const refs = result.references!;
|
|
575
|
+
expect(refs["local:lib.ts"]).toBe("export const original = 42;");
|
|
576
|
+
expect(refs["local:lib.ts"]).not.toContain("neighbor");
|
|
577
|
+
expect(refs["Shape:lib.ts"]).toBe(
|
|
578
|
+
"export interface Shape { first: number }\n…\nexport interface Shape { second: string }",
|
|
579
|
+
);
|
|
580
|
+
expect(refs["pair:lib.ts"]).toContain("\n…\n");
|
|
581
|
+
expect(refs["pair:lib.ts"]).toContain("export function pair(value: number): number;");
|
|
582
|
+
expect(refs["pair:lib.ts"]).toContain("export function pair(value: string): string;");
|
|
583
|
+
expect(Object.keys(refs).sort()).toEqual(["Shape:lib.ts", "local:lib.ts", "pair:lib.ts"]);
|
|
584
|
+
expect(Object.values(refs).every((text) => typeof text === "string")).toBe(true);
|
|
585
|
+
expect(JSON.stringify(refs)).not.toMatch(/ordinary comment|never send this directive/);
|
|
586
|
+
expect(refs["local:lib.ts"]).not.toContain("@adhere");
|
|
587
|
+
expect(JSON.stringify(refs)).not.toContain(library);
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
it("includes full function and initializer bodies, complete types, and only the referenced class members", async () => {
|
|
591
|
+
const calculate = [
|
|
592
|
+
"export function calculate(value: number): number {",
|
|
593
|
+
"\tconst doubled = value * 2;",
|
|
594
|
+
"\tconst increased = doubled + 1;",
|
|
595
|
+
"\tconst rounded = Math.round(increased);",
|
|
596
|
+
"\tconst bounded = Math.max(0, rounded);",
|
|
597
|
+
"\tconst final = bounded + 2;",
|
|
598
|
+
"\treturn final;",
|
|
599
|
+
"}",
|
|
600
|
+
].join("\n");
|
|
601
|
+
const transform = [
|
|
602
|
+
"export const transform = (value: number): number => {",
|
|
603
|
+
" const first = value + 1;",
|
|
604
|
+
" const second = first + 1;",
|
|
605
|
+
" const third = second + 1;",
|
|
606
|
+
" const fourth = third + 1;",
|
|
607
|
+
" return fourth;",
|
|
608
|
+
"};",
|
|
609
|
+
].join("\n");
|
|
610
|
+
const settings = [
|
|
611
|
+
"export const settings = {",
|
|
612
|
+
" first: 1,",
|
|
613
|
+
" second: 2,",
|
|
614
|
+
" third: 3,",
|
|
615
|
+
" fourth: 4,",
|
|
616
|
+
" fifth: 5,",
|
|
617
|
+
" sixth: 6,",
|
|
618
|
+
"};",
|
|
619
|
+
].join("\n");
|
|
620
|
+
const method = [
|
|
621
|
+
" run(value: number): number {",
|
|
622
|
+
" const first = value + 1;",
|
|
623
|
+
" const second = first + 1;",
|
|
624
|
+
" const third = second + 1;",
|
|
625
|
+
" const fourth = third + 1;",
|
|
626
|
+
" return fourth;",
|
|
627
|
+
" }",
|
|
628
|
+
].join("\n");
|
|
629
|
+
const accessors = [
|
|
630
|
+
" get current(): number {",
|
|
631
|
+
" return this.stored;",
|
|
632
|
+
" }",
|
|
633
|
+
" set current(value: number) {",
|
|
634
|
+
" this.stored = value;",
|
|
635
|
+
" }",
|
|
636
|
+
].join("\n");
|
|
637
|
+
const worker = [
|
|
638
|
+
"export class Worker {",
|
|
639
|
+
" private stored = 0;",
|
|
640
|
+
method,
|
|
641
|
+
accessors,
|
|
642
|
+
" unrelated() { return -1; }",
|
|
643
|
+
"}",
|
|
644
|
+
].join("\n");
|
|
645
|
+
const shape = [
|
|
646
|
+
"export interface Shape {",
|
|
647
|
+
" first: number;",
|
|
648
|
+
" second: number;",
|
|
649
|
+
" third: number;",
|
|
650
|
+
" fourth: number;",
|
|
651
|
+
" fifth: number;",
|
|
652
|
+
" sixth: number;",
|
|
653
|
+
"}",
|
|
654
|
+
].join("\n");
|
|
655
|
+
const alias = shape.replace("interface Shape", "type Alias =") + ";";
|
|
656
|
+
const destructured = "export const { first, second } = settings;";
|
|
657
|
+
const declarations = {
|
|
658
|
+
calculate,
|
|
659
|
+
transform,
|
|
660
|
+
settings,
|
|
661
|
+
Worker: worker,
|
|
662
|
+
Shape: shape,
|
|
663
|
+
Alias: alias,
|
|
664
|
+
};
|
|
665
|
+
const files = {
|
|
666
|
+
"lib.ts": [
|
|
667
|
+
"export const outsideBefore = 0;",
|
|
668
|
+
...Object.values(declarations),
|
|
669
|
+
destructured,
|
|
670
|
+
"export const outsideAfter = 0;",
|
|
671
|
+
].join("\n\n"),
|
|
672
|
+
"use.ts": [
|
|
673
|
+
'import { calculate, transform, settings, Worker, Shape, Alias, first } from "./lib";',
|
|
674
|
+
"export const worker = new Worker();",
|
|
675
|
+
"worker.current = calculate(transform(settings.first));",
|
|
676
|
+
"export const result = worker.run(worker.current) + first;",
|
|
677
|
+
"export type SelectedShape = Shape;",
|
|
678
|
+
"export type SelectedAlias = Alias;",
|
|
679
|
+
].join("\n"),
|
|
680
|
+
"types.d.ts": "",
|
|
681
|
+
};
|
|
682
|
+
const { root, read } = await fixture(files);
|
|
683
|
+
const result = await run(root, read("use.ts", { ...both, symbols: false }));
|
|
684
|
+
for (const [name, text] of Object.entries(declarations)) {
|
|
685
|
+
expect(result.references![`${name}:lib.ts`]).toBe(text);
|
|
686
|
+
}
|
|
687
|
+
expect(result.references!["run:lib.ts"]).toBe(method);
|
|
688
|
+
expect(result.references!["current:lib.ts"]).toBe(accessors);
|
|
689
|
+
expect(result.references!["first:lib.ts"]).toBe(` first: 1,\n…\n${destructured}`);
|
|
690
|
+
expect(JSON.stringify(result.references)).not.toMatch(/outsideBefore|outsideAfter/);
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
it("respects selectors and source comment/directive policy while always preserving compiler JSDoc", async () => {
|
|
694
|
+
const { root, read, prepare } = await fixture();
|
|
695
|
+
const result = await run(
|
|
696
|
+
root,
|
|
697
|
+
Effect.gen(function* () {
|
|
698
|
+
yield* prepare(["use.ts"]);
|
|
699
|
+
return {
|
|
700
|
+
symbols: yield* read("use.ts", { ...both, references: false }),
|
|
701
|
+
references: yield* read("use.ts", { ...both, symbols: false, includeComments: true }),
|
|
702
|
+
};
|
|
703
|
+
}),
|
|
704
|
+
);
|
|
705
|
+
expect(Object.keys(result.symbols)).toEqual(["symbols"]);
|
|
706
|
+
expect(result.symbols.symbols!.local).toContain("The original documented value.");
|
|
707
|
+
expect(Object.keys(result.references)).toEqual(["references"]);
|
|
708
|
+
expect(result.references.references!["local:lib.ts"]).toBe(
|
|
709
|
+
normalized(library, true).slice(1, 6).join("\n"),
|
|
710
|
+
);
|
|
711
|
+
expect(result.references.references!["local:lib.ts"]).toContain(
|
|
712
|
+
"/** The original documented value. */",
|
|
713
|
+
);
|
|
714
|
+
expect(result.references.references!["local:lib.ts"]).not.toContain("adhere-ignore");
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
it("serializes concurrent same-file and child-project reads while isolating aliases and inferred ownership", async () => {
|
|
718
|
+
const child = (target: string) =>
|
|
719
|
+
JSON.stringify({
|
|
720
|
+
compilerOptions: {
|
|
721
|
+
strict: true,
|
|
722
|
+
types: [],
|
|
723
|
+
module: "Preserve",
|
|
724
|
+
moduleResolution: "Bundler",
|
|
725
|
+
paths: { "#value": [target] },
|
|
726
|
+
},
|
|
727
|
+
include: ["src/**/*.ts"],
|
|
728
|
+
});
|
|
729
|
+
const files = {
|
|
730
|
+
"tsconfig.json": JSON.stringify({
|
|
731
|
+
files: [],
|
|
732
|
+
references: [{ path: "./packages/a" }, { path: "./packages/b" }],
|
|
733
|
+
}),
|
|
734
|
+
"packages/a/tsconfig.json": child("./src/value.ts"),
|
|
735
|
+
"packages/b/tsconfig.json": child("./alt/value.ts"),
|
|
736
|
+
"packages/a/src/value.ts": "/** First project. */\nexport const value = 1;",
|
|
737
|
+
"packages/b/alt/value.ts": "/** Second project. */\nexport const value = 2;",
|
|
738
|
+
"packages/a/src/use.ts":
|
|
739
|
+
'import { value as local } from "#value";\nexport const result = local;',
|
|
740
|
+
"packages/b/src/use.ts":
|
|
741
|
+
'import { value as local } from "#value";\nexport const result = local;',
|
|
742
|
+
"packages/a/excluded.ts": "export const outside = 3;\nexport const use = outside;",
|
|
743
|
+
};
|
|
744
|
+
const { root, read, prepare } = await fixture(files, false);
|
|
745
|
+
const update = vi.spyOn(API.prototype, "getCurrentLanguageServerSnapshot");
|
|
746
|
+
const openDocument = NativeLsp.prototype.openDocument;
|
|
747
|
+
const closeDocument = NativeLsp.prototype.closeDocument;
|
|
748
|
+
const opening = vi.spyOn(NativeLsp.prototype, "openDocument");
|
|
749
|
+
const closing = vi.spyOn(NativeLsp.prototype, "closeDocument");
|
|
750
|
+
let active = 0;
|
|
751
|
+
let peak = 0;
|
|
752
|
+
opening.mockImplementation(async function (this: NativeLsp, file, text) {
|
|
753
|
+
active += 1;
|
|
754
|
+
peak = Math.max(peak, active);
|
|
755
|
+
await openDocument.call(this, file, text);
|
|
756
|
+
});
|
|
757
|
+
closing.mockImplementation(async function (this: NativeLsp, file) {
|
|
758
|
+
await closeDocument.call(this, file);
|
|
759
|
+
active -= 1;
|
|
760
|
+
});
|
|
761
|
+
const names = ["packages/a/src/use.ts", "packages/b/src/use.ts", "packages/a/excluded.ts"];
|
|
762
|
+
const result = await run(
|
|
763
|
+
root,
|
|
764
|
+
prepare(names).pipe(
|
|
765
|
+
Effect.andThen(
|
|
766
|
+
Effect.all(
|
|
767
|
+
[...names, names[0]!].map((name) => read(name)),
|
|
768
|
+
{ concurrency: 8 },
|
|
769
|
+
),
|
|
770
|
+
),
|
|
771
|
+
),
|
|
772
|
+
);
|
|
773
|
+
expect(result[0]!.symbols!.local).toContain("First project.");
|
|
774
|
+
expect(result[1]!.symbols!.local).toContain("Second project.");
|
|
775
|
+
expect(result[0]!.references!["local:packages/a/src/value.ts"]).toBe("export const value = 1;");
|
|
776
|
+
expect(result[1]!.references!["local:packages/b/alt/value.ts"]).toBe("export const value = 2;");
|
|
777
|
+
expect(result[2]!.symbols!.outside).toContain("```typescript\n3\n```");
|
|
778
|
+
expect(result[2]!.references).toEqual({});
|
|
779
|
+
expect(result[3]).toEqual(result[0]);
|
|
780
|
+
expect(peak).toBe(1);
|
|
781
|
+
expect(active).toBe(0);
|
|
782
|
+
expect(update.mock.calls).toEqual([[], [], [], []]);
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
it("supports declarations and compiler types/docs without any tsconfig", async () => {
|
|
786
|
+
const files = {
|
|
787
|
+
"lib.ts": "/** Inferred docs. */\nexport const value = 1;",
|
|
788
|
+
"use.ts": 'import { value as local } from "./lib";\nexport const used = local;',
|
|
789
|
+
};
|
|
790
|
+
const { root, read, prepare } = await fixture(files, false);
|
|
791
|
+
const result = await run(
|
|
792
|
+
root,
|
|
793
|
+
prepare(["lib.ts", "use.ts"]).pipe(Effect.andThen(read("use.ts"))),
|
|
794
|
+
);
|
|
795
|
+
expect(result.symbols!.local).toContain("Inferred docs.");
|
|
796
|
+
expect(result.references!["local:lib.ts"]).toBe("export const value = 1;");
|
|
797
|
+
expect(Object.keys(result).sort()).toEqual(["references", "symbols"]);
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
it("follows used namespace members with UTF-16 positions and excludes declarations outside the audit root", async () => {
|
|
801
|
+
const files = {
|
|
802
|
+
"outside.ts": "/** Outside documentation. */\nexport const outside = 1;",
|
|
803
|
+
"workspace/tsconfig.json": JSON.stringify({
|
|
804
|
+
compilerOptions: {
|
|
805
|
+
strict: true,
|
|
806
|
+
types: [],
|
|
807
|
+
module: "Preserve",
|
|
808
|
+
moduleResolution: "Bundler",
|
|
809
|
+
},
|
|
810
|
+
files: ["use.ts", "lib.ts"],
|
|
811
|
+
}),
|
|
812
|
+
"workspace/lib.ts": "/** Unicode documentation 😀. */\nexport const original = 42;",
|
|
813
|
+
"workspace/use.ts": [
|
|
814
|
+
'import * as api from "./lib";',
|
|
815
|
+
'import { outside } from "../outside";',
|
|
816
|
+
'export const emoji = "😀"; export const used = api.original;',
|
|
817
|
+
"export const external = outside;",
|
|
818
|
+
].join("\n"),
|
|
819
|
+
};
|
|
820
|
+
const { root, read } = await fixture(files, false);
|
|
821
|
+
const result = await run(join(root, "workspace"), read("workspace/use.ts"));
|
|
822
|
+
expect(result.symbols!.original).toContain("Unicode documentation 😀.");
|
|
823
|
+
expect(result.symbols!.outside).toContain("Outside documentation.");
|
|
824
|
+
expect(result.references!["original:lib.ts"]).toBe("export const original = 42;");
|
|
825
|
+
expect(Object.keys(result.references!).some((key) => key.startsWith("outside:"))).toBe(false);
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
it("pins each read to its supplied text despite disk changes and accepts new text in a later read", async () => {
|
|
829
|
+
const { root, read, prepare } = await fixture();
|
|
830
|
+
const openDocument = NativeLsp.prototype.openDocument;
|
|
831
|
+
const opening = vi.spyOn(NativeLsp.prototype, "openDocument");
|
|
832
|
+
opening.mockImplementationOnce(async function (this: NativeLsp, file, text) {
|
|
833
|
+
await openDocument.call(this, file, text);
|
|
834
|
+
await writeFile(file, consumer.replace("local: string", "local: number"));
|
|
835
|
+
});
|
|
836
|
+
const result = await run(
|
|
837
|
+
root,
|
|
838
|
+
Effect.gen(function* () {
|
|
839
|
+
yield* prepare(["use.ts"]);
|
|
840
|
+
const original = yield* read("use.ts");
|
|
841
|
+
const reads = yield* SourceReads;
|
|
842
|
+
const changed = yield* Effect.promise(() => readFile(join(root, "use.ts"), "utf8"));
|
|
843
|
+
const updated = yield* reads.read(
|
|
844
|
+
{ path: join(root, "use.ts"), contents: changed },
|
|
845
|
+
normalized(changed),
|
|
846
|
+
both,
|
|
847
|
+
);
|
|
848
|
+
return { original, updated: updated as SourceReadState };
|
|
849
|
+
}),
|
|
850
|
+
);
|
|
851
|
+
expect(result.original.symbols!.local).toContain("```typescript\nstring\n```");
|
|
852
|
+
expect(result.updated.symbols!.local).toContain("```typescript\nnumber\n```");
|
|
853
|
+
expect(result.updated.symbols!.local).not.toContain("```typescript\nstring\n```");
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
it("refuses a native snapshot whose source differs from the supplied judged text", async () => {
|
|
857
|
+
const { root, read } = await fixture();
|
|
858
|
+
const getSourceFile = Program.prototype.getSourceFile;
|
|
859
|
+
const gettingSource = vi.spyOn(Program.prototype, "getSourceFile");
|
|
860
|
+
gettingSource.mockImplementationOnce(async function (this: Program, file) {
|
|
861
|
+
const source = await getSourceFile.call(this, file);
|
|
862
|
+
if (source) Object.defineProperty(source, "text", { value: source.text + "\n// mismatch" });
|
|
863
|
+
return source;
|
|
864
|
+
});
|
|
865
|
+
const failure = await run(root, read("use.ts").pipe(Effect.flip));
|
|
866
|
+
expect(failure.message).toContain(
|
|
867
|
+
"changed between the source walk and native TypeScript snapshot",
|
|
868
|
+
);
|
|
869
|
+
expect(failure.message).not.toMatch(/reinstall/i);
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
it("stays lazy, bootstraps one direct file, and closes its only compiler and attached SDK", async () => {
|
|
873
|
+
const { root, read, prepare } = await fixture();
|
|
874
|
+
const open = vi.spyOn(NativeLsp, "open");
|
|
875
|
+
const attach = vi.spyOn(API, "fromLSPConnection");
|
|
876
|
+
const close = vi.spyOn(API.prototype, "close");
|
|
877
|
+
await run(
|
|
878
|
+
root,
|
|
879
|
+
prepare([]).pipe(
|
|
880
|
+
Effect.andThen(read("use.ts", { ...both, symbols: false, references: false })),
|
|
881
|
+
Effect.tap((state) =>
|
|
882
|
+
Effect.sync(() => {
|
|
883
|
+
expect(state).toEqual({});
|
|
884
|
+
expect(open).not.toHaveBeenCalled();
|
|
885
|
+
}),
|
|
886
|
+
),
|
|
887
|
+
Effect.andThen(Effect.all([read("use.ts"), read("use.ts")], { concurrency: 8 })),
|
|
888
|
+
),
|
|
889
|
+
);
|
|
890
|
+
expect(open).toHaveBeenCalledOnce();
|
|
891
|
+
expect(attach).toHaveBeenCalledOnce();
|
|
892
|
+
expect(close).toHaveBeenCalledOnce();
|
|
893
|
+
const connection = (await open.mock.results[0]!.value) as NativeLsp;
|
|
894
|
+
expect(() => process.kill(connection.pid!, 0)).toThrow();
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
it("closes both resources after a compiler documentation failure, without reinstall advice", async () => {
|
|
898
|
+
const { root, read, prepare } = await fixture();
|
|
899
|
+
const open = vi.spyOn(NativeLsp, "open");
|
|
900
|
+
const close = vi.spyOn(API.prototype, "close");
|
|
901
|
+
const documenting = vi.spyOn(Checker.prototype, "getDocumentationCommentOfSymbol");
|
|
902
|
+
const failure = await run(
|
|
903
|
+
root,
|
|
904
|
+
Effect.gen(function* () {
|
|
905
|
+
yield* prepare(["use.ts"]);
|
|
906
|
+
documenting.mockRejectedValueOnce(new Error("native documentation failed"));
|
|
907
|
+
return yield* read("use.ts").pipe(Effect.flip);
|
|
908
|
+
}),
|
|
909
|
+
);
|
|
910
|
+
expect(failure.message).toContain("native documentation failed");
|
|
911
|
+
expect(failure.message).not.toMatch(/reinstall/i);
|
|
912
|
+
expect(close).toHaveBeenCalledOnce();
|
|
913
|
+
const connection = (await open.mock.results[0]!.value) as NativeLsp;
|
|
914
|
+
expect(() => process.kill(connection.pid!, 0)).toThrow();
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
it.each(["project lookup", "checker lookup"])(
|
|
918
|
+
"refuses a pending SDK %s when its compiler exits, including late rejection cleanup",
|
|
919
|
+
async (phase) => {
|
|
920
|
+
const { root, read, prepare } = await fixture();
|
|
921
|
+
const open = vi.spyOn(NativeLsp, "open");
|
|
922
|
+
const entered = Promise.withResolvers<void>();
|
|
923
|
+
const pending = Promise.withResolvers<never>();
|
|
924
|
+
const outcome = run(
|
|
925
|
+
root,
|
|
926
|
+
Effect.gen(function* () {
|
|
927
|
+
yield* prepare(["use.ts"]);
|
|
928
|
+
const blocked = () => {
|
|
929
|
+
entered.resolve();
|
|
930
|
+
return pending.promise;
|
|
931
|
+
};
|
|
932
|
+
if (phase === "project lookup")
|
|
933
|
+
vi.spyOn(Snapshot.prototype, "getDefaultProjectForFile").mockImplementationOnce(
|
|
934
|
+
blocked,
|
|
935
|
+
);
|
|
936
|
+
else vi.spyOn(Checker.prototype, "getSymbolAtLocation").mockImplementationOnce(blocked);
|
|
937
|
+
return yield* read("use.ts").pipe(Effect.flip);
|
|
938
|
+
}),
|
|
939
|
+
);
|
|
940
|
+
await entered.promise;
|
|
941
|
+
const connection = (await open.mock.results[0]!.value) as NativeLsp;
|
|
942
|
+
process.kill(connection.pid!, "SIGKILL");
|
|
943
|
+
try {
|
|
944
|
+
const failure = await within(outcome, 3000);
|
|
945
|
+
expect(failure.message).toMatch(/Native TypeScript LSP (closed|exited)/);
|
|
946
|
+
expect(failure.message).not.toMatch(/reinstall/i);
|
|
947
|
+
expect(() => process.kill(connection.pid!, 0)).toThrow();
|
|
948
|
+
} finally {
|
|
949
|
+
pending.reject(new Error("late SDK rejection"));
|
|
950
|
+
await outcome.catch(() => {});
|
|
951
|
+
}
|
|
952
|
+
},
|
|
953
|
+
);
|
|
954
|
+
|
|
955
|
+
it("cancels a queued read without opening its document or interrupting the native owner", async () => {
|
|
956
|
+
const { root, read, prepare } = await fixture();
|
|
957
|
+
const entered = Promise.withResolvers<void>();
|
|
958
|
+
const release = Promise.withResolvers<void>();
|
|
959
|
+
const getSymbol = Checker.prototype.getSymbolAtLocation;
|
|
960
|
+
const gettingSymbol = vi.spyOn(Checker.prototype, "getSymbolAtLocation");
|
|
961
|
+
const openDocument = vi.spyOn(NativeLsp.prototype, "openDocument");
|
|
962
|
+
gettingSymbol.mockImplementationOnce(async function (this: Checker, nodes) {
|
|
963
|
+
entered.resolve();
|
|
964
|
+
await release.promise;
|
|
965
|
+
return getSymbol.call(this, nodes);
|
|
966
|
+
});
|
|
967
|
+
try {
|
|
968
|
+
const result = await run(
|
|
969
|
+
root,
|
|
970
|
+
Effect.gen(function* () {
|
|
971
|
+
yield* prepare(["use.ts"]);
|
|
972
|
+
const reads = yield* SourceReads;
|
|
973
|
+
return yield* Effect.all(
|
|
974
|
+
[
|
|
975
|
+
read("use.ts"),
|
|
976
|
+
Effect.promise(async () => {
|
|
977
|
+
await entered.promise;
|
|
978
|
+
const queued = Effect.runFork(
|
|
979
|
+
reads.read(
|
|
980
|
+
{ path: join(root, "use.ts"), contents: consumer },
|
|
981
|
+
normalized(consumer),
|
|
982
|
+
both,
|
|
983
|
+
),
|
|
984
|
+
);
|
|
985
|
+
try {
|
|
986
|
+
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
987
|
+
await within(Effect.runPromise(Fiber.interrupt(queued)), 1000);
|
|
988
|
+
expect(openDocument).toHaveBeenCalledOnce();
|
|
989
|
+
} finally {
|
|
990
|
+
release.resolve();
|
|
991
|
+
await Effect.runPromise(Fiber.interrupt(queued));
|
|
992
|
+
}
|
|
993
|
+
}),
|
|
994
|
+
],
|
|
995
|
+
{ concurrency: 8 },
|
|
996
|
+
);
|
|
997
|
+
}),
|
|
998
|
+
);
|
|
999
|
+
expect(result[0].symbols!.local).toContain("The original documented value.");
|
|
1000
|
+
expect(openDocument).toHaveBeenCalledOnce();
|
|
1001
|
+
} finally {
|
|
1002
|
+
release.resolve();
|
|
1003
|
+
}
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
it("finishes a healthy interrupted owner and reuses its layer and compiler for the next read", async () => {
|
|
1007
|
+
const { root, read, prepare } = await fixture();
|
|
1008
|
+
const entered = Promise.withResolvers<void>();
|
|
1009
|
+
const release = Promise.withResolvers<void>();
|
|
1010
|
+
const open = vi.spyOn(NativeLsp, "open");
|
|
1011
|
+
const closeDocument = vi.spyOn(NativeLsp.prototype, "closeDocument");
|
|
1012
|
+
const dispose = vi.spyOn(Snapshot.prototype, "dispose");
|
|
1013
|
+
const close = vi.spyOn(API.prototype, "close");
|
|
1014
|
+
const typeToString = Checker.prototype.typeToString;
|
|
1015
|
+
vi.spyOn(Checker.prototype, "typeToString").mockImplementationOnce(async function (
|
|
1016
|
+
this: Checker,
|
|
1017
|
+
...args
|
|
1018
|
+
) {
|
|
1019
|
+
entered.resolve();
|
|
1020
|
+
await release.promise;
|
|
1021
|
+
return typeToString.apply(this, args);
|
|
1022
|
+
});
|
|
1023
|
+
const connection = await run(
|
|
1024
|
+
root,
|
|
1025
|
+
Effect.gen(function* () {
|
|
1026
|
+
yield* prepare(["use.ts"]);
|
|
1027
|
+
const reads = yield* SourceReads;
|
|
1028
|
+
const reading = read("use.ts").pipe(Effect.provideService(SourceReads, reads));
|
|
1029
|
+
const owner = Effect.runFork(reading);
|
|
1030
|
+
try {
|
|
1031
|
+
yield* Effect.promise(() => within(entered.promise, 3000));
|
|
1032
|
+
let interrupted = false;
|
|
1033
|
+
const cancellation = Effect.runPromise(Fiber.interrupt(owner)).then(() => {
|
|
1034
|
+
interrupted = true;
|
|
1035
|
+
});
|
|
1036
|
+
yield* Effect.promise(() => new Promise<void>((resolve) => setImmediate(resolve)));
|
|
1037
|
+
expect(interrupted).toBe(false);
|
|
1038
|
+
expect(closeDocument).not.toHaveBeenCalled();
|
|
1039
|
+
expect(dispose).not.toHaveBeenCalled();
|
|
1040
|
+
release.resolve();
|
|
1041
|
+
yield* Effect.promise(() => within(cancellation, 3000));
|
|
1042
|
+
expect(interrupted).toBe(true);
|
|
1043
|
+
expect(closeDocument).toHaveBeenCalledExactlyOnceWith(join(root, "use.ts"));
|
|
1044
|
+
expect(dispose).toHaveBeenCalledOnce();
|
|
1045
|
+
expect(close).not.toHaveBeenCalled();
|
|
1046
|
+
const native = (yield* Effect.promise(() => open.mock.results[0]!.value)) as NativeLsp;
|
|
1047
|
+
expect(() => process.kill(native.pid!, 0)).not.toThrow();
|
|
1048
|
+
const state = yield* Effect.promise(() => within(Effect.runPromise(reading), 3000));
|
|
1049
|
+
expect(state.symbols!.local).toContain("```typescript\n42\n```");
|
|
1050
|
+
expect(state.symbols!.local).toContain("The original documented value.");
|
|
1051
|
+
expect(state.references!["local:lib.ts"]).toBe("export const original = 42;");
|
|
1052
|
+
expect(closeDocument).toHaveBeenCalledTimes(2);
|
|
1053
|
+
expect(dispose).toHaveBeenCalledTimes(2);
|
|
1054
|
+
expect(close).not.toHaveBeenCalled();
|
|
1055
|
+
expect(open).toHaveBeenCalledOnce();
|
|
1056
|
+
expect(() => process.kill(native.pid!, 0)).not.toThrow();
|
|
1057
|
+
return native;
|
|
1058
|
+
} finally {
|
|
1059
|
+
release.resolve();
|
|
1060
|
+
yield* Fiber.interrupt(owner);
|
|
1061
|
+
}
|
|
1062
|
+
}),
|
|
1063
|
+
);
|
|
1064
|
+
expect(close).toHaveBeenCalledOnce();
|
|
1065
|
+
expect(() => process.kill(connection.pid!, 0)).toThrow();
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
it("bounds cancellation during a stalled SDK call by its phase deadline and reaps its compiler", async () => {
|
|
1069
|
+
const { root, read, prepare } = await fixture();
|
|
1070
|
+
const open = vi.spyOn(NativeLsp, "open");
|
|
1071
|
+
const entered = Promise.withResolvers<void>();
|
|
1072
|
+
const pending = Promise.withResolvers<never>();
|
|
1073
|
+
const fiber = Effect.runFork(
|
|
1074
|
+
Effect.gen(function* () {
|
|
1075
|
+
yield* prepare(["use.ts"]);
|
|
1076
|
+
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] });
|
|
1077
|
+
vi.spyOn(Checker.prototype, "getSymbolAtLocation").mockImplementationOnce(() => {
|
|
1078
|
+
entered.resolve();
|
|
1079
|
+
return pending.promise;
|
|
1080
|
+
});
|
|
1081
|
+
return yield* read("use.ts");
|
|
1082
|
+
}).pipe(Effect.provide(SourceReadsLive(root).pipe(Layer.provide(Path.layer)))),
|
|
1083
|
+
);
|
|
1084
|
+
await entered.promise;
|
|
1085
|
+
const connection = (await open.mock.results[0]!.value) as NativeLsp;
|
|
1086
|
+
let interrupted = false;
|
|
1087
|
+
const cancellation = Effect.runPromise(Fiber.interrupt(fiber)).then(() => {
|
|
1088
|
+
interrupted = true;
|
|
1089
|
+
});
|
|
1090
|
+
try {
|
|
1091
|
+
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
1092
|
+
expect(interrupted).toBe(false);
|
|
1093
|
+
await vi.advanceTimersByTimeAsync(30_000);
|
|
1094
|
+
await vi.advanceTimersByTimeAsync(2000);
|
|
1095
|
+
vi.useRealTimers();
|
|
1096
|
+
await within(cancellation, 3000);
|
|
1097
|
+
expect(interrupted).toBe(true);
|
|
1098
|
+
expect(() => process.kill(connection.pid!, 0)).toThrow();
|
|
1099
|
+
} finally {
|
|
1100
|
+
vi.useRealTimers();
|
|
1101
|
+
pending.reject(new Error("late SDK rejection after cancellation"));
|
|
1102
|
+
await cancellation;
|
|
1103
|
+
}
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1106
|
+
it("bounds a stalled snapshot release after a documentation failure and still reaps its compiler", async () => {
|
|
1107
|
+
const { root, read, prepare } = await fixture();
|
|
1108
|
+
const open = vi.spyOn(NativeLsp, "open");
|
|
1109
|
+
const close = vi.spyOn(API.prototype, "close");
|
|
1110
|
+
const entered = Promise.withResolvers<void>();
|
|
1111
|
+
const release = Promise.withResolvers<void>();
|
|
1112
|
+
const lateDispose = Promise.withResolvers<void>();
|
|
1113
|
+
const dispose = Snapshot.prototype.dispose;
|
|
1114
|
+
const disposing = vi.spyOn(Snapshot.prototype, "dispose");
|
|
1115
|
+
const documenting = vi.spyOn(Checker.prototype, "getDocumentationCommentOfSymbol");
|
|
1116
|
+
disposing.mockImplementationOnce(async function (this: Snapshot) {
|
|
1117
|
+
entered.resolve();
|
|
1118
|
+
try {
|
|
1119
|
+
await release.promise;
|
|
1120
|
+
await dispose.call(this);
|
|
1121
|
+
} finally {
|
|
1122
|
+
lateDispose.resolve();
|
|
1123
|
+
}
|
|
1124
|
+
});
|
|
1125
|
+
const outcome = run(
|
|
1126
|
+
root,
|
|
1127
|
+
Effect.gen(function* () {
|
|
1128
|
+
yield* prepare(["use.ts"]);
|
|
1129
|
+
documenting.mockRejectedValueOnce(new Error("native documentation failed"));
|
|
1130
|
+
return yield* read("use.ts").pipe(Effect.flip);
|
|
1131
|
+
}),
|
|
1132
|
+
);
|
|
1133
|
+
await entered.promise;
|
|
1134
|
+
const connection = (await open.mock.results[0]!.value) as NativeLsp;
|
|
1135
|
+
try {
|
|
1136
|
+
const failure = await within(outcome, 3500);
|
|
1137
|
+
expect(failure.message).toContain("native documentation failed");
|
|
1138
|
+
expect(close).toHaveBeenCalledOnce();
|
|
1139
|
+
expect(() => process.kill(connection.pid!, 0)).toThrow();
|
|
1140
|
+
} finally {
|
|
1141
|
+
release.resolve();
|
|
1142
|
+
await lateDispose.promise;
|
|
1143
|
+
await outcome.catch(() => {});
|
|
1144
|
+
}
|
|
1145
|
+
});
|
|
1146
|
+
|
|
1147
|
+
it("refuses a successful query if its per-read snapshot release stalls past the SDK deadline", async () => {
|
|
1148
|
+
const { root, read } = await fixture();
|
|
1149
|
+
const open = vi.spyOn(NativeLsp, "open");
|
|
1150
|
+
const close = vi.spyOn(API.prototype, "close");
|
|
1151
|
+
const entered = Promise.withResolvers<void>();
|
|
1152
|
+
const release = Promise.withResolvers<void>();
|
|
1153
|
+
// The SDK memoizes disposal: the read and API.close must await the same stalled release.
|
|
1154
|
+
vi.spyOn(Snapshot.prototype, "dispose").mockImplementation(() => {
|
|
1155
|
+
entered.resolve();
|
|
1156
|
+
return release.promise;
|
|
1157
|
+
});
|
|
1158
|
+
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] });
|
|
1159
|
+
const outcome = run(root, read("use.ts").pipe(Effect.flip));
|
|
1160
|
+
try {
|
|
1161
|
+
await entered.promise;
|
|
1162
|
+
const connection = (await open.mock.results[0]!.value) as NativeLsp;
|
|
1163
|
+
await vi.advanceTimersByTimeAsync(30_000);
|
|
1164
|
+
await vi.advanceTimersByTimeAsync(2000);
|
|
1165
|
+
vi.useRealTimers();
|
|
1166
|
+
const failure = await within(outcome, 3000);
|
|
1167
|
+
expect(failure.message).toContain("Native TypeScript SDK timed out: disposeSnapshot");
|
|
1168
|
+
expect(failure.message).not.toMatch(/reinstall/i);
|
|
1169
|
+
expect(close).toHaveBeenCalledOnce();
|
|
1170
|
+
expect(() => process.kill(connection.pid!, 0)).toThrow();
|
|
1171
|
+
release.reject(new Error("late snapshot release rejection"));
|
|
1172
|
+
await expect(within(close.mock.results[0]!.value as Promise<void>, 3000)).rejects.toThrow(
|
|
1173
|
+
"late snapshot release rejection",
|
|
1174
|
+
);
|
|
1175
|
+
} finally {
|
|
1176
|
+
vi.useRealTimers();
|
|
1177
|
+
release.reject(new Error("late snapshot release rejection"));
|
|
1178
|
+
const closing = close.mock.results[0]?.value as Promise<void> | undefined;
|
|
1179
|
+
await within(Promise.all([closing?.catch(() => {}), outcome.catch(() => {})]), 3000);
|
|
1180
|
+
}
|
|
1181
|
+
});
|
|
1182
|
+
|
|
1183
|
+
it("validates the packaged compiler handoff and preserves standalone laziness", async () => {
|
|
1184
|
+
const { root, read } = await fixture();
|
|
1185
|
+
vi.stubGlobal("Bun", { isStandaloneExecutable: true });
|
|
1186
|
+
vi.stubEnv("ADHERE_TYPESCRIPT_PACKAGE", undefined);
|
|
1187
|
+
expect(await run(root, read("use.ts", { ...both, symbols: false, references: false }))).toEqual(
|
|
1188
|
+
{},
|
|
1189
|
+
);
|
|
1190
|
+
const missing = await run(root, read("use.ts").pipe(Effect.flip));
|
|
1191
|
+
expect(missing.message).toContain("raw executable has no installed compiler");
|
|
1192
|
+
vi.stubEnv(
|
|
1193
|
+
"ADHERE_TYPESCRIPT_PACKAGE",
|
|
1194
|
+
createRequire(import.meta.url).resolve(
|
|
1195
|
+
`@typescript/typescript-${process.platform}-${process.arch}/package.json`,
|
|
1196
|
+
),
|
|
1197
|
+
);
|
|
1198
|
+
expect((await run(root, read("use.ts"))).symbols!.local).toContain(
|
|
1199
|
+
"The original documented value.",
|
|
1200
|
+
);
|
|
1201
|
+
const manifest = join(root, "wrong-compiler.json");
|
|
1202
|
+
await writeFile(
|
|
1203
|
+
manifest,
|
|
1204
|
+
JSON.stringify({
|
|
1205
|
+
name: `@typescript/typescript-${process.platform}-${process.arch}`,
|
|
1206
|
+
version: "7.0.1",
|
|
1207
|
+
os: [process.platform],
|
|
1208
|
+
cpu: [process.arch],
|
|
1209
|
+
}),
|
|
1210
|
+
);
|
|
1211
|
+
vi.stubEnv("ADHERE_TYPESCRIPT_PACKAGE", manifest);
|
|
1212
|
+
const mismatch = await run(root, read("use.ts").pipe(Effect.flip));
|
|
1213
|
+
expect(mismatch.message).toContain(`@${packageJson.devDependencies.typescript}`);
|
|
1214
|
+
expect(mismatch.message).toContain("Reinstall @drkmttr/adhere");
|
|
1215
|
+
});
|
|
1216
|
+
|
|
1217
|
+
it("bounds SDK attachment and closes a late successful attachment after reaping its compiler", async () => {
|
|
1218
|
+
const { root, read } = await fixture();
|
|
1219
|
+
const starting = Promise.withResolvers<void>();
|
|
1220
|
+
const release = Promise.withResolvers<void>();
|
|
1221
|
+
const attach = API.fromLSPConnection.bind(API);
|
|
1222
|
+
const attaching = vi.spyOn(API, "fromLSPConnection");
|
|
1223
|
+
const nativeClose = vi.spyOn(NativeLsp.prototype, "close");
|
|
1224
|
+
const close = vi.spyOn(API.prototype, "close");
|
|
1225
|
+
attaching.mockImplementationOnce(async (options) => {
|
|
1226
|
+
const attached = await attach.call(API, options);
|
|
1227
|
+
starting.resolve();
|
|
1228
|
+
await release.promise;
|
|
1229
|
+
return attached;
|
|
1230
|
+
});
|
|
1231
|
+
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] });
|
|
1232
|
+
const outcome = run(root, read("use.ts").pipe(Effect.flip));
|
|
1233
|
+
try {
|
|
1234
|
+
await starting.promise;
|
|
1235
|
+
await vi.advanceTimersByTimeAsync(30_000);
|
|
1236
|
+
await vi.advanceTimersByTimeAsync(2000);
|
|
1237
|
+
vi.useRealTimers();
|
|
1238
|
+
const failure = await within(outcome, 3500);
|
|
1239
|
+
expect(failure.message).toContain("Native TypeScript SDK timed out: API attachment");
|
|
1240
|
+
const connection = nativeClose.mock.contexts[0] as NativeLsp;
|
|
1241
|
+
expect(() => process.kill(connection.pid!, 0)).toThrow();
|
|
1242
|
+
expect(close).not.toHaveBeenCalled();
|
|
1243
|
+
release.resolve();
|
|
1244
|
+
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
1245
|
+
expect(close).toHaveBeenCalledOnce();
|
|
1246
|
+
await within(close.mock.results[0]!.value as Promise<void>, 3000);
|
|
1247
|
+
} finally {
|
|
1248
|
+
vi.useRealTimers();
|
|
1249
|
+
release.resolve();
|
|
1250
|
+
await outcome.catch(() => {});
|
|
1251
|
+
}
|
|
1252
|
+
});
|
|
1253
|
+
|
|
1254
|
+
it("waits for late SDK attachment before cancellation closes the native process", async () => {
|
|
1255
|
+
const { root, read } = await fixture();
|
|
1256
|
+
const starting = Promise.withResolvers<void>();
|
|
1257
|
+
const release = Promise.withResolvers<void>();
|
|
1258
|
+
const attach = API.fromLSPConnection.bind(API);
|
|
1259
|
+
const attaching = vi.spyOn(API, "fromLSPConnection");
|
|
1260
|
+
const open = vi.spyOn(NativeLsp, "open");
|
|
1261
|
+
const close = vi.spyOn(API.prototype, "close");
|
|
1262
|
+
attaching.mockImplementationOnce(async (options) => {
|
|
1263
|
+
starting.resolve();
|
|
1264
|
+
await release.promise;
|
|
1265
|
+
return attach.call(API, options);
|
|
1266
|
+
});
|
|
1267
|
+
const fiber = Effect.runFork(
|
|
1268
|
+
read("use.ts").pipe(Effect.provide(SourceReadsLive(root).pipe(Layer.provide(Path.layer)))),
|
|
1269
|
+
);
|
|
1270
|
+
await starting.promise;
|
|
1271
|
+
let interrupted = false;
|
|
1272
|
+
const cancellation = Effect.runPromise(Fiber.interrupt(fiber)).then(() => {
|
|
1273
|
+
interrupted = true;
|
|
1274
|
+
});
|
|
1275
|
+
try {
|
|
1276
|
+
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
1277
|
+
expect(interrupted).toBe(false);
|
|
1278
|
+
expect(close).not.toHaveBeenCalled();
|
|
1279
|
+
release.resolve();
|
|
1280
|
+
await cancellation;
|
|
1281
|
+
expect(close).toHaveBeenCalledOnce();
|
|
1282
|
+
const connection = (await open.mock.results[0]!.value) as NativeLsp;
|
|
1283
|
+
expect(() => process.kill(connection.pid!, 0)).toThrow();
|
|
1284
|
+
} finally {
|
|
1285
|
+
release.resolve();
|
|
1286
|
+
await cancellation;
|
|
1287
|
+
}
|
|
1288
|
+
});
|
|
1289
|
+
});
|