Files
rea/tests/process-global/apktoolEnvironment.test.ts
Kaoru0822andClaude Opus 5.5 089ef732e8 fix(apktool): own the version probe's process group and keep its timeout typed (#3808)
* 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>
2026-10-14 21:37:52 -07:00

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");
},
);