oh-my-opencode 4.17.1 → 4.18.1
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/.agents/skills/codex-qa/SKILL.md +2 -0
- package/.agents/skills/codex-qa/scripts/lsp-e2e.sh +3654 -0
- package/.agents/skills/opencode-qa/scripts/lsp-e2e.sh +3071 -0
- package/.agents/skills/work-with-pr/SKILL.md +16 -37
- package/.agents/skills/work-with-pr-workspace/evals/evals.json +3 -3
- package/.opencode/skills/work-with-pr/SKILL.md +16 -37
- package/.opencode/skills/work-with-pr-workspace/evals/evals.json +3 -3
- package/dist/cli/get-local-version/types.d.ts +1 -1
- package/dist/cli/index.js +498 -165
- package/dist/cli-node/index.js +498 -165
- package/dist/index.js +425 -392
- package/dist/skills/frontend/SKILL.md +1 -1
- package/dist/skills/frontend/references/design/README.md +9 -0
- package/dist/skills/frontend/references/design/design-system-architecture.md +4 -2
- package/dist/skills/frontend/references/design/layout-skill.md +107 -0
- package/dist/skills/programming/SKILL.md +12 -2
- package/package.json +17 -16
- package/packages/lsp-core/package.json +4 -0
- package/packages/lsp-core/src/index.ts +1 -0
- package/packages/lsp-core/src/lsp/cleanup-errors.test.ts +18 -0
- package/packages/lsp-core/src/lsp/cleanup-errors.ts +12 -3
- package/packages/lsp-core/src/lsp/client-diagnostics-freshness.integration.test.ts +261 -0
- package/packages/lsp-core/src/lsp/client-wrapper.test.ts +63 -0
- package/packages/lsp-core/src/lsp/client-wrapper.ts +35 -5
- package/packages/lsp-core/src/lsp/client.ts +262 -80
- package/packages/lsp-core/src/lsp/config-loader.ts +5 -17
- package/packages/lsp-core/src/lsp/connection.ts +12 -6
- package/packages/lsp-core/src/lsp/directory-diagnostics.test.ts +104 -0
- package/packages/lsp-core/src/lsp/directory-diagnostics.ts +60 -27
- package/packages/lsp-core/src/lsp/errors.ts +11 -0
- package/packages/lsp-core/src/lsp/fixtures/diagnostics-freshness-contract-probe.ts +283 -0
- package/packages/lsp-core/src/lsp/fixtures/workspace-edit-contract-probe.ts +196 -0
- package/packages/lsp-core/src/lsp/fixtures/workspace-edit-server.mjs +215 -0
- package/packages/lsp-core/src/lsp/formatters.ts +3 -0
- package/packages/lsp-core/src/lsp/json-rpc-connection-cancellation.test.ts +97 -0
- package/packages/lsp-core/src/lsp/json-rpc-connection.ts +73 -5
- package/packages/lsp-core/src/lsp/server-install-state.ts +3 -6
- package/packages/lsp-core/src/lsp/transport-protocol.ts +52 -0
- package/packages/lsp-core/src/lsp/transport.ts +96 -70
- package/packages/lsp-core/src/lsp/workspace-apply-edit-failure.ts +19 -0
- package/packages/lsp-core/src/lsp/workspace-apply-edit-lease.integration.test.ts +214 -0
- package/packages/lsp-core/src/lsp/workspace-apply-edit-sync.integration.test.ts +113 -0
- package/packages/lsp-core/src/lsp/workspace-apply-edit-test-support.ts +163 -0
- package/packages/lsp-core/src/lsp/workspace-apply-edit.integration.test.ts +163 -0
- package/packages/lsp-core/src/lsp/workspace-document-state.test.ts +67 -0
- package/packages/lsp-core/src/lsp/workspace-document-state.ts +368 -0
- package/packages/lsp-core/src/lsp/workspace-edit-adversarial.test.ts +113 -0
- package/packages/lsp-core/src/lsp/workspace-edit-commit.test.ts +140 -0
- package/packages/lsp-core/src/lsp/workspace-edit-commit.ts +220 -0
- package/packages/lsp-core/src/lsp/workspace-edit-contract-evidence.test.ts +56 -0
- package/packages/lsp-core/src/lsp/workspace-edit-contract-evidence.ts +30 -0
- package/packages/lsp-core/src/lsp/workspace-edit-fingerprint.ts +44 -0
- package/packages/lsp-core/src/lsp/workspace-edit-options.test.ts +147 -0
- package/packages/lsp-core/src/lsp/workspace-edit-parse-helpers.ts +59 -0
- package/packages/lsp-core/src/lsp/workspace-edit-parser.ts +130 -0
- package/packages/lsp-core/src/lsp/workspace-edit-path.ts +98 -0
- package/packages/lsp-core/src/lsp/workspace-edit-plan-types.ts +60 -0
- package/packages/lsp-core/src/lsp/workspace-edit-plan.ts +73 -0
- package/packages/lsp-core/src/lsp/workspace-edit-prevalidation.test.ts +174 -0
- package/packages/lsp-core/src/lsp/workspace-edit-resource-parser.ts +89 -0
- package/packages/lsp-core/src/lsp/workspace-edit-simulation.ts +183 -0
- package/packages/lsp-core/src/lsp/workspace-edit-snapshot.ts +53 -0
- package/packages/lsp-core/src/lsp/workspace-edit-text.ts +125 -0
- package/packages/lsp-core/src/lsp/workspace-edit-types.ts +121 -0
- package/packages/lsp-core/src/lsp/workspace-edit.characterization.test.ts +95 -0
- package/packages/lsp-core/src/lsp/workspace-edit.ts +49 -200
- package/packages/lsp-core/src/lsp/workspace-mutation-controller.ts +182 -0
- package/packages/lsp-core/src/mcp.ts +18 -7
- package/packages/lsp-core/src/missing-dependency-result.test.ts +105 -0
- package/packages/lsp-core/src/missing-dependency-result.ts +57 -0
- package/packages/lsp-core/src/post-edit/index.ts +1 -0
- package/packages/lsp-core/src/post-edit/orchestration.test.ts +157 -0
- package/packages/lsp-core/src/post-edit/orchestration.ts +178 -0
- package/packages/lsp-core/src/request-context.test.ts +171 -0
- package/packages/lsp-core/src/request-context.ts +222 -9
- package/packages/lsp-core/src/tool-surface.test.ts +4 -1
- package/packages/lsp-core/src/tools/diagnostics.ts +32 -13
- package/packages/lsp-core/src/tools/navigation.ts +12 -12
- package/packages/lsp-core/src/tools/rename.ts +10 -15
- package/packages/lsp-core/src/tools/symbols.ts +11 -11
- package/packages/lsp-core/src/tools/types.ts +2 -1
- package/packages/lsp-daemon/dist/cli.js +3114 -747
- package/packages/lsp-daemon/dist/client.d.ts +105 -0
- package/packages/lsp-daemon/dist/client.js +5851 -0
- package/packages/lsp-daemon/dist/daemon-client.d.ts +11 -6
- package/packages/lsp-daemon/dist/daemon-client.js +113 -30
- package/packages/lsp-daemon/dist/daemon-server.d.ts +1 -0
- package/packages/lsp-daemon/dist/daemon-server.js +40 -15
- package/packages/lsp-daemon/dist/ensure-daemon.d.ts +8 -7
- package/packages/lsp-daemon/dist/ensure-daemon.js +67 -44
- package/packages/lsp-daemon/dist/index.d.ts +2 -2
- package/packages/lsp-daemon/dist/index.js +2862 -754
- package/packages/lsp-daemon/dist/ipc-protocol.d.ts +46 -0
- package/packages/lsp-daemon/dist/ipc-protocol.js +187 -0
- package/packages/lsp-daemon/dist/lock.js +14 -4
- package/packages/lsp-daemon/dist/ownership.d.ts +49 -0
- package/packages/lsp-daemon/dist/ownership.js +168 -0
- package/packages/lsp-daemon/dist/paths.d.ts +33 -9
- package/packages/lsp-daemon/dist/paths.js +72 -33
- package/packages/lsp-daemon/dist/proxy.d.ts +3 -0
- package/packages/lsp-daemon/dist/proxy.js +54 -3
- package/packages/lsp-daemon/dist/request-routing.d.ts +7 -2
- package/packages/lsp-daemon/dist/request-routing.js +71 -22
- package/packages/lsp-daemon/dist/run-daemon.js +9 -2
- package/packages/lsp-daemon/dist/runtime-contract.d.ts +21 -0
- package/packages/lsp-daemon/dist/runtime-contract.js +58 -0
- package/packages/lsp-daemon/dist/socket-jsonrpc.js +6 -1
- package/packages/lsp-daemon/package.json +12 -3
- package/packages/lsp-tools-mcp/dist/cli.js +2115 -442
- package/packages/lsp-tools-mcp/dist/lsp/manager.js +1741 -148
- package/packages/lsp-tools-mcp/dist/mcp.js +2127 -454
- package/packages/lsp-tools-mcp/dist/request-context.js +176 -6
- package/packages/lsp-tools-mcp/dist/tools.js +2118 -446
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
- package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
- package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
- package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +55 -0
- package/packages/omo-codex/plugin/components/lsp/dist/cli.js +2959 -944
- package/packages/omo-codex/plugin/components/lsp/dist/codex-hook-cli.js +0 -4
- package/packages/omo-codex/plugin/components/lsp/dist/codex-hook.d.ts +5 -2
- package/packages/omo-codex/plugin/components/lsp/dist/codex-hook.js +41 -62
- package/packages/omo-codex/plugin/components/lsp/dist/daemon-cli-path.d.ts +1 -1
- package/packages/omo-codex/plugin/components/lsp/dist/daemon-cli-path.js +24 -15
- package/packages/omo-codex/plugin/components/lsp/dist/lsp-session-state.d.ts +3 -7
- package/packages/omo-codex/plugin/components/lsp/dist/lsp-session-state.js +23 -49
- package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/package.json +3 -2
- package/packages/omo-codex/plugin/components/lsp/scripts/build-lsp-daemon.mjs +31 -1
- package/packages/omo-codex/plugin/components/lsp/scripts/build-lsp-daemon.test.mjs +76 -0
- package/packages/omo-codex/plugin/components/lsp/scripts/build-runtime.mjs +201 -0
- package/packages/omo-codex/plugin/components/lsp/scripts/build-runtime.test.mjs +55 -0
- package/packages/omo-codex/plugin/components/lsp/src/codex-hook-cli.ts +0 -4
- package/packages/omo-codex/plugin/components/lsp/src/codex-hook.ts +49 -71
- package/packages/omo-codex/plugin/components/lsp/src/daemon-cli-path.ts +26 -15
- package/packages/omo-codex/plugin/components/lsp/src/lsp-session-state.ts +26 -64
- package/packages/omo-codex/plugin/components/lsp/test/codex-hook-unavailable.test.ts +16 -17
- package/packages/omo-codex/plugin/components/lsp/test/codex-hook.test.ts +30 -4
- package/packages/omo-codex/plugin/components/lsp/test/package-smoke.test.ts +19 -5
- package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-5.5.md +1 -1
- package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-5.6.md +8 -6
- package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
- package/packages/omo-codex/plugin/components/rules/package.json +1 -1
- package/packages/omo-codex/plugin/components/start-work-continuation/directive.md +1 -1
- package/packages/omo-codex/plugin/components/start-work-continuation/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/start-work-continuation/package.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/directive.md +37 -10
- package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/skills/ultrawork/SKILL.md +37 -10
- package/packages/omo-codex/plugin/components/ulw-loop/directive.md +37 -10
- package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +4 -4
- package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md +2 -2
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +10 -9
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-codegraph-init-guidance.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-start-work-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-checking-start-work-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/package-lock.json +26 -14
- package/packages/omo-codex/plugin/package.json +1 -1
- package/packages/omo-codex/plugin/scripts/build-bundled-mcp-runtimes.mjs +2 -3
- package/packages/omo-codex/plugin/scripts/build-components.mjs +13 -1
- package/packages/omo-codex/plugin/scripts/sync-skills.mjs +1 -1
- package/packages/omo-codex/plugin/skills/frontend/SKILL.md +1 -1
- package/packages/omo-codex/plugin/skills/frontend/references/design/README.md +9 -0
- package/packages/omo-codex/plugin/skills/frontend/references/design/design-system-architecture.md +4 -2
- package/packages/omo-codex/plugin/skills/frontend/references/design/layout-skill.md +107 -0
- package/packages/omo-codex/plugin/skills/programming/SKILL.md +12 -2
- package/packages/omo-codex/plugin/skills/start-work/SKILL.md +1 -1
- package/packages/omo-codex/plugin/skills/ultrawork/SKILL.md +37 -10
- package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +2 -2
- package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +10 -9
- package/packages/omo-codex/plugin/test/aggregate-build.test.mjs +8 -0
- package/packages/omo-codex/plugin/test/component-bundled-cli.test.mjs +128 -15
- package/packages/omo-codex/plugin/test/install-time-build-runtime.test.mjs +10 -0
- package/packages/omo-codex/plugin/test/lsp-prebuild-layouts.test.mjs +2 -0
- package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +1 -1
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +351 -74
- package/packages/shared-skills/skills/frontend/SKILL.md +1 -1
- package/packages/shared-skills/skills/frontend/references/design/README.md +9 -0
- package/packages/shared-skills/skills/frontend/references/design/design-system-architecture.md +4 -2
- package/packages/shared-skills/skills/frontend/references/design/layout-skill.md +107 -0
- package/packages/shared-skills/skills/programming/SKILL.md +12 -2
|
@@ -2,12 +2,19 @@ import { pathToFileURL } from "node:url";
|
|
|
2
2
|
|
|
3
3
|
import { LspClientTransport } from "./transport.js";
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
interface InitializeCapabilities {
|
|
6
|
+
readonly diagnosticProvider?: unknown;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function supportsDiagnosticPull(capabilities: InitializeCapabilities | undefined): boolean {
|
|
10
|
+
if (capabilities === undefined) return false;
|
|
11
|
+
return Object.hasOwn(capabilities, "diagnosticProvider");
|
|
12
|
+
}
|
|
6
13
|
|
|
7
14
|
export class LspClientConnection extends LspClientTransport {
|
|
8
15
|
async initialize(): Promise<void> {
|
|
9
16
|
const rootUri = pathToFileURL(this.root).href;
|
|
10
|
-
await this.sendRequest(
|
|
17
|
+
const result = await this.sendRequest<{ readonly capabilities?: InitializeCapabilities }>(
|
|
11
18
|
"initialize",
|
|
12
19
|
{
|
|
13
20
|
processId: process.pid,
|
|
@@ -24,7 +31,6 @@ export class LspClientConnection extends LspClientTransport {
|
|
|
24
31
|
rename: {
|
|
25
32
|
prepareSupport: true,
|
|
26
33
|
prepareSupportDefaultBehavior: 1,
|
|
27
|
-
honorsChangeAnnotations: true,
|
|
28
34
|
},
|
|
29
35
|
codeAction: {
|
|
30
36
|
codeActionLiteralSupport: {
|
|
@@ -53,9 +59,10 @@ export class LspClientConnection extends LspClientTransport {
|
|
|
53
59
|
symbol: {},
|
|
54
60
|
workspaceFolders: true,
|
|
55
61
|
configuration: true,
|
|
56
|
-
applyEdit: true,
|
|
62
|
+
...(this.hasWorkspaceApplyEditHandler() ? { applyEdit: true } : {}),
|
|
57
63
|
workspaceEdit: {
|
|
58
64
|
documentChanges: true,
|
|
65
|
+
resourceOperations: ["create", "rename", "delete"],
|
|
59
66
|
},
|
|
60
67
|
},
|
|
61
68
|
},
|
|
@@ -63,11 +70,10 @@ export class LspClientConnection extends LspClientTransport {
|
|
|
63
70
|
},
|
|
64
71
|
{ timeoutMs: this.initializeTimeoutMs },
|
|
65
72
|
);
|
|
73
|
+
this.setDiagnosticPullSupported(supportsDiagnosticPull(result?.capabilities));
|
|
66
74
|
await this.sendNotification("initialized");
|
|
67
75
|
await this.sendNotification("workspace/didChangeConfiguration", {
|
|
68
76
|
settings: { json: { validate: { enable: true } } },
|
|
69
77
|
});
|
|
70
|
-
// Some servers accept initialized before their diagnostics/indexing handlers are ready.
|
|
71
|
-
await new Promise((r) => setTimeout(r, INITIALIZE_SETTLE_MS));
|
|
72
78
|
}
|
|
73
79
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it } from "bun:test";
|
|
6
|
+
|
|
7
|
+
import { aggregateDiagnosticsForDirectory } from "./directory-diagnostics.js";
|
|
8
|
+
import type { LspClient } from "./client.js";
|
|
9
|
+
import type { Diagnostic, ResolvedServer } from "./types.js";
|
|
10
|
+
|
|
11
|
+
describe("directory diagnostics", () => {
|
|
12
|
+
it("#given multiple files and one per-file failure #when directory diagnostics run #then work stays bounded and file failures stay structured", async () => {
|
|
13
|
+
const workspace = mkdtempSync(join(tmpdir(), "lsp-directory-diag-"));
|
|
14
|
+
const active: string[] = [];
|
|
15
|
+
let maxActive = 0;
|
|
16
|
+
const diagnosticsByFile = new Map<string, Diagnostic[] | Error>([
|
|
17
|
+
[join(workspace, "a.ts"), [diagnostic("a")]],
|
|
18
|
+
[join(workspace, "b.ts"), new Error("b failed")],
|
|
19
|
+
[join(workspace, "c.ts"), [diagnostic("c")]],
|
|
20
|
+
[join(workspace, "d.ts"), [diagnostic("d")]],
|
|
21
|
+
[join(workspace, "e.ts"), [diagnostic("e")]],
|
|
22
|
+
]);
|
|
23
|
+
const client = {
|
|
24
|
+
async diagnostics(filePath: string) {
|
|
25
|
+
active.push(filePath);
|
|
26
|
+
maxActive = Math.max(maxActive, active.length);
|
|
27
|
+
await Promise.resolve();
|
|
28
|
+
active.splice(active.indexOf(filePath), 1);
|
|
29
|
+
const entry = diagnosticsByFile.get(filePath);
|
|
30
|
+
if (entry instanceof Error) throw entry;
|
|
31
|
+
return { items: entry ?? [] };
|
|
32
|
+
},
|
|
33
|
+
} as Pick<LspClient, "diagnostics"> as LspClient;
|
|
34
|
+
const server: ResolvedServer = { id: "typescript", command: ["typescript-language-server"], extensions: [".ts"], priority: 1 };
|
|
35
|
+
const manager = {
|
|
36
|
+
async getClient() {
|
|
37
|
+
return client;
|
|
38
|
+
},
|
|
39
|
+
releaseClient() {},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const result = await aggregateDiagnosticsForDirectory(workspace, ".ts", "all", 4, {
|
|
44
|
+
manager: manager as never,
|
|
45
|
+
listFiles: () => [...diagnosticsByFile.keys()],
|
|
46
|
+
workspaceRoot: workspace,
|
|
47
|
+
server,
|
|
48
|
+
maxConcurrency: 2,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
expect(maxActive).toBeLessThanOrEqual(2);
|
|
52
|
+
expect(result.fileFailures).toEqual([{ file: join(workspace, "b.ts"), error: "b failed" }]);
|
|
53
|
+
expect(result.totalDiagnostics).toBe(3);
|
|
54
|
+
expect(result.output).toContain("File processing errors:");
|
|
55
|
+
} finally {
|
|
56
|
+
rmSync(workspace, { recursive: true, force: true });
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("#given directory diagnostics are aborted after one file #when the worker loops #then no later file is scheduled", async () => {
|
|
61
|
+
const workspace = mkdtempSync(join(tmpdir(), "lsp-directory-diag-cancel-"));
|
|
62
|
+
const controller = new AbortController();
|
|
63
|
+
const visited: string[] = [];
|
|
64
|
+
const firstFile = join(workspace, "a.ts");
|
|
65
|
+
const files = [firstFile, join(workspace, "b.ts"), join(workspace, "c.ts")];
|
|
66
|
+
const client = {
|
|
67
|
+
async diagnostics(filePath: string) {
|
|
68
|
+
visited.push(filePath);
|
|
69
|
+
controller.abort();
|
|
70
|
+
return { items: [] };
|
|
71
|
+
},
|
|
72
|
+
} as Pick<LspClient, "diagnostics"> as LspClient;
|
|
73
|
+
const server: ResolvedServer = { id: "typescript", command: ["typescript-language-server"], extensions: [".ts"], priority: 1 };
|
|
74
|
+
const manager = {
|
|
75
|
+
async getClient() {
|
|
76
|
+
return client;
|
|
77
|
+
},
|
|
78
|
+
releaseClient() {},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const result = await aggregateDiagnosticsForDirectory(workspace, ".ts", "all", 3, {
|
|
83
|
+
manager: manager as never,
|
|
84
|
+
listFiles: () => files,
|
|
85
|
+
workspaceRoot: workspace,
|
|
86
|
+
server,
|
|
87
|
+
maxConcurrency: 1,
|
|
88
|
+
signal: controller.signal,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
expect(result.totalDiagnostics).toBe(0);
|
|
92
|
+
expect(visited).toEqual([firstFile]);
|
|
93
|
+
} finally {
|
|
94
|
+
rmSync(workspace, { recursive: true, force: true });
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
function diagnostic(message: string): Diagnostic {
|
|
100
|
+
return {
|
|
101
|
+
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 1 } },
|
|
102
|
+
message,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -6,17 +6,38 @@ import { DEFAULT_MAX_DIAGNOSTICS, DEFAULT_MAX_DIRECTORY_FILES } from "./constant
|
|
|
6
6
|
import { effectiveExtension } from "./effective-extension.js";
|
|
7
7
|
import { LspInvalidPathError, LspServerLookupError } from "./errors.js";
|
|
8
8
|
import { filterDiagnosticsBySeverity, formatDiagnostic } from "./formatters.js";
|
|
9
|
-
import { getLspManager } from "./manager.js";
|
|
9
|
+
import { getLspManager, type LspManager } from "./manager.js";
|
|
10
10
|
import { findServerForExtension } from "./server-resolution.js";
|
|
11
|
-
import type { Diagnostic, SeverityFilter } from "./types.js";
|
|
11
|
+
import type { Diagnostic, ResolvedServer, SeverityFilter } from "./types.js";
|
|
12
12
|
|
|
13
13
|
const SKIP_DIRECTORIES = new Set(["node_modules", ".git", "dist", "build", ".next", "out"]);
|
|
14
|
+
const DIRECTORY_DIAGNOSTICS_MAX_CONCURRENCY = 4;
|
|
14
15
|
|
|
15
16
|
interface FileDiagnostic {
|
|
16
17
|
filePath: string;
|
|
17
18
|
diagnostic: Diagnostic;
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
export interface DirectoryDiagnosticsFileFailure {
|
|
22
|
+
readonly file: string;
|
|
23
|
+
readonly error: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface DirectoryDiagnosticsResult {
|
|
27
|
+
readonly output: string;
|
|
28
|
+
readonly totalDiagnostics: number;
|
|
29
|
+
readonly fileFailures: readonly DirectoryDiagnosticsFileFailure[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface DirectoryDiagnosticsOptions {
|
|
33
|
+
readonly listFiles?: (directory: string, extension: string, maxFiles: number) => string[];
|
|
34
|
+
readonly manager?: LspManager;
|
|
35
|
+
readonly maxConcurrency?: number;
|
|
36
|
+
readonly workspaceRoot?: string;
|
|
37
|
+
readonly server?: ResolvedServer;
|
|
38
|
+
readonly signal?: AbortSignal;
|
|
39
|
+
}
|
|
40
|
+
|
|
20
41
|
export function collectFilesWithExtension(dir: string, extension: string, maxFiles: number): string[] {
|
|
21
42
|
const files: string[] = [];
|
|
22
43
|
|
|
@@ -63,61 +84,73 @@ export async function aggregateDiagnosticsForDirectory(
|
|
|
63
84
|
extension: string,
|
|
64
85
|
severity?: SeverityFilter,
|
|
65
86
|
maxFiles: number = DEFAULT_MAX_DIRECTORY_FILES,
|
|
66
|
-
|
|
87
|
+
options: DirectoryDiagnosticsOptions = {},
|
|
88
|
+
): Promise<DirectoryDiagnosticsResult> {
|
|
67
89
|
if (!extension.startsWith(".")) {
|
|
68
90
|
throw new LspInvalidPathError(
|
|
69
91
|
`Extension must start with a dot (e.g., ".ts", not "${extension}"). Use ".${extension}" instead.`,
|
|
70
92
|
);
|
|
71
93
|
}
|
|
72
94
|
|
|
73
|
-
const absDir = resolve(contextCwd(), directory);
|
|
95
|
+
const absDir = resolve(options.workspaceRoot ?? contextCwd(), directory);
|
|
74
96
|
if (!existsSync(absDir)) {
|
|
75
97
|
throw new LspInvalidPathError(`Directory does not exist: ${absDir}`);
|
|
76
98
|
}
|
|
77
99
|
|
|
78
|
-
const serverResult = findServerForExtension(extension);
|
|
100
|
+
const serverResult = options.server === undefined ? findServerForExtension(extension) : { status: "found" as const, server: options.server };
|
|
79
101
|
if (serverResult.status !== "found") {
|
|
80
102
|
throw new LspServerLookupError(formatServerLookupError(serverResult));
|
|
81
103
|
}
|
|
82
104
|
|
|
83
105
|
const server = serverResult.server;
|
|
84
|
-
const allFiles = collectFilesWithExtension(absDir, extension, maxFiles + 1);
|
|
106
|
+
const allFiles = (options.listFiles ?? collectFilesWithExtension)(absDir, extension, maxFiles + 1);
|
|
85
107
|
const wasCapped = allFiles.length > maxFiles;
|
|
86
108
|
const filesToProcess = allFiles.slice(0, maxFiles);
|
|
87
109
|
|
|
88
110
|
if (filesToProcess.length === 0) {
|
|
89
|
-
|
|
111
|
+
const output = [
|
|
90
112
|
`Directory: ${absDir}`,
|
|
91
113
|
`Extension: ${extension}`,
|
|
92
114
|
"Files scanned: 0",
|
|
93
115
|
`No files found with extension "${extension}".`,
|
|
94
116
|
].join("\n");
|
|
117
|
+
return { output, totalDiagnostics: 0, fileFailures: [] };
|
|
95
118
|
}
|
|
96
119
|
|
|
97
|
-
const root = findWorkspaceRoot(absDir);
|
|
98
|
-
const manager = getLspManager();
|
|
120
|
+
const root = options.workspaceRoot ?? findWorkspaceRoot(absDir);
|
|
121
|
+
const manager = options.manager ?? getLspManager();
|
|
99
122
|
const allDiagnostics: FileDiagnostic[] = [];
|
|
100
|
-
const fileErrors:
|
|
123
|
+
const fileErrors: DirectoryDiagnosticsFileFailure[] = [];
|
|
124
|
+
const maxConcurrency = Math.max(1, options.maxConcurrency ?? DIRECTORY_DIAGNOSTICS_MAX_CONCURRENCY);
|
|
101
125
|
|
|
126
|
+
options.signal?.throwIfAborted();
|
|
102
127
|
const client = await manager.getClient(root, server);
|
|
103
128
|
try {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
129
|
+
let nextIndex = 0;
|
|
130
|
+
const workers = Array.from({ length: Math.min(maxConcurrency, filesToProcess.length) }, async () => {
|
|
131
|
+
for (;;) {
|
|
132
|
+
if (options.signal?.aborted) return;
|
|
133
|
+
const file = filesToProcess[nextIndex];
|
|
134
|
+
nextIndex += 1;
|
|
135
|
+
if (file === undefined) return;
|
|
136
|
+
try {
|
|
137
|
+
const result = await client.diagnostics(file, options.signal);
|
|
138
|
+
const filtered = filterDiagnosticsBySeverity(result.items, severity);
|
|
139
|
+
allDiagnostics.push(
|
|
140
|
+
...filtered.map((diagnostic) => ({
|
|
141
|
+
filePath: file,
|
|
142
|
+
diagnostic,
|
|
143
|
+
})),
|
|
144
|
+
);
|
|
145
|
+
} catch (e) {
|
|
146
|
+
fileErrors.push({
|
|
147
|
+
file,
|
|
148
|
+
error: e instanceof Error ? e.message : String(e),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
119
151
|
}
|
|
120
|
-
}
|
|
152
|
+
});
|
|
153
|
+
await Promise.all(workers);
|
|
121
154
|
} finally {
|
|
122
155
|
manager.releaseClient(root, server.id);
|
|
123
156
|
}
|
|
@@ -150,5 +183,5 @@ export async function aggregateDiagnosticsForDirectory(
|
|
|
150
183
|
}
|
|
151
184
|
}
|
|
152
185
|
|
|
153
|
-
return lines.join("\n");
|
|
186
|
+
return { output: lines.join("\n"), totalDiagnostics: allDiagnostics.length, fileFailures: fileErrors };
|
|
154
187
|
}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import type { ServerLookupResult } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export type FailedServerLookupResult = Exclude<ServerLookupResult, { status: "found" }>;
|
|
4
|
+
|
|
1
5
|
export class LspConnectionClosedError extends Error {
|
|
2
6
|
override readonly name = "LspConnectionClosedError";
|
|
3
7
|
|
|
@@ -42,6 +46,13 @@ export class LspInvalidPathError extends Error {
|
|
|
42
46
|
|
|
43
47
|
export class LspServerLookupError extends Error {
|
|
44
48
|
override readonly name = "LspServerLookupError";
|
|
49
|
+
|
|
50
|
+
constructor(
|
|
51
|
+
message: string,
|
|
52
|
+
readonly lookup?: FailedServerLookupResult,
|
|
53
|
+
) {
|
|
54
|
+
super(message);
|
|
55
|
+
}
|
|
45
56
|
}
|
|
46
57
|
|
|
47
58
|
export class LspServerInitializingError extends Error {
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { writeFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createWorkspaceEditTestHarness,
|
|
5
|
+
diagnostic,
|
|
6
|
+
readEvents,
|
|
7
|
+
waitForEventCount,
|
|
8
|
+
} from "../workspace-apply-edit-test-support.js";
|
|
9
|
+
|
|
10
|
+
class DiagnosticsFreshnessContractProbeError extends Error {
|
|
11
|
+
override readonly name = "DiagnosticsFreshnessContractProbeError";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function requireProbe(condition: boolean, message: string): asserts condition {
|
|
15
|
+
if (!condition) throw new DiagnosticsFreshnessContractProbeError(message);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const outputPath = process.argv[2];
|
|
19
|
+
if (!outputPath) throw new DiagnosticsFreshnessContractProbeError("output path is required");
|
|
20
|
+
|
|
21
|
+
const harness = createWorkspaceEditTestHarness();
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const exactCurrent = await harness.makeClient(
|
|
25
|
+
{
|
|
26
|
+
publishDiagnostics: [
|
|
27
|
+
{
|
|
28
|
+
trigger: "didOpen",
|
|
29
|
+
version: 1,
|
|
30
|
+
diagnostics: [diagnostic("exact-current")],
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
{ diagnosticsFreshnessTimeoutMs: 50, versionlessPublishQuiescenceMs: 5 },
|
|
35
|
+
);
|
|
36
|
+
const [exactFirst, exactSecond] = await Promise.all([
|
|
37
|
+
exactCurrent.client.diagnostics(exactCurrent.source),
|
|
38
|
+
exactCurrent.client.diagnostics(exactCurrent.source),
|
|
39
|
+
]);
|
|
40
|
+
const exactDidOpenCount = readEvents(exactCurrent.events).filter(
|
|
41
|
+
(event) => event.type === "clientNotification" && event.method === "textDocument/didOpen",
|
|
42
|
+
).length;
|
|
43
|
+
const exactCurrentEvidence = {
|
|
44
|
+
outcome: "exact-current",
|
|
45
|
+
items: exactFirst.items.map((entry) => entry.message),
|
|
46
|
+
concurrentItems: exactSecond.items.map((entry) => entry.message),
|
|
47
|
+
didOpenCount: exactDidOpenCount,
|
|
48
|
+
ok:
|
|
49
|
+
exactDidOpenCount === 1
|
|
50
|
+
&& JSON.stringify(exactFirst.items) === JSON.stringify([diagnostic("exact-current")])
|
|
51
|
+
&& JSON.stringify(exactSecond.items) === JSON.stringify([diagnostic("exact-current")]),
|
|
52
|
+
};
|
|
53
|
+
requireProbe(exactCurrentEvidence.ok, "exact-current diagnostics did not resolve immediately with a single didOpen");
|
|
54
|
+
|
|
55
|
+
const postGenerationVersionless = await harness.makeClient(
|
|
56
|
+
{
|
|
57
|
+
publishDiagnostics: [
|
|
58
|
+
{
|
|
59
|
+
trigger: "didChange",
|
|
60
|
+
diagnostics: [diagnostic("post-generation-versionless")],
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
{ diagnosticsFreshnessTimeoutMs: 80, versionlessPublishQuiescenceMs: 20 },
|
|
65
|
+
);
|
|
66
|
+
await postGenerationVersionless.client.openFile(postGenerationVersionless.source);
|
|
67
|
+
await waitForEventCount(
|
|
68
|
+
postGenerationVersionless.events,
|
|
69
|
+
(event) => event.type === "serverNotification" && event.method === "textDocument/publishDiagnostics",
|
|
70
|
+
1,
|
|
71
|
+
);
|
|
72
|
+
writeFileSync(postGenerationVersionless.source, "const post_generation = 1;\n", "utf-8");
|
|
73
|
+
await postGenerationVersionless.client.openFile(postGenerationVersionless.source);
|
|
74
|
+
const versionlessStartedAt = Date.now();
|
|
75
|
+
const versionlessResult = await postGenerationVersionless.client.diagnostics(postGenerationVersionless.source);
|
|
76
|
+
const versionlessElapsedMs = Date.now() - versionlessStartedAt;
|
|
77
|
+
const postGenerationVersionlessEvidence = {
|
|
78
|
+
outcome: "post-generation-versionless",
|
|
79
|
+
items: versionlessResult.items.map((entry) => entry.message),
|
|
80
|
+
elapsedMs: versionlessElapsedMs,
|
|
81
|
+
ok:
|
|
82
|
+
JSON.stringify(versionlessResult.items) === JSON.stringify([diagnostic("post-generation-versionless")])
|
|
83
|
+
&& versionlessElapsedMs >= 15,
|
|
84
|
+
};
|
|
85
|
+
requireProbe(
|
|
86
|
+
postGenerationVersionlessEvidence.ok,
|
|
87
|
+
"post-generation versionless diagnostics did not wait for quiescence before resolving",
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const stale = await harness.makeClient(
|
|
91
|
+
{
|
|
92
|
+
publishDiagnostics: [
|
|
93
|
+
{
|
|
94
|
+
trigger: "didChange",
|
|
95
|
+
version: 1,
|
|
96
|
+
diagnostics: [diagnostic("stale")],
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
{ diagnosticsFreshnessTimeoutMs: 30, versionlessPublishQuiescenceMs: 5 },
|
|
101
|
+
);
|
|
102
|
+
await stale.client.openFile(stale.source);
|
|
103
|
+
writeFileSync(stale.source, "const stale = 1;\n", "utf-8");
|
|
104
|
+
await stale.client.openFile(stale.source);
|
|
105
|
+
const staleResult = await stale.client.diagnostics(stale.source);
|
|
106
|
+
const staleEvidence = {
|
|
107
|
+
outcome: "stale",
|
|
108
|
+
items: staleResult.items.map((entry) => entry.message),
|
|
109
|
+
transientError: staleResult.transientError ?? null,
|
|
110
|
+
ok: staleResult.items.length === 0 && staleResult.transientError?.kind === "freshness_timeout",
|
|
111
|
+
};
|
|
112
|
+
requireProbe(staleEvidence.ok, "stale diagnostics unexpectedly satisfied the current request");
|
|
113
|
+
|
|
114
|
+
const future = await harness.makeClient(
|
|
115
|
+
{
|
|
116
|
+
publishDiagnostics: [
|
|
117
|
+
{
|
|
118
|
+
trigger: "didChange",
|
|
119
|
+
version: 3,
|
|
120
|
+
diagnostics: [diagnostic("future")],
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
},
|
|
124
|
+
{ diagnosticsFreshnessTimeoutMs: 30, versionlessPublishQuiescenceMs: 5 },
|
|
125
|
+
);
|
|
126
|
+
await future.client.openFile(future.source);
|
|
127
|
+
writeFileSync(future.source, "const future = 1;\n", "utf-8");
|
|
128
|
+
await future.client.openFile(future.source);
|
|
129
|
+
const futureResult = await future.client.diagnostics(future.source);
|
|
130
|
+
const futureEvidence = {
|
|
131
|
+
outcome: "future",
|
|
132
|
+
items: futureResult.items.map((entry) => entry.message),
|
|
133
|
+
transientError: futureResult.transientError ?? null,
|
|
134
|
+
ok: futureResult.items.length === 0 && futureResult.transientError?.kind === "freshness_timeout",
|
|
135
|
+
};
|
|
136
|
+
requireProbe(futureEvidence.ok, "future diagnostics unexpectedly satisfied the current request");
|
|
137
|
+
|
|
138
|
+
const pullOvertaken = await harness.makeClient(
|
|
139
|
+
{
|
|
140
|
+
capabilities: { diagnosticProvider: { interFileDependencies: false, workspaceDiagnostics: false } },
|
|
141
|
+
diagnosticResponses: [
|
|
142
|
+
{
|
|
143
|
+
delayMs: 20,
|
|
144
|
+
report: { kind: "full", resultId: "v1", items: [diagnostic("pull-stale")] },
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
report: { kind: "full", resultId: "v2", items: [diagnostic("pull-fresh")] },
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
},
|
|
151
|
+
{ diagnosticsFreshnessTimeoutMs: 80, versionlessPublishQuiescenceMs: 5 },
|
|
152
|
+
);
|
|
153
|
+
await pullOvertaken.client.openFile(pullOvertaken.source);
|
|
154
|
+
const pendingPull = pullOvertaken.client.diagnostics(pullOvertaken.source);
|
|
155
|
+
await waitForEventCount(
|
|
156
|
+
pullOvertaken.events,
|
|
157
|
+
(event) => event.type === "clientRequest" && event.method === "textDocument/diagnostic",
|
|
158
|
+
1,
|
|
159
|
+
);
|
|
160
|
+
writeFileSync(pullOvertaken.source, "const pull_fresh = 1;\n", "utf-8");
|
|
161
|
+
await pullOvertaken.client.openFile(pullOvertaken.source);
|
|
162
|
+
const pullOvertakenResult = await pendingPull;
|
|
163
|
+
const pullRequestCount = readEvents(pullOvertaken.events).filter(
|
|
164
|
+
(event) => event.type === "clientRequest" && event.method === "textDocument/diagnostic",
|
|
165
|
+
).length;
|
|
166
|
+
const pullOvertakenEvidence = {
|
|
167
|
+
outcome: "pull-overtaken",
|
|
168
|
+
items: pullOvertakenResult.items.map((entry) => entry.message),
|
|
169
|
+
requestCount: pullRequestCount,
|
|
170
|
+
ok:
|
|
171
|
+
JSON.stringify(pullOvertakenResult.items) === JSON.stringify([diagnostic("pull-fresh")])
|
|
172
|
+
&& pullRequestCount === 2,
|
|
173
|
+
};
|
|
174
|
+
requireProbe(
|
|
175
|
+
pullOvertakenEvidence.ok,
|
|
176
|
+
"pull diagnostics were not invalidated and retried after a local change overtook the in-flight request",
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
const silent = await harness.makeClient({}, { diagnosticsFreshnessTimeoutMs: 35, versionlessPublishQuiescenceMs: 5 });
|
|
180
|
+
const silentResult = await silent.client.diagnostics(silent.source);
|
|
181
|
+
const silentEvidence = {
|
|
182
|
+
outcome: "silent",
|
|
183
|
+
items: silentResult.items.map((entry) => entry.message),
|
|
184
|
+
transientError: silentResult.transientError ?? null,
|
|
185
|
+
ok: silentResult.items.length === 0 && silentResult.transientError?.kind === "freshness_timeout",
|
|
186
|
+
};
|
|
187
|
+
requireProbe(silentEvidence.ok, "silent diagnostics were labeled clean instead of timing out transiently");
|
|
188
|
+
|
|
189
|
+
const closedServer = await harness.makeClient(
|
|
190
|
+
{
|
|
191
|
+
capabilities: { diagnosticProvider: { interFileDependencies: false, workspaceDiagnostics: false } },
|
|
192
|
+
diagnosticResponses: [{ action: "close" }],
|
|
193
|
+
},
|
|
194
|
+
{ diagnosticsFreshnessTimeoutMs: 50, versionlessPublishQuiescenceMs: 5 },
|
|
195
|
+
);
|
|
196
|
+
await closedServer.client.openFile(closedServer.source);
|
|
197
|
+
let closedServerError = "";
|
|
198
|
+
try {
|
|
199
|
+
await closedServer.client.diagnostics(closedServer.source);
|
|
200
|
+
} catch (error) {
|
|
201
|
+
closedServerError = error instanceof Error ? error.message : String(error);
|
|
202
|
+
}
|
|
203
|
+
const closedServerEvidence = {
|
|
204
|
+
outcome: "closed-server",
|
|
205
|
+
error: closedServerError,
|
|
206
|
+
ok: /exited|closed/i.test(closedServerError),
|
|
207
|
+
};
|
|
208
|
+
requireProbe(closedServerEvidence.ok, "closed-server diagnostics failure was not surfaced as a transport error");
|
|
209
|
+
|
|
210
|
+
const unsupportedPull = await harness.makeClient(
|
|
211
|
+
{
|
|
212
|
+
capabilities: { diagnosticProvider: { interFileDependencies: false, workspaceDiagnostics: false } },
|
|
213
|
+
publishDiagnostics: [
|
|
214
|
+
{
|
|
215
|
+
trigger: "didOpen",
|
|
216
|
+
version: 1,
|
|
217
|
+
diagnostics: [diagnostic("push-fallback")],
|
|
218
|
+
},
|
|
219
|
+
],
|
|
220
|
+
diagnosticResponses: [{ error: { code: -32601, message: "Method not found" } }],
|
|
221
|
+
},
|
|
222
|
+
{ diagnosticsFreshnessTimeoutMs: 50, versionlessPublishQuiescenceMs: 5 },
|
|
223
|
+
);
|
|
224
|
+
const unsupportedPullResult = await unsupportedPull.client.diagnostics(unsupportedPull.source);
|
|
225
|
+
const unsupportedPullEvidence = {
|
|
226
|
+
outcome: "unsupported-pull",
|
|
227
|
+
items: unsupportedPullResult.items.map((entry) => entry.message),
|
|
228
|
+
ok: JSON.stringify(unsupportedPullResult.items) === JSON.stringify([diagnostic("push-fallback")]),
|
|
229
|
+
};
|
|
230
|
+
requireProbe(unsupportedPullEvidence.ok, "explicitly unsupported pull did not fall back to push diagnostics");
|
|
231
|
+
|
|
232
|
+
const unchanged = await harness.makeClient(
|
|
233
|
+
{
|
|
234
|
+
capabilities: { diagnosticProvider: { interFileDependencies: false, workspaceDiagnostics: false } },
|
|
235
|
+
diagnosticResponses: [
|
|
236
|
+
{
|
|
237
|
+
report: { kind: "full", resultId: "same-version", items: [diagnostic("cached-full")] },
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
report: { kind: "unchanged", resultId: "same-version" },
|
|
241
|
+
},
|
|
242
|
+
],
|
|
243
|
+
},
|
|
244
|
+
{ diagnosticsFreshnessTimeoutMs: 50, versionlessPublishQuiescenceMs: 5 },
|
|
245
|
+
);
|
|
246
|
+
await unchanged.client.openFile(unchanged.source);
|
|
247
|
+
const unchangedFirst = await unchanged.client.diagnostics(unchanged.source);
|
|
248
|
+
const unchangedSecond = await unchanged.client.diagnostics(unchanged.source);
|
|
249
|
+
const unchangedEvidence = {
|
|
250
|
+
outcome: "same-version-unchanged",
|
|
251
|
+
firstItems: unchangedFirst.items.map((entry) => entry.message),
|
|
252
|
+
secondItems: unchangedSecond.items.map((entry) => entry.message),
|
|
253
|
+
ok:
|
|
254
|
+
JSON.stringify(unchangedFirst.items) === JSON.stringify([diagnostic("cached-full")])
|
|
255
|
+
&& JSON.stringify(unchangedSecond.items) === JSON.stringify([diagnostic("cached-full")]),
|
|
256
|
+
};
|
|
257
|
+
requireProbe(unchangedEvidence.ok, "unchanged pull did not reuse the cached same-version result");
|
|
258
|
+
|
|
259
|
+
writeFileSync(
|
|
260
|
+
outputPath,
|
|
261
|
+
`${JSON.stringify(
|
|
262
|
+
{
|
|
263
|
+
schemaVersion: 1,
|
|
264
|
+
outcomes: {
|
|
265
|
+
exactCurrent: exactCurrentEvidence,
|
|
266
|
+
postGenerationVersionless: postGenerationVersionlessEvidence,
|
|
267
|
+
stale: staleEvidence,
|
|
268
|
+
future: futureEvidence,
|
|
269
|
+
pullOvertaken: pullOvertakenEvidence,
|
|
270
|
+
silent: silentEvidence,
|
|
271
|
+
closedServer: closedServerEvidence,
|
|
272
|
+
unsupportedPull: unsupportedPullEvidence,
|
|
273
|
+
sameVersionUnchanged: unchangedEvidence,
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
null,
|
|
277
|
+
2,
|
|
278
|
+
)}\n`,
|
|
279
|
+
"utf-8",
|
|
280
|
+
);
|
|
281
|
+
} finally {
|
|
282
|
+
await harness.cleanup();
|
|
283
|
+
}
|