* fix(apktool): own the version probe's process group and keep its timeout typed The version probe ran its launcher through execFileOutput, which waits for every inherited output pipe to close. A shell launcher's child that holds those pipes outlived the stop request, so the probe stayed pending past its deadline and its grace period, and cancellation hung the same way. An expired probe also surfaced as a non-retryable unusable-version refusal. Run the probe through runOwnedCommand like the decode run, so the owned process group is stopped with its children. Report its deadline as AnalysisTimeoutError with timeout_ms and retain an unresolved cleanup owner for close. Apktool launchers are interpreter scripts, so both the probe and the decode run prove ownership by run token instead of the launcher's argv prefix. A close with unresolved process cleanup reports a typed ProviderCleanupError instead of an untyped AggregateError. Fixes #3342 Fixes #3347 Fixes #3337 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(apktool): carry unresolved probe cleanup through both provider entry points A probe failure other than cancellation or timeout, such as an output overflow whose launcher child survives, now keeps its cleanup reason and resources on ApktoolConfigurationFailure. inspectAvailability and execute report it as ProviderCleanupError instead of ordinary unavailability or capability_unavailable. Add a real-process lifecycle test: an interpreter launcher starts a child that holds the inherited output pipes, the probe is cancelled after launch, and both processes must be stopped within the bound. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(apktool): ignore zombies and always clean up the probe lifecycle fixture Treat a zombie as exited when checking the launcher and child, bound the cancellation wait so a regressed probe still reaches cleanup, and stop any remaining fixture processes in a finally block. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(apktool): treat a failed process inspection as unknown, not stopped Only ESRCH, a zombie state or ps reporting no match counts as exited; a ps launch failure leaves the process presumed live so the assertion fails and the finally block still stops it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
59 lines
2.2 KiB
TypeScript
59 lines
2.2 KiB
TypeScript
import { afterEach, expect, it, onTestFinished, vi } from "vitest";
|
|
|
|
import { ApktoolProvider } from "../../src/apktool/ApktoolProvider.js";
|
|
import { runOwnedCommand } from "../../src/process/OwnedCommand.js";
|
|
import type { ProviderProcessSnapshot } from "../../src/process/ProviderProcess.js";
|
|
|
|
vi.mock("../../src/process/OwnedCommand.js", async (importOriginal) => ({
|
|
...(await importOriginal<
|
|
typeof import("../../src/process/OwnedCommand.js")
|
|
>()),
|
|
runOwnedCommand: vi.fn(
|
|
async () =>
|
|
({
|
|
stdout: { text: "2.7.0-stub\n" },
|
|
stderr: { text: "" },
|
|
}) as unknown as ProviderProcessSnapshot,
|
|
),
|
|
}));
|
|
afterEach(() => {
|
|
vi.unstubAllEnvs();
|
|
vi.mocked(runOwnedCommand).mockClear();
|
|
});
|
|
const lastSpawn = () => vi.mocked(runOwnedCommand).mock.lastCall?.[0];
|
|
|
|
it.skipIf(process.platform === "win32")(
|
|
"keeps an explicit environment's PATH choice and samples ambient inheritance per operation",
|
|
async () => {
|
|
vi.stubEnv("REA_APKTOOL_COMMAND", "/ambient/initial-apktool");
|
|
vi.stubEnv("REA_APKTOOL_TEST_INHERITED_VALUE", "initial");
|
|
const provider = new ApktoolProvider({ environment: {} });
|
|
onTestFinished(() => provider.close());
|
|
vi.stubEnv("REA_APKTOOL_COMMAND", "/ambient/changed-apktool");
|
|
vi.stubEnv("REA_APKTOOL_TEST_INHERITED_VALUE", "changed");
|
|
await provider.inspectAvailability();
|
|
expect(lastSpawn()?.command).toBe("apktool");
|
|
expect(lastSpawn()?.hostEnvironment).toMatchObject({
|
|
REA_APKTOOL_COMMAND: undefined,
|
|
REA_APKTOOL_TEST_INHERITED_VALUE: "changed",
|
|
});
|
|
vi.stubEnv("REA_APKTOOL_TEST_INHERITED_VALUE", undefined);
|
|
await provider.inspectAvailability();
|
|
expect(
|
|
lastSpawn()?.hostEnvironment?.REA_APKTOOL_TEST_INHERITED_VALUE,
|
|
).toBeUndefined();
|
|
},
|
|
);
|
|
|
|
it.skipIf(process.platform === "win32")(
|
|
"inherits the current default launcher when no environment was selected",
|
|
async () => {
|
|
vi.stubEnv("REA_APKTOOL_COMMAND", "/ambient/initial-apktool");
|
|
const provider = new ApktoolProvider();
|
|
onTestFinished(() => provider.close());
|
|
vi.stubEnv("REA_APKTOOL_COMMAND", "/ambient/later-apktool");
|
|
await provider.inspectAvailability();
|
|
expect(lastSpawn()?.command).toBe("/ambient/later-apktool");
|
|
},
|
|
);
|