diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml index b47502bf..eb48bea6 100644 --- a/.github/workflows/canary.yml +++ b/.github/workflows/canary.yml @@ -210,6 +210,23 @@ jobs: SOURCE="$(node -p "JSON.parse(require('fs').readFileSync(process.argv[1],'utf8')).languages.find(l=>l.language==='rust').backend.source" "$TRANSCRIPTS/doctor.json")" echo "backend.source=$SOURCE (expected $EXPECT)" [ "$SOURCE" = "$EXPECT" ] + if [ "$LEG" = "omit-optional" ] && [ "$SLIM" = "true" ]; then + # The engine on this leg is the VSIX provisioned above at the + # checkout's pin, and doctor reads the version from that + # extraction's own package.json (#1066) — so it must name the pin. + # Releases up to 0.26.0 reported the CLI's compiled default instead; + # the assertion applies to everything published after. + CLI_VERSION="$(node -p "require(process.argv[1]).version" "$(npm root -g)/@debugmcp/mcp-debugger/package.json")" + FIXED="$(node -e 'const core=v=>v.split("-")[0].split(".").map(Number);const [a,b]=[core(process.argv[1]),core("0.26.0")];console.log(a[0]>b[0]||(a[0]===b[0]&&(a[1]>b[1]||(a[1]===b[1]&&a[2]>b[2]))))' "$CLI_VERSION")" + if [ "$FIXED" = "true" ]; then + PIN="$(node -p "require('./packages/codelldb-common/vendor-manifest.json').codelldb.version")" + ENGINE="$(node -p "JSON.parse(require('fs').readFileSync(process.argv[1],'utf8')).languages.find(l=>l.language==='rust').backend.version" "$TRANSCRIPTS/doctor.json")" + echo "backend.version=$ENGINE (expected $PIN)" + [ "$ENGINE" = "$PIN" ] + else + echo "::notice::$CLI_VERSION predates the engine-version fix (#1066) — backend.version assertion skipped" + fi + fi - name: Smoke cycle (mock) run: | diff --git a/changelog.d/1066.fixed.md b/changelog.d/1066.fixed.md new file mode 100644 index 00000000..a0254c2e --- /dev/null +++ b/changelog.d/1066.fixed.md @@ -0,0 +1 @@ +**`doctor` reports the CodeLLDB that actually resolved, not the CLI's compiled default** — under a `CODELLDB_PATH` pointing into a raw VSIX extraction (the documented fallback for `--omit=optional` installs, and what the canary's own leg provisions) `doctor rust`, `cpp` and `cobol` named the version the CLI was built with, because the version lookup knew only the `version.json` the vendor script writes and then walked other installs' metadata before settling on the constant. The version now comes from the resolved binary's own root and nowhere else: its `version.json` (vendored copies, the `@debugmcp/codelldb-*` packages), else the VSIX's own `package.json` when it is CodeLLDB's (`name: vscode-lldb`); a symlinked `CODELLDB_PATH` is followed to where it points; a vendored copy with no readable metadata keeps the pin, and any other binary with none reports no version rather than a guess. The canary asserts the engine version on its `CODELLDB_PATH` leg for releases after 0.26.0. Thanks @gezerui888 for the fix and the regression tests (#1066) diff --git a/docs/diagnostics.md b/docs/diagnostics.md index 91a7bc21..08173990 100644 --- a/docs/diagnostics.md +++ b/docs/diagnostics.md @@ -65,7 +65,7 @@ Usage notes: | dart | Dart SDK 3.x (`dart`), or Flutter (bundles its own Dart) | `dart debug_adapter` / `flutter debug-adapter` — the SDK's own DAP servers, behind a TCP-to-stdio bridge; nothing vendored | [dart.dev/get-dart](https://dart.dev/get-dart) (Windows `winget install Google.DartSDK`, macOS `brew install dart`, Linux the `dart` apt package) or Flutter; run `flutter --version` once so the Flutter tool cache is built | `DART_SDK` (alias `DART_PATH`), `FLUTTER_ROOT` (alias `FLUTTER_PATH`); a project's fvm pin (`.fvmrc`) is honoured | | mock | — | — | nothing (testing adapter) | — | -CodeLLDB resolution order (rust, cpp and cobol): **vendored copy → `CODELLDB_PATH` → `@debugmcp/codelldb-` package** (npm installs exactly the one matching your platform as an optional dependency). Doctor's backend column shows which source won. If you installed with `--omit=optional`, set `CODELLDB_PATH` to a [CodeLLDB release](https://github.com/vadimcn/codelldb/releases) binary. +CodeLLDB resolution order (rust, cpp and cobol): **vendored copy → `CODELLDB_PATH` → `@debugmcp/codelldb-` package** (npm installs exactly the one matching your platform as an optional dependency). Doctor's backend column shows which source won, and the engine version read from that copy's own root: its `version.json` (vendored copies, the platform packages), else a raw VSIX extraction's `package.json`; a bare binary with neither shows no version. If you installed with `--omit=optional`, set `CODELLDB_PATH` to a [CodeLLDB release](https://github.com/vadimcn/codelldb/releases) binary (extract the `.vsix` and point at `extension/adapter/codelldb`). ## Failure signatures diff --git a/packages/codelldb-common/src/codelldb-resolver.ts b/packages/codelldb-common/src/codelldb-resolver.ts index 44e68e44..4ccd4ed6 100644 --- a/packages/codelldb-common/src/codelldb-resolver.ts +++ b/packages/codelldb-common/src/codelldb-resolver.ts @@ -277,48 +277,65 @@ export async function resolveCodeLLDBExecutable(options?: { return resolved?.path ?? null; } +/** The `name` of the upstream VSIX manifest (vadimcn/codelldb's package.json). */ +const CODELLDB_VSIX_MANIFEST_NAME = 'vscode-lldb'; + +/** A parsed JSON object file, or null when it is unreadable, not JSON, or not an object. */ +async function readJsonObject(file: string): Promise | null> { + try { + const parsed: unknown = JSON.parse(await fs.readFile(file, 'utf-8')); + return parsed !== null && typeof parsed === 'object' ? (parsed as Record) : null; + } catch { + return null; + } +} + +/** The object's `version` when it is a non-empty string. */ +function versionField(data: Record | null): string | undefined { + const version = data?.version; + return typeof version === 'string' && version.length > 0 ? version : undefined; +} + /** - * Check if CodeLLDB is installed and get version + * The version of the CodeLLDB that resolves, read from that binary's own root + * and nowhere else (/adapter/codelldb → ): `version.json` — the + * vendor script writes it, the platform packages ship it — then the upstream + * VSIX manifest `package.json` when it is CodeLLDB's own, which is what a raw + * extraction under CODELLDB_PATH carries (issue #1066). Another install's + * metadata is never consulted, so the reported version cannot be + * misattributed: not a stale vendor tree under the cwd fallback, not the + * platform package's version.json when CODELLDB_PATH won, not a project's + * package.json above a hand-placed binary. A vendored copy with no readable metadata keeps the + * pin the vendor script fetched; any other source reports no version (null) + * rather than the compiled default. */ export async function getCodeLLDBVersion(options?: { resolvePlatformPackageRoot?: (platformDir: string) => string | null; }): Promise { - const resolvePlatformPackageRoot = - options?.resolvePlatformPackageRoot ?? resolveCodeLLDBPlatformPackageRoot; - const codelldbPath = await resolveCodeLLDBExecutable({ resolvePlatformPackageRoot }); - - if (!codelldbPath) { + const resolved = await resolveCodeLLDBExecutableWithSource(options); + if (!resolved) { return null; } - const platformDir = getCodeLLDBPlatformDir(process.platform, process.arch); - if (!platformDir) { - return DEFAULT_CODELLDB_VERSION; + // A symlinked CODELLDB_PATH (~/.local/bin/codelldb → the extraction) keeps + // its metadata where it points. + let binary = resolved.path; + try { + binary = await fs.realpath(resolved.path); + } catch { + // Read beside the path as given } + const root = path.resolve(path.dirname(binary), '..'); - // The version.json that describes the binary we actually resolved sits - // beside it (/adapter/codelldb -> /version.json) — probe that - // first so the reported version cannot be misattributed to a different - // install (e.g. a stale vendor tree under the cwd fallback). - const versionFileCandidates = [ - path.resolve(path.dirname(codelldbPath), '..', 'version.json'), - ...buildVendorCandidatePaths(defaultPackageRoot(), platformDir, 'version.json') - ]; - - const platformPackageRoot = resolvePlatformPackageRoot(platformDir); - if (platformPackageRoot) { - versionFileCandidates.push(path.join(platformPackageRoot, 'version.json')); + const fromVersionJson = versionField(await readJsonObject(path.join(root, 'version.json'))); + if (fromVersionJson) { + return fromVersionJson; } - - for (const versionFile of versionFileCandidates) { - try { - const versionData = await fs.readFile(versionFile, 'utf-8'); - const parsed = JSON.parse(versionData); - return parsed.version || DEFAULT_CODELLDB_VERSION; - } catch { - // Continue to next candidate - } + const manifest = await readJsonObject(path.join(root, 'package.json')); + const fromManifest = manifest?.name === CODELLDB_VSIX_MANIFEST_NAME ? versionField(manifest) : undefined; + if (fromManifest) { + return fromManifest; } - return DEFAULT_CODELLDB_VERSION; // Default version fallback + return resolved.source === 'vendored' ? DEFAULT_CODELLDB_VERSION : null; } diff --git a/packages/codelldb-common/tests/codelldb-resolver.test.ts b/packages/codelldb-common/tests/codelldb-resolver.test.ts index cc3babbe..c92d0488 100644 --- a/packages/codelldb-common/tests/codelldb-resolver.test.ts +++ b/packages/codelldb-common/tests/codelldb-resolver.test.ts @@ -10,10 +10,12 @@ import { readFileSync } from 'node:fs'; const accessMock: Mock = vi.fn(); const readFileMock: Mock = vi.fn(); +const realpathMock: Mock = vi.fn(); vi.mock('fs/promises', () => ({ access: (...args: unknown[]) => accessMock(...args), - readFile: (...args: unknown[]) => readFileMock(...args) + readFile: (...args: unknown[]) => readFileMock(...args), + realpath: (...args: unknown[]) => realpathMock(...args) })); import { @@ -48,6 +50,9 @@ describe('codelldb-resolver', () => { beforeEach(() => { accessMock.mockReset(); readFileMock.mockReset(); + // No symlinks unless a test says so + realpathMock.mockReset(); + realpathMock.mockImplementation(async (p: string) => p); // Hermetic: never let the host machine's CODELLDB_PATH leak into tests vi.stubEnv('CODELLDB_PATH', ''); }); @@ -213,6 +218,23 @@ describe('codelldb-resolver', () => { }); describe('getCodeLLDBVersion', () => { + // The version comes from the resolved binary's own root (/adapter/codelldb): + // /version.json (the vendor script's, the platform package's), then the + // upstream VSIX manifest /package.json when it is CodeLLDB's own + // (issue #1066). Another install's metadata is never read: a vendored copy with + // no readable metadata keeps the pin, any other source reports no version. + const vsixManifest = (version: string): string => + JSON.stringify({ name: 'vscode-lldb', publisher: 'vadimcn', version }); + + /** Serve `files` (path -> content; undefined = ENOENT) and ENOENT for any other path. */ + function serveFiles(files: ReadonlyMap): void { + readFileMock.mockImplementation(async (candidate: string) => { + const content = files.get(candidate); + if (content === undefined) throw new Error('ENOENT'); + return content; + }); + } + it('returns null when the executable cannot be resolved', async () => { stubPlatform('win32'); accessMock.mockRejectedValue(new Error('ENOENT')); @@ -228,22 +250,37 @@ describe('codelldb-resolver', () => { await expect(getCodeLLDBVersion()).resolves.toBe('1.12.3'); + expect(readFileMock).toHaveBeenCalledTimes(1); const versionFile = readFileMock.mock.calls[0][0] as string; expect(versionFile.endsWith(path.join('linux-x64', 'version.json'))).toBe(true); }); - it('skips a malformed version.json and reads the next candidate', async () => { + it('a vendored copy with a malformed version.json and no manifest reports the pin, reading nothing of another install', async () => { stubPlatform('linux', 'x64'); accessMock.mockResolvedValue(undefined); - readFileMock - .mockResolvedValueOnce('not-json{') - .mockResolvedValueOnce(JSON.stringify({ version: '9.9.9' })); + readFileMock.mockResolvedValueOnce('not-json{').mockRejectedValue(new Error('ENOENT')); + + await expect(getCodeLLDBVersion()).resolves.toBe(DEFAULT_CODELLDB_VERSION); + + // version.json, then package.json, both beside the resolved binary — not the + // other vendor candidates, not the platform package + const vendorDir = path.dirname(path.dirname(accessMock.mock.calls[0][0] as string)); + expect(readFileMock.mock.calls.map((call) => call[0])).toEqual([ + path.join(vendorDir, 'version.json'), + path.join(vendorDir, 'package.json') + ]); + }); + + it('a vendored version.json without a version field falls through to the VSIX manifest beside it', async () => { + stubPlatform('darwin', 'arm64'); + accessMock.mockResolvedValue(undefined); + readFileMock.mockResolvedValueOnce('{}').mockResolvedValueOnce(vsixManifest('1.12.3')); - await expect(getCodeLLDBVersion()).resolves.toBe('9.9.9'); + await expect(getCodeLLDBVersion()).resolves.toBe('1.12.3'); expect(readFileMock).toHaveBeenCalledTimes(2); }); - it('falls back to DEFAULT_CODELLDB_VERSION when no version.json is readable', async () => { + it('falls back to DEFAULT_CODELLDB_VERSION for a vendored copy with no readable metadata', async () => { stubPlatform('win32'); accessMock.mockResolvedValue(undefined); readFileMock.mockRejectedValue(new Error('ENOENT')); @@ -251,16 +288,150 @@ describe('codelldb-resolver', () => { await expect( getCodeLLDBVersion({ resolvePlatformPackageRoot: () => null }) ).resolves.toBe(DEFAULT_CODELLDB_VERSION); - // Sibling-of-resolved-binary candidate + the 4 vendor candidates - expect(readFileMock).toHaveBeenCalledTimes(5); + // The two files beside the binary and nothing else + expect(readFileMock).toHaveBeenCalledTimes(2); }); - it('falls back to DEFAULT_CODELLDB_VERSION when version.json lacks a version field', async () => { - stubPlatform('darwin', 'arm64'); - accessMock.mockResolvedValue(undefined); - readFileMock.mockResolvedValue('{}'); + describe('under CODELLDB_PATH (a raw VSIX extraction, issue #1066)', () => { + const extensionRoot = path.resolve('/raw-vsix/extension'); + const executable = path.join(extensionRoot, 'adapter', 'codelldb'); + + /** CODELLDB_PATH names the extraction's binary; the vendor candidates do not exist. */ + function stubExtraction(versionJson: string | undefined, packageJson: string | undefined): void { + stubPlatform('linux', 'x64'); + vi.stubEnv('CODELLDB_PATH', executable); + accessMock.mockImplementation(async (candidate: string) => { + if (candidate !== executable) throw new Error('ENOENT'); + }); + serveFiles(new Map([ + [path.join(extensionRoot, 'version.json'), versionJson], + [path.join(extensionRoot, 'package.json'), packageJson] + ])); + } - await expect(getCodeLLDBVersion()).resolves.toBe(DEFAULT_CODELLDB_VERSION); + it.each([ + { + name: 'reads the VSIX manifest when there is no version.json', + versionJson: undefined, + packageJson: vsixManifest('1.11.8'), + expected: '1.11.8' + }, + { + name: 'keeps version.json ahead of the manifest', + versionJson: '{"version":"1.11.7"}', + packageJson: vsixManifest('1.11.8'), + expected: '1.11.7' + }, + { + name: 'falls through to the manifest when version.json has no version field', + versionJson: '{}', + packageJson: vsixManifest('1.11.8'), + expected: '1.11.8' + }, + { + name: 'falls through to the manifest when version.json is malformed', + versionJson: '{invalid', + packageJson: vsixManifest('1.11.8'), + expected: '1.11.8' + }, + { + name: 'reports no version when the manifest is malformed', + versionJson: undefined, + packageJson: '{invalid', + expected: null + }, + { + name: 'reports no version when there is no metadata at all', + versionJson: undefined, + packageJson: undefined, + expected: null + }, + { + name: 'reports no version when the manifest has no version', + versionJson: undefined, + packageJson: vsixManifest(''), + expected: null + }, + { + name: "reports no version when the package.json beside the binary is not CodeLLDB's", + versionJson: undefined, + packageJson: '{"name":"my-app","version":"0.4.2"}', + expected: null + } + ])('$name', async ({ versionJson, packageJson, expected }) => { + stubExtraction(versionJson, packageJson); + + await expect( + getCodeLLDBVersion({ resolvePlatformPackageRoot: () => null }) + ).resolves.toBe(expected); + + // Never the compiled default, never another install's metadata: every read + // is beside the resolved binary. + expect(readFileMock.mock.calls.length).toBeGreaterThan(0); + for (const call of readFileMock.mock.calls) { + expect(path.dirname(call[0] as string)).toBe(extensionRoot); + } + }); + + it('follows a symlinked CODELLDB_PATH to the extraction it points into', async () => { + const link = path.resolve('/usr/local/bin/codelldb'); + stubExtraction(undefined, vsixManifest('1.11.8')); + vi.stubEnv('CODELLDB_PATH', link); + accessMock.mockImplementation(async (candidate: string) => { + if (candidate !== link) throw new Error('ENOENT'); + }); + realpathMock.mockImplementation(async (candidate: string) => (candidate === link ? executable : candidate)); + + await expect( + getCodeLLDBVersion({ resolvePlatformPackageRoot: () => null }) + ).resolves.toBe('1.11.8'); + expect(readFileMock.mock.calls.map((call) => call[0])).toEqual([ + path.join(extensionRoot, 'version.json'), + path.join(extensionRoot, 'package.json') + ]); + }); + + it('reads beside the path as given when it cannot be resolved', async () => { + stubExtraction(undefined, vsixManifest('1.11.8')); + realpathMock.mockRejectedValue(new Error('EACCES')); + + await expect( + getCodeLLDBVersion({ resolvePlatformPackageRoot: () => null }) + ).resolves.toBe('1.11.8'); + }); + }); + + describe('under the @debugmcp/codelldb- package', () => { + const packageRoot = path.resolve('/pkg/node_modules/@debugmcp/codelldb-linux-x64'); + const executable = path.join(packageRoot, 'adapter', 'codelldb'); + + /** Only the platform package's binary exists; its npm manifest is not CodeLLDB's. */ + function stubPlatformPackage(versionJson: string | undefined): void { + stubPlatform('linux', 'x64'); + accessMock.mockImplementation(async (candidate: string) => { + if (candidate !== executable) throw new Error('ENOENT'); + }); + serveFiles(new Map([ + [path.join(packageRoot, 'version.json'), versionJson], + [path.join(packageRoot, 'package.json'), '{"name":"@debugmcp/codelldb-linux-x64","version":"1.12.4"}'] + ])); + } + + it('reads the version.json the package ships', async () => { + stubPlatformPackage('{"version":"1.12.3"}'); + + await expect( + getCodeLLDBVersion({ resolvePlatformPackageRoot: () => packageRoot }) + ).resolves.toBe('1.12.3'); + }); + + it("reports no version without it — the npm manifest's version is the package's, not the engine's", async () => { + stubPlatformPackage(undefined); + + await expect( + getCodeLLDBVersion({ resolvePlatformPackageRoot: () => packageRoot }) + ).resolves.toBeNull(); + }); }); }); diff --git a/tests/integration/cli/doctor-codelldb-version.test.ts b/tests/integration/cli/doctor-codelldb-version.test.ts new file mode 100644 index 00000000..6589f789 --- /dev/null +++ b/tests/integration/cli/doctor-codelldb-version.test.ts @@ -0,0 +1,132 @@ +/** + * The shipped doctor command reads the CodeLLDB version from the resolved + * engine's own root (issue #1066). Under a CODELLDB_PATH pointing into the + * documented raw VSIX layout (/extension/adapter/codelldb) that is the + * extraction's `version.json` when there is one, else the VSIX's own + * `package.json`; a binary with neither reports no version at all rather than + * the CLI's compiled default. + * + * Runs the built CLI bundle from an isolated directory, so the checkout's + * vendored engine cannot win the resolution, with every language but rust + * disabled, so only the rust probe runs. + */ +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { execFile } from 'node:child_process'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { promisify } from 'node:util'; +import { DebugLanguage } from '@debugmcp/shared'; +import type { DoctorReport } from '../../../src/cli/commands/doctor/diagnose.js'; +import { scrubbedEnvironment } from '../../test-utils/helpers/mcp-stdio-client.js'; +import { requireCliBundle } from '../../test-utils/helpers/cli-bundle.js'; + +const root = fileURLToPath(new URL('../../../', import.meta.url)); +const run = promisify(execFile); +const vsixManifest = (version: string): string => + JSON.stringify({ name: 'vscode-lldb', publisher: 'vadimcn', version }); + +/** + * The rust probe runs `cargo -vV`, which the canary has seen take over 30 s on + * a cold Windows runner (its doctor step budgets 120 s): the doctor's default + * 10 s probe budget would time the row out and lose the backend cell. + */ +const DOCTOR_TIMEOUT_MS = 60_000; +const CLI_KILL_MS = 90_000; +const CASE_TIMEOUT_MS = 120_000; + +let fixtureRoot: string; +let cli: string; + +beforeAll(() => { + fixtureRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'doctor-codelldb-')); + // One copy of the bundle: its own directory and cwd keep the checkout's + // vendor tree out of the candidate walk. + cli = path.join(fixtureRoot, 'cli.mjs'); + fs.copyFileSync(requireCliBundle(root), cli); +}); + +afterAll(() => { + fs.rmSync(fixtureRoot, { recursive: true, force: true }); +}); + +/** A raw VSIX extraction: /extension/adapter/codelldb plus the metadata the case wants. */ +function extraction(versionJson: string | undefined, packageJson: string | undefined): string { + const extensionRoot = path.join(fs.mkdtempSync(path.join(fixtureRoot, 'vsix-')), 'extension'); + fs.mkdirSync(path.join(extensionRoot, 'adapter'), { recursive: true }); + const executable = path.join(extensionRoot, 'adapter', process.platform === 'win32' ? 'codelldb.exe' : 'codelldb'); + // Version discovery checks existence and reads metadata; nothing executes the engine. + fs.writeFileSync(executable, ''); + if (versionJson !== undefined) fs.writeFileSync(path.join(extensionRoot, 'version.json'), versionJson); + if (packageJson !== undefined) fs.writeFileSync(path.join(extensionRoot, 'package.json'), packageJson); + return executable; +} + +async function doctorRust(executable: string): Promise { + const { stdout } = await run( + process.execPath, + [cli, 'doctor', 'rust', '--json', '--timeout', String(DOCTOR_TIMEOUT_MS)], + { + cwd: fixtureRoot, + env: { + ...scrubbedEnvironment(), + CODELLDB_PATH: executable, + // Only the rust probe runs: the other toolchains are neither needed nor waited on. + DEBUG_MCP_DISABLE_LANGUAGES: Object.values(DebugLanguage) + .filter((language) => language !== DebugLanguage.RUST) + .join(',') + }, + timeout: CLI_KILL_MS, + windowsHide: true + } + ); + return JSON.parse(stdout) as DoctorReport; +} + +function rustBackend(report: DoctorReport): unknown { + return report.languages.find((language) => language.language === 'rust')?.backend; +} + +describe('doctor CodeLLDB version under CODELLDB_PATH (issue #1066)', () => { + it.each([ + { + name: 'reads the raw VSIX manifest', + versionJson: undefined, + packageJson: vsixManifest('1.11.8'), + expected: '1.11.8' + }, + { + name: 'prefers version.json', + versionJson: '{"version":"1.11.7"}', + packageJson: vsixManifest('1.11.8'), + expected: '1.11.7' + }, + { + name: 'falls through to the manifest when version.json has no version field', + versionJson: '{}', + packageJson: vsixManifest('1.11.8'), + expected: '1.11.8' + } + ])('$name', async ({ versionJson, packageJson, expected }) => { + const executable = extraction(versionJson, packageJson); + const report = await doctorRust(executable); + + expect(report.exitCode).toBe(0); + expect(report.requested).toEqual(['rust']); + expect(rustBackend(report)).toEqual({ + label: 'CodeLLDB', path: executable, version: expected, source: 'env:CODELLDB_PATH' + }); + }, CASE_TIMEOUT_MS); + + it("reports no version for a binary whose only neighbour is someone else's package.json", async () => { + const executable = extraction(undefined, '{"name":"my-app","version":"0.4.2"}'); + const report = await doctorRust(executable); + + expect(report.exitCode).toBe(0); + // No version key at all — not the compiled default, not my-app's 0.4.2. + expect(rustBackend(report)).toEqual({ + label: 'CodeLLDB', path: executable, source: 'env:CODELLDB_PATH' + }); + }, CASE_TIMEOUT_MS); +});