mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
fix(config): preserve captured environments across provider launches
This commit is contained in:
1 parent
ccdeda7a27
commit
3864c7d667
4 files changed
+141
-50
No files matched your search
@@ -56,7 +56,6 @@ import {
|
|||||||
import { snapshotEnvironment } from "../process/snapshotEnvironment.js";
|
import { snapshotEnvironment } from "../process/snapshotEnvironment.js";
|
||||||
import { WindowsBatchCommandError } from "../process/WindowsBatchCommand.js";
|
import { WindowsBatchCommandError } from "../process/WindowsBatchCommand.js";
|
||||||
import {
|
import {
|
||||||
APKTOOL_COMMAND_ENV,
|
|
||||||
ApktoolConfigurationFailure,
|
ApktoolConfigurationFailure,
|
||||||
apktoolConfigurationAvailability,
|
apktoolConfigurationAvailability,
|
||||||
apktoolInvocation,
|
apktoolInvocation,
|
||||||
@@ -291,15 +290,7 @@ export class ApktoolProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private operationEnvironment(): Readonly<NodeJS.ProcessEnv> {
|
private operationEnvironment(): Readonly<NodeJS.ProcessEnv> {
|
||||||
const ambient = snapshotEnvironment(process.env);
|
return this.selectedEnvironment ?? snapshotEnvironment(process.env);
|
||||||
if (this.selectedEnvironment === undefined) return ambient;
|
|
||||||
// Child inheritance is sampled per operation. An explicit map keeps its
|
|
||||||
// launcher choice authoritative, including an omitted command selecting PATH.
|
|
||||||
return snapshotEnvironment({
|
|
||||||
...ambient,
|
|
||||||
...this.selectedEnvironment,
|
|
||||||
[APKTOOL_COMMAND_ENV]: this.selectedEnvironment[APKTOOL_COMMAND_ENV],
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Probe the selected apktool launcher; decodes nothing. */
|
/** Probe the selected apktool launcher; decodes nothing. */
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { snapshotEnvironment } from "../../process/snapshotEnvironment.js";
|
||||||
import { isAbsolute } from "node:path";
|
import { isAbsolute } from "node:path";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import type {
|
import type {
|
||||||
@@ -78,10 +79,14 @@ export class NativeModuleResolver implements WebModuleResolutionPort {
|
|||||||
#closePromise: Promise<void> | undefined;
|
#closePromise: Promise<void> | undefined;
|
||||||
#pendingCleanup: AnalysisCleanupLease | undefined;
|
#pendingCleanup: AnalysisCleanupLease | undefined;
|
||||||
|
|
||||||
|
readonly environment: Readonly<Record<string, string | undefined>>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
readonly environment: Readonly<Record<string, string | undefined>>,
|
environment: Readonly<Record<string, string | undefined>>,
|
||||||
readonly launcher: ModuleResolverLauncher = launch,
|
readonly launcher: ModuleResolverLauncher = launch,
|
||||||
) {}
|
) {
|
||||||
|
this.environment = snapshotEnvironment(environment);
|
||||||
|
}
|
||||||
|
|
||||||
resolve(
|
resolve(
|
||||||
input: Parameters<WebModuleResolutionPort["resolve"]>[0],
|
input: Parameters<WebModuleResolutionPort["resolve"]>[0],
|
||||||
@@ -180,6 +185,7 @@ export class NativeModuleResolver implements WebModuleResolutionPort {
|
|||||||
const browser = await observe(() =>
|
const browser = await observe(() =>
|
||||||
this.launcher({
|
this.launcher({
|
||||||
executablePath: executable,
|
executablePath: executable,
|
||||||
|
env: this.environment,
|
||||||
headless: true,
|
headless: true,
|
||||||
timeout: Math.max(1, deadline.remainingMs()),
|
timeout: Math.max(1, deadline.remainingMs()),
|
||||||
handleSIGINT: false,
|
handleSIGINT: false,
|
||||||
|
|||||||
@@ -1,58 +1,89 @@
|
|||||||
|
import { writeFile } from "node:fs/promises";
|
||||||
|
import { join } from "node:path";
|
||||||
import { afterEach, expect, it, onTestFinished, vi } from "vitest";
|
import { afterEach, expect, it, onTestFinished, vi } from "vitest";
|
||||||
|
|
||||||
|
import { createCli } from "../../src/cli.js";
|
||||||
import { ApktoolProvider } from "../../src/apktool/ApktoolProvider.js";
|
import { ApktoolProvider } from "../../src/apktool/ApktoolProvider.js";
|
||||||
import { runOwnedCommand } from "../../src/process/OwnedCommand.js";
|
import { ApktoolResourceAnalysisService } from "../../src/application/apktool/ApktoolResourceAnalysisService.js";
|
||||||
import type { ProviderProcessSnapshot } from "../../src/process/ProviderProcess.js";
|
import { parseEvidence } from "../../src/domain/evidence.js";
|
||||||
|
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
|
||||||
|
|
||||||
vi.mock("../../src/process/OwnedCommand.js", async (importOriginal) => ({
|
const initialExitCode = process.exitCode;
|
||||||
...(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(() => {
|
afterEach(() => {
|
||||||
vi.unstubAllEnvs();
|
vi.unstubAllEnvs();
|
||||||
vi.mocked(runOwnedCommand).mockClear();
|
process.exitCode = initialExitCode;
|
||||||
});
|
});
|
||||||
const lastSpawn = () => vi.mocked(runOwnedCommand).mock.lastCall?.[0];
|
|
||||||
|
|
||||||
it.skipIf(process.platform === "win32")(
|
// These executable probes validate the real CLI and process environment boundary;
|
||||||
"keeps an explicit environment's PATH choice and samples ambient inheritance per operation",
|
// they do not establish native Apktool decoding behavior.
|
||||||
async () => {
|
it.skipIf(process.platform === "win32").each([
|
||||||
vi.stubEnv("REA_APKTOOL_COMMAND", "/ambient/initial-apktool");
|
{ name: "missing", selected: {}, expected: "absent" },
|
||||||
vi.stubEnv("REA_APKTOOL_TEST_INHERITED_VALUE", "initial");
|
{
|
||||||
const provider = new ApktoolProvider({ environment: {} });
|
name: "unset",
|
||||||
onTestFinished(() => provider.close());
|
selected: { REA_APKTOOL_ENV_PROFILE: undefined },
|
||||||
vi.stubEnv("REA_APKTOOL_COMMAND", "/ambient/changed-apktool");
|
expected: "absent",
|
||||||
vi.stubEnv("REA_APKTOOL_TEST_INHERITED_VALUE", "changed");
|
},
|
||||||
await provider.inspectAvailability();
|
{
|
||||||
expect(lastSpawn()?.command).toBe("apktool");
|
name: "empty",
|
||||||
expect(lastSpawn()?.hostEnvironment).toMatchObject({
|
selected: { REA_APKTOOL_ENV_PROFILE: "" },
|
||||||
REA_APKTOOL_COMMAND: undefined,
|
expected: "empty",
|
||||||
REA_APKTOOL_TEST_INHERITED_VALUE: "changed",
|
},
|
||||||
|
{
|
||||||
|
name: "selected",
|
||||||
|
selected: { REA_APKTOOL_ENV_PROFILE: "selected-profile" },
|
||||||
|
expected: "selected-profile",
|
||||||
|
},
|
||||||
|
])(
|
||||||
|
"preserves $name Apktool settings after the ambient environment changes",
|
||||||
|
async ({ selected, expected }) => {
|
||||||
|
const root = await createTestTempDirectory(
|
||||||
|
"rea-apktool-selected-environment-",
|
||||||
|
);
|
||||||
|
const command = join(root, "apktool");
|
||||||
|
await writeFile(
|
||||||
|
command,
|
||||||
|
'#!/bin/sh\nprofile=${REA_APKTOOL_ENV_PROFILE-absent}\n[ -n "$profile" ] || profile=empty\nprintf "2.11.1+%s\\n" "$profile"\n',
|
||||||
|
{ mode: 0o755 },
|
||||||
|
);
|
||||||
|
const cli = createCli({ REA_APKTOOL_COMMAND: command, ...selected });
|
||||||
|
vi.stubEnv("REA_APKTOOL_ENV_PROFILE", "late-ambient-profile");
|
||||||
|
let stdout = "";
|
||||||
|
await cli.serve(["inspect-apktool-client", "--json"], {
|
||||||
|
env: {},
|
||||||
|
stdout: (text) => {
|
||||||
|
stdout += text;
|
||||||
|
},
|
||||||
|
exit: () => undefined,
|
||||||
});
|
});
|
||||||
vi.stubEnv("REA_APKTOOL_TEST_INHERITED_VALUE", undefined);
|
const evidence = parseEvidence(JSON.parse(stdout));
|
||||||
await provider.inspectAvailability();
|
expect(evidence.normalized_result).toMatchObject({
|
||||||
expect(
|
client: { apktool_version: `2.11.1+${expected}` },
|
||||||
lastSpawn()?.hostEnvironment?.REA_APKTOOL_TEST_INHERITED_VALUE,
|
});
|
||||||
).toBeUndefined();
|
expect(evidence.environment).toBeNull();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
it.skipIf(process.platform === "win32")(
|
it.skipIf(process.platform === "win32")(
|
||||||
"inherits the current default launcher when no environment was selected",
|
"inherits the current default launcher when no environment was selected",
|
||||||
async () => {
|
async () => {
|
||||||
|
const root = await createTestTempDirectory(
|
||||||
|
"rea-apktool-default-environment-",
|
||||||
|
);
|
||||||
|
const command = join(root, "apktool");
|
||||||
|
await writeFile(command, '#!/bin/sh\nprintf "2.11.1+later\\n"\n', {
|
||||||
|
mode: 0o755,
|
||||||
|
});
|
||||||
vi.stubEnv("REA_APKTOOL_COMMAND", "/ambient/initial-apktool");
|
vi.stubEnv("REA_APKTOOL_COMMAND", "/ambient/initial-apktool");
|
||||||
const provider = new ApktoolProvider();
|
const provider = new ApktoolProvider();
|
||||||
onTestFinished(() => provider.close());
|
onTestFinished(() => provider.close());
|
||||||
vi.stubEnv("REA_APKTOOL_COMMAND", "/ambient/later-apktool");
|
vi.stubEnv("REA_APKTOOL_COMMAND", command);
|
||||||
await provider.inspectAvailability();
|
const result = await new ApktoolResourceAnalysisService(provider).execute(
|
||||||
expect(lastSpawn()?.command).toBe("/ambient/later-apktool");
|
"inspect_apktool_client",
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
if (!result.ok) throw result.error;
|
||||||
|
expect(result.value.normalized_result).toMatchObject({
|
||||||
|
client: { command, apktool_version: "2.11.1+later" },
|
||||||
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { readFile, writeFile } from "node:fs/promises";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { afterEach, expect, it, onTestFinished, vi } from "vitest";
|
||||||
|
|
||||||
|
import { NativeModuleResolver } from "../../src/browser/modules/NativeModuleResolver.js";
|
||||||
|
import { observeProcessStartIdentity } from "../../src/process/ProcessOwnershipObservation.js";
|
||||||
|
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
|
||||||
|
|
||||||
|
afterEach(() => vi.unstubAllEnvs());
|
||||||
|
|
||||||
|
// Playwright starts the actual selected executable, which records its environment
|
||||||
|
// and exits before the browser handshake. This proves launch inheritance only.
|
||||||
|
it.skipIf(process.platform === "win32").each([
|
||||||
|
{ name: "missing", selected: {}, expected: "absent" },
|
||||||
|
{
|
||||||
|
name: "unset",
|
||||||
|
selected: { REA_MODULE_ENV_PROFILE: undefined },
|
||||||
|
expected: "absent",
|
||||||
|
},
|
||||||
|
{ name: "empty", selected: { REA_MODULE_ENV_PROFILE: "" }, expected: "" },
|
||||||
|
{
|
||||||
|
name: "selected",
|
||||||
|
selected: { REA_MODULE_ENV_PROFILE: "selected-profile" },
|
||||||
|
expected: "selected-profile",
|
||||||
|
},
|
||||||
|
])(
|
||||||
|
"preserves $name module-resolver settings in the real Playwright child",
|
||||||
|
async ({ selected, expected }) => {
|
||||||
|
const root = await createTestTempDirectory(
|
||||||
|
"rea-module-resolver-environment-",
|
||||||
|
);
|
||||||
|
const command = join(root, "browser"),
|
||||||
|
receipt = join(root, "receipt");
|
||||||
|
const quotedReceipt = `'${receipt.replaceAll("'", "'\\''")}'`;
|
||||||
|
await writeFile(
|
||||||
|
command,
|
||||||
|
'#!/bin/sh\nprintf "%s\\n%s" "$$" "${REA_MODULE_ENV_PROFILE-absent}" > ' +
|
||||||
|
quotedReceipt +
|
||||||
|
"\nexit 0\n",
|
||||||
|
{ mode: 0o755 },
|
||||||
|
);
|
||||||
|
const environment = { REA_BROWSER_EXECUTABLE: command, ...selected };
|
||||||
|
const resolver = new NativeModuleResolver(environment);
|
||||||
|
onTestFinished(() => resolver.close());
|
||||||
|
// Constructor selection is immutable even when the original caller map changes.
|
||||||
|
Object.assign(environment, {
|
||||||
|
REA_MODULE_ENV_PROFILE: "later-caller-profile",
|
||||||
|
});
|
||||||
|
vi.stubEnv("REA_MODULE_ENV_PROFILE", "late-ambient-profile");
|
||||||
|
const result = await resolver.resolve({
|
||||||
|
importerUrl: "https://app.test/main.js",
|
||||||
|
importMap: null,
|
||||||
|
specifiers: ["./dep.js"],
|
||||||
|
});
|
||||||
|
expect(result.ok).toBe(false);
|
||||||
|
const observed = await readFile(receipt, "utf8");
|
||||||
|
const separator = observed.indexOf("\n");
|
||||||
|
expect(observed.slice(separator + 1)).toBe(expected);
|
||||||
|
const pid = Number(observed.slice(0, separator));
|
||||||
|
expect(Number.isSafeInteger(pid) && pid > 0).toBe(true);
|
||||||
|
expect(await observeProcessStartIdentity(pid)).toBeUndefined();
|
||||||
|
},
|
||||||
|
);
|
||||||
Reference in new issue
Block a user