mirror of
https://github.com/morluto/rea.git
synced 2026-11-14 18:52:48 +00:00
test: let the deadline policy own test waits
Vitest's expect.poll and vi.waitFor stop after one second by default, a host-speed guess that the project's deadline policy does not see. Under hosted-CI load, kunaProjectConcurrency failed that way twice on unrelated PRs. Another 256 waits relied on the default, and 97 passed literals picked to fit some host. Waits now take as long as their test may run, less the reporting reserve that children already keep. Each Vitest project sets expect.poll's default, and the new waitFor in tests/support/testWait.ts replaces vi.waitFor. Fixture readiness waits (awaitAcknowledgement and the provider process waits) default to the same deadline, and their host-speed literal arguments are removed. One remainingTestDeadline owns the arithmetic for both children and waits. Ten waits whose short timeout was the real check now assert it directly (Kuna version probe, Cutter discovery, doctor and Inspector cancellation, apktool fixture teardown, JADX disconnect): settlement before the production deadline that would otherwise also satisfy the wait, or a deadline pinned beyond every test deadline. The rea/project-wait-deadline lint rule rejects vi.waitFor and explicit expect.poll timeouts. The testing guide and the CI tests audit skill now document the policy. Co-Authored-By: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
This commit is contained in:
1 parent
054fbc4c26
commit
07e4b700ac
119 files changed
+818
-648
No files matched your search
@@ -97,7 +97,10 @@ Run the repository's applicable checks and inspect complete current-head CI
|
||||
results before an authorized merge. Preserve required coverage and discovery
|
||||
gates. Do not suppress a failure, add blanket retries, raise timeouts, or delete
|
||||
distinct assertions solely to make CI green. Changes to these controls need
|
||||
their own demonstrated reason and retained failure detection.
|
||||
their own demonstrated reason and retained failure detection. A slow-host
|
||||
failure at a literal test, child, or wait deadline belongs to the
|
||||
[test deadline policy](../../../docs/development/testing.md#developer-commands):
|
||||
remove the literal rather than raise it, and keep promptness oracles explicit.
|
||||
|
||||
Report each confirmed issue with exact revision, reproduction, consequence,
|
||||
proposed correction, and validation limits. Report performance with workload
|
||||
|
||||
@@ -172,6 +172,12 @@
|
||||
"vitest/no-focused-tests": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["tests/**/*.ts", "src/**/*.test.ts", "src/**/*.fixture.ts"],
|
||||
"rules": {
|
||||
"rea/project-wait-deadline": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"scripts/lib/http3-capture-assertions.test.mjs",
|
||||
|
||||
@@ -186,6 +186,9 @@ the caller's reason. Keep focused boundary and lifecycle tests for those facts.
|
||||
operation names, schemas or types; display-only selection remains allowed.
|
||||
`rea/no-fixture-root-cleanup` follows `const` fixture-root aliases into teardown
|
||||
deletion; mutable bindings and mixed root arrays remain outside its scope.
|
||||
`rea/project-wait-deadline` resolves Vitest's `vi` and `expect` bindings,
|
||||
including aliases and namespace imports; a shadowed local or a fixture method
|
||||
with the same name is not reported.
|
||||
|
||||
`rea/no-shared-module-mock` resolves Vitest import bindings before rejecting
|
||||
module mocks in projects without per-file isolation. The test-project definitions
|
||||
@@ -316,6 +319,16 @@ child fails with its captured output. Pass `timeoutMs` only when the child's dea
|
||||
behavior under test. Derive an explicit deadline only when a test must outlive
|
||||
an operation deadline whose diagnostics it checks.
|
||||
|
||||
Waits follow the same policy; Vitest's one-second `expect.poll` and
|
||||
`vi.waitFor` defaults assume a host's speed. A wait may take as long as its
|
||||
test may run, less the reporting reserve: projects configure `expect.poll`
|
||||
that way, and `waitFor` from `tests/support/testWait.ts` replaces `vi.waitFor`.
|
||||
Fixture waits such as `awaitAcknowledgement` default to the same deadline.
|
||||
`rea/project-wait-deadline` rejects `vi.waitFor` and explicit poll timeouts. When promptness is the behavior under test, such as
|
||||
cancellation that must preempt a slower production deadline, a wait timeout is
|
||||
not its oracle: order the condition before releasing the gate that would
|
||||
otherwise satisfy it, or compare elapsed time with the deadline it must beat.
|
||||
|
||||
Keep capacity fixtures intact when diagnosing a test failure. For byte equality
|
||||
of large buffers, use `Buffer.equals` or `Buffer.compare`; a generic structural
|
||||
diff can allocate far more than the production operation.
|
||||
|
||||
@@ -7,6 +7,7 @@ import noPositionalToolContract from "./rules/no-positional-tool-contract.mjs";
|
||||
import noSharedModuleMock from "./rules/no-shared-module-mock.mjs";
|
||||
import noLocaleCompare from "./rules/no-locale-compare.mjs";
|
||||
import noBabelVisitorKeys from "./rules/no-babel-visitor-keys.mjs";
|
||||
import projectWaitDeadline from "./rules/project-wait-deadline.mjs";
|
||||
|
||||
export default {
|
||||
meta: { name: "rea" },
|
||||
@@ -19,5 +20,6 @@ export default {
|
||||
"no-shared-module-mock": noSharedModuleMock,
|
||||
"no-locale-compare": noLocaleCompare,
|
||||
"no-babel-visitor-keys": noBabelVisitorKeys,
|
||||
"project-wait-deadline": projectWaitDeadline,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
import type { RuleTester } from "oxlint/plugins-dev";
|
||||
|
||||
declare const rule: Parameters<RuleTester["run"]>[1];
|
||||
export default rule;
|
||||
@@ -0,0 +1,92 @@
|
||||
import { findBinding } from "../bindings.mjs";
|
||||
|
||||
const unwrap = (node) =>
|
||||
node?.type === "ChainExpression" ? node.expression : node;
|
||||
|
||||
const propertyName = (member) => {
|
||||
if (!member?.computed && member?.property?.type === "Identifier")
|
||||
return member.property.name;
|
||||
if (member?.computed && member.property?.type === "Literal")
|
||||
return member.property.value;
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/** The `vitest` import specifier a local identifier is bound to, if any. */
|
||||
const vitestImport = (sourceCode, identifier) => {
|
||||
const definition = findBinding(sourceCode, identifier)?.defs[0];
|
||||
if (definition?.type !== "ImportBinding") return undefined;
|
||||
const declaration = definition.parent ?? definition.node.parent;
|
||||
if (declaration?.source?.value !== "vitest") return undefined;
|
||||
return definition.node;
|
||||
};
|
||||
|
||||
/** Whether `node` is Vitest's `name` export: `name` or `vitest.name`. */
|
||||
const isVitestExport = (sourceCode, node, name) => {
|
||||
const target = unwrap(node);
|
||||
if (target?.type === "Identifier") {
|
||||
const specifier = vitestImport(sourceCode, target);
|
||||
return (
|
||||
specifier?.type === "ImportSpecifier" &&
|
||||
(specifier.imported.name ?? specifier.imported.value) === name
|
||||
);
|
||||
}
|
||||
return (
|
||||
target?.type === "MemberExpression" &&
|
||||
propertyName(target) === name &&
|
||||
target.object?.type === "Identifier" &&
|
||||
vitestImport(sourceCode, target.object)?.type === "ImportNamespaceSpecifier"
|
||||
);
|
||||
};
|
||||
|
||||
const isVitestMethod = (sourceCode, callee, receiver, method) => {
|
||||
const member = unwrap(callee);
|
||||
return (
|
||||
member?.type === "MemberExpression" &&
|
||||
propertyName(member) === method &&
|
||||
isVitestExport(sourceCode, member.object, receiver)
|
||||
);
|
||||
};
|
||||
|
||||
/** A wait timeout written at the call: a number, or an options `timeout`. */
|
||||
const explicitTimeout = (options) => {
|
||||
if (options === undefined) return undefined;
|
||||
if (options.type !== "ObjectExpression") return options;
|
||||
return options.properties.find(
|
||||
(property) =>
|
||||
property.type === "Property" &&
|
||||
!property.computed &&
|
||||
(property.key.name ?? property.key.value) === "timeout",
|
||||
);
|
||||
};
|
||||
|
||||
export default {
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description:
|
||||
"Keep test wait deadlines owned by the project test deadline policy.",
|
||||
},
|
||||
schema: [],
|
||||
messages: {
|
||||
viWaitFor:
|
||||
"vi.waitFor defaults to a one-second host-speed guess; use waitFor from tests/support/testWait.ts, which the test deadline policy bounds.",
|
||||
pollTimeout:
|
||||
"The project owns expect.poll's deadline; remove this timeout. When promptness is the behavior under test, assert elapsed time or order the condition before a release gate instead.",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const sourceCode = context.sourceCode;
|
||||
return {
|
||||
CallExpression(node) {
|
||||
if (isVitestMethod(sourceCode, node.callee, "vi", "waitFor")) {
|
||||
context.report({ node, messageId: "viWaitFor" });
|
||||
return;
|
||||
}
|
||||
if (!isVitestMethod(sourceCode, node.callee, "expect", "poll")) return;
|
||||
const timeout = explicitTimeout(node.arguments[1]);
|
||||
if (timeout !== undefined)
|
||||
context.report({ node: timeout, messageId: "pollTimeout" });
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -72,6 +72,7 @@ import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import { parseEvidence } from "../domain/evidence.js";
|
||||
import type { AdbRequest } from "../domain/adb/adbDeviceAnalysis.js";
|
||||
import { awaitAcknowledgement } from "../../tests/fixtures/lifecycleAcknowledgement.js";
|
||||
import { waitFor } from "../../tests/support/testWait.js";
|
||||
|
||||
const it = test.skipIf(process.platform === "win32");
|
||||
|
||||
@@ -166,13 +167,11 @@ fi`,
|
||||
let closing: Promise<void> | undefined;
|
||||
try {
|
||||
await awaitAcknowledgement(
|
||||
vi.waitFor(
|
||||
async () => expect(await lstat(join(root, "started"))).toBeDefined(),
|
||||
{ timeout: 10_000 },
|
||||
waitFor(async () =>
|
||||
expect(await lstat(join(root, "started"))).toBeDefined(),
|
||||
),
|
||||
inspection,
|
||||
"ADB version probe",
|
||||
10_000,
|
||||
);
|
||||
let closed = false;
|
||||
closing = provider.close().then(() => {
|
||||
|
||||
@@ -36,7 +36,7 @@ it.skipIf(process.platform === "win32")(
|
||||
({ kind }) => kind === "running",
|
||||
);
|
||||
},
|
||||
{ timeout: 10_000, interval: 25 },
|
||||
{ interval: 25 },
|
||||
)
|
||||
.toBe(true);
|
||||
controller.abort();
|
||||
@@ -50,7 +50,7 @@ it.skipIf(process.platform === "win32")(
|
||||
(await Promise.all(launched.map(observePosixProcess))).every(
|
||||
({ kind }) => kind !== "running",
|
||||
),
|
||||
{ timeout: 5_000, interval: 25 },
|
||||
{ interval: 25 },
|
||||
)
|
||||
.toBe(true);
|
||||
} finally {
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
execFileOutput,
|
||||
} from "../process/ExecFileOutput.js";
|
||||
import { inspectLinuxHopperPythonRuntime } from "./LinuxHopperPython.js";
|
||||
import { waitFor } from "../../tests/support/testWait.js";
|
||||
|
||||
const response = (value: unknown) => {
|
||||
const stdout = JSON.stringify(value);
|
||||
@@ -157,8 +158,7 @@ setInterval(() => {}, 1000);
|
||||
execFileOutput(process.execPath, ["-e", program], options),
|
||||
);
|
||||
try {
|
||||
await vi.waitFor(() => access(stopping), {
|
||||
timeout: 5_000,
|
||||
await waitFor(() => access(stopping), {
|
||||
interval: 10,
|
||||
});
|
||||
controller.abort({ request: "cancel during failed command cleanup" });
|
||||
@@ -209,10 +209,9 @@ while True:
|
||||
return result;
|
||||
});
|
||||
try {
|
||||
await vi.waitFor(() => access(ready), { timeout: 5_000, interval: 10 });
|
||||
await waitFor(() => access(ready), { interval: 10 });
|
||||
controller.abort(reason);
|
||||
await vi.waitFor(() => access(stopping), {
|
||||
timeout: 3_000,
|
||||
await waitFor(() => access(stopping), {
|
||||
interval: 10,
|
||||
});
|
||||
// The child cannot exit until released; early settlement loses ownership.
|
||||
@@ -224,8 +223,7 @@ while True:
|
||||
controller.abort(reason);
|
||||
await execution;
|
||||
}
|
||||
await vi.waitFor(() => access(stopped), {
|
||||
timeout: 5_000,
|
||||
await waitFor(() => access(stopped), {
|
||||
interval: 10,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import { hashReadable } from "./ArtifactHash.js";
|
||||
import { translateArtifactFailure } from "./ArtifactOperationFailure.js";
|
||||
|
||||
import { analysisErrorUserMessage } from "../domain/analysisErrorPresentation.js";
|
||||
import { waitFor } from "../../tests/support/testWait.js";
|
||||
|
||||
const MINIMUM_ARTIFACT_EXPANDED_BYTES = 256 * 1024 * 1024;
|
||||
|
||||
@@ -398,27 +399,21 @@ describe("ZIP Zstandard member streams", () => {
|
||||
expect(decoders).toHaveLength(1);
|
||||
// A fresh decoder can succeed while an abandoned producer is stuck.
|
||||
// Verify the first actual getData and native close before starting it.
|
||||
await vi.waitFor(
|
||||
() => {
|
||||
expect(producers[0]?.settled).toBe(true);
|
||||
expect(decoders[0]?.closed).toBe(true);
|
||||
expect(decoders[0]?.destroyed).toBe(true);
|
||||
},
|
||||
{ timeout: 5_000 },
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(producers[0]?.settled).toBe(true);
|
||||
expect(decoders[0]?.closed).toBe(true);
|
||||
expect(decoders[0]?.destroyed).toBe(true);
|
||||
});
|
||||
const chunks: Buffer[] = [];
|
||||
for await (const chunk of await reader.open(entry))
|
||||
chunks.push(Buffer.from(chunk));
|
||||
expect(Buffer.concat(chunks).equals(content)).toBe(true);
|
||||
expect(producers).toHaveLength(2);
|
||||
expect(decoders).toHaveLength(2);
|
||||
await vi.waitFor(
|
||||
() => {
|
||||
expect(producers.every(({ settled }) => settled)).toBe(true);
|
||||
expect(decoders.every(({ closed }) => closed)).toBe(true);
|
||||
},
|
||||
{ timeout: 5_000 },
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(producers.every(({ settled }) => settled)).toBe(true);
|
||||
expect(decoders.every(({ closed }) => closed)).toBe(true);
|
||||
});
|
||||
} finally {
|
||||
controller.abort();
|
||||
await reader.close();
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
analyzeAppleAssetCatalogs,
|
||||
collectInterfaceBuilderResourceKeys,
|
||||
} from "./AppleAssetCatalogAnalysis.js";
|
||||
import { waitFor } from "../../../tests/support/testWait.js";
|
||||
|
||||
describe("Apple asset catalog traversal coverage", () => {
|
||||
it.for([false, true])(
|
||||
@@ -412,7 +413,7 @@ describe("Apple asset catalog and Interface Builder concurrency", () => {
|
||||
settled = true;
|
||||
});
|
||||
analysis.catch(() => undefined);
|
||||
await vi.waitFor(() => expect(catalogStarted).toBe(true));
|
||||
await waitFor(() => expect(catalogStarted).toBe(true));
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
expect(settled).toBe(false);
|
||||
finishCatalog("[]");
|
||||
@@ -452,7 +453,7 @@ describe("Apple asset catalog and Interface Builder concurrency", () => {
|
||||
signal: controller.signal,
|
||||
analyzeInterfaceBuilder: async () => emptyGraph,
|
||||
}).catch((cause: unknown) => cause);
|
||||
await vi.waitFor(() => expect(access(ready)).resolves.toBeUndefined());
|
||||
await waitFor(() => expect(access(ready)).resolves.toBeUndefined());
|
||||
controller.abort("stop asset inspection");
|
||||
expect(await settled).toMatchObject({ reason: "cancelled" });
|
||||
await expect(access(exited)).resolves.toBeUndefined();
|
||||
@@ -746,7 +747,7 @@ it.skipIf(process.platform === "win32")(
|
||||
settled = true;
|
||||
});
|
||||
try {
|
||||
await vi.waitFor(() => expect(access(exited)).resolves.toBeUndefined());
|
||||
await waitFor(() => expect(access(exited)).resolves.toBeUndefined());
|
||||
expect(settled).toBe(false);
|
||||
controller.abort("late cancellation");
|
||||
expect(settled).toBe(false);
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
PlaywrightElectronActiveProvider,
|
||||
safeElectronEnvironment,
|
||||
} from "./PlaywrightElectronActiveProvider.js";
|
||||
import { waitFor } from "../../tests/support/testWait.js";
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
@@ -1051,7 +1052,7 @@ it("reports an expired final state grace period as the caller's cancellation", a
|
||||
};
|
||||
|
||||
const capture = captureWithController(controller, application, []);
|
||||
await vi.waitFor(() => expect(evaluate).toHaveBeenCalled());
|
||||
await waitFor(() => expect(evaluate).toHaveBeenCalled());
|
||||
await vi.advanceTimersByTimeAsync(5_000);
|
||||
const result = await capture;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { BrowserContext, Page, Request, Response } from "playwright-core";
|
||||
import { browserScenarioSchema } from "../domain/browserScenario.js";
|
||||
import { resolveScenarioSecrets } from "../../tests/fixtures/browserScenarioSecrets.js";
|
||||
import { PlaywrightScenarioEvents } from "./PlaywrightScenarioEvents.js";
|
||||
import { waitFor } from "../../tests/support/testWait.js";
|
||||
|
||||
describe("declared secrets in scenario events", () => {
|
||||
it("redacts encoded declared secrets from text evidence and fingerprints", () => {
|
||||
@@ -544,7 +545,7 @@ const sourceTransactionCapture = async (
|
||||
};
|
||||
listeners.get("request")!(request);
|
||||
if (!options.metadataOnly)
|
||||
await vi.waitFor(() =>
|
||||
await waitFor(() =>
|
||||
expect(capture.result().items).toContainEqual(
|
||||
expect.objectContaining({ kind: "network-content", phase: "request" }),
|
||||
),
|
||||
@@ -592,7 +593,7 @@ const sourceTransactionCapture = async (
|
||||
});
|
||||
listeners.get("requestfinished")!(followup);
|
||||
if (!options.metadataOnly)
|
||||
await vi.waitFor(() =>
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
capture
|
||||
.result()
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { BrowserScenarioAction } from "../domain/browserScenarioValues.js";
|
||||
import { BrowserScenarioSecrets } from "./BrowserScenarioSecrets.js";
|
||||
import { responsePointerValue } from "./BrowserScenarioResponseValues.js";
|
||||
import { performPlaywrightScenarioAction } from "./PlaywrightScenarioActions.js";
|
||||
import { waitFor } from "../../tests/support/testWait.js";
|
||||
|
||||
const ORIGIN = "https://app.example.test";
|
||||
|
||||
@@ -1614,7 +1615,7 @@ describe("ambiguous saved response observations", () => {
|
||||
]);
|
||||
const running = perform(page, parsed.actions[0]!, secrets);
|
||||
const rejected = expect(running).rejects.toThrow("ambiguous");
|
||||
await vi.waitFor(() => expect(reads.count).toBe(1));
|
||||
await waitFor(() => expect(reads.count).toBe(1));
|
||||
for (const listener of listeners)
|
||||
listener({ method: () => "POST", url: () => `${ORIGIN}/api/items` });
|
||||
release('{"id":"wrong-object"}');
|
||||
|
||||
@@ -121,6 +121,7 @@ vi.mock("./PlaywrightScenarioEvents.js", () => ({
|
||||
import { browserScenarioSchema } from "../domain/browserScenario.js";
|
||||
import { PlaywrightScenarioSession } from "./PlaywrightScenarioSession.js";
|
||||
import { PlaywrightScenarioBrowserCleanupOwner } from "./PlaywrightScenarioBrowser.js";
|
||||
import { waitFor } from "../../tests/support/testWait.js";
|
||||
|
||||
const scenario = browserScenarioSchema.parse({
|
||||
browser: { mode: "launch", executable_path: process.execPath },
|
||||
@@ -1304,7 +1305,7 @@ it("can retry cleanup of a browser that opened after request cancellation", asyn
|
||||
controller.abort();
|
||||
await cancelled;
|
||||
resolveOpening?.(lateOpened);
|
||||
await vi.waitFor(() => expect(closeBrowser).toHaveBeenCalledTimes(1));
|
||||
await waitFor(() => expect(closeBrowser).toHaveBeenCalledTimes(1));
|
||||
expect(retained).toHaveLength(1);
|
||||
await expect(retained[0]?.release()).resolves.toBeUndefined();
|
||||
expect(closeBrowser).toHaveBeenCalledTimes(2);
|
||||
|
||||
@@ -70,7 +70,7 @@ describe("runSolveHelper", () => {
|
||||
// agentenv keeps a sandbox's folder until its teardown is confirmed, so the
|
||||
// emptied private root shows nothing is left up: only the cancellation remains.
|
||||
try {
|
||||
await awaitAcknowledgement(started, solved, "solve helper start", 10_000);
|
||||
await awaitAcknowledgement(started, solved, "solve helper start");
|
||||
stop.abort(new Error("cancelled by the test"));
|
||||
const failure: unknown = await solved.then(
|
||||
() => undefined,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { chmod, readFile, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { expect, it, vi } from "vitest";
|
||||
import { expect, it } from "vitest";
|
||||
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
|
||||
import { ScreenJudgeProcess } from "./ScreenJudgeProcess.js";
|
||||
import { waitFor } from "../../tests/support/testWait.js";
|
||||
|
||||
const check = { id: "x", kind: "screen_judge", question: "ready?" } as const;
|
||||
|
||||
@@ -68,13 +69,10 @@ it.skipIf(process.platform === "win32")(
|
||||
void running.catch(() => undefined);
|
||||
let pid = 0;
|
||||
try {
|
||||
await vi.waitFor(
|
||||
async () => {
|
||||
pid = Number(await readFile(marker, "utf8"));
|
||||
expect(pid).toBeGreaterThan(0);
|
||||
},
|
||||
{ timeout: 10000 },
|
||||
);
|
||||
await waitFor(async () => {
|
||||
pid = Number(await readFile(marker, "utf8"));
|
||||
expect(pid).toBeGreaterThan(0);
|
||||
});
|
||||
controller.abort(new Error("cancelled by caller"));
|
||||
await expect(running).rejects.toThrow();
|
||||
let state = "";
|
||||
|
||||
@@ -14,6 +14,7 @@ import type {
|
||||
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import { ScreenJudgeProcess } from "./ScreenJudgeProcess.js";
|
||||
import { waitFor } from "../../tests/support/testWait.js";
|
||||
|
||||
vi.mock("./AgentEnvComputerHelper.js", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("./AgentEnvComputerHelper.js")>()),
|
||||
@@ -261,7 +262,7 @@ it("waits for final process cleanup after cancellation and keeps its owner", asy
|
||||
settled = true;
|
||||
});
|
||||
// Wait for the mocked command to receive control, not just for the judge to start.
|
||||
await vi.waitFor(() =>
|
||||
await waitFor(() =>
|
||||
expect(vi.mocked(runOwnedCommand).mock.calls.at(-1)?.[2]?.signal).toBe(
|
||||
controller.signal,
|
||||
),
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from "./ProcessCaptureError.js";
|
||||
import { ProcessCaptureResourceScope } from "./ProcessCaptureLifecycle.js";
|
||||
import { startProcessSampler } from "./ProcessSampling.js";
|
||||
import { waitFor } from "../../../tests/support/testWait.js";
|
||||
|
||||
vi.mock("./LinuxProcessSampling.js", async (original) => ({
|
||||
...(await original<typeof import("./LinuxProcessSampling.js")>()),
|
||||
@@ -135,7 +136,7 @@ it.each(["plain", "all-cancel", "mixed-fd", "opaque-peer"] as const)(
|
||||
Object.defineProperty(process, "platform", platform);
|
||||
}
|
||||
});
|
||||
await vi.waitFor(() => expect(environment).toHaveBeenCalledOnce());
|
||||
await waitFor(() => expect(environment).toHaveBeenCalledOnce());
|
||||
const outcome = stop();
|
||||
if (kind === "opaque-peer") {
|
||||
const failure: unknown = await outcome.catch((cause: unknown) => cause);
|
||||
|
||||
@@ -2,13 +2,14 @@ import { once } from "node:events";
|
||||
import { createServer, type Server } from "node:http";
|
||||
import { connect } from "node:net";
|
||||
import type { AddressInfo } from "node:net";
|
||||
import { afterEach, expect, it, vi } from "vitest";
|
||||
import { afterEach, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
toWebRequest,
|
||||
writeWebResponse,
|
||||
type WebResponseOptions,
|
||||
} from "./nodeHttpBridge.js";
|
||||
import { waitFor } from "../../../tests/support/testWait.js";
|
||||
|
||||
const servers: Server[] = [];
|
||||
const encoder = new TextEncoder();
|
||||
@@ -66,7 +67,7 @@ it("ends a HEAD reply after the headers and cancels a body the handler attached"
|
||||
const response = await fetch(`${origin}/events`, { method: "HEAD" });
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get("content-type")).toBe("text/event-stream");
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(cancelled).toBe(true);
|
||||
});
|
||||
expect(pulls).toBeLessThanOrEqual(1);
|
||||
@@ -93,7 +94,7 @@ it("finishes a HEAD reply without waiting on a teed body whose sibling stays liv
|
||||
method: "HEAD",
|
||||
});
|
||||
expect(response.status).toBe(200);
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(finished).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -126,11 +127,11 @@ it("cancels the body when an asynchronous route resolves after its client left",
|
||||
const socket = connect(port, "127.0.0.1");
|
||||
await once(socket, "connect");
|
||||
socket.write("GET /late HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n");
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(received).toBe(true);
|
||||
});
|
||||
socket.destroy();
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(settled).toBe(true);
|
||||
expect(cancelled).toBe(true);
|
||||
});
|
||||
@@ -165,7 +166,7 @@ it("returns when a client leaves a teed streaming body whose sibling stays live"
|
||||
const first = await response.body.getReader().read();
|
||||
expect(decoder.decode(first.value)).toBe("data: first\n\n");
|
||||
leave.abort();
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(settled).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -266,7 +267,7 @@ it("aborts the web Request and cancels its response body when the client leaves"
|
||||
if (reader === undefined) throw new Error("Response body missing");
|
||||
await reader.read();
|
||||
controller.abort();
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(aborted).toBe(true);
|
||||
expect(cancelled).toBe(true);
|
||||
});
|
||||
|
||||
@@ -25,6 +25,7 @@ import { StreamableHttpListenError } from "./streamableHttpListenError.js";
|
||||
import { mcpAllowedHostsTextSchema } from "../../config/mcpHttpEndpoint.js";
|
||||
import { parseMcpHttpArguments } from "../../main/mcpHttpArguments.js";
|
||||
import { RequestTrackingMcpServer } from "../RequestTrackingMcpServer.js";
|
||||
import { waitFor } from "../../../tests/support/testWait.js";
|
||||
|
||||
const servers: StreamableHttpServer[] = [];
|
||||
|
||||
@@ -1318,7 +1319,7 @@ it.each(["chunk", "headers"])(
|
||||
.catch(() => undefined);
|
||||
let closing: Promise<void> | undefined;
|
||||
try {
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(cancelled).toBe(1);
|
||||
});
|
||||
expect((await fetch(new URL("/health", served.url))).status).toBe(503);
|
||||
@@ -1403,7 +1404,7 @@ it("retains failed response cleanup and blocks provider teardown and further adm
|
||||
const response = await fetch(new URL("/failed", served.url));
|
||||
try {
|
||||
await response.body?.cancel();
|
||||
await vi.waitFor(async () => {
|
||||
await waitFor(async () => {
|
||||
expect((await fetch(new URL("/health", served.url))).status).toBe(503);
|
||||
});
|
||||
await expect(served.prepareShutdown()).rejects.toMatchObject({
|
||||
@@ -1459,7 +1460,7 @@ it("retains plain-response admission until asynchronous cancellation settles", a
|
||||
expect((await fetch(new URL("/health", served.url))).status).toBe(503);
|
||||
expect(health).not.toHaveBeenCalled();
|
||||
cleanup.release();
|
||||
await vi.waitFor(async () => {
|
||||
await waitFor(async () => {
|
||||
expect((await fetch(new URL("/health", served.url))).status).toBe(200);
|
||||
});
|
||||
} finally {
|
||||
@@ -1562,10 +1563,10 @@ it.each(["pending handler", "open response"])(
|
||||
await first.promise;
|
||||
socket.write(request("/second"));
|
||||
socket.write(request("/health").repeat(32));
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(socket.destroyed).toBe(true);
|
||||
});
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(finalized).toBe(mode === "pending handler" ? 1 : 0);
|
||||
});
|
||||
expect(secondCalled).toBe(0);
|
||||
@@ -1687,12 +1688,12 @@ it("retains admission for an unread upload attached to a long response", async (
|
||||
upload.once("error", () => undefined);
|
||||
upload.write("prefix");
|
||||
try {
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(received).toBe(true);
|
||||
});
|
||||
expect((await fetch(new URL("/health", served.url))).status).toBe(503);
|
||||
upload.destroy();
|
||||
await vi.waitFor(async () => {
|
||||
await waitFor(async () => {
|
||||
expect((await fetch(new URL("/health", served.url))).status).toBe(200);
|
||||
});
|
||||
} finally {
|
||||
@@ -1737,7 +1738,7 @@ it("bounds HTTP input dispatch globally and releases admission when an upload le
|
||||
}
|
||||
// A plain route observes the same budget while both sessionless MCP probes
|
||||
// wait for the rest of their bodies. Idle SSE connections consume no permit.
|
||||
await vi.waitFor(async () => {
|
||||
await waitFor(async () => {
|
||||
expect((await fetch(new URL("/health", served.url))).status).toBe(503);
|
||||
});
|
||||
const refused = await rawHttpRequest(new URL(served.url), {
|
||||
@@ -1752,7 +1753,7 @@ it("bounds HTTP input dispatch globally and releases admission when an upload le
|
||||
expect(refused.headers.connection).toBe("close");
|
||||
uploads[0]?.destroy();
|
||||
await pending[0];
|
||||
await vi.waitFor(async () => {
|
||||
await waitFor(async () => {
|
||||
expect((await fetch(new URL("/health", served.url))).status).toBe(200);
|
||||
});
|
||||
expect(await echo(first.client, "kept during overload")).toEqual([
|
||||
@@ -1862,7 +1863,7 @@ it.each(["DELETE", "shutdown"])(
|
||||
upload.write('{"jsonrpc":"2.0","id":2,"method":"tools/call",');
|
||||
});
|
||||
try {
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
entered.mock.calls.some(
|
||||
([request]) =>
|
||||
|
||||
@@ -8,13 +8,14 @@ import { dirname, resolve } from "node:path";
|
||||
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
import { expect, onTestFinished, vi } from "vitest";
|
||||
import { expect, onTestFinished } from "vitest";
|
||||
import { z } from "zod";
|
||||
|
||||
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
|
||||
import { awaitAcknowledgement } from "../../fixtures/lifecycleAcknowledgement.js";
|
||||
import { workspaceCliTest } from "../../support/cli/workspaceCliFixture.js";
|
||||
import { acquireHopperTargetLease } from "../../../src/process/HopperLease.js";
|
||||
import { waitFor } from "../../support/testWait.js";
|
||||
|
||||
const statusSchema = z.object({ result: z.object({ open: z.boolean() }) });
|
||||
const test = workspaceCliTest.skipIf(process.platform !== "darwin");
|
||||
@@ -365,7 +366,7 @@ test.for(["handoff", "not-found"] as const)(
|
||||
[runId, "different-token"],
|
||||
] as const) {
|
||||
const wrong = lateBridge(identity, credential);
|
||||
await vi.waitFor(() => access(socket));
|
||||
await waitFor(() => access(socket));
|
||||
expect(parseMcpToolError(await call("close_binary")).error.code).toBe(
|
||||
"cleanup_incomplete",
|
||||
);
|
||||
@@ -375,7 +376,7 @@ test.for(["handoff", "not-found"] as const)(
|
||||
await rm(socket, { force: true });
|
||||
}
|
||||
const owned = await closeWithLateBridge(mcp, () => lateBridge(runId));
|
||||
await vi.waitFor(() => expect(owned.exitCode).toBe(0));
|
||||
await waitFor(() => expect(owned.exitCode).toBe(0));
|
||||
await expect(access(runtime)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
await writeFile(mode, "bridge");
|
||||
expect((await call("open_binary", { path: target })).isError).not.toBe(
|
||||
|
||||
@@ -53,7 +53,7 @@ it("preserves file URL scheme spelling and script identity after real Inspector
|
||||
stderr += chunk.toString();
|
||||
});
|
||||
const address = /ws:\/\/127\.0\.0\.1:\d+\/[^\s]+/u;
|
||||
await expect.poll(() => stderr, { timeout: 5000 }).toMatch(address);
|
||||
await expect.poll(() => stderr).toMatch(address);
|
||||
const socket = address.exec(stderr)?.[0];
|
||||
if (socket === undefined) throw new Error("Node omitted its Inspector URL");
|
||||
const endpoint = `http://127.0.0.1:${new URL(socket).port}`;
|
||||
|
||||
@@ -29,7 +29,7 @@ it("observes a real Node Inspector and releases attachments after cancellation a
|
||||
stderr += chunk.toString();
|
||||
});
|
||||
const address = /ws:\/\/127\.0\.0\.1:\d+\/[^\s]+/u;
|
||||
await expect.poll(() => stderr, { timeout: 5000 }).toMatch(address);
|
||||
await expect.poll(() => stderr).toMatch(address);
|
||||
const socket = address.exec(stderr)?.[0];
|
||||
if (socket === undefined) throw new Error("Node omitted its Inspector URL");
|
||||
const endpoint = `http://127.0.0.1:${new URL(socket).port}`;
|
||||
@@ -91,17 +91,21 @@ it("observes a real Node Inspector and releases attachments after cancellation a
|
||||
const detachments = () => (stderr.match(/Debugger ending on/gu) ?? []).length;
|
||||
const before = attachments();
|
||||
const controller = new AbortController();
|
||||
const observationMs = 10_000;
|
||||
const started = performance.now();
|
||||
const cancelled = expect(
|
||||
call(
|
||||
"observe_javascript_runtime",
|
||||
{ ...request, observation_ms: 10_000 },
|
||||
{ ...request, observation_ms: observationMs },
|
||||
{ signal: controller.signal },
|
||||
),
|
||||
).rejects.toThrow("cancel live observation");
|
||||
await expect.poll(attachments, { timeout: 5000 }).toBe(before + 1);
|
||||
await expect.poll(attachments).toBe(before + 1);
|
||||
controller.abort(new Error("cancel live observation"));
|
||||
await cancelled;
|
||||
await expect.poll(detachments, { timeout: 5000 }).toBe(attachments());
|
||||
await expect.poll(detachments).toBe(attachments());
|
||||
// An ignored cancellation still detaches, but only when the window ends.
|
||||
expect(performance.now() - started).toBeLessThan(observationMs);
|
||||
expect(child.exitCode).toBeNull();
|
||||
expect(child.signalCode).toBeNull();
|
||||
expect((await call("observe_javascript_runtime", request)).isError).not.toBe(
|
||||
|
||||
@@ -181,10 +181,8 @@ cliTest.skipIf(process.platform === "win32")(
|
||||
if (decoder === undefined)
|
||||
throw new Error("No decoder process was observed");
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
observeProcessStartIdentity(decoder.pid, undefined, decoder.run_id),
|
||||
{ timeout: 6000 },
|
||||
.poll(() =>
|
||||
observeProcessStartIdentity(decoder.pid, undefined, decoder.run_id),
|
||||
)
|
||||
.toBeUndefined();
|
||||
}
|
||||
|
||||
@@ -182,7 +182,7 @@ it.skipIf(process.platform === "win32")(
|
||||
({ size }) => size > 0,
|
||||
() => false,
|
||||
),
|
||||
{ timeout: 10_000, interval: 10 },
|
||||
{ interval: 10 },
|
||||
)
|
||||
.toBe(true);
|
||||
controller.abort();
|
||||
|
||||
@@ -358,13 +358,11 @@ it("cancels an in-flight Java readiness probe", async () => {
|
||||
);
|
||||
try {
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
readFile(marker, "utf8").then(
|
||||
() => true,
|
||||
() => false,
|
||||
),
|
||||
{ timeout: 10_000 },
|
||||
.poll(() =>
|
||||
readFile(marker, "utf8").then(
|
||||
() => true,
|
||||
() => false,
|
||||
),
|
||||
)
|
||||
.toBe(true);
|
||||
controller.abort();
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { expect, it, vi } from "vitest";
|
||||
import { expect, it } from "vitest";
|
||||
import { WebRuntimeService } from "../../../src/application/WebRuntimeService.js";
|
||||
import { CdpWebRuntimeProvider } from "../../../src/browser/execution/CdpWebRuntimeProvider.js";
|
||||
import { AnalysisOutputError } from "../../../src/domain/analysisErrorCore.js";
|
||||
import { startRuntimeBrowser } from "../../fixtures/webRuntime.js";
|
||||
import { waitFor } from "../../support/testWait.js";
|
||||
|
||||
it.each([
|
||||
new Error("operator stopped observation"),
|
||||
@@ -41,10 +42,10 @@ it.each([
|
||||
(command) => command.method === "Profiler.stopPreciseCoverage",
|
||||
),
|
||||
).toHaveLength(2);
|
||||
await vi.waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
await waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
const after = await service.observe(input);
|
||||
if (!after.ok) throw after.error;
|
||||
await vi.waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
await waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
} finally {
|
||||
await provider.close();
|
||||
await browser.close();
|
||||
@@ -85,7 +86,7 @@ it("preserves the caller reason when a navigation observation is cancelled at ar
|
||||
expect(result.error._tag).toBe("AnalysisCancelledError");
|
||||
expect(result.error.cause).toBe(reason);
|
||||
expect(result.error.cleanupIncomplete).toBe(false);
|
||||
await vi.waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
await waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
} finally {
|
||||
await provider.close();
|
||||
await browser.close();
|
||||
|
||||
@@ -19,6 +19,7 @@ import { captureWebScreenshotInputSchema } from "../../../src/domain/webScreensh
|
||||
import { startFakeCdpBrowser } from "../../fixtures/fakeCdpBrowser.js";
|
||||
import { awaitAcknowledgement } from "../../fixtures/lifecycleAcknowledgement.js";
|
||||
import { trackBrowser } from "./cdpBrowserProvider.support.js";
|
||||
import { waitFor } from "../../support/testWait.js";
|
||||
|
||||
describe("CdpBrowserProvider: browser socket without HTTP discovery", () => {
|
||||
it("reuses one admitted browser socket across sequential operations and closes on provider shutdown", async () => {
|
||||
@@ -97,7 +98,7 @@ describe("CdpBrowserProvider: browser socket without HTTP discovery", () => {
|
||||
} finally {
|
||||
await provider.close();
|
||||
}
|
||||
await vi.waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
await waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -117,7 +118,7 @@ describe("CdpBrowserProvider: browser socket eviction and per-call scoping", ()
|
||||
expect(browser.activeConnectionCount()).toBe(1);
|
||||
|
||||
browser.closeActiveConnections();
|
||||
await vi.waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
await waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
|
||||
const second = await provider.inspectPage(
|
||||
inspectWebPageInputSchema.parse({
|
||||
@@ -133,7 +134,7 @@ describe("CdpBrowserProvider: browser socket eviction and per-call scoping", ()
|
||||
} finally {
|
||||
await provider.close();
|
||||
}
|
||||
await vi.waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
await waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
});
|
||||
|
||||
it("closes a shared browser socket when Target.detachFromTarget fails so no target session leaks", async () => {
|
||||
@@ -154,7 +155,7 @@ describe("CdpBrowserProvider: browser socket eviction and per-call scoping", ()
|
||||
}),
|
||||
);
|
||||
if (!inspected.ok) throw inspected.error;
|
||||
await vi.waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
await waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
expect(browser.openedConnectionCount()).toBe(1);
|
||||
|
||||
const listed = await provider.listTargets(
|
||||
@@ -168,7 +169,7 @@ describe("CdpBrowserProvider: browser socket eviction and per-call scoping", ()
|
||||
} finally {
|
||||
await provider.close();
|
||||
}
|
||||
await vi.waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
await waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
});
|
||||
|
||||
it("preserves per-operation command_timeout_ms and operation identity on a reused browser socket", async () => {
|
||||
@@ -208,7 +209,7 @@ describe("CdpBrowserProvider: browser socket eviction and per-call scoping", ()
|
||||
} finally {
|
||||
await provider.close();
|
||||
}
|
||||
await vi.waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
await waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -275,7 +276,7 @@ it("retains browser-socket siblings and reports invalid attachment as unknown, n
|
||||
} finally {
|
||||
await provider.close();
|
||||
}
|
||||
await vi.waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
await waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
});
|
||||
|
||||
describe("CdpBrowserProvider: browser socket failures and cleanup", () => {
|
||||
@@ -315,7 +316,7 @@ describe("CdpBrowserProvider: browser socket failures and cleanup", () => {
|
||||
} finally {
|
||||
await provider.close();
|
||||
}
|
||||
await vi.waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
await waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
});
|
||||
|
||||
it("rejects malformed browser-socket discovery and closes the socket", async () => {
|
||||
@@ -340,7 +341,7 @@ describe("CdpBrowserProvider: browser socket failures and cleanup", () => {
|
||||
reason: "invalid_endpoint_response",
|
||||
},
|
||||
});
|
||||
await vi.waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
await waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
});
|
||||
|
||||
it("reports a refused browser socket as unreachable", async () => {
|
||||
@@ -379,7 +380,7 @@ describe("CdpBrowserProvider: browser socket failures and cleanup", () => {
|
||||
}),
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
await vi.waitFor(() =>
|
||||
await waitFor(() =>
|
||||
expect(browser.commands.map(({ method }) => method)).toContain(
|
||||
"Target.getTargets",
|
||||
),
|
||||
@@ -393,7 +394,7 @@ describe("CdpBrowserProvider: browser socket failures and cleanup", () => {
|
||||
expect(browser.commands.map(({ method }) => method)).not.toContain(
|
||||
"Target.attachToTarget",
|
||||
);
|
||||
await vi.waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
await waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -429,7 +430,7 @@ describe("CdpBrowserProvider: browser socket discovery deadline", () => {
|
||||
});
|
||||
trackBrowser(browser);
|
||||
await expectDiscoveryTimeout(browser.browserWebSocketUrl);
|
||||
await vi.waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
await waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
});
|
||||
|
||||
it("times out a WebSocket handshake that never completes", async () => {
|
||||
@@ -451,7 +452,7 @@ describe("CdpBrowserProvider: browser socket discovery deadline", () => {
|
||||
await expectDiscoveryTimeout(
|
||||
`ws://127.0.0.1:${String(address.port)}/devtools/browser/stalled`,
|
||||
);
|
||||
await vi.waitFor(() => expect(sockets.size).toBe(0));
|
||||
await waitFor(() => expect(sockets.size).toBe(0));
|
||||
} finally {
|
||||
for (const socket of sockets) socket.destroy();
|
||||
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||
@@ -510,7 +511,7 @@ describe("CdpBrowserProvider: shared browser socket lifecycle regressions", () =
|
||||
if (!inspected.ok) throw inspected.error;
|
||||
expect(closeSpy).toHaveBeenCalledTimes(1);
|
||||
expect(browser.openedConnectionCount()).toBe(1);
|
||||
await vi.waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
await waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
|
||||
const session = await observing;
|
||||
if (!session.ok) throw session.error;
|
||||
@@ -586,7 +587,7 @@ describe("CdpBrowserProvider: shared browser socket lifecycle regressions", () =
|
||||
denyClose = false;
|
||||
await provider.close();
|
||||
expect(inspected.error.cleanupIncomplete).toBe(false);
|
||||
await vi.waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
await waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
} finally {
|
||||
denyClose = false;
|
||||
controller.abort(new Error("test teardown"));
|
||||
@@ -625,7 +626,7 @@ describe("CdpTargetSessionOwner: callback failures and idle protocol eviction",
|
||||
await owner.retryPending();
|
||||
expect(cleanup).toHaveBeenCalledTimes(1);
|
||||
expect(failure.cleanupIncomplete).toBe(false);
|
||||
await vi.waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
await waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
} finally {
|
||||
await owner.close();
|
||||
}
|
||||
@@ -654,7 +655,7 @@ describe("CdpTargetSessionOwner: callback failures and idle protocol eviction",
|
||||
expect(browser.activeConnectionCount()).toBe(1);
|
||||
|
||||
browser.emitRawMessage("{not-json");
|
||||
await vi.waitFor(() => expect(closeSpy).toHaveBeenCalledTimes(1));
|
||||
await waitFor(() => expect(closeSpy).toHaveBeenCalledTimes(1));
|
||||
|
||||
const blocked = await provider.listTargets(
|
||||
listBrowserTargetsInputSchema.parse({
|
||||
@@ -676,7 +677,7 @@ describe("CdpTargetSessionOwner: callback failures and idle protocol eviction",
|
||||
expect(browser.activeConnectionCount()).toBe(1);
|
||||
|
||||
browser.emitRawMessage("{not-json-idle-second");
|
||||
await vi.waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
await waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
} finally {
|
||||
closeSpy.mockRestore();
|
||||
await provider.close();
|
||||
@@ -777,7 +778,7 @@ it("blocks new admissions while forced shared-socket cleanup fails with an activ
|
||||
closeSpy.mockRestore();
|
||||
await provider.close();
|
||||
}
|
||||
await vi.waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
await waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
});
|
||||
|
||||
it("keeps an admitted shared browser socket alive for concurrent peers when a caller aborts or hits discovery_timeout_ms", async () => {
|
||||
@@ -884,7 +885,7 @@ it("keeps an admitted shared browser socket alive for concurrent peers when a ca
|
||||
await Promise.allSettled([observing, cancelling]);
|
||||
await provider.close();
|
||||
}
|
||||
await vi.waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
await waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
});
|
||||
|
||||
describe("CdpBrowserProvider: coalesced cold-start browser sockets", () => {
|
||||
@@ -946,7 +947,7 @@ describe("CdpBrowserProvider: coalesced cold-start browser sockets", () => {
|
||||
await batch?.catch(() => undefined);
|
||||
await provider.close();
|
||||
}
|
||||
await vi.waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
await waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
});
|
||||
|
||||
it("keeps an in-flight cold-start handshake alive for a longer-deadline peer when one waiter times out", async () => {
|
||||
@@ -1003,7 +1004,7 @@ describe("CdpBrowserProvider: coalesced cold-start browser sockets", () => {
|
||||
await Promise.allSettled([shortCall, longCall]);
|
||||
await provider.close();
|
||||
}
|
||||
await vi.waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
await waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1167,7 +1168,7 @@ it("isolates concurrent target sessions on one shared browser socket when a shor
|
||||
await Promise.allSettled([observing, inspecting, capturing]);
|
||||
await provider.close();
|
||||
}
|
||||
await vi.waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
await waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
});
|
||||
|
||||
it("reuses one browser socket across equivalent normalized spellings while keeping 127.0.0.1 and [::1] distinct", async () => {
|
||||
@@ -1232,7 +1233,7 @@ it("reuses one browser socket across equivalent normalized spellings while keepi
|
||||
connectSpy.mockRestore();
|
||||
await provider.close();
|
||||
}
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(ipv6Browser.activeConnectionCount()).toBe(0);
|
||||
expect(ipv4Browser.activeConnectionCount()).toBe(0);
|
||||
});
|
||||
@@ -1328,7 +1329,7 @@ it("keeps distinct /devtools/browser/<id> paths on separate browser sockets whil
|
||||
connectSpy.mockRestore();
|
||||
await provider.close();
|
||||
}
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(browserA.activeConnectionCount()).toBe(0);
|
||||
expect(browserB.activeConnectionCount()).toBe(0);
|
||||
expect(browserCaseA.activeConnectionCount()).toBe(0);
|
||||
|
||||
@@ -16,6 +16,7 @@ import { analyzeWebBundleInputSchema } from "../../../src/domain/webBundleAnalys
|
||||
import { inspectElectronPageInputSchema } from "../../../src/domain/javascript/electronObservation.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { startFakeCdpBrowser } from "../../fixtures/fakeCdpBrowser.js";
|
||||
import { waitFor } from "../../support/testWait.js";
|
||||
|
||||
it.each(["Page.disable", "Target.detachFromTarget"])(
|
||||
"finishes caller cancellation even when %s has no response",
|
||||
@@ -53,7 +54,7 @@ it.each(["Page.disable", "Target.detachFromTarget"])(
|
||||
expect(methods).toContain("Target.detachFromTarget");
|
||||
expect(methods).not.toContain("Target.closeTarget");
|
||||
expect(methods).not.toContain("Browser.close");
|
||||
await vi.waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
await waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
},
|
||||
);
|
||||
|
||||
@@ -72,7 +73,7 @@ it("releases cleanup when cancellation arrives after capture completes", async (
|
||||
}),
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
browser.commands.some(({ method }) => method === "Page.disable"),
|
||||
).toBe(true);
|
||||
@@ -89,7 +90,7 @@ it("releases cleanup when cancellation arrives after capture completes", async (
|
||||
expect(browser.commands.map(({ method }) => method)).toContain(
|
||||
"Target.detachFromTarget",
|
||||
);
|
||||
await vi.waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
await waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
const methods = browser.commands.map(({ method }) => method);
|
||||
expect(methods).not.toContain("Target.closeTarget");
|
||||
expect(methods).not.toContain("Browser.close");
|
||||
@@ -132,7 +133,7 @@ it("retains the page inspection when bundle cleanup is cancelled", async () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
await vi.waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
await waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
});
|
||||
|
||||
it("closes an Electron inspection session when cleanup is cancelled", async () => {
|
||||
@@ -183,7 +184,7 @@ it("closes an Electron inspection session when cleanup is cancelled", async () =
|
||||
expect(methods).toContain("Target.detachFromTarget");
|
||||
expect(methods).not.toContain("Target.closeTarget");
|
||||
expect(methods).not.toContain("Browser.close");
|
||||
await vi.waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
await waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
expect((await fetch(`${browser.endpoint}/json/version`)).ok).toBe(true);
|
||||
});
|
||||
|
||||
@@ -234,7 +235,7 @@ it("classifies cancellation that arrives during final Electron detach", async ()
|
||||
expect(methods).toContain("Target.detachFromTarget");
|
||||
expect(methods).not.toContain("Target.closeTarget");
|
||||
expect(methods).not.toContain("Browser.close");
|
||||
await vi.waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
await waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
expect((await fetch(`${browser.endpoint}/json/version`)).ok).toBe(true);
|
||||
});
|
||||
|
||||
@@ -327,7 +328,7 @@ it.each(
|
||||
expect(retry.cleanupIncomplete).toBe(true);
|
||||
denied = false;
|
||||
await provider.close();
|
||||
await vi.waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
await waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
expect(result.error.cleanupIncomplete).toBe(false);
|
||||
expect(retry.cleanupIncomplete).toBe(false);
|
||||
expect(result.error.partialObservation).toBe(observation);
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
startFakeCdpBrowser,
|
||||
type FakeCdpBrowser,
|
||||
} from "../../fixtures/fakeCdpBrowser.js";
|
||||
import { waitFor } from "../../support/testWait.js";
|
||||
|
||||
const INVALID_REPLIES = [
|
||||
["missing result and error", (id: number) => ({ id })],
|
||||
@@ -127,7 +128,7 @@ describe("CDP connection response correlation", () => {
|
||||
const first = connection.send("Page.enable", { selected: "first" });
|
||||
const second = connection.send("Page.enable", { selected: "second" });
|
||||
const replies = Promise.all([first, second]);
|
||||
await vi.waitFor(() => expect(browser.commands).toHaveLength(5));
|
||||
await waitFor(() => expect(browser.commands).toHaveLength(5));
|
||||
const firstCommand = browser.commands[3];
|
||||
const secondCommand = browser.commands[4];
|
||||
if (firstCommand === undefined || secondCommand === undefined)
|
||||
@@ -231,7 +232,7 @@ describe("CDP command cancellation causes", () => {
|
||||
undefined,
|
||||
controller.signal,
|
||||
);
|
||||
await vi.waitFor(() => expect(browser.commands).toHaveLength(1));
|
||||
await waitFor(() => expect(browser.commands).toHaveLength(1));
|
||||
const reason = new Error("cancel the pending CDP command");
|
||||
controller.abort(reason);
|
||||
const failure = await pending.then(
|
||||
@@ -263,7 +264,7 @@ describe("CDP command cancellation causes", () => {
|
||||
undefined,
|
||||
controller.signal,
|
||||
);
|
||||
await vi.waitFor(() => expect(browser.commands).toHaveLength(1));
|
||||
await waitFor(() => expect(browser.commands).toHaveLength(1));
|
||||
controller.abort();
|
||||
const failure = await pending.then(
|
||||
() => undefined,
|
||||
@@ -303,7 +304,7 @@ describe("CDP handshake cancellation", () => {
|
||||
controller.signal,
|
||||
);
|
||||
try {
|
||||
await vi.waitFor(() => expect(sockets.size).toBe(1));
|
||||
await waitFor(() => expect(sockets.size).toBe(1));
|
||||
controller.abort(reason);
|
||||
const failure = await connecting.then(
|
||||
() => undefined,
|
||||
@@ -313,7 +314,7 @@ describe("CDP handshake cancellation", () => {
|
||||
if (!(failure instanceof AnalysisCancelledError))
|
||||
throw new Error("Expected handshake cancellation");
|
||||
expect(failure.cause).toBe(reason);
|
||||
await vi.waitFor(() => expect(sockets.size).toBe(0));
|
||||
await waitFor(() => expect(sockets.size).toBe(0));
|
||||
} finally {
|
||||
if (sockets.size > 0) for (const socket of sockets) socket.destroy();
|
||||
await new Promise<void>((resolve, reject) =>
|
||||
@@ -341,7 +342,7 @@ describe("CDP transport failure precedence", () => {
|
||||
() => undefined,
|
||||
(cause: unknown) => cause,
|
||||
);
|
||||
await vi.waitFor(() => expect(browser.commands).toHaveLength(1));
|
||||
await waitFor(() => expect(browser.commands).toHaveLength(1));
|
||||
const failure = await pending;
|
||||
expect(failure).toMatchObject({
|
||||
_tag: "BrowserObservationError",
|
||||
@@ -368,7 +369,7 @@ describe("CDP connection command responses", () => {
|
||||
);
|
||||
try {
|
||||
const control = connection.send("Page.enable");
|
||||
await vi.waitFor(() => expect(browser.commands).toHaveLength(1));
|
||||
await waitFor(() => expect(browser.commands).toHaveLength(1));
|
||||
browser.emitRawMessage(
|
||||
Buffer.from(
|
||||
JSON.stringify({
|
||||
@@ -382,7 +383,7 @@ describe("CDP connection command responses", () => {
|
||||
const rejected = expect(pending).rejects.toMatchObject({
|
||||
reason: "protocol_error",
|
||||
});
|
||||
await vi.waitFor(() => expect(browser.commands).toHaveLength(2));
|
||||
await waitFor(() => expect(browser.commands).toHaveLength(2));
|
||||
browser.emitRawMessage(
|
||||
Buffer.concat([
|
||||
Buffer.from(
|
||||
@@ -437,7 +438,7 @@ describe("CDP connection command responses", () => {
|
||||
_tag: "BrowserObservationError",
|
||||
reason: "protocol_error",
|
||||
});
|
||||
await vi.waitFor(() => expect(browser.commands).toHaveLength(1));
|
||||
await waitFor(() => expect(browser.commands).toHaveLength(1));
|
||||
const command = browser.commands[0];
|
||||
if (command === undefined)
|
||||
throw new Error("Fake browser received no command");
|
||||
@@ -466,7 +467,7 @@ describe("CDP connection command responses", () => {
|
||||
code: -32_001,
|
||||
reportedMessage: "Session with given id not found.",
|
||||
});
|
||||
await vi.waitFor(() => expect(browser.commands).toHaveLength(1));
|
||||
await waitFor(() => expect(browser.commands).toHaveLength(1));
|
||||
const command = browser.commands[0];
|
||||
if (command === undefined)
|
||||
throw new Error("Fake browser received no command");
|
||||
|
||||
@@ -74,7 +74,7 @@ describe("CDP discovery response UTF-8", () => {
|
||||
operation: "list_browser_targets",
|
||||
reason: "invalid_endpoint_response",
|
||||
});
|
||||
await expect.poll(() => responseClosed, { timeout: 2_000 }).toBe(true);
|
||||
await expect.poll(() => responseClosed).toBe(true);
|
||||
sendMalformed = false;
|
||||
await expect(
|
||||
discoverCdpEndpoint(server.url.href, "list_browser_targets"),
|
||||
@@ -146,7 +146,7 @@ describe("CDP discovery response bounds", () => {
|
||||
_tag: "BrowserObservationError",
|
||||
reason: "payload_limit",
|
||||
});
|
||||
await expect.poll(() => responseClosed, { timeout: 2_000 }).toBe(true);
|
||||
await expect.poll(() => responseClosed).toBe(true);
|
||||
});
|
||||
|
||||
it("ends a response that exceeds the total elapsed deadline", async () => {
|
||||
@@ -177,7 +177,7 @@ describe("CDP discovery response bounds", () => {
|
||||
_tag: "BrowserObservationError",
|
||||
reason: "timeout",
|
||||
});
|
||||
await expect.poll(() => responseClosed, { timeout: 2_000 }).toBe(true);
|
||||
await expect.poll(() => responseClosed).toBe(true);
|
||||
}, [
|
||||
() => controller.abort(),
|
||||
async () => {
|
||||
@@ -215,7 +215,7 @@ describe("CDP discovery response bounds", () => {
|
||||
_tag: "AnalysisCancelledError",
|
||||
operation: "list_browser_targets",
|
||||
});
|
||||
await expect.poll(() => responseClosed, { timeout: 2_000 }).toBe(true);
|
||||
await expect.poll(() => responseClosed).toBe(true);
|
||||
}, [
|
||||
() => controller.abort(),
|
||||
async () => {
|
||||
@@ -242,7 +242,7 @@ describe("CDP discovery response bounds", () => {
|
||||
_tag: "BrowserObservationError",
|
||||
reason: "invalid_endpoint_response",
|
||||
});
|
||||
await expect.poll(() => responseClosed, { timeout: 2_000 }).toBe(true);
|
||||
await expect.poll(() => responseClosed).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ import { V8InspectorProvider } from "../../../src/inspector/V8InspectorProvider.
|
||||
import { startFakeCdpBrowser } from "../../fixtures/fakeCdpBrowser.js";
|
||||
import { startRuntimeBrowser } from "../../fixtures/webRuntime.js";
|
||||
import { startFakeV8Inspector } from "../../fixtures/inspector/fakeV8Inspector.js";
|
||||
import { waitFor } from "../../support/testWait.js";
|
||||
|
||||
// Keep real socket scheduling; expire only a timer whose owning phase is ready.
|
||||
beforeEach(() => {
|
||||
@@ -96,10 +97,10 @@ it("bounds a stalled WebSocket handshake and releases its socket", async () => {
|
||||
AbortSignal.timeout(10_000),
|
||||
),
|
||||
).rejects.toMatchObject({ reason: "timeout" });
|
||||
await vi.waitFor(() => expect(sockets.size).toBe(1), { timeout: 5_000 });
|
||||
await waitFor(() => expect(sockets.size).toBe(1));
|
||||
expireTimer(WEB_RUNTIME_LIMITS.commandTimeoutMs);
|
||||
await rejection;
|
||||
await vi.waitFor(() => expect(sockets.size).toBe(0));
|
||||
await waitFor(() => expect(sockets.size).toBe(0));
|
||||
} finally {
|
||||
for (const socket of sockets) socket.destroy();
|
||||
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||
@@ -119,12 +120,10 @@ it.each(["Target.attachToTarget", "Page.enable", "Runtime.enable"])(
|
||||
}),
|
||||
{ signal: AbortSignal.timeout(10_000) },
|
||||
);
|
||||
await vi.waitFor(
|
||||
() =>
|
||||
expect(browser.commands.map(({ method }) => method)).toContain(
|
||||
hangOnMethod,
|
||||
),
|
||||
{ timeout: 5_000 },
|
||||
await waitFor(() =>
|
||||
expect(browser.commands.map(({ method }) => method)).toContain(
|
||||
hangOnMethod,
|
||||
),
|
||||
);
|
||||
expireTimer(WEB_RUNTIME_LIMITS.commandTimeoutMs);
|
||||
const result = await pending;
|
||||
@@ -136,7 +135,7 @@ it.each(["Target.attachToTarget", "Page.enable", "Runtime.enable"])(
|
||||
expect(browser.commands.map(({ method }) => method)).toContain(
|
||||
hangOnMethod,
|
||||
);
|
||||
await vi.waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
await waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
@@ -158,9 +157,7 @@ it("ignores a late timed-out reply and keeps following commands correlated", asy
|
||||
AbortSignal.timeout(10_000),
|
||||
),
|
||||
).rejects.toMatchObject({ reason: "timeout" });
|
||||
await vi.waitFor(() => expect(browser.commands).toHaveLength(1), {
|
||||
timeout: 5_000,
|
||||
});
|
||||
await waitFor(() => expect(browser.commands).toHaveLength(1));
|
||||
expireTimer(WEB_RUNTIME_LIMITS.commandTimeoutMs);
|
||||
await rejection;
|
||||
const command = browser.commands[0];
|
||||
@@ -208,9 +205,7 @@ it.each(["success", "rejection", "abort", "disconnect"])(
|
||||
: "BrowserObservationError",
|
||||
});
|
||||
if (outcome === "success") {
|
||||
await vi.waitFor(() => expect(browser.commands).toHaveLength(1), {
|
||||
timeout: 5_000,
|
||||
});
|
||||
await waitFor(() => expect(browser.commands).toHaveLength(1));
|
||||
// A healthy reply may take longer than the former 100 ms test budget.
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 250));
|
||||
const command = browser.commands[0];
|
||||
@@ -253,12 +248,10 @@ it.each(["Runtime.enable", "Debugger.enable"])(
|
||||
},
|
||||
{ signal: AbortSignal.timeout(10_000) },
|
||||
);
|
||||
await vi.waitFor(
|
||||
() =>
|
||||
expect(inspector.commands.map(({ method }) => method)).toContain(
|
||||
hangOnMethod,
|
||||
),
|
||||
{ timeout: 5_000 },
|
||||
await waitFor(() =>
|
||||
expect(inspector.commands.map(({ method }) => method)).toContain(
|
||||
hangOnMethod,
|
||||
),
|
||||
);
|
||||
expireTimer(WEB_RUNTIME_LIMITS.commandTimeoutMs);
|
||||
const result = await pending;
|
||||
@@ -295,12 +288,7 @@ it("keeps the chosen observation window independent of command deadlines", async
|
||||
observation_ms: observationMs,
|
||||
}),
|
||||
);
|
||||
await vi.waitFor(
|
||||
() => expect(activeTimers(observationMs)).toHaveLength(1),
|
||||
{
|
||||
timeout: 5_000,
|
||||
},
|
||||
);
|
||||
await waitFor(() => expect(activeTimers(observationMs)).toHaveLength(1));
|
||||
expect(activeTimers(WEB_RUNTIME_LIMITS.commandTimeoutMs)).toHaveLength(0);
|
||||
expireTimer(observationMs);
|
||||
const result = await pending;
|
||||
@@ -339,7 +327,7 @@ it("keeps a short command deadline off HTTP discovery and names Page.getFrameTre
|
||||
expect(browser.httpRequests.map(({ url }) => url)).toEqual(
|
||||
expect.arrayContaining(["/json/version", "/json/list"]),
|
||||
);
|
||||
await vi.waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
await waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
} finally {
|
||||
await provider.close();
|
||||
await browser.close();
|
||||
@@ -363,12 +351,10 @@ it("gives browser-socket attach its own command deadline after the handshake", a
|
||||
}),
|
||||
{ signal: AbortSignal.timeout(10_000) },
|
||||
);
|
||||
await vi.waitFor(
|
||||
() =>
|
||||
expect(browser.commands.map(({ method }) => method)).toContain(
|
||||
"Target.attachToTarget",
|
||||
),
|
||||
{ timeout: 5_000 },
|
||||
await waitFor(() =>
|
||||
expect(browser.commands.map(({ method }) => method)).toContain(
|
||||
"Target.attachToTarget",
|
||||
),
|
||||
);
|
||||
expect(activeTimers(commandTimeoutMs)).toHaveLength(1);
|
||||
expireTimer(commandTimeoutMs);
|
||||
@@ -379,7 +365,7 @@ it("gives browser-socket attach its own command deadline after the handshake", a
|
||||
`CDP command Target.attachToTarget exceeded its ${String(commandTimeoutMs)} ms command_timeout_ms`,
|
||||
);
|
||||
expect(text).not.toContain("WebSocket handshake exceeded");
|
||||
await vi.waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
await waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
} finally {
|
||||
await provider.close();
|
||||
await browser.close();
|
||||
@@ -410,7 +396,7 @@ it("stops a stalled blocked-service-worker check before discovery", async () =>
|
||||
const clock = vi.spyOn(performance, "now").mockReturnValue(1_000);
|
||||
const pending = workers.initialize();
|
||||
try {
|
||||
await vi.waitFor(() =>
|
||||
await waitFor(() =>
|
||||
expect(activeTimers(SERVICE_WORKER_POLICY_CHECK_TIMEOUT_MS)).toHaveLength(
|
||||
1,
|
||||
),
|
||||
@@ -464,12 +450,10 @@ it("keeps blocked service-worker attachment outside the discovery deadline", asy
|
||||
);
|
||||
const pending = workers.initialize();
|
||||
try {
|
||||
await vi.waitFor(
|
||||
() =>
|
||||
expect(browser.commands.map(({ method }) => method)).toContain(
|
||||
"Target.attachToTarget",
|
||||
),
|
||||
{ timeout: 5_000 },
|
||||
await waitFor(() =>
|
||||
expect(browser.commands.map(({ method }) => method)).toContain(
|
||||
"Target.attachToTarget",
|
||||
),
|
||||
);
|
||||
expect(activeTimers(CDP_DISCOVERY_RESPONSE_TIMEOUT_MS)).toHaveLength(0);
|
||||
expect(activeTimers(commandTimeoutMs)).toHaveLength(1);
|
||||
@@ -554,14 +538,14 @@ it("applies a non-default handshake deadline and releases its socket", async ()
|
||||
const rejection = expect(pending).rejects.toMatchObject({
|
||||
reason: "timeout",
|
||||
});
|
||||
await vi.waitFor(() => expect(sockets.size).toBe(1), { timeout: 5_000 });
|
||||
await waitFor(() => expect(sockets.size).toBe(1));
|
||||
expireTimer(1_500);
|
||||
await rejection;
|
||||
expect(captured).toBeInstanceOf(BrowserObservationError);
|
||||
if (!(captured instanceof BrowserObservationError))
|
||||
throw new Error("Expected a handshake deadline");
|
||||
expect(captured.userMessage).toContain("1500 ms command_timeout_ms");
|
||||
await vi.waitFor(() => expect(sockets.size).toBe(0));
|
||||
await waitFor(() => expect(sockets.size).toBe(0));
|
||||
} finally {
|
||||
for (const socket of sockets) socket.destroy();
|
||||
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||
@@ -580,12 +564,10 @@ it("applies a non-default command deadline after HTTP discovery", async () => {
|
||||
}),
|
||||
{ signal: AbortSignal.timeout(10_000) },
|
||||
);
|
||||
await vi.waitFor(
|
||||
() =>
|
||||
expect(browser.commands.map(({ method }) => method)).toContain(
|
||||
"Page.enable",
|
||||
),
|
||||
{ timeout: 5_000 },
|
||||
await waitFor(() =>
|
||||
expect(browser.commands.map(({ method }) => method)).toContain(
|
||||
"Page.enable",
|
||||
),
|
||||
);
|
||||
expireTimer(1_500);
|
||||
const result = await pending;
|
||||
@@ -593,7 +575,7 @@ it("applies a non-default command deadline after HTTP discovery", async () => {
|
||||
expect(result.error.userMessage ?? "").toContain(
|
||||
"1500 ms command_timeout_ms",
|
||||
);
|
||||
await vi.waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
await waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
@@ -609,9 +591,7 @@ it("reschedules a raised command deadline onto the fixed cleanup grace", async (
|
||||
);
|
||||
try {
|
||||
const pending = connection.send("Runtime.enable");
|
||||
await vi.waitFor(() => expect(browser.commands).toHaveLength(1), {
|
||||
timeout: 5_000,
|
||||
});
|
||||
await waitFor(() => expect(browser.commands).toHaveLength(1));
|
||||
expect(activeTimers(60_000)).toHaveLength(1);
|
||||
connection.beginCleanupGrace(5_000);
|
||||
expect(activeTimers(60_000)).toHaveLength(0);
|
||||
@@ -645,12 +625,10 @@ it("keeps runtime cleanup on the fixed grace when command_timeout_ms is shorter"
|
||||
}),
|
||||
{ signal: AbortSignal.timeout(10_000) },
|
||||
);
|
||||
await vi.waitFor(
|
||||
() =>
|
||||
expect(browser.commands.map(({ method }) => method)).toContain(
|
||||
"Profiler.disable",
|
||||
),
|
||||
{ timeout: 5_000 },
|
||||
await waitFor(() =>
|
||||
expect(browser.commands.map(({ method }) => method)).toContain(
|
||||
"Profiler.disable",
|
||||
),
|
||||
);
|
||||
expect(activeTimers(commandTimeoutMs)).toHaveLength(0);
|
||||
expect(
|
||||
@@ -691,12 +669,10 @@ it("names the fixed cleanup grace when runtime restoration stalls", async () =>
|
||||
}),
|
||||
{ signal: AbortSignal.timeout(10_000) },
|
||||
);
|
||||
await vi.waitFor(
|
||||
() =>
|
||||
expect(browser.commands.map(({ method }) => method)).toContain(
|
||||
"Profiler.disable",
|
||||
),
|
||||
{ timeout: 5_000 },
|
||||
await waitFor(() =>
|
||||
expect(browser.commands.map(({ method }) => method)).toContain(
|
||||
"Profiler.disable",
|
||||
),
|
||||
);
|
||||
expect(activeTimers(commandTimeoutMs)).toHaveLength(0);
|
||||
expireEveryTimer(WEB_RUNTIME_LIMITS.cleanupTimeoutMs);
|
||||
@@ -728,12 +704,10 @@ it("applies caller-selected discovery_timeout_ms to browser-socket discovery", a
|
||||
discovery_timeout_ms: discoveryTimeoutMs,
|
||||
}),
|
||||
);
|
||||
await vi.waitFor(
|
||||
() =>
|
||||
expect(browser.commands.map(({ method }) => method)).toContain(
|
||||
"Target.getTargets",
|
||||
),
|
||||
{ timeout: 5_000 },
|
||||
await waitFor(() =>
|
||||
expect(browser.commands.map(({ method }) => method)).toContain(
|
||||
"Target.getTargets",
|
||||
),
|
||||
);
|
||||
expect(activeTimers(CDP_DISCOVERY_RESPONSE_TIMEOUT_MS)).toHaveLength(0);
|
||||
expect(activeTimers(discoveryTimeoutMs)).not.toHaveLength(0);
|
||||
@@ -750,7 +724,7 @@ it("applies caller-selected discovery_timeout_ms to browser-socket discovery", a
|
||||
`Increase discovery_timeout_ms above ${String(discoveryTimeoutMs)}`,
|
||||
);
|
||||
expect(projected.remediation?.action).toContain("--discovery-timeout-ms");
|
||||
await vi.waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
await waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
} finally {
|
||||
await provider.close();
|
||||
await browser.close();
|
||||
@@ -779,9 +753,7 @@ it("applies caller-selected discovery_timeout_ms to HTTP discovery independently
|
||||
command_timeout_ms: commandTimeoutMs,
|
||||
}),
|
||||
);
|
||||
await vi.waitFor(() => expect(versionRequested).toBe(true), {
|
||||
timeout: 5_000,
|
||||
});
|
||||
await waitFor(() => expect(versionRequested).toBe(true));
|
||||
expect(activeTimers(CDP_DISCOVERY_RESPONSE_TIMEOUT_MS)).toHaveLength(0);
|
||||
expect(activeTimers(commandTimeoutMs)).toHaveLength(0);
|
||||
expect(activeTimers(discoveryTimeoutMs)).toHaveLength(1);
|
||||
@@ -850,7 +822,7 @@ it("clamps browser-socket handshake timers at MAX_BROWSER_DEADLINE_MS without 32
|
||||
undefined,
|
||||
MAX_BROWSER_DEADLINE_MS,
|
||||
).catch((cause: unknown) => cause);
|
||||
await vi.waitFor(() => expect(sockets.size).toBe(1), { timeout: 5_000 });
|
||||
await waitFor(() => expect(sockets.size).toBe(1));
|
||||
expireEveryTimer(MAX_BROWSER_DEADLINE_MS);
|
||||
const failure = await pending;
|
||||
expect(failure).toBeInstanceOf(BrowserObservationError);
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
import { ProviderCleanupError } from "../../../src/domain/providerCleanupError.js";
|
||||
import { startFakeCdpBrowser } from "../../fixtures/fakeCdpBrowser.js";
|
||||
import { awaitAcknowledgement } from "../../fixtures/lifecycleAcknowledgement.js";
|
||||
import { waitFor } from "../../support/testWait.js";
|
||||
|
||||
it.each(["connect", "acquireBrowserSocket"] as const)(
|
||||
"refuses %s after clean shutdown without inventing retained cleanup",
|
||||
@@ -109,7 +110,7 @@ it("joins a late owned handshake and waits for its active operation", async () =
|
||||
expect(closed).toBe(false);
|
||||
await owner.finish(connection);
|
||||
await closing;
|
||||
await vi.waitFor(() => expect(webSockets.clients.size).toBe(0));
|
||||
await waitFor(() => expect(webSockets.clients.size).toBe(0));
|
||||
});
|
||||
|
||||
it("retries only the failed transport and does not repeat successful child cleanup", async () => {
|
||||
@@ -157,13 +158,13 @@ it("retries only the failed transport and does not repeat successful child clean
|
||||
}
|
||||
|
||||
await owner.retryPending();
|
||||
await vi.waitFor(() => expect(browser.activeConnectionCount()).toBe(1));
|
||||
await waitFor(() => expect(browser.activeConnectionCount()).toBe(1));
|
||||
const cleanup = vi.fn(async () => undefined);
|
||||
await owner.finish(active, cleanup);
|
||||
await owner.retryPending();
|
||||
await owner.close();
|
||||
expect(cleanup).toHaveBeenCalledTimes(1);
|
||||
await vi.waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
await waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
expect(browser.commands.map(({ method }) => method)).not.toContain(
|
||||
"Target.closeTarget",
|
||||
);
|
||||
@@ -201,7 +202,7 @@ it("accepts asynchronous terminate success only after the close event", async ()
|
||||
terminate.mockRestore();
|
||||
}
|
||||
await owner.close();
|
||||
await vi.waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
await waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
});
|
||||
|
||||
it("clears returned inspection and shutdown cleanup status only after socket release", async () => {
|
||||
@@ -241,7 +242,7 @@ it("clears returned inspection and shutdown cleanup status only after socket rel
|
||||
expect(browser.activeConnectionCount()).toBe(1);
|
||||
denied = false;
|
||||
await provider.close();
|
||||
await vi.waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
await waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
expect(result.error.cleanupIncomplete).toBe(false);
|
||||
expect(retry.cleanupIncomplete).toBe(false);
|
||||
expect(result.error.partialObservation).toBe(observation);
|
||||
@@ -308,7 +309,7 @@ it("keeps retry aggregates incomplete until every reported socket is released",
|
||||
expect(errors.map((error) => error.cleanupIncomplete)).toEqual([false, true]);
|
||||
expect(aggregate.cleanupIncomplete).toBe(true);
|
||||
expect(mixed.cleanupResources).toEqual(["second-socket"]);
|
||||
await vi.waitFor(() => expect(browser.activeConnectionCount()).toBe(2));
|
||||
await waitFor(() => expect(browser.activeConnectionCount()).toBe(2));
|
||||
denied.clear();
|
||||
await owner.retryPending();
|
||||
expect(errors.map((error) => error.cleanupIncomplete)).toEqual([
|
||||
@@ -317,7 +318,7 @@ it("keeps retry aggregates incomplete until every reported socket is released",
|
||||
]);
|
||||
expect(aggregate.cleanupIncomplete).toBe(false);
|
||||
expect(mixed.cleanupIncomplete).toBe(false);
|
||||
await vi.waitFor(() => expect(browser.activeConnectionCount()).toBe(1));
|
||||
await waitFor(() => expect(browser.activeConnectionCount()).toBe(1));
|
||||
await owner.finish(active);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { describe, expect, it, onTestFinished, vi } from "vitest";
|
||||
import { describe, expect, it, onTestFinished } from "vitest";
|
||||
import { CdpWebRuntimeProvider } from "../../../src/browser/execution/CdpWebRuntimeProvider.js";
|
||||
import type { CdpEvent } from "../../../src/browser/CdpConnection.js";
|
||||
import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js";
|
||||
import { observeWebExecutionInputSchema } from "../../../src/domain/webExecution.js";
|
||||
import { startRuntimeBrowser } from "../../fixtures/webRuntime.js";
|
||||
import { waitFor } from "../../support/testWait.js";
|
||||
|
||||
describe("runtime flat-session lifecycle", () => {
|
||||
it("rejects session detachment between counter startup and local arming", async () => {
|
||||
@@ -228,7 +229,7 @@ describe("runtime disconnect ownership", () => {
|
||||
}),
|
||||
{ signal: AbortSignal.timeout(2_000) },
|
||||
);
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
browser.commands.some(
|
||||
({ method }) => method === "Profiler.stopPreciseCoverage",
|
||||
|
||||
@@ -33,6 +33,7 @@ import { decodeHarCapture } from "../../../src/browser/history/HarCapture.js";
|
||||
import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js";
|
||||
import { primaryAnalysisError } from "../../../src/domain/analysisErrorBase.js";
|
||||
import { OwnedFileHandle } from "../../../src/filesystem/OwnedFileHandle.js";
|
||||
import { waitFor } from "../../support/testWait.js";
|
||||
|
||||
const linux = it.skipIf(process.platform !== "linux");
|
||||
|
||||
@@ -336,12 +337,9 @@ it.each(["cancelled", "process"] as const)(
|
||||
sensitive_values: ["capture-private-runtime"],
|
||||
});
|
||||
const target = decoder.inspect(input, { signal: controller.signal });
|
||||
await vi.waitFor(
|
||||
async () => {
|
||||
targetPid = JSON.parse(await readFile(markerPath, "utf8"));
|
||||
},
|
||||
{ timeout: 5000 },
|
||||
);
|
||||
await waitFor(async () => {
|
||||
targetPid = JSON.parse(await readFile(markerPath, "utf8"));
|
||||
});
|
||||
const peer = decoder.inspect(input);
|
||||
await peerEntered.promise;
|
||||
if (outcome === "cancelled") controller.abort(reason);
|
||||
|
||||
@@ -74,9 +74,7 @@ cliTest.for([false, true])(
|
||||
child.once("close", () => resolve());
|
||||
});
|
||||
await expect
|
||||
.poll(() => /REA_TEST_STDIN_READ (\d+)/u.test(stderr), {
|
||||
timeout: 10_000,
|
||||
})
|
||||
.poll(() => /REA_TEST_STDIN_READ (\d+)/u.test(stderr))
|
||||
.toBe(true);
|
||||
const heapSizeLimit = Number(
|
||||
/REA_TEST_STDIN_READ (\d+)/u.exec(stderr)?.[1],
|
||||
@@ -182,9 +180,7 @@ cliTest.skipIf(process.platform === "win32").for(
|
||||
child.once("error", reject);
|
||||
child.once("close", () => resolve());
|
||||
});
|
||||
await expect
|
||||
.poll(() => stderr.includes("REA_TEST_STDIN_READ"), { timeout: 10_000 })
|
||||
.toBe(true);
|
||||
await expect.poll(() => stderr.includes("REA_TEST_STDIN_READ")).toBe(true);
|
||||
// Keep the pipe open: EOF must not be what releases the pending read.
|
||||
child.kill(signal);
|
||||
expect(await waitForExit(child, 5_000), stderr).toBe(true);
|
||||
|
||||
@@ -63,12 +63,7 @@ process.stdout.write = (chunk, ...arguments_) => {
|
||||
if (frame.ready) ready.resolve();
|
||||
}),
|
||||
);
|
||||
await awaitAcknowledgement(
|
||||
ready.promise,
|
||||
closed,
|
||||
"CLI worker startup",
|
||||
15_000,
|
||||
);
|
||||
await awaitAcknowledgement(ready.promise, closed, "CLI worker startup");
|
||||
child.stdin.end(
|
||||
encodeCliSessionFrame({
|
||||
id: 1,
|
||||
|
||||
@@ -98,7 +98,9 @@ describe("Cutter CLI commands", () => {
|
||||
ownsProcessLifetime: true,
|
||||
});
|
||||
try {
|
||||
await expect.poll(() => accepted, { timeout: 5_000 }).toBe(8);
|
||||
// Probes that time out close their sockets, so only a full concurrent
|
||||
// wave holds eight open at once.
|
||||
await expect.poll(() => sockets.size).toBe(8);
|
||||
launched.process.kill("SIGINT");
|
||||
expect(await supervisor.waitForExit(5_000)).toBe(true);
|
||||
expect(await supervisor.waitForOutputClose(1_000)).toBe(true);
|
||||
|
||||
@@ -259,9 +259,7 @@ const startMcpSession = async (
|
||||
child.stdin.write(
|
||||
`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}\n`,
|
||||
);
|
||||
await expect
|
||||
.poll(() => stdout, { timeout: EXIT_DEADLINE_MS })
|
||||
.toContain(`"id":${String(id)}`);
|
||||
await expect.poll(() => stdout).toContain(`"id":${String(id)}`);
|
||||
};
|
||||
await request(1, "initialize", {
|
||||
protocolVersion: "2025-06-18",
|
||||
@@ -305,9 +303,7 @@ describe.skipIf(process.platform === "win32")(
|
||||
expectRetainedOutcome(name, result, pid);
|
||||
// REA reports the debugger as retained and did not stop it. Exiting
|
||||
// closes its command stream; that is the only thing it sees from REA.
|
||||
await expect
|
||||
.poll(launch.notes, { timeout: EXIT_DEADLINE_MS })
|
||||
.toContain("stdin-end");
|
||||
await expect.poll(launch.notes).toContain("stdin-end");
|
||||
expect(await launch.notes()).not.toContain("sigterm");
|
||||
expect(isAlive(pid)).toBe(true);
|
||||
},
|
||||
@@ -332,9 +328,7 @@ describe.skipIf(process.platform === "win32")(
|
||||
});
|
||||
expect(result.stdout).not.toContain("retained_debugger_pids");
|
||||
const pid = await launch.debuggerPid();
|
||||
await expect
|
||||
.poll(() => isAlive(pid), { timeout: EXIT_DEADLINE_MS })
|
||||
.toBe(false);
|
||||
await expect.poll(() => isAlive(pid)).toBe(false);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -353,16 +347,10 @@ describe.skipIf(process.platform === "win32")(
|
||||
child.stdout.on("data", (chunk: Buffer) => (stdout += String(chunk)));
|
||||
child.stdin.end();
|
||||
// The unanswered request is issued by final cleanup, after the command.
|
||||
await expect
|
||||
.poll(launch.notes, { timeout: EXIT_DEADLINE_MS })
|
||||
.toContain("inventory-request");
|
||||
await expect.poll(launch.notes).toContain("inventory-request");
|
||||
child.kill("SIGINT");
|
||||
await expect
|
||||
.poll(() => child.exitCode, { timeout: EXIT_DEADLINE_MS })
|
||||
.toBe(130);
|
||||
await expect
|
||||
.poll(() => stdout, { timeout: EXIT_DEADLINE_MS })
|
||||
.toContain("retained_debugger_pids");
|
||||
await expect.poll(() => child.exitCode).toBe(130);
|
||||
await expect.poll(() => stdout).toContain("retained_debugger_pids");
|
||||
|
||||
const pid = await launch.debuggerPid();
|
||||
expectRetainedOutcome(
|
||||
@@ -395,18 +383,14 @@ describe.skipIf(process.platform === "win32")(
|
||||
if (trigger === "SIGTERM") child.kill("SIGTERM");
|
||||
else child.stdin.end();
|
||||
|
||||
await expect
|
||||
.poll(() => child.exitCode, { timeout: EXIT_DEADLINE_MS })
|
||||
.toBe(1);
|
||||
await expect.poll(() => child.exitCode).toBe(1);
|
||||
const pid = await launch.debuggerPid();
|
||||
// Nothing else reports these once the server has exited.
|
||||
const guidance = () =>
|
||||
stderr()
|
||||
.split("\n")
|
||||
.find((line) => line === retainedDebuggerRemediation([pid]));
|
||||
await expect
|
||||
.poll(guidance, { timeout: EXIT_DEADLINE_MS })
|
||||
.toBeDefined();
|
||||
await expect.poll(guidance).toBeDefined();
|
||||
expectNamesProcess(guidance()!, pid);
|
||||
const logs = stderr()
|
||||
.split("\n")
|
||||
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
type OwnedProcessGroup,
|
||||
} from "../../../src/process/ProcessOwnership.js";
|
||||
|
||||
const PROBE_DEADLINE_MS = 10_000;
|
||||
|
||||
cliTest.skipIf(!["darwin", "linux"].includes(process.platform)).for(
|
||||
(["jadx", "kuna"] as const).flatMap((provider) =>
|
||||
(
|
||||
@@ -85,13 +87,11 @@ setTimeout(() => process.exit(99), 20000);
|
||||
let ownership: OwnedProcessGroup | undefined;
|
||||
try {
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
readFile(ready, "utf8").then(
|
||||
() => true,
|
||||
() => false,
|
||||
),
|
||||
{ timeout: 10_000 },
|
||||
.poll(() =>
|
||||
readFile(ready, "utf8").then(
|
||||
() => true,
|
||||
() => false,
|
||||
),
|
||||
)
|
||||
.toBe(true);
|
||||
const readyRecord: unknown = JSON.parse(await readFile(ready, "utf8"));
|
||||
@@ -119,10 +119,12 @@ setTimeout(() => process.exit(99), 20000);
|
||||
runId: observed.runId,
|
||||
};
|
||||
}
|
||||
const signalled = performance.now();
|
||||
child.kill(signal);
|
||||
await expect
|
||||
.poll(() => child.exitCode, { timeout: 10_000 })
|
||||
.not.toBeNull();
|
||||
await expect.poll(() => child.exitCode).not.toBeNull();
|
||||
// Without cancellation, doctor would end when the probe's own deadline
|
||||
// (JadxJavaProbe and KunaInstallation: 10 s) stopped the probe.
|
||||
expect(performance.now() - signalled).toBeLessThan(PROBE_DEADLINE_MS);
|
||||
const result = await closed;
|
||||
expect(JSON.parse(stdout)).toMatchObject({ code: "cancelled" });
|
||||
if (ownership !== undefined) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { rm, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
|
||||
import { closeVerifierResources } from "../../../scripts/lib/verifier-cleanup.mjs";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
electronRuntimeEvidence,
|
||||
} from "../../fixtures/javascriptRuntimeReconciliation.js";
|
||||
import { JAVASCRIPT_RUNTIME_RECONCILIATION_EXAMPLE } from "../../../src/contracts/javascript/javascriptRuntimeReconciliationExample.js";
|
||||
import { waitFor } from "../../support/testWait.js";
|
||||
|
||||
const cliProcesses = createTestProcessRegistry();
|
||||
const cli = createTestCli(cliProcesses);
|
||||
@@ -198,14 +199,11 @@ it.skipIf(process.platform === "win32")(
|
||||
});
|
||||
const hardStop = setTimeout(() => child.kill("SIGKILL"), 15_000);
|
||||
try {
|
||||
await vi.waitFor(
|
||||
() => {
|
||||
expect(
|
||||
browser.commands.some(({ method }) => method === "Runtime.enable"),
|
||||
).toBe(true);
|
||||
},
|
||||
{ timeout: 5_000 },
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
browser.commands.some(({ method }) => method === "Runtime.enable"),
|
||||
).toBe(true);
|
||||
});
|
||||
child.kill("SIGINT");
|
||||
|
||||
const result = await closed;
|
||||
@@ -219,7 +217,7 @@ it.skipIf(process.platform === "win32")(
|
||||
expect(methods).toContain("Target.detachFromTarget");
|
||||
expect(methods).not.toContain("Target.closeTarget");
|
||||
expect(methods).not.toContain("Browser.close");
|
||||
await vi.waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
await waitFor(() => expect(browser.activeConnectionCount()).toBe(0));
|
||||
expect((await fetch(`${browser.endpoint}/json/version`)).ok).toBe(true);
|
||||
} finally {
|
||||
clearTimeout(hardStop);
|
||||
|
||||
@@ -105,6 +105,16 @@ const cases: readonly AcceptanceCase[] = [
|
||||
exitCode: 0,
|
||||
diagnostics: [],
|
||||
},
|
||||
{
|
||||
name: "rejects test-owned wait deadlines through the repository plugin",
|
||||
filename: testSource,
|
||||
source: `
|
||||
import { expect } from "vitest";
|
||||
await expect.poll(() => true, { timeout: 5_000 }).toBe(true);
|
||||
`,
|
||||
exitCode: 1,
|
||||
diagnostics: [{ code: "rea(project-wait-deadline)", severity: "error" }],
|
||||
},
|
||||
{
|
||||
name: "rejects an unreturned promise callback containing assertions",
|
||||
filename: testSource,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { join, resolve } from "node:path";
|
||||
|
||||
import type { Client } from "@modelcontextprotocol/client";
|
||||
import { expect, it, vi } from "vitest";
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { seedEndCapture } from "../../../src/application/desktop/desktopCapture.fixture.js";
|
||||
import { toolContract } from "../../../src/contracts/toolContracts.js";
|
||||
@@ -21,6 +21,7 @@ import { callOk, startBuildComputer } from "../../fixtures/buildComputerMcp.js";
|
||||
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
|
||||
import { cancelDuringCommit } from "../../fixtures/outputTreeCommit.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { waitFor } from "../../support/testWait.js";
|
||||
|
||||
const call = (client: Client, name: string, args: Record<string, unknown>) =>
|
||||
client.callTool({ name, arguments: args });
|
||||
@@ -201,7 +202,7 @@ it("releases a capture whose start was cancelled after launch, so another start
|
||||
{ name: "start_build_capture", arguments: { computer_id: id } },
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
await vi.waitFor(() => expect(screenshots()).toBe(2), { timeout: 30_000 });
|
||||
await waitFor(() => expect(screenshots()).toBe(2));
|
||||
controller.abort();
|
||||
await expect(started).rejects.toThrow();
|
||||
const again = await callOk(client, "start_build_capture", {
|
||||
@@ -235,7 +236,7 @@ it("cancels a capture's start when the computer closes while it waits for the op
|
||||
computer.calls.filter(({ name }) => name === "computer_screenshot").length;
|
||||
expect(screenshots()).toBe(1);
|
||||
const started = call(client, "start_build_capture", { computer_id: id });
|
||||
await vi.waitFor(() => expect(screenshots()).toBe(2), { timeout: 30_000 });
|
||||
await waitFor(() => expect(screenshots()).toBe(2));
|
||||
await callOk(client, "close_build_computer", { computer_id: id });
|
||||
expect(parseMcpToolError(await started).error.code).toBe("cancelled");
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ import { setTimeout as delay } from "node:timers/promises";
|
||||
|
||||
import type { Client } from "@modelcontextprotocol/client";
|
||||
import type { CallToolResult } from "@modelcontextprotocol/server";
|
||||
import { expect, it, vi } from "vitest";
|
||||
import { expect, it } from "vitest";
|
||||
import { z } from "zod";
|
||||
|
||||
import { toolContract } from "../../../src/contracts/toolContracts.js";
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
type FakeComputer,
|
||||
} from "../../fixtures/fakeAgentEnvComputer.js";
|
||||
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
|
||||
import { waitFor } from "../../support/testWait.js";
|
||||
|
||||
const images = (result: CallToolResult) =>
|
||||
result.content.filter((block) => block.type === "image");
|
||||
@@ -677,7 +678,7 @@ it("runs one computer's calls in the order they arrive and another computer's be
|
||||
callOk(client, "act_on_build_computer", { computer_id: id, action });
|
||||
const order: string[] = [];
|
||||
const reset = act(first, "reset").then(() => order.push("reset"));
|
||||
await vi.waitFor(() => expect(app.methods).toEqual(["data/reset"]));
|
||||
await waitFor(() => expect(app.methods).toEqual(["data/reset"]));
|
||||
const behind = act(first, "screenshot").then(() =>
|
||||
order.push("screenshot behind the reset"),
|
||||
);
|
||||
@@ -703,7 +704,7 @@ it("sends progress while an action waits for the call before it on its computer"
|
||||
computer_id: id,
|
||||
action: "reset",
|
||||
});
|
||||
await vi.waitFor(() => expect(app.methods).toEqual(["data/reset"]));
|
||||
await waitFor(() => expect(app.methods).toEqual(["data/reset"]));
|
||||
const progress: unknown[] = [];
|
||||
const waited = await client.callTool(
|
||||
{
|
||||
@@ -729,7 +730,7 @@ it("cancels a computer's call in progress on close and refuses the calls waiting
|
||||
const call = (name: string, fields: Record<string, unknown> = {}) =>
|
||||
client.callTool({ name, arguments: { computer_id: id, ...fields } });
|
||||
const reset = call("act_on_build_computer", { action: "reset" });
|
||||
await vi.waitFor(() => expect(app.methods).toEqual(["data/reset"]));
|
||||
await waitFor(() => expect(app.methods).toEqual(["data/reset"]));
|
||||
const waiting = [
|
||||
call("act_on_build_computer", { action: "screenshot" }),
|
||||
call("stop_build_capture"),
|
||||
|
||||
@@ -2,7 +2,7 @@ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import type { CallToolResult } from "@modelcontextprotocol/server";
|
||||
import { expect, it, vi } from "vitest";
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import type {
|
||||
BuildSettle,
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
|
||||
import { cancelDuringCommit } from "../../fixtures/outputTreeCommit.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { waitFor } from "../../support/testWait.js";
|
||||
|
||||
const TASK = {
|
||||
name: "add-eggs",
|
||||
@@ -895,7 +896,7 @@ it("keeps a completed reset when the screenshot after it is cancelled", async ()
|
||||
},
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
await vi.waitFor(() => expect(witness.app.methods).toContain("data/reset"));
|
||||
await waitFor(() => expect(witness.app.methods).toContain("data/reset"));
|
||||
controller.abort();
|
||||
await expect(resetting).rejects.toThrow();
|
||||
const near = parseMcpToolError(await witness.check({ stage: "near_miss" }));
|
||||
@@ -909,7 +910,7 @@ it("checks after_reset once the reset sent before it has relaunched the app", as
|
||||
await throughNearMiss(witness);
|
||||
const order: string[] = [];
|
||||
const reset = witness.act("reset").then(() => order.push("reset"));
|
||||
await vi.waitFor(() => expect(app.methods.at(-1)).toBe("data/reset"));
|
||||
await waitFor(() => expect(app.methods.at(-1)).toBe("data/reset"));
|
||||
const last = await check({ stage: "after_reset" });
|
||||
order.push("check");
|
||||
await reset;
|
||||
|
||||
@@ -19,6 +19,9 @@ import { createToolResultDelivery } from "../../../src/server/toolResult.js";
|
||||
import { silentLogger } from "../../../src/logger.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
|
||||
// CutterBridgeClient's per-probe request deadline.
|
||||
const CUTTER_REQUEST_DEADLINE_MS = 25_000;
|
||||
|
||||
it("propagates MCP cancellation into an active Cutter command", async () => {
|
||||
let markDispatched: (() => void) | undefined;
|
||||
let observedAbort = false;
|
||||
@@ -253,15 +256,20 @@ it("cancels active discovery sockets, stops queued probes and recovers on the sa
|
||||
try {
|
||||
await server.connect(serverTransport);
|
||||
await client.connect(clientTransport);
|
||||
const requested = performance.now();
|
||||
const request = client.callTool(
|
||||
{ name: "list_cutter_sessions", arguments: {} },
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
await awaitAcknowledgement(connected, request, "Cutter discovery", 10_000);
|
||||
await awaitAcknowledgement(connected, request, "Cutter discovery");
|
||||
controller.abort(new Error("selected discovery cancellation"));
|
||||
await expect(request).rejects.toThrow("selected discovery cancellation");
|
||||
await expect.poll(() => settled).toBe(true);
|
||||
await expect.poll(() => sockets.size).toBe(0);
|
||||
// Probes the abort missed would still close at their own request deadline.
|
||||
expect(performance.now() - requested).toBeLessThan(
|
||||
CUTTER_REQUEST_DEADLINE_MS,
|
||||
);
|
||||
expect(connections).toBe(8);
|
||||
await Promise.all(paths.map((path) => rm(path)));
|
||||
const recovered = await client.callTool({
|
||||
|
||||
@@ -82,7 +82,7 @@ it.each([false, true])(
|
||||
message: expect.stringContaining("SDK export cancellation verification"),
|
||||
});
|
||||
await expect
|
||||
.poll(() => readdir(root), { timeout: 5000 })
|
||||
.poll(() => readdir(root))
|
||||
.toEqual(overwrite ? ["bundle.json"] : []);
|
||||
if (overwrite)
|
||||
expect(await readFile(path, "utf8")).toBe("original destination");
|
||||
|
||||
@@ -44,6 +44,7 @@ import { ToolResultDelivery } from "../../../src/server/ToolResultDelivery.js";
|
||||
import { createToolResultDelivery } from "../../../src/server/toolResult.js";
|
||||
import { createTestBinarySession } from "../../fixtures/binarySession.js";
|
||||
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
|
||||
import { waitFor } from "../../support/testWait.js";
|
||||
|
||||
const gate = () => {
|
||||
let markEntered = () => {};
|
||||
@@ -486,7 +487,7 @@ it.each([
|
||||
const serverReason: unknown = serverSignal?.reason;
|
||||
held.release();
|
||||
const result = await pending;
|
||||
await vi.waitFor(() =>
|
||||
await waitFor(() =>
|
||||
expect(success.mock.calls.length + partial.mock.calls.length).toBe(1),
|
||||
);
|
||||
expect(fixture.controls.detachCalls).toBe(1);
|
||||
@@ -629,7 +630,7 @@ it("preserves the native resume failure after MCP cancellation reaches the serve
|
||||
expect(serverSignal?.aborted).toBe(true);
|
||||
held.release();
|
||||
await pending;
|
||||
await vi.waitFor(() => expect(delivery).toHaveBeenCalledOnce());
|
||||
await waitFor(() => expect(delivery).toHaveBeenCalledOnce());
|
||||
expect(delivery.mock.calls[0]?.[0]).toMatchObject({
|
||||
_tag: "AnalysisCapabilityUnavailableError",
|
||||
operation: "resume_frida_session",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { access } from "node:fs/promises";
|
||||
|
||||
import { expect, it, vi } from "vitest";
|
||||
import { expect, it } from "vitest";
|
||||
import { z } from "zod";
|
||||
|
||||
import { AnalysisProviderRegistry } from "../../../src/application/binary/AnalysisProviderRegistry.js";
|
||||
@@ -18,6 +18,7 @@ import { silentLogger } from "../../../src/logger.js";
|
||||
import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js";
|
||||
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
|
||||
import { GhidraFixtureLauncher } from "../providers/ghidra/ghidraClient.fixture.js";
|
||||
import { waitFor } from "../../support/testWait.js";
|
||||
|
||||
// Only installation discovery and the Java bridge are simulated. The bridge
|
||||
// fixture answers with the production address_to_file_offset codes; requests
|
||||
@@ -193,7 +194,7 @@ it("reports file-backed, uninitialized and unmapped addresses through MCP withou
|
||||
});
|
||||
expect(mcp.launcher.processes).toHaveLength(1);
|
||||
// The malformed spelling never reached the bridge.
|
||||
await vi.waitFor(() => expect(mcp.bridgeCalls()).toHaveLength(4));
|
||||
await waitFor(() => expect(mcp.bridgeCalls()).toHaveLength(4));
|
||||
} finally {
|
||||
await mcp.close();
|
||||
}
|
||||
@@ -255,7 +256,7 @@ it("maps the last byte of a file-backed block and refuses the first address past
|
||||
diagnostics: { remote_code: remoteCode },
|
||||
},
|
||||
});
|
||||
await vi.waitFor(() => expect(mcp.bridgeCalls()).toHaveLength(5));
|
||||
await waitFor(() => expect(mcp.bridgeCalls()).toHaveLength(5));
|
||||
} finally {
|
||||
await mcp.close();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { access } from "node:fs/promises";
|
||||
|
||||
import { expect, it, vi } from "vitest";
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { AnalysisProviderRegistry } from "../../../src/application/binary/AnalysisProviderRegistry.js";
|
||||
import { composeBinarySession } from "../../../src/application/binary/BinarySessionComposition.js";
|
||||
@@ -13,6 +13,7 @@ import { silentLogger } from "../../../src/logger.js";
|
||||
import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js";
|
||||
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
|
||||
import { GhidraFixtureLauncher } from "../providers/ghidra/ghidraClient.fixture.js";
|
||||
import { waitFor } from "../../support/testWait.js";
|
||||
|
||||
it("reports local Ghidra admission through SDK MCP without dispatching or losing the warm session", async () => {
|
||||
const launcher = new GhidraFixtureLauncher();
|
||||
@@ -90,7 +91,7 @@ it("reports local Ghidra admission through SDK MCP without dispatching or losing
|
||||
arguments: {},
|
||||
});
|
||||
expect(after.isError).not.toBe(true);
|
||||
await vi.waitFor(() =>
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
launcher.requests.filter((method) => method === "list_strings"),
|
||||
).toHaveLength(2),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { access } from "node:fs/promises";
|
||||
|
||||
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
||||
import { expect, it, vi } from "vitest";
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { AnalysisProviderRegistry } from "../../../src/application/binary/AnalysisProviderRegistry.js";
|
||||
import { composeBinarySession } from "../../../src/application/binary/BinarySessionComposition.js";
|
||||
@@ -16,6 +16,7 @@ import { createDeferred } from "../../fixtures/binarySession.js";
|
||||
import { awaitAcknowledgement } from "../../fixtures/lifecycleAcknowledgement.js";
|
||||
|
||||
import { GhidraFixtureLauncher } from "../providers/ghidra/ghidraClient.fixture.js";
|
||||
import { waitFor } from "../../support/testWait.js";
|
||||
|
||||
it.each([0, 1])(
|
||||
"releases cancelled MCP startup caller %s while its peer completes and stays warm",
|
||||
@@ -122,7 +123,7 @@ it.each([0, 1])(
|
||||
"Ghidra startup",
|
||||
);
|
||||
controllers[cancelledIndex]?.abort();
|
||||
await vi.waitFor(() =>
|
||||
await waitFor(() =>
|
||||
expect(startResults[cancelledIndex]).toMatchObject({
|
||||
ok: false,
|
||||
error: { kind: "cancelled" },
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
||||
import { expect, it, onTestFinished, vi } from "vitest";
|
||||
import { expect, it, onTestFinished } from "vitest";
|
||||
import {
|
||||
createTestBinarySession,
|
||||
createDeferred,
|
||||
@@ -14,6 +14,7 @@ import type { JavaScriptRuntimeObservationPort } from "../../../src/application/
|
||||
import type { BrowserObservationPort } from "../../../src/application/BrowserObservationPort.js";
|
||||
import type { BrowserTargetList } from "../../../src/domain/browserObservationSchemas.js";
|
||||
import type { JavaScriptRuntimeTargetList } from "../../../src/domain/javascript/javascriptRuntimeObservation.js";
|
||||
import { waitFor } from "../../support/testWait.js";
|
||||
|
||||
it("retains late-cancelled Go artifact Evidence through MCP and keeps the server usable", async () => {
|
||||
const gate = createDeferred<void>();
|
||||
@@ -65,7 +66,7 @@ it("retains late-cancelled Go artifact Evidence through MCP and keeps the server
|
||||
controller.abort();
|
||||
gate.resolve();
|
||||
await expect(call).rejects.toThrow();
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(session.exportEvidenceBundle().records).toContainEqual(
|
||||
expect.objectContaining({
|
||||
operation: "inspect_go_binary",
|
||||
@@ -132,7 +133,7 @@ it("retains late-cancelled Inspector target Evidence through MCP and keeps the s
|
||||
controller.abort();
|
||||
gate.resolve();
|
||||
await expect(call).rejects.toThrow();
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(session.exportEvidenceBundle().records).toContainEqual(
|
||||
expect.objectContaining({
|
||||
operation: "list_javascript_runtime_targets",
|
||||
@@ -219,7 +220,7 @@ it("records late-cancelled Browser target Evidence and keeps the server usable",
|
||||
controller.abort();
|
||||
gate.resolve();
|
||||
await expect(call).rejects.toThrow();
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(session.exportEvidenceBundle().records).toContainEqual(
|
||||
expect.objectContaining({
|
||||
operation: "list_browser_targets",
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
MCP_STARTUP_POLICY,
|
||||
} from "../../../src/mcpStartupPolicy.js";
|
||||
import { createMcpSchemaValidation } from "../../fixtures/mcpSchemaValidation.js";
|
||||
import { waitFor } from "../../support/testWait.js";
|
||||
|
||||
// Compile SDK output validators for the tools a test calls, not the whole catalog.
|
||||
const schemaValidation = createMcpSchemaValidation();
|
||||
@@ -790,16 +791,13 @@ describe("production MCP doctor child lifecycle", () => {
|
||||
REA_MCP_DOCTOR_DEADLINE_MS: "60000",
|
||||
},
|
||||
});
|
||||
const pid = await vi.waitFor(
|
||||
async () => {
|
||||
const value = Number.parseInt(await readFile(pidPath, "utf8"), 10);
|
||||
expect(Number.isSafeInteger(value) && value > 0).toBe(true);
|
||||
return value;
|
||||
},
|
||||
{ timeout: 10_000 },
|
||||
);
|
||||
const pid = await waitFor(async () => {
|
||||
const value = Number.parseInt(await readFile(pidPath, "utf8"), 10);
|
||||
expect(Number.isSafeInteger(value) && value > 0).toBe(true);
|
||||
return value;
|
||||
});
|
||||
await vi.advanceTimersByTimeAsync(60_000);
|
||||
const result = await vi.waitFor(() => pending, { timeout: 10_000 });
|
||||
const result = await waitFor(() => pending);
|
||||
vi.useRealTimers();
|
||||
expect(result.healthy).toBe(false);
|
||||
expectDoctorDeadline(result.checks[0]?.detail, 60_000);
|
||||
@@ -855,19 +853,16 @@ describe("production MCP doctor child ownership", () => {
|
||||
REA_MCP_DOCTOR_DEADLINE_MS: "60000",
|
||||
},
|
||||
});
|
||||
const pids = await vi.waitFor(
|
||||
async () => {
|
||||
const value = JSON.parse(await readFile(pidPath, "utf8")) as {
|
||||
launcher: number;
|
||||
descendant: number;
|
||||
};
|
||||
expect(Number.isSafeInteger(value.descendant)).toBe(true);
|
||||
return value;
|
||||
},
|
||||
{ timeout: 10_000 },
|
||||
);
|
||||
const pids = await waitFor(async () => {
|
||||
const value = JSON.parse(await readFile(pidPath, "utf8")) as {
|
||||
launcher: number;
|
||||
descendant: number;
|
||||
};
|
||||
expect(Number.isSafeInteger(value.descendant)).toBe(true);
|
||||
return value;
|
||||
});
|
||||
await vi.advanceTimersByTimeAsync(60_000);
|
||||
const result = await vi.waitFor(() => pending, { timeout: 10_000 });
|
||||
const result = await waitFor(() => pending);
|
||||
vi.useRealTimers();
|
||||
// The launcher exits at once, so only owned cleanup can stop the descendant.
|
||||
// The SDK transport signaled only its direct child and reported success.
|
||||
@@ -895,14 +890,11 @@ describe("production MCP doctor request-flow deadline", () => {
|
||||
REA_MCP_DOCTOR_DEADLINE_MS: "60000",
|
||||
},
|
||||
});
|
||||
await vi.waitFor(
|
||||
async () => {
|
||||
await readFile(readyPath, "utf8");
|
||||
},
|
||||
{ timeout: 10_000 },
|
||||
);
|
||||
await waitFor(async () => {
|
||||
await readFile(readyPath, "utf8");
|
||||
});
|
||||
await vi.advanceTimersByTimeAsync(60_000);
|
||||
const result = await vi.waitFor(() => pending, { timeout: 10_000 });
|
||||
const result = await waitFor(() => pending);
|
||||
vi.useRealTimers();
|
||||
expect(result.server).not.toBeNull();
|
||||
const requestFlow = result.checks.find(
|
||||
|
||||
@@ -11,6 +11,7 @@ import { createJavaScriptRuntimeObservationProvider } from "../../../src/composi
|
||||
import { run } from "../../../src/main.js";
|
||||
import { MCP_SHUTDOWN_FAILED } from "../../../src/main/messages.js";
|
||||
import { createServer } from "../../../src/server/createServer.js";
|
||||
import { waitFor } from "../../support/testWait.js";
|
||||
|
||||
mkdirSync(".cache", { recursive: true });
|
||||
const root = mkdtempSync(resolve(".cache/optional-provider-shutdown-"));
|
||||
@@ -169,15 +170,15 @@ it("retries failed process-owned optional cleanup without reclosing completed pe
|
||||
throw new Error("Shutdown hook was not installed");
|
||||
|
||||
shutdown();
|
||||
await vi.waitFor(() => expect(runtimeClose).toHaveBeenCalledTimes(1));
|
||||
await vi.waitFor(() => expect(exitCodes).toEqual([1]));
|
||||
await waitFor(() => expect(runtimeClose).toHaveBeenCalledTimes(1));
|
||||
await waitFor(() => expect(exitCodes).toEqual([1]));
|
||||
expect(browserClose).toHaveBeenCalledTimes(1);
|
||||
expect(transportCloses).toBe(1);
|
||||
expect(servers).toBe(1);
|
||||
expect(unregisterCalls).toBe(0);
|
||||
|
||||
shutdown();
|
||||
await vi.waitFor(() => expect(unregisterCalls).toBe(1));
|
||||
await waitFor(() => expect(unregisterCalls).toBe(1));
|
||||
|
||||
expect(runtimeClose).toHaveBeenCalledTimes(2);
|
||||
expect(browserClose).toHaveBeenCalledTimes(1);
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
wasmArtifactFixture,
|
||||
wasmArtifactExecution,
|
||||
} from "../../fixtures/wasm/artifact.js";
|
||||
import { waitFor } from "../../support/testWait.js";
|
||||
|
||||
const schemaValidation = createMcpSchemaValidation();
|
||||
afterAll(() => schemaValidation.clear());
|
||||
@@ -344,7 +345,7 @@ it.each([undefined, "summary"] as const)(
|
||||
candidate_paths: [],
|
||||
});
|
||||
await harness.client.ping();
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
// Failed summary delivery retains the complete producer, without a derived view.
|
||||
expect(session.exportEvidenceBundle().records).toEqual([partial]);
|
||||
});
|
||||
|
||||
@@ -815,12 +815,7 @@ fs.writeFileSync(${JSON.stringify(marker)}, 'ready'); setInterval(() => {}, 1000
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
await withVerifierCleanup(async () => {
|
||||
await awaitAcknowledgement(
|
||||
ready,
|
||||
pending,
|
||||
`${phase} tool startup`,
|
||||
5_000,
|
||||
);
|
||||
await awaitAcknowledgement(ready, pending, `${phase} tool startup`);
|
||||
controller.abort(new Error("caller cancellation"));
|
||||
const result = await pending;
|
||||
if (result.ok) throw new Error("expected cancellation");
|
||||
|
||||
@@ -302,7 +302,14 @@ it("retains partial JADX replies when owned cleanup also fails", async () => {
|
||||
});
|
||||
|
||||
it("cleans an active engine when the MCP client disconnects", async () => {
|
||||
const { provider, apk, launches } = await createJadxProtocolFixture("stall");
|
||||
// Without a disconnect-driven stop, the operation deadline would also clean
|
||||
// the stalled engine; keep it beyond any test deadline.
|
||||
const { provider, apk, launches } = await createJadxProtocolFixture(
|
||||
"stall",
|
||||
undefined,
|
||||
undefined,
|
||||
{ REA_JADX_OPERATION_TIMEOUT_MS: "2147483647" },
|
||||
);
|
||||
const session = createTestBinarySession(() => {
|
||||
throw new Error("Unexpected native provider acquisition");
|
||||
});
|
||||
@@ -314,7 +321,7 @@ it("cleans an active engine when the MCP client disconnects", async () => {
|
||||
const pending = client
|
||||
.callTool({ name: "inspect_android_package", arguments: { path: apk } })
|
||||
.catch(() => undefined);
|
||||
await expect.poll(() => launches.length, { timeout: 10_000 }).toBe(1);
|
||||
await expect.poll(() => launches.length).toBe(1);
|
||||
await client.close();
|
||||
await pending;
|
||||
await expect
|
||||
|
||||
@@ -148,13 +148,10 @@ it.each([false, true])(
|
||||
vi.advanceTimersByTime(60_000);
|
||||
vi.useRealTimers();
|
||||
await expect
|
||||
.poll(
|
||||
() => ({
|
||||
fd: owner.handle.fd,
|
||||
cleanupIncomplete: failed.error.cleanupIncomplete,
|
||||
}),
|
||||
{ timeout: 5_000 },
|
||||
)
|
||||
.poll(() => ({
|
||||
fd: owner.handle.fd,
|
||||
cleanupIncomplete: failed.error.cleanupIncomplete,
|
||||
}))
|
||||
.toEqual({ fd: -1, cleanupIncomplete: false });
|
||||
expect(failed.error.cause).toBe(originalCause);
|
||||
expect(fixture.attempts.every((attempt) => attempt === owner)).toBe(true);
|
||||
|
||||
@@ -1661,7 +1661,10 @@ const stopNodeChild = async (child: ChildProcess) => {
|
||||
await exited;
|
||||
};
|
||||
|
||||
const waitForChildReadiness = async (child: ChildProcess): Promise<unknown> => {
|
||||
const waitForChildReadiness = async (
|
||||
child: ChildProcess,
|
||||
timeoutMs?: number,
|
||||
): Promise<unknown> => {
|
||||
const listeners = new AbortController();
|
||||
const message = once(child, "message", { signal: listeners.signal });
|
||||
const exit = once(child, "exit", { signal: listeners.signal });
|
||||
@@ -1670,6 +1673,7 @@ const waitForChildReadiness = async (child: ChildProcess): Promise<unknown> => {
|
||||
message.then(() => undefined),
|
||||
exit,
|
||||
"title-mutating child readiness",
|
||||
timeoutMs,
|
||||
);
|
||||
return (await message)[0];
|
||||
} finally {
|
||||
@@ -1682,7 +1686,8 @@ it("stops a child that never acknowledges readiness", async () => {
|
||||
stdio: ["ignore", "ignore", "ignore", "ipc"],
|
||||
});
|
||||
try {
|
||||
await expect(waitForChildReadiness(child)).rejects.toThrow(
|
||||
// The gate deadline is the behavior under test.
|
||||
await expect(waitForChildReadiness(child, 2_000)).rejects.toThrow(
|
||||
"title-mutating child readiness was not acknowledged before the gate deadline",
|
||||
);
|
||||
} finally {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { mkdir, readFile, stat } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { expect, it, vi } from "vitest";
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { ArtifactResourceScope } from "../../../src/artifacts/ArtifactResourceScope.js";
|
||||
import { runDesktopBuildOwnedCommand } from "../../../src/desktop/DesktopBuildOwnedCommand.js";
|
||||
import { prepareProcessOwnershipInspection } from "../../../src/process/ProcessOwnershipObservation.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { waitFor } from "../../support/testWait.js";
|
||||
|
||||
it.skipIf(process.platform === "win32")(
|
||||
"stops the packing process family before removing its partial archive on timeout",
|
||||
@@ -77,12 +78,9 @@ it.skipIf(process.platform === "win32")(
|
||||
});
|
||||
void running.catch(() => undefined);
|
||||
try {
|
||||
await vi.waitFor(
|
||||
async () => {
|
||||
expect(Number(await readFile(pidFile, "utf8"))).toBeGreaterThan(0);
|
||||
},
|
||||
{ timeout: 5000 },
|
||||
);
|
||||
await waitFor(async () => {
|
||||
expect(Number(await readFile(pidFile, "utf8"))).toBeGreaterThan(0);
|
||||
});
|
||||
const pid = Number(await readFile(pidFile, "utf8"));
|
||||
controller.abort(reason);
|
||||
await expect(running).rejects.toMatchObject({
|
||||
|
||||
@@ -1219,9 +1219,7 @@ it("resolves every reported shutdown cleanup failure after ownership recovers",
|
||||
cleanupFails = false;
|
||||
// Closing the transport can leave the child between live and exited
|
||||
// snapshots. Retry retained ownership until cleanup is actually verified.
|
||||
await expect
|
||||
.poll(() => provider.close(), { timeout: 5_000 })
|
||||
.toBeUndefined();
|
||||
await expect.poll(() => provider.close()).toBeUndefined();
|
||||
};
|
||||
try {
|
||||
const outcome = await service.execute("inspect_android_package", {
|
||||
|
||||
@@ -324,7 +324,7 @@ it.skipIf(process.platform === "win32").each([
|
||||
await expectHarnessFailureCleanup(supervisor, events);
|
||||
return;
|
||||
}
|
||||
await waitForProviderProcessReady(child.process, 25_000);
|
||||
await waitForProviderProcessReady(child.process);
|
||||
const observed = JSON.parse(await readFile(report, "utf8"));
|
||||
expect(observed.reason).toBe("timeout");
|
||||
expect(observed.pids).toHaveLength(2);
|
||||
|
||||
@@ -59,9 +59,7 @@ const isRunning = async (pid: number): Promise<boolean> => {
|
||||
};
|
||||
|
||||
const waitForExit = async (pid: number): Promise<void> => {
|
||||
await expect
|
||||
.poll(() => isRunning(pid), { timeout: 3_000, interval: 25 })
|
||||
.toBe(false);
|
||||
await expect.poll(() => isRunning(pid), { interval: 25 }).toBe(false);
|
||||
};
|
||||
|
||||
const cleanupFixture = async (marker: Marker | undefined): Promise<void> => {
|
||||
|
||||
@@ -608,12 +608,10 @@ itWithCaptureCapability(
|
||||
);
|
||||
try {
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
cancelling.notifications.some((note) =>
|
||||
note.params.message?.startsWith("running:"),
|
||||
),
|
||||
{ timeout: 4_000 },
|
||||
.poll(() =>
|
||||
cancelling.notifications.some((note) =>
|
||||
note.params.message?.startsWith("running:"),
|
||||
),
|
||||
)
|
||||
.toBe(true);
|
||||
controller.abort();
|
||||
@@ -634,7 +632,7 @@ itWithCaptureCapability(
|
||||
cancelling.notifications
|
||||
.map((note) => note.params.message)
|
||||
.find((message) => message?.startsWith("cleanup:"));
|
||||
await expect.poll(cleanupMessage, { timeout: 15_000 }).toBeDefined();
|
||||
await expect.poll(cleanupMessage).toBeDefined();
|
||||
expect(cleanupMessage()).toContain("disposition=cancelled");
|
||||
expect(cleanupMessage()).toContain("owned_process_group=cleaned");
|
||||
expect(JSON.stringify(cancelling.notifications)).not.toMatch(
|
||||
|
||||
@@ -202,7 +202,6 @@ describe("process CLI cleanup errors", () => {
|
||||
expect(Number.isSafeInteger(targetPid) && targetPid > 0).toBe(true);
|
||||
await expect
|
||||
.poll(() => observeProcessStartIdentity(targetPid), {
|
||||
timeout: 7_000,
|
||||
interval: 25,
|
||||
})
|
||||
.toBeUndefined();
|
||||
|
||||
@@ -6,10 +6,7 @@ import type { ChildProcessWithoutNullStreams } from "node:child_process";
|
||||
import { expect, it, vi } from "vitest";
|
||||
|
||||
import { awaitAcknowledgement } from "../../fixtures/lifecycleAcknowledgement.js";
|
||||
import {
|
||||
createTestProcessRegistry,
|
||||
remainingProcessDeadline,
|
||||
} from "../../support/process/processFixture.js";
|
||||
import { createTestProcessRegistry } from "../../support/process/processFixture.js";
|
||||
|
||||
const within = async <T>(
|
||||
operation: Promise<T>,
|
||||
@@ -281,15 +278,6 @@ it("terminates a stalled real child at its deadline while retaining captured out
|
||||
}
|
||||
});
|
||||
|
||||
it("derives a child deadline from the test deadline, keeping a reporting reserve", () => {
|
||||
expect(remainingProcessDeadline(120_000, 0)).toBe(108_000);
|
||||
expect(remainingProcessDeadline(30_000, 20_000)).toBe(5_000);
|
||||
// The reserve never drops below five seconds, nor the deadline below one.
|
||||
expect(remainingProcessDeadline(8_000, 0)).toBe(3_000);
|
||||
expect(remainingProcessDeadline(8_000, 7_500)).toBe(1_000);
|
||||
expect(remainingProcessDeadline(0, 0)).toBeUndefined();
|
||||
});
|
||||
|
||||
it(
|
||||
"stops a hung child before its running test's deadline and reports its output",
|
||||
// The test deadline is the behavior under test: the child gets what remains.
|
||||
|
||||
@@ -143,7 +143,6 @@ function release() { clearInterval(timer); clearTimeout(deadline); socket.end();
|
||||
ready.promise,
|
||||
closed,
|
||||
"test-owned descendant readiness",
|
||||
5_000,
|
||||
);
|
||||
runner.kill(signal);
|
||||
expect(await closed).toEqual({
|
||||
|
||||
@@ -44,7 +44,7 @@ const deferred = () => {
|
||||
const waitForBoundary = (
|
||||
boundary: Promise<void>,
|
||||
running: Promise<Outcome<unknown>>,
|
||||
) => awaitAcknowledgement(boundary, running, "terminal lifecycle", 5_000);
|
||||
) => awaitAcknowledgement(boundary, running, "terminal lifecycle");
|
||||
|
||||
const terminalFixture = async () => {
|
||||
const parent = await createTestTempDirectory("rea-verifier-terminal-");
|
||||
|
||||
@@ -74,9 +74,7 @@ const openTerminal = async (): Promise<Terminal> => {
|
||||
if (!exited) pty.kill();
|
||||
await ended;
|
||||
});
|
||||
await expect
|
||||
.poll(() => rizinTestTtyPathPattern.test(output), { timeout: 2_000 })
|
||||
.toBe(true);
|
||||
await expect.poll(() => rizinTestTtyPathPattern.test(output)).toBe(true);
|
||||
return {
|
||||
path: rizinTestTtyPathPattern.exec(output)![0],
|
||||
output: () => output,
|
||||
@@ -334,10 +332,7 @@ it.skipIf(process.platform === "win32")(
|
||||
// read it from the selected TTY, so the caller's input channel really works.
|
||||
current.terminal.write("rea-input-probe\n");
|
||||
await expect
|
||||
.poll(
|
||||
() => current.terminal.output().split("rea-input-probe").length - 1,
|
||||
{ timeout: 2_000 },
|
||||
)
|
||||
.poll(() => current.terminal.output().split("rea-input-probe").length - 1)
|
||||
.toBe(2);
|
||||
successValue(await manager.close(sessionId));
|
||||
expect(await current.commands()).toEqual(["resume-target", "dr", "olj"]);
|
||||
|
||||
@@ -106,9 +106,7 @@ process.stdin.resume();`,
|
||||
expect(await supervisor.waitForExit(1_000)).toBe(true);
|
||||
else
|
||||
await expect
|
||||
.poll(() => readFile(redirected, "utf8"), {
|
||||
timeout: 1_000,
|
||||
})
|
||||
.poll(() => readFile(redirected, "utf8"))
|
||||
.toBe("redirected");
|
||||
}
|
||||
} else if (line === "e dbg.exitkills") {
|
||||
@@ -137,9 +135,7 @@ process.stdin.resume();`,
|
||||
},
|
||||
);
|
||||
try {
|
||||
await expect
|
||||
.poll(() => readFile(ready, "utf8"), { timeout: 2_000 })
|
||||
.toBe("ready");
|
||||
await expect.poll(() => readFile(ready, "utf8")).toBe("ready");
|
||||
const started = await manager.start({
|
||||
path: "fixture.bin",
|
||||
inferior_tty: tty,
|
||||
|
||||
@@ -17,6 +17,7 @@ import { parseEvidence } from "../../../../src/domain/evidence.js";
|
||||
import { projectAnalysisError } from "../../../../src/domain/analysisErrorProjection.js";
|
||||
import { AnalysisTimeoutError } from "../../../../src/domain/analysisErrorCore.js";
|
||||
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
|
||||
import { waitFor } from "../../../support/testWait.js";
|
||||
|
||||
const hooks = vi.hoisted(() => ({
|
||||
afterCommand: undefined as (() => void) | undefined,
|
||||
@@ -131,7 +132,7 @@ test("retains the command owner's exact cancellation reason after active process
|
||||
{ timeoutMs: 5000, maxOutputBytes: 1024 },
|
||||
);
|
||||
try {
|
||||
await vi.waitFor(async () =>
|
||||
await waitFor(async () =>
|
||||
expect(await readFile(ready, "utf8")).toBe("ready"),
|
||||
);
|
||||
controller.abort(null);
|
||||
|
||||
@@ -47,7 +47,7 @@ it.skipIf(process.platform === "win32")(
|
||||
({ kind }) => kind === "running",
|
||||
);
|
||||
},
|
||||
{ timeout: 8_000, interval: 25 },
|
||||
{ interval: 25 },
|
||||
)
|
||||
.toBe(true);
|
||||
const outcome = await awaitApktoolOperation(pending, 15_000);
|
||||
@@ -72,7 +72,7 @@ it.skipIf(process.platform === "win32")(
|
||||
(await Promise.all(launched.map(observePosixProcess))).every(
|
||||
({ kind }) => kind !== "running",
|
||||
),
|
||||
{ timeout: 5_000, interval: 25 },
|
||||
{ interval: 25 },
|
||||
)
|
||||
.toBe(true);
|
||||
} finally {
|
||||
|
||||
@@ -7,6 +7,8 @@ import { createApktoolProcessFixture } from "../../fixtures/apktoolProcessTree.j
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { observePosixProcess } from "../../support/process/observePosixProcess.js";
|
||||
|
||||
const VERSION_PROBE_DEADLINE_MS = 20_000;
|
||||
|
||||
it
|
||||
.skipIf(process.platform === "win32")
|
||||
.each(["malformed", "unreadable"] as const)(
|
||||
@@ -17,6 +19,7 @@ it
|
||||
await expect(fixture.readMarker()).resolves.toBeUndefined();
|
||||
const commands = new ApktoolCommandCleanup();
|
||||
const controller = new AbortController();
|
||||
const probeStarted = performance.now();
|
||||
const probe = inspectApktoolClient(
|
||||
{ command: fixture.command, commandSource: "environment" },
|
||||
commands,
|
||||
@@ -25,7 +28,6 @@ it
|
||||
try {
|
||||
await expect
|
||||
.poll(async () => (await fixture.readMarker())?.ready, {
|
||||
timeout: 10_000,
|
||||
interval: 25,
|
||||
})
|
||||
.toBe(true);
|
||||
@@ -52,9 +54,14 @@ it
|
||||
(await Promise.all(pids.map(observePosixProcess))).every(
|
||||
({ kind }) => kind !== "running",
|
||||
),
|
||||
{ timeout: 5_000, interval: 25 },
|
||||
{ interval: 25 },
|
||||
)
|
||||
.toBe(true);
|
||||
// The still-running version probe would also stop them at its own
|
||||
// deadline (ApktoolConfiguration: 20 s); fixture teardown must be first.
|
||||
expect(performance.now() - probeStarted).toBeLessThan(
|
||||
VERSION_PROBE_DEADLINE_MS,
|
||||
);
|
||||
} finally {
|
||||
controller.abort();
|
||||
await fixture.cleanup([probe, commands.close()]);
|
||||
|
||||
@@ -52,6 +52,7 @@ import {
|
||||
GhidraFixtureLauncher as FixtureLauncher,
|
||||
ghidraFixturePath as fixturePath,
|
||||
} from "./ghidraClient.fixture.js";
|
||||
import { waitFor } from "../../../support/testWait.js";
|
||||
|
||||
const PROFILE_DIGEST = "a".repeat(64);
|
||||
const PROVIDER_VERSION = "12.1.4";
|
||||
@@ -1383,7 +1384,7 @@ it.each([0, 1])(
|
||||
cancelled = result;
|
||||
});
|
||||
controllers[cancelledIndex]?.abort();
|
||||
await vi.waitFor(() =>
|
||||
await waitFor(() =>
|
||||
expect(cancelled).toMatchObject({
|
||||
ok: false,
|
||||
error: { kind: "cancelled" },
|
||||
@@ -1545,7 +1546,7 @@ it("applies one startup deadline and removes the private runtime", async () => {
|
||||
const pending = client.start();
|
||||
const child = await launcher.started;
|
||||
await vi.advanceTimersByTimeAsync(30);
|
||||
const result = await vi.waitFor(() => pending, { timeout: 10_000 });
|
||||
const result = await waitFor(() => pending);
|
||||
vi.useRealTimers();
|
||||
|
||||
expect(result).toMatchObject({
|
||||
@@ -1610,7 +1611,7 @@ describe("GhidraClient established requests", () => {
|
||||
{ document: null, procedure: "fixture_main" },
|
||||
{ signal: activeController.signal },
|
||||
);
|
||||
await vi.waitFor(() =>
|
||||
await waitFor(() =>
|
||||
expect(launcher.requests).toContain("procedure_pseudo_code"),
|
||||
);
|
||||
const queuedController = new AbortController();
|
||||
@@ -1675,7 +1676,7 @@ describe("GhidraClient runtime removal failures", () => {
|
||||
{ document: null, procedure: "fixture_main" },
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
await vi.waitFor(() =>
|
||||
await waitFor(() =>
|
||||
expect(launcher.requests).toContain("procedure_pseudo_code"),
|
||||
);
|
||||
const queued = client.callTool("procedure_info", {
|
||||
@@ -1769,7 +1770,7 @@ describe("GhidraClient cleanup and diagnostics", () => {
|
||||
client.callTool("list_strings", { document: null }),
|
||||
).resolves.toMatchObject({ ok: true });
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(client.diagnostics()).toMatchObject({
|
||||
diagnostic_truncated: true,
|
||||
});
|
||||
@@ -2416,7 +2417,7 @@ it.each([null, "caller stopped", new Error("caller stopped")])(
|
||||
{ document: null, procedure: "fixture_main" },
|
||||
{ signal: activeController.signal },
|
||||
);
|
||||
await vi.waitFor(() =>
|
||||
await waitFor(() =>
|
||||
expect(launcher.requests).toContain("procedure_pseudo_code"),
|
||||
);
|
||||
const queuedController = new AbortController();
|
||||
@@ -2498,7 +2499,7 @@ it.each([null, "caller stopped", new Error("caller stopped")])(
|
||||
const client = clientFor(launcher, { startupTimeoutMs: 10000 });
|
||||
const controller = new AbortController();
|
||||
const pending = client.start(controller.signal);
|
||||
await vi.waitFor(() => expect(observed).toHaveBeenCalled());
|
||||
await waitFor(() => expect(observed).toHaveBeenCalled());
|
||||
controller.abort(reason);
|
||||
const result = await pending;
|
||||
if (result.ok) throw new Error("Expected startup cancellation");
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
ghidraHeadlessCommand,
|
||||
GhidraHeadlessLauncher,
|
||||
} from "../../../../src/ghidra/GhidraLauncher.js";
|
||||
import { waitFor } from "../../../support/testWait.js";
|
||||
|
||||
const fixturePath = fileURLToPath(
|
||||
new URL(
|
||||
@@ -1193,9 +1194,7 @@ describe("Ghidra Windows launcher process ownership", () => {
|
||||
if (!launched.ok) throw launched.error;
|
||||
try {
|
||||
const capturePath = join(runtimeRoot, "launch-capture.json");
|
||||
await vi.waitFor(() => access(`${capturePath}.ready`), {
|
||||
timeout: 10_000,
|
||||
});
|
||||
await waitFor(() => access(`${capturePath}.ready`));
|
||||
const capture = launchCaptureSchema.parse(
|
||||
JSON.parse(await readFile(capturePath, "utf8")),
|
||||
);
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
HopperFixtureLauncher,
|
||||
trackHopperClient,
|
||||
} from "./hopperClient.fixture.js";
|
||||
import { waitFor } from "../../../support/testWait.js";
|
||||
|
||||
const limit = (maximumBytes: number) =>
|
||||
({ maximumBytes, source: "setting" }) as const;
|
||||
@@ -177,12 +178,9 @@ it("serializes a queued Hopper request only when it reaches the wire", async ()
|
||||
},
|
||||
},
|
||||
);
|
||||
await vi.waitFor(
|
||||
() => {
|
||||
if (!queuedNotice) throw new Error("echo is not queued yet");
|
||||
},
|
||||
{ timeout: 5_000 },
|
||||
);
|
||||
await waitFor(() => {
|
||||
if (!queuedNotice) throw new Error("echo is not queued yet");
|
||||
});
|
||||
expect(serializedPayload(stringify.mock.calls, value)).toBe(false);
|
||||
await launcher.release(held);
|
||||
await expect(active).resolves.toMatchObject({
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
HopperFixtureLauncher,
|
||||
startHopperFixtureClient,
|
||||
} from "./hopperClient.fixture.js";
|
||||
import { waitFor } from "../../../support/testWait.js";
|
||||
|
||||
describe("HopperClient request deadlines", () => {
|
||||
it("propagates deadlines through the socket client and accepts late replies safely", async () => {
|
||||
@@ -16,17 +17,15 @@ describe("HopperClient request deadlines", () => {
|
||||
.then(settled);
|
||||
const request = await launcher.waitForRequest("echo", "deadline");
|
||||
try {
|
||||
await vi.waitFor(
|
||||
() =>
|
||||
expect(settled).toHaveBeenCalledWith({
|
||||
ok: false,
|
||||
error: expect.objectContaining({
|
||||
_tag: "HopperTimeoutError",
|
||||
timeoutMs: 100,
|
||||
operation: "echo",
|
||||
}),
|
||||
await waitFor(() =>
|
||||
expect(settled).toHaveBeenCalledWith({
|
||||
ok: false,
|
||||
error: expect.objectContaining({
|
||||
_tag: "HopperTimeoutError",
|
||||
timeoutMs: 100,
|
||||
operation: "echo",
|
||||
}),
|
||||
{ timeout: 1000 },
|
||||
}),
|
||||
);
|
||||
expect(client.requestActivity()).toMatchObject({
|
||||
callerState: "cancelled",
|
||||
@@ -92,7 +91,7 @@ describe("HopperClient request deadlines", () => {
|
||||
{ value: "queued" },
|
||||
{ timeoutMs: 1000 },
|
||||
);
|
||||
await vi.waitFor(() =>
|
||||
await waitFor(() =>
|
||||
expect(client.requestActivity()?.queuedRequests).toBe(1),
|
||||
);
|
||||
await client.close();
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { access } from "node:fs/promises";
|
||||
|
||||
import { expect, it, onTestFinished, vi } from "vitest";
|
||||
import { expect, it, onTestFinished } from "vitest";
|
||||
|
||||
import { HopperTimeoutError } from "../../../../src/domain/hopperErrors.js";
|
||||
import { HopperClient } from "../../../../src/hopper/HopperClient.js";
|
||||
import { HopperFixtureLauncher } from "./hopperClient.fixture.js";
|
||||
import { waitFor } from "../../../support/testWait.js";
|
||||
|
||||
const HOPPER_PROCESS_DIAGNOSTIC_BYTES = 8 * 1024 * 1024;
|
||||
|
||||
@@ -31,7 +32,7 @@ it("bounds long-lived launcher output while keeping the bridge usable", async ()
|
||||
await expect(client.start()).resolves.toMatchObject({ ok: true });
|
||||
const session = await client.callTool("echo", { value: "still usable" });
|
||||
expect(session).toEqual({ ok: true, value: { value: "still usable" } });
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(observedStderrBytes).toBeGreaterThan(
|
||||
HOPPER_PROCESS_DIAGNOSTIC_BYTES,
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { spawn, type ChildProcess } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { onTestFinished, vi } from "vitest";
|
||||
import { onTestFinished } from "vitest";
|
||||
import { z } from "zod";
|
||||
|
||||
import { ok } from "../../../../src/domain/result.js";
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
} from "../../../../src/hopper/BridgeLauncher.js";
|
||||
import { waitForExit } from "../../../support/process/processFixture.js";
|
||||
import { HopperClient } from "../../../../src/hopper/HopperClient.js";
|
||||
import { waitFor } from "../../../support/testWait.js";
|
||||
|
||||
export const hopperFixturePath = fileURLToPath(
|
||||
new URL("../../../fixtures/fakeHopper.mjs", import.meta.url),
|
||||
@@ -93,19 +94,16 @@ export class HopperFixtureLauncher implements BridgeLauncher {
|
||||
|
||||
/** Wait for acknowledgement that the fixture actually received a request. */
|
||||
waitForRequest(method: string, gate?: string): Promise<FixtureRequest> {
|
||||
return vi.waitFor(
|
||||
() => {
|
||||
const request = this.requests.find(
|
||||
(value) =>
|
||||
value.method === method &&
|
||||
(gate === undefined || value.params?.gate === gate),
|
||||
);
|
||||
if (request === undefined)
|
||||
throw new Error(`Waiting for Hopper fixture request ${method}`);
|
||||
return request;
|
||||
},
|
||||
{ timeout: 10_000 },
|
||||
);
|
||||
return waitFor(() => {
|
||||
const request = this.requests.find(
|
||||
(value) =>
|
||||
value.method === method &&
|
||||
(gate === undefined || value.params?.gate === gate),
|
||||
);
|
||||
if (request === undefined)
|
||||
throw new Error(`Waiting for Hopper fixture request ${method}`);
|
||||
return request;
|
||||
});
|
||||
}
|
||||
|
||||
/** Release a response held by the fixture's gate parameter. */
|
||||
|
||||
@@ -3,7 +3,7 @@ import { access, rm } from "node:fs/promises";
|
||||
import { setTimeout as delay } from "node:timers/promises";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
import { expect, it, onTestFinished, vi } from "vitest";
|
||||
import { expect, it, onTestFinished } from "vitest";
|
||||
|
||||
import { projectAnalysisError } from "../../../../src/domain/analysisErrorProjection.js";
|
||||
import { ok } from "../../../../src/domain/result.js";
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
} from "../../../../src/hopper/BridgeLauncher.js";
|
||||
import { HopperClient } from "../../../../src/hopper/HopperClient.js";
|
||||
import { HopperFixtureLauncher } from "./hopperClient.fixture.js";
|
||||
import { waitFor } from "../../../support/testWait.js";
|
||||
|
||||
const stop = async (child: ChildProcess): Promise<void> => {
|
||||
if (child.exitCode !== null || child.signalCode !== null) return;
|
||||
@@ -145,7 +146,7 @@ it.each(["missing", "not-listening"] as const)(
|
||||
"health",
|
||||
"shutdown_document",
|
||||
]);
|
||||
await vi.waitFor(() => expect(fixture.processes[0]?.exitCode).toBe(0));
|
||||
await waitFor(() => expect(fixture.processes[0]?.exitCode).toBe(0));
|
||||
await expect(access(session.directory)).rejects.toMatchObject({
|
||||
code: "ENOENT",
|
||||
});
|
||||
@@ -202,9 +203,7 @@ it("reconnects for authenticated cleanup after a previously valid bridge transpo
|
||||
"health",
|
||||
"shutdown_document",
|
||||
]);
|
||||
await vi.waitFor(() =>
|
||||
expect(launcher.fixture.processes[0]?.exitCode).toBe(0),
|
||||
);
|
||||
await waitFor(() => expect(launcher.fixture.processes[0]?.exitCode).toBe(0));
|
||||
const session = launcher.sessions[0];
|
||||
if (session === undefined) throw new Error("Fixture did not launch");
|
||||
await expect(access(session.directory)).rejects.toMatchObject({
|
||||
@@ -235,7 +234,7 @@ it("preserves launcher diagnostics after a health deadline and reauthenticates f
|
||||
});
|
||||
expect(client.requestActivity()).toBeNull();
|
||||
const closing = client.close();
|
||||
const next = await vi.waitFor(() => {
|
||||
const next = await waitFor(() => {
|
||||
const request = launcher.fixture.requests.find(
|
||||
({ method, id }) => method === "health" && id !== first.id,
|
||||
);
|
||||
@@ -277,7 +276,7 @@ it("recovers an application exit after the first valid handshake occurs during c
|
||||
});
|
||||
|
||||
const closing = client.close();
|
||||
const next = await vi.waitFor(() => {
|
||||
const next = await waitFor(() => {
|
||||
const request = launcher.fixture.requests.find(
|
||||
({ method, id }) => method === "health" && id !== first.id,
|
||||
);
|
||||
@@ -299,7 +298,7 @@ it("recovers an application exit after the first valid handshake occurs during c
|
||||
code: "ENOENT",
|
||||
});
|
||||
const restarted = client.start();
|
||||
const health = await vi.waitFor(() => {
|
||||
const health = await waitFor(() => {
|
||||
const request = launcher.fixture.requests.find(
|
||||
({ method, id }) => method === "health" && id > next.id,
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { EventEmitter } from "node:events";
|
||||
import { PassThrough } from "node:stream";
|
||||
|
||||
import { describe, expect, it, onTestFinished, vi } from "vitest";
|
||||
import { describe, expect, it, onTestFinished } from "vitest";
|
||||
import { awaitAcknowledgement } from "../../../fixtures/lifecycleAcknowledgement.js";
|
||||
|
||||
import { projectAnalysisError } from "../../../../src/domain/analysisErrorProjection.js";
|
||||
@@ -12,6 +12,7 @@ import type {
|
||||
} from "../../../../src/hopper/BridgeLauncher.js";
|
||||
import { HopperClient } from "../../../../src/hopper/HopperClient.js";
|
||||
import { HopperFixtureLauncher } from "./hopperClient.fixture.js";
|
||||
import { waitFor } from "../../../support/testWait.js";
|
||||
|
||||
class ObservedProcess extends EventEmitter {
|
||||
readonly stdout = new PassThrough();
|
||||
@@ -90,7 +91,7 @@ it("reports launch work until the first authenticated handshake completes", asyn
|
||||
await client.close();
|
||||
});
|
||||
const starting = client.callTool("current_document");
|
||||
await awaitAcknowledgement(entered, starting, "Hopper launch", 10_000);
|
||||
await awaitAcknowledgement(entered, starting, "Hopper launch");
|
||||
expect(client.requestActivity()).toBeNull();
|
||||
expect(client.operationHealth()).toMatchObject({
|
||||
state: "busy",
|
||||
@@ -121,7 +122,7 @@ describe("Hopper provider lifecycle health", () => {
|
||||
const pending = client.callTool("hang");
|
||||
await launcher.fixture.waitForRequest("hang");
|
||||
launcher.fixture.processes.at(-1)?.send({ disconnect: true });
|
||||
await vi.waitFor(() =>
|
||||
await waitFor(() =>
|
||||
expect(client.operationHealth().state).toBe("unreachable"),
|
||||
);
|
||||
expect(await pending).toMatchObject({
|
||||
@@ -204,7 +205,7 @@ describe("Hopper provider lifecycle health", () => {
|
||||
},
|
||||
},
|
||||
);
|
||||
await vi.waitFor(() =>
|
||||
await waitFor(() =>
|
||||
expect(client.requestActivity()?.queuedRequests).toBe(1),
|
||||
);
|
||||
await launcher.fixture.release(request);
|
||||
@@ -228,9 +229,7 @@ it("preserves an owned signal exit and diagnostics for active, queued and later
|
||||
const active = client.callTool("hang");
|
||||
const request = await launcher.fixture.waitForRequest("hang");
|
||||
const queued = client.callTool("echo");
|
||||
await vi.waitFor(() =>
|
||||
expect(client.requestActivity()?.queuedRequests).toBe(1),
|
||||
);
|
||||
await waitFor(() => expect(client.requestActivity()?.queuedRequests).toBe(1));
|
||||
const token = launcher.fixture.tokens[0];
|
||||
launcher.observed.stderr.write(`API_KEY=local-evidence-kept ${token}\n`);
|
||||
launcher.observed.exit(null, "SIGTERM");
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
HopperFixtureLauncher,
|
||||
startHopperFixtureClient as startClient,
|
||||
} from "./hopperClient.fixture.js";
|
||||
import { waitFor } from "../../../support/testWait.js";
|
||||
|
||||
describe("HopperClient progress", () => {
|
||||
it("forwards correlated Python progress before the terminal response", async () => {
|
||||
@@ -27,7 +28,7 @@ describe("HopperClient progress", () => {
|
||||
},
|
||||
);
|
||||
const request = await launcher.waitForRequest("echo", "progress");
|
||||
await vi.waitFor(() =>
|
||||
await waitFor(() =>
|
||||
expect(updates).toContainEqual(
|
||||
expect.objectContaining({ phase: "hopper_bridge", completed: 0 }),
|
||||
),
|
||||
@@ -152,7 +153,7 @@ describe("HopperClient progress", () => {
|
||||
]),
|
||||
);
|
||||
await launcher.release(request);
|
||||
await vi.waitFor(() => expect(client.requestActivity()).toBeNull());
|
||||
await waitFor(() => expect(client.requestActivity()).toBeNull());
|
||||
await expect(client.callTool("echo", { alive: true })).resolves.toEqual({
|
||||
ok: true,
|
||||
value: { alive: true },
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { once } from "node:events";
|
||||
|
||||
import { describe, expect, it, onTestFinished, vi } from "vitest";
|
||||
import { describe, expect, it, onTestFinished } from "vitest";
|
||||
|
||||
import { HopperProcessError } from "../../../../src/domain/hopperErrors.js";
|
||||
import { HOPPER_OPERATIONS } from "../../../../src/hopper/HopperProviderMetadata.js";
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
HopperFixtureLauncher as FixtureLauncher,
|
||||
startHopperFixtureClient as startClient,
|
||||
} from "./hopperClient.fixture.js";
|
||||
import { waitFor } from "../../../support/testWait.js";
|
||||
|
||||
describe("HopperClient protocol", () => {
|
||||
it("does not start cold and preserves normal dispatch once ready", async () => {
|
||||
@@ -82,7 +83,7 @@ describe("HopperClient protocol", () => {
|
||||
void fast.then(() => {
|
||||
fastSettled = true;
|
||||
});
|
||||
await vi.waitFor(() =>
|
||||
await waitFor(() =>
|
||||
expect(client.requestActivity()).toMatchObject({
|
||||
operation: "echo",
|
||||
queuedRequests: 1,
|
||||
@@ -264,7 +265,7 @@ it("preserves caller cancellation while a late valid UTF-8 reply drains", async
|
||||
expect(result.error.cause).toBe(reason);
|
||||
expect(client.requestActivity()).toMatchObject({ callerState: "cancelled" });
|
||||
await launcher.release(request);
|
||||
await vi.waitFor(() => expect(client.requestActivity()).toBeNull());
|
||||
await waitFor(() => expect(client.requestActivity()).toBeNull());
|
||||
await expect(
|
||||
client.callTool("echo", { label: "next �雪🦊" }),
|
||||
).resolves.toEqual({
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
HopperFixtureLauncher,
|
||||
trackHopperClient,
|
||||
} from "./hopperClient.fixture.js";
|
||||
import { waitFor } from "../../../support/testWait.js";
|
||||
|
||||
it("preserves a warm provider when an already cancelled caller joins", async () => {
|
||||
const launcher = new HopperFixtureLauncher();
|
||||
@@ -144,7 +145,7 @@ it.each([0, 1])(
|
||||
});
|
||||
const reason = { source: "caller", opaque: cancelledIndex };
|
||||
controllers[cancelledIndex]?.abort(reason);
|
||||
await vi.waitFor(() =>
|
||||
await waitFor(() =>
|
||||
expect(cancelled).toMatchObject({
|
||||
ok: false,
|
||||
error: { _tag: "HopperCancelledError" },
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
LINUX_PRIVATE_DISPLAY_DIAGNOSTIC_PREFIX,
|
||||
parseLinuxPrivateDisplayDiagnostic,
|
||||
} from "../../../../src/hopper/LinuxPrivateDisplayDiagnostic.js";
|
||||
import { waitFor } from "../../../support/testWait.js";
|
||||
|
||||
const helperPath = fileURLToPath(
|
||||
new URL("../../../../scripts/hopper-demo-x11.py", import.meta.url),
|
||||
@@ -543,7 +544,7 @@ it.skipIf(process.platform === "win32").each(["exited", "cancelled"] as const)(
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (outcome === "cancelled") {
|
||||
await vi.waitFor(async () => expect(await access(ready)).toBeUndefined());
|
||||
await waitFor(async () => expect(await access(ready)).toBeUndefined());
|
||||
controller.abort("caller stopped");
|
||||
}
|
||||
const result = await operation;
|
||||
|
||||
@@ -26,12 +26,10 @@ it.each([
|
||||
);
|
||||
try {
|
||||
await expect
|
||||
.poll(
|
||||
async () =>
|
||||
(await fakeKunaCalls(fixture.kuna)).some(
|
||||
({ argv }) => argv[0] === "strings",
|
||||
),
|
||||
{ timeout: 5000 },
|
||||
.poll(async () =>
|
||||
(await fakeKunaCalls(fixture.kuna)).some(
|
||||
({ argv }) => argv[0] === "strings",
|
||||
),
|
||||
)
|
||||
.toBe(true);
|
||||
caller.abort(reason);
|
||||
@@ -70,12 +68,10 @@ it.each(["peer", "last"] as const)(
|
||||
: undefined;
|
||||
try {
|
||||
await expect
|
||||
.poll(
|
||||
async () =>
|
||||
(await fakeKunaCalls(fixture.kuna)).some(
|
||||
({ argv }) => argv[0] === "decompile",
|
||||
),
|
||||
{ timeout: 5000 },
|
||||
.poll(async () =>
|
||||
(await fakeKunaCalls(fixture.kuna)).some(
|
||||
({ argv }) => argv[0] === "decompile",
|
||||
),
|
||||
)
|
||||
.toBe(true);
|
||||
caller.abort(reason);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { afterAll, expect, it as test, vi } from "vitest";
|
||||
import { afterAll, expect, it as test } from "vitest";
|
||||
import { AnalysisProviderRegistry } from "../../../../src/application/binary/AnalysisProviderRegistry.js";
|
||||
import { composeBinarySession } from "../../../../src/application/binary/BinarySessionComposition.js";
|
||||
import { parseEvidence } from "../../../../src/domain/evidence.js";
|
||||
@@ -13,6 +13,7 @@ import { awaitAcknowledgement } from "../../../fixtures/lifecycleAcknowledgement
|
||||
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
|
||||
import { fakeKunaCalls, writeFakeKuna } from "./fakeKuna.js";
|
||||
import { FUNCTIONS, unprovenCleanupLauncher } from "./kunaSessionFixture.js";
|
||||
import { waitFor } from "../../../support/testWait.js";
|
||||
|
||||
const it = test.skipIf(process.platform === "win32");
|
||||
const schemas = createMcpSchemaValidation();
|
||||
@@ -309,7 +310,7 @@ it("retains the project failure when cancellation stops the independent retry",
|
||||
{ signal: controller.signal, timeout: 15000 },
|
||||
)
|
||||
.catch(() => undefined);
|
||||
const launched = vi.waitFor(
|
||||
const launched = waitFor(
|
||||
async () => {
|
||||
expect(
|
||||
(await fakeKunaCalls(kuna)).some(
|
||||
@@ -317,14 +318,9 @@ it("retains the project failure when cancellation stops the independent retry",
|
||||
),
|
||||
).toBe(true);
|
||||
},
|
||||
{ timeout: 5000, interval: 20 },
|
||||
);
|
||||
await awaitAcknowledgement(
|
||||
launched,
|
||||
pending,
|
||||
"independent retry launch",
|
||||
6000,
|
||||
{ interval: 20 },
|
||||
);
|
||||
await awaitAcknowledgement(launched, pending, "independent retry launch");
|
||||
controller.abort();
|
||||
await pending;
|
||||
expect(
|
||||
|
||||
@@ -13,6 +13,10 @@ import { STARTUP_OWNER_CLOSED } from "../../../../src/process/SharedStartupAttem
|
||||
|
||||
const it = test.skipIf(process.platform === "win32");
|
||||
|
||||
// The production `kuna --version` deadline (KunaInstallation). A stop that
|
||||
// regressed would still end the gated probe there, so cancellation must win.
|
||||
const VERSION_PROBE_DEADLINE_MS = 10_000;
|
||||
|
||||
it("reports an unconfigured Kuna as a missing optional engine", async () => {
|
||||
expect(await inspectKunaProvider({})).toMatchObject({
|
||||
id: "kuna",
|
||||
@@ -284,18 +288,15 @@ it.each(["caller", "host"] as const)(
|
||||
try {
|
||||
// The preceding Ghidra probe may prepare Darwin process ownership before
|
||||
// Kuna starts; keep startup separate from the bounded post-abort join.
|
||||
await expect
|
||||
.poll(async () => (await fakeKunaCalls(kuna)).length, {
|
||||
timeout: 10_000,
|
||||
})
|
||||
.toBe(1);
|
||||
await expect.poll(async () => (await fakeKunaCalls(kuna)).length).toBe(1);
|
||||
const probe = (await fakeKunaCalls(kuna))[0];
|
||||
if (probe === undefined) throw new Error("Missing Kuna version probe");
|
||||
expect(probe.argv).toEqual(["--version"]);
|
||||
if (source === "caller") controller.abort(reason);
|
||||
else closing = host.close!();
|
||||
// The unreleased probe normally waits for its ten-second deadline.
|
||||
await expect.poll(() => settled, { timeout: 2_000 }).toBe(true);
|
||||
await expect.poll(() => settled).toBe(true);
|
||||
// The unreleased probe would otherwise end at its own deadline.
|
||||
expect(Date.now() - probe.at).toBeLessThan(VERSION_PROBE_DEADLINE_MS);
|
||||
const failure = await inspection;
|
||||
expect(failure).toBeInstanceOf(AnalysisCancelledError);
|
||||
if (!(failure instanceof AnalysisCancelledError))
|
||||
|
||||
@@ -374,13 +374,11 @@ setTimeout(() => process.exit(9), 10000).unref();
|
||||
);
|
||||
try {
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
readFile(stopped).then(
|
||||
() => true,
|
||||
() => false,
|
||||
),
|
||||
{ timeout: 5000 },
|
||||
.poll(() =>
|
||||
readFile(stopped).then(
|
||||
() => true,
|
||||
() => false,
|
||||
),
|
||||
)
|
||||
.toBe(true);
|
||||
controller.abort({ request: "late doctor cancellation" });
|
||||
|
||||
@@ -7,6 +7,10 @@ import { session } from "./kunaSessionFixture.js";
|
||||
|
||||
const it = test.skipIf(process.platform === "win32");
|
||||
|
||||
// The production `kuna --version` deadline (KunaInstallation). A stop that
|
||||
// regressed would still end the gated probe there, so cancellation must win.
|
||||
const VERSION_PROBE_DEADLINE_MS = 10_000;
|
||||
|
||||
const gatedInstallation = async () => {
|
||||
const fixture = await session("rea-kuna-installation-cancel-");
|
||||
const gate = join(fixture.root, "release-version");
|
||||
@@ -52,7 +56,7 @@ it("lets one caller leave acquisition with its exact reason while a peer keeps t
|
||||
try {
|
||||
const probe = await fixture.started();
|
||||
controller.abort(reason);
|
||||
await expect.poll(() => settled, { timeout: 1500 }).toBe(true);
|
||||
await expect.poll(() => settled).toBe(true);
|
||||
const result = await leaving;
|
||||
if (result.ok) throw new Error("Expected cancellation");
|
||||
expect(result.error).toMatchObject({
|
||||
@@ -86,7 +90,8 @@ it.each([null, "caller stopped"])(
|
||||
try {
|
||||
const probe = await fixture.started();
|
||||
controller.abort(reason);
|
||||
await expect.poll(() => settled, { timeout: 1500 }).toBe(true);
|
||||
await expect.poll(() => settled).toBe(true);
|
||||
expect(Date.now() - probe.at).toBeLessThan(VERSION_PROBE_DEADLINE_MS);
|
||||
const result = await pending;
|
||||
if (result.ok) throw new Error("Expected cancellation");
|
||||
expect(result.error).toMatchObject({
|
||||
@@ -118,7 +123,8 @@ it("close cancels and joins an installation probe while callers wait", async ()
|
||||
void closing.then(() => {
|
||||
closed = true;
|
||||
});
|
||||
await expect.poll(() => closed, { timeout: 1500 }).toBe(true);
|
||||
await expect.poll(() => closed).toBe(true);
|
||||
expect(Date.now() - probe.at).toBeLessThan(VERSION_PROBE_DEADLINE_MS);
|
||||
expect((await closing).ok).toBe(true);
|
||||
const result = await pending;
|
||||
if (result.ok) throw new Error("Expected cancellation");
|
||||
|
||||
@@ -130,9 +130,7 @@ it.each([true, false])(
|
||||
if (!closed.ok) throw closed.error;
|
||||
});
|
||||
await expect
|
||||
.poll(() => actual.readdir(listing.directory).catch(() => []), {
|
||||
timeout: 10_000,
|
||||
})
|
||||
.poll(() => actual.readdir(listing.directory).catch(() => []))
|
||||
.toHaveLength(2);
|
||||
listing.names = await actual.readdir(listing.directory);
|
||||
releaseListing();
|
||||
|
||||
@@ -750,12 +750,7 @@ it("keeps an overflow as the primary failure when cancellation arrives during fi
|
||||
{ signal: caller.signal },
|
||||
);
|
||||
try {
|
||||
await awaitAcknowledgement(
|
||||
entered,
|
||||
pending,
|
||||
"Kuna overflow cleanup",
|
||||
10_000,
|
||||
);
|
||||
await awaitAcknowledgement(entered, pending, "Kuna overflow cleanup");
|
||||
caller.abort("cancel during overflow cleanup");
|
||||
releaseCleanup();
|
||||
const result = await pending;
|
||||
|
||||
@@ -45,7 +45,6 @@ const ready = async () => {
|
||||
async () =>
|
||||
(await run(client, "procedure_pseudo_code", { procedure: "main" }))
|
||||
.rawResult,
|
||||
{ timeout: 5000 },
|
||||
)
|
||||
.toMatchObject({ source: { kind: "project-export" } });
|
||||
const [key] = await readdir(join(cacheRoot, "projects"));
|
||||
@@ -86,7 +85,7 @@ it.each([
|
||||
await expect.poll(() => held.streams.length).toBe(1);
|
||||
const reason = Object.freeze({ requested: "reader-stop", operation });
|
||||
controller.abort(reason);
|
||||
await expect.poll(() => finished, { timeout: 1000 }).toBe(true);
|
||||
await expect.poll(() => finished).toBe(true);
|
||||
const cancelledResult = await cancelled;
|
||||
expect(cancelledResult).toMatchObject({
|
||||
ok: false,
|
||||
@@ -126,18 +125,14 @@ it("close aborts and drains the shared scan after its only caller cancels", asyn
|
||||
try {
|
||||
await expect.poll(() => held.streams.length).toBe(1);
|
||||
controller.abort();
|
||||
await expect
|
||||
.poll(() => result, { timeout: 1000 })
|
||||
.toMatchObject({ ok: false });
|
||||
await expect.poll(() => result).toMatchObject({ ok: false });
|
||||
expect(held.streams[0]?.destroyed).toBe(false);
|
||||
let closed: unknown;
|
||||
closing = client.close().then((value) => {
|
||||
closed = value;
|
||||
return value;
|
||||
});
|
||||
await expect
|
||||
.poll(() => closed, { timeout: 1000 })
|
||||
.toEqual({ ok: true, value: null });
|
||||
await expect.poll(() => closed).toEqual({ ok: true, value: null });
|
||||
expect(held.streams[0]?.destroyed).toBe(true);
|
||||
expect(held.streams[0]?.closed).toBe(true);
|
||||
} finally {
|
||||
|
||||
@@ -305,10 +305,8 @@ it("caps distinct pending xrefs and cancels queued work without launching it", a
|
||||
);
|
||||
try {
|
||||
await expect
|
||||
.poll(
|
||||
async () =>
|
||||
(await fakeKunaCalls(kuna)).some(({ argv }) => argv[0] === "xrefs"),
|
||||
{ timeout: 5000 },
|
||||
.poll(async () =>
|
||||
(await fakeKunaCalls(kuna)).some(({ argv }) => argv[0] === "xrefs"),
|
||||
)
|
||||
.toBe(true);
|
||||
expect(await client.execute("xrefs", { address: "0x5000" })).toMatchObject({
|
||||
|
||||
@@ -143,13 +143,10 @@ export const awaitKunaCommand = async (
|
||||
}
|
||||
})();
|
||||
try {
|
||||
// Cold installation, workspace and inventory preparation precede decompile.
|
||||
// The phase probe took over two seconds with the two-core resource bound.
|
||||
await awaitAcknowledgement(
|
||||
acknowledged,
|
||||
operation,
|
||||
`Kuna ${command} child`,
|
||||
5_000,
|
||||
);
|
||||
} finally {
|
||||
waiting = false;
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
RegularFileCancelled,
|
||||
} from "../../../../src/filesystem/OwnedRegularFile.js";
|
||||
import { run, session } from "./kunaSessionFixture.js";
|
||||
import { waitFor } from "../../../support/testWait.js";
|
||||
|
||||
const fault = vi.hoisted(() => ({
|
||||
fail: false,
|
||||
@@ -375,7 +376,7 @@ it("does not open a second span while a peer still holds a failed close", async
|
||||
{ procedure: "main" },
|
||||
{},
|
||||
);
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(fault.closeEntered).toBe(1);
|
||||
});
|
||||
const second = client.execute(
|
||||
@@ -440,7 +441,7 @@ it("does not open a queued span after its caller aborts", async () => {
|
||||
{ procedure: "main" },
|
||||
{},
|
||||
);
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(fault.closeEntered).toBe(1);
|
||||
});
|
||||
const abort = new AbortController();
|
||||
@@ -504,7 +505,7 @@ it.each(["before close", "during verification", "during close"] as const)(
|
||||
{ signal: abort.signal },
|
||||
);
|
||||
if (when === "during close") {
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(fault.closeEntered).toBe(1);
|
||||
});
|
||||
abort.abort(reason);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { expect, it as test, vi } from "vitest";
|
||||
import { expect, it as test } from "vitest";
|
||||
import { readKunaConfiguration } from "../../../../src/kuna/KunaConfiguration.js";
|
||||
import { KUNA_DIAGNOSTIC_BYTES } from "../../../../src/kuna/KunaLimits.js";
|
||||
import { projectAnalysisError } from "../../../../src/domain/analysisErrorProjection.js";
|
||||
@@ -6,6 +6,7 @@ import { ProviderCleanupError } from "../../../../src/domain/providerCleanupErro
|
||||
|
||||
import { fakeKunaCalls, setFakeKunaBehavior } from "./fakeKuna.js";
|
||||
import { run, session, unprovenCleanupLauncher } from "./kunaSessionFixture.js";
|
||||
import { waitFor } from "../../../support/testWait.js";
|
||||
|
||||
const it = test.skipIf(process.platform === "win32");
|
||||
const scans = async (kuna: string) =>
|
||||
@@ -241,7 +242,7 @@ it("cancels one waiter without stopping the shared scan, and rebuilds in a fresh
|
||||
{ signal: abort.signal },
|
||||
);
|
||||
const survivor = first.execute("search_strings", { pattern: "fixture" });
|
||||
await vi.waitFor(async () => expect(await scans(kuna)).toHaveLength(1));
|
||||
await waitFor(async () => expect(await scans(kuna)).toHaveLength(1));
|
||||
abort.abort();
|
||||
expect(await cancelled).toMatchObject({
|
||||
ok: false,
|
||||
@@ -273,7 +274,7 @@ it("drains a cancelled last scan before retrying", async () => {
|
||||
{},
|
||||
{ signal: abort.signal },
|
||||
);
|
||||
await vi.waitFor(async () => expect(await scans(kuna)).toHaveLength(1));
|
||||
await waitFor(async () => expect(await scans(kuna)).toHaveLength(1));
|
||||
abort.abort();
|
||||
expect(await pending).toMatchObject({
|
||||
ok: false,
|
||||
@@ -303,7 +304,7 @@ it("retains metadata admission until a string process stop is proven", async ()
|
||||
{},
|
||||
{ signal: abort.signal },
|
||||
);
|
||||
await vi.waitFor(async () => expect(await scans(kuna)).toHaveLength(1));
|
||||
await waitFor(async () => expect(await scans(kuna)).toHaveLength(1));
|
||||
abort.abort();
|
||||
expect(await pending).toMatchObject({
|
||||
ok: false,
|
||||
|
||||
@@ -161,15 +161,12 @@ setTimeout(() => process.exit(0), 10000);
|
||||
controller.signal,
|
||||
);
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
started = readySchema.parse(
|
||||
JSON.parse(await readFile(readyPath, "utf8")),
|
||||
);
|
||||
return started.run_id;
|
||||
},
|
||||
{ timeout: 5_000 },
|
||||
)
|
||||
.poll(async () => {
|
||||
started = readySchema.parse(
|
||||
JSON.parse(await readFile(readyPath, "utf8")),
|
||||
);
|
||||
return started.run_id;
|
||||
})
|
||||
.toEqual(expect.any(String));
|
||||
if (started === undefined) throw new Error("Wrapper did not start");
|
||||
const identity = await observeProcessStartIdentity(
|
||||
@@ -199,9 +196,7 @@ setTimeout(() => process.exit(0), 10000);
|
||||
// fixture self-exits. Do not bypass ownership with an unverified PID kill.
|
||||
const pid = started.pid;
|
||||
await expect
|
||||
.poll(async () => await observeProcessStartIdentity(pid), {
|
||||
timeout: 12_000,
|
||||
})
|
||||
.poll(async () => await observeProcessStartIdentity(pid))
|
||||
.toBeUndefined();
|
||||
expect((await tracer.close()).ok).toBe(true);
|
||||
} else expect(closed.ok).toBe(true);
|
||||
|
||||
@@ -493,7 +493,7 @@ describe("native command failure capture and limits", () => {
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
const result = await withVerifierCleanup(async () => {
|
||||
await awaitAcknowledgement(ready, pending, "command output", 10_000);
|
||||
await awaitAcknowledgement(ready, pending, "command output");
|
||||
controller.abort();
|
||||
return await pending;
|
||||
}, [
|
||||
|
||||
Loaded 100 of 119 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user