mirror of
https://github.com/software-mansion/argent.git
synced 2026-09-14 19:27:14 +08:00
c0a63875df
Found while reviewing #610 and set aside as out of scope: the code and the prose it comes from are byte-identical at that PR's merge base, and the same mutant survives there too. ## The gap `debugger-status` returns `sourceMapReady: true` as a **hardcoded literal** (`debugger-status.ts:57`). The only thing that makes the literal true is the line above it: ```ts await api.sourceMaps.waitForPending(); ``` and the description sells exactly that: *"sourceMapReady (always true — **waits for pending source maps before returning**; no-op on Chromium)"*. Nothing pins it. Deleting the await: | | existing suite (`test/debugger test/metro test/flows test/utils`) | |---|---| | unmutated | 856 passed | | await deleted | **856 passed** | So a status that reports readiness it never established would ship green. ## Scope, stated honestly This is a test-only change; the shipped behaviour is correct. The impact of a regression is narrow but real: `js-runtime-debugger.ts:214` already drains the registry once while the service is being created, so the await in the tool is what covers maps registered **after** that point - a Fast Refresh, or a lazily loaded bundle chunk. In that window a non-awaiting status would return `sourceMapReady: true` while `SourceMapsRegistry.pendingRegistrations` is non-empty, and a `debugger-inspect-element` issued straight after can miss the file:line it would otherwise resolve. I did not manufacture a user-visible failure end to end, and I am not claiming one - the finding is that the single promised behaviour of this field has no test. ## The tests Both assert the **ordering**, not the literal - a test that only checked `sourceMapReady === true` would pass on the mutant, since the literal is unconditional. 1. `does not resolve until the pending source-map registration settles` - hands the tool a `waitForPending` that returns a deferred promise, drains several macrotask turns, asserts the tool has not returned, then releases it. 2. `reports sourceMapReady only after the wait` - records `["maps-settled", "status-returned"]` and asserts that exact order. | | new tests | |---|---| | on `main` as shipped | 2 passed | | with the await deleted | **2 failed** | eslint `--max-warnings 0`, `tsc --noEmit -p tsconfig.test.json` and prettier all clean.
113 lines
3.8 KiB
TypeScript
113 lines
3.8 KiB
TypeScript
import { describe, it, expect, vi } from "vitest";
|
|
import type { Registry } from "@argent/registry";
|
|
import type { JsRuntimeDebuggerApi } from "../../src/blueprints/js-runtime-debugger";
|
|
import { createDebuggerStatusTool } from "../../src/tools/debugger/debugger-status";
|
|
|
|
// The tool resolves its api through the registry, so the fake is handed over
|
|
// there rather than injected as a service.
|
|
const resolved = vi.hoisted(() => ({ api: undefined as JsRuntimeDebuggerApi | undefined }));
|
|
vi.mock("../../src/tools/debugger/not-connected", async (importOriginal) => ({
|
|
...(await importOriginal<typeof import("../../src/tools/debugger/not-connected")>()),
|
|
resolveDebuggerService: () => Promise.resolve(resolved.api),
|
|
}));
|
|
|
|
/**
|
|
* `debugger-status` promises in its own description that `sourceMapReady` is
|
|
* "always true — waits for pending source maps before returning". The wait is the
|
|
* whole content of that promise: `sourceMapReady` is a hardcoded literal, so a
|
|
* status that returns *before* pending maps settle reports readiness it never
|
|
* established. These tests pin the ordering, not the literal.
|
|
*/
|
|
|
|
interface Deferred {
|
|
promise: Promise<void>;
|
|
resolve: () => void;
|
|
settled: boolean;
|
|
}
|
|
|
|
function deferred(): Deferred {
|
|
const d = { settled: false } as Deferred;
|
|
d.promise = new Promise<void>((res) => {
|
|
d.resolve = () => {
|
|
d.settled = true;
|
|
res();
|
|
};
|
|
});
|
|
return d;
|
|
}
|
|
|
|
function fakeApi(waitForPending: () => Promise<void>): JsRuntimeDebuggerApi {
|
|
return {
|
|
port: 8081,
|
|
projectRoot: "/mock/project",
|
|
deviceName: "MockDevice",
|
|
appName: "MockApp",
|
|
logicalDeviceId: "logical-1",
|
|
isNewDebugger: true,
|
|
cdp: {
|
|
isConnected: () => true,
|
|
getLoadedScripts: () => new Map([["1", {}]]),
|
|
getEnabledDomains: () => new Set(["Runtime", "Debugger"]),
|
|
},
|
|
sourceMaps: { waitForPending },
|
|
} as unknown as JsRuntimeDebuggerApi;
|
|
}
|
|
|
|
const PARAMS = { port: 8081, device_id: "mock-device" };
|
|
|
|
type StatusResult = Awaited<ReturnType<typeof statusTool.execute>>;
|
|
|
|
function assertConnected(
|
|
result: StatusResult
|
|
): asserts result is Extract<StatusResult, { status: "connected" }> {
|
|
expect(result.status).toBe("connected");
|
|
}
|
|
|
|
// Only the stale-connection branch reaches the registry, and `isConnected` is
|
|
// true throughout, so no member of it is ever read here.
|
|
const statusTool = createDebuggerStatusTool({} as Registry);
|
|
|
|
describe("debugger-status waits for pending source maps", () => {
|
|
it("does not resolve until the pending source-map registration settles", async () => {
|
|
const pending = deferred();
|
|
resolved.api = fakeApi(() => pending.promise);
|
|
|
|
let returned = false;
|
|
const call = statusTool.execute({}, PARAMS, undefined).then((r) => {
|
|
returned = true;
|
|
return r;
|
|
});
|
|
|
|
// Ample opportunity for a non-awaiting implementation to have returned:
|
|
// several macrotask turns, far more than the microtask drain a bare
|
|
// `return {...}` (or an `await Promise.resolve()`) would need.
|
|
for (let i = 0; i < 5; i++) await new Promise((r) => setTimeout(r, 10));
|
|
|
|
expect(pending.settled).toBe(false);
|
|
expect(returned).toBe(false);
|
|
|
|
pending.resolve();
|
|
const result = await call;
|
|
|
|
expect(pending.settled).toBe(true);
|
|
expect(returned).toBe(true);
|
|
assertConnected(result);
|
|
expect(result.sourceMapReady).toBe(true);
|
|
});
|
|
|
|
it("reports sourceMapReady only after the wait, so the flag is never ahead of the registry", async () => {
|
|
const order: string[] = [];
|
|
resolved.api = fakeApi(async () => {
|
|
await new Promise((r) => setTimeout(r, 20));
|
|
order.push("maps-settled");
|
|
});
|
|
|
|
const result = await statusTool.execute({}, PARAMS, undefined);
|
|
order.push("status-returned");
|
|
|
|
expect(order).toEqual(["maps-settled", "status-returned"]);
|
|
assertConnected(result);
|
|
expect(result.sourceMapReady).toBe(true);
|
|
});
|
|
});
|