

























@@ -3,8 +3,18 @@ import { tmpdir } from "node:os";
33import { delimiter, join, win32 } from "node:path";
44import { pathToFileURL } from "node:url";
55import { describe, expect, it } from "vitest";
6+import {
7+modelProviderConfigBatchJson,
8+resolveParallelsModelTimeoutSeconds,
9+resolveProviderAuth as resolveProviderAuthDirect,
10+resolveSnapshot,
11+resolveUbuntuVmName,
12+resolveWindowsProviderAuth,
13+run,
14+shellQuote,
15+} from "../../scripts/e2e/parallels/common.ts";
616import { resolveHostCommandInvocation } from "../../scripts/e2e/parallels/host-command.ts";
7-import { execNodeEvalSync, spawnNodeEvalSync } from "../../src/test-utils/node-process.js";
17+import { spawnNodeEvalSync } from "../../src/test-utils/node-process.js";
818919const WRAPPERS = {
1020linux: "scripts/e2e/parallels-linux-smoke.sh",
@@ -47,10 +57,6 @@ function countNonEmptyLines(value: string): number {
4757return count;
4858}
495950-function runTsEval(source: string, env: Record<string, string> = {}) {
51-return execNodeEvalSync(source, { env: { ...process.env, ...env }, imports: ["tsx"] });
52-}
53-5460function fakePrlctlEnv(tempDir: string): Record<string, string> {
5561const pathValue = `${tempDir}${delimiter}${process.env.Path ?? process.env.PATH ?? ""}`;
5662const fakeBootstrap = pathToFileURL(join(tempDir, "prlctl-bootstrap.mjs")).href;
@@ -68,30 +74,25 @@ function writeFakePrlctl(tempDir: string, posixScript: string, windowsBootstrap:
6874writeFileSync(join(tempDir, "prlctl-bootstrap.mjs"), windowsBootstrap);
6975}
707671-function resolveProviderAuth(
72-provider: string,
73-options: {
74-apiKeyEnv?: string;
75-env?: Record<string, string>;
76-modelId?: string;
77-} = {},
78-) {
79-const source = `
80-import { resolveProviderAuth } from "./${TS_PATHS.common}";
81-const result = resolveProviderAuth({
82- provider: ${JSON.stringify(provider)},
83- apiKeyEnv: ${JSON.stringify(options.apiKeyEnv)},
84- modelId: ${JSON.stringify(options.modelId)},
85-});
86-console.log(JSON.stringify(result));
87-`;
88-return JSON.parse(runTsEval(source, options.env)) as {
89-apiKeyEnv: string;
90-apiKeyValue: string;
91-authChoice: string;
92-authKeyFlag: string;
93-modelId: string;
94-};
77+function withEnv<T>(env: Record<string, string>, callback: () => T): T {
78+const previous = new Map<string, string | undefined>();
79+for (const [key, value] of Object.entries(env)) {
80+previous.set(key, process.env[key]);
81+}
82+for (const [key, value] of Object.entries(env)) {
83+process.env[key] = value;
84+}
85+try {
86+return callback();
87+} finally {
88+for (const [key, value] of previous) {
89+if (value === undefined) {
90+delete process.env[key];
91+} else {
92+process.env[key] = value;
93+}
94+}
95+}
9596}
96979798describe("Parallels smoke model selection", () => {
@@ -129,12 +130,7 @@ describe("Parallels smoke model selection", () => {
129130});
130131131132it("writes full model ids as config map keys in provider batches", () => {
132-const source = `
133-import { modelProviderConfigBatchJson } from "./${TS_PATHS.common}";
134-const result = modelProviderConfigBatchJson("openai/gpt-5.5", "windows");
135-console.log(result);
136-`;
137-const batch = JSON.parse(runTsEval(source, { OPENAI_API_KEY: "sk-openai" })) as Array<{
133+const batch = JSON.parse(modelProviderConfigBatchJson("openai/gpt-5.5", "windows")) as Array<{
138134path: string;
139135value: unknown;
140136}>;
@@ -215,15 +211,10 @@ if (isPrlctl) {
215211);
216212217213try {
218-const output = runTsEval(
219-`
220-import { resolveSnapshot, shellQuote } from "./${TS_PATHS.common}";
221-console.log(shellQuote("it's ok"));
222-const snapshot = resolveSnapshot("vm", "fresh");
223-console.log([snapshot.id, snapshot.state, snapshot.name].join("\\t"));
224-`,
225-fakePrlctlEnv(tempDir),
226-);
214+const output = withEnv(fakePrlctlEnv(tempDir), () => {
215+const snapshot = resolveSnapshot("vm", "fresh");
216+return `${shellQuote("it's ok")}\n${[snapshot.id, snapshot.state, snapshot.name].join("\t")}`;
217+});
227218228219expect(output.split("\n")[0]).toBe("'it'\"'\"'s ok'");
229220expect(output).toContain("{wanted}\tpoweroff\tfresh-poweroff-2026-04-01");
@@ -267,16 +258,12 @@ if (isPrlctl) {
267258);
268259269260try {
270-const output = runTsEval(
271-`
272-import { resolveSnapshot } from "./${TS_PATHS.common}";
273-const snapshot = resolveSnapshot("vm", "macOS 26.5 latest");
274-console.log([snapshot.id, snapshot.state, snapshot.name].join("\\t"));
275-`,
276-fakePrlctlEnv(tempDir),
277-);
261+const output = withEnv(fakePrlctlEnv(tempDir), () => {
262+const snapshot = resolveSnapshot("vm", "macOS 26.5 latest");
263+return [snapshot.id, snapshot.state, snapshot.name].join("\t");
264+});
278265279-expect(output.trim()).toBe("{wanted}\tpoweron\tmacOS 26.5");
266+expect(output).toBe("{wanted}\tpoweron\tmacOS 26.5");
280267} finally {
281268rmSync(tempDir, { force: true, recursive: true });
282269}
@@ -321,15 +308,9 @@ if (isPrlctl) {
321308);
322309323310try {
324-const output = runTsEval(
325-`
326-import { resolveUbuntuVmName } from "./${TS_PATHS.common}";
327-console.log(resolveUbuntuVmName("Ubuntu missing"));
328-`,
329-fakePrlctlEnv(tempDir),
330-);
311+const output = withEnv(fakePrlctlEnv(tempDir), () => resolveUbuntuVmName("Ubuntu missing"));
331312332-expect(output.trim()).toBe("Ubuntu 26.04");
313+expect(output).toBe("Ubuntu 26.04");
333314} finally {
334315rmSync(tempDir, { force: true, recursive: true });
335316}
@@ -353,7 +334,11 @@ console.log(resolveUbuntuVmName("Ubuntu missing"));
353334});
354335355336it("resolves provider defaults and explicit model overrides", () => {
356-expect(resolveProviderAuth("openai", { env: { OPENAI_API_KEY: "sk-openai" } })).toEqual({
337+expect(
338+withEnv({ OPENAI_API_KEY: "sk-openai" }, () =>
339+resolveProviderAuthDirect({ provider: "openai" }),
340+),
341+).toEqual({
357342apiKeyEnv: "OPENAI_API_KEY",
358343apiKeyValue: "sk-openai",
359344authChoice: "openai-api-key",
@@ -362,11 +347,13 @@ console.log(resolveUbuntuVmName("Ubuntu missing"));
362347});
363348364349expect(
365-resolveProviderAuth("anthropic", {
366-apiKeyEnv: "CUSTOM_ANTHROPIC_KEY",
367-env: { CUSTOM_ANTHROPIC_KEY: "sk-anthropic" },
368-modelId: "anthropic/custom",
369-}),
350+withEnv({ CUSTOM_ANTHROPIC_KEY: "sk-anthropic" }, () =>
351+resolveProviderAuthDirect({
352+apiKeyEnv: "CUSTOM_ANTHROPIC_KEY",
353+modelId: "anthropic/custom",
354+provider: "anthropic",
355+}),
356+),
370357).toEqual({
371358apiKeyEnv: "CUSTOM_ANTHROPIC_KEY",
372359apiKeyValue: "sk-anthropic",
@@ -377,14 +364,11 @@ console.log(resolveUbuntuVmName("Ubuntu missing"));
377364});
378365379366it("uses the shared GPT-5 OpenAI model for Windows smoke unless overridden", () => {
380-const source = `
381-import { resolveWindowsProviderAuth } from "./${TS_PATHS.common}";
382-const result = resolveWindowsProviderAuth({
383- provider: "openai",
384-});
385-console.log(JSON.stringify(result));
386-`;
387-expect(JSON.parse(runTsEval(source, { OPENAI_API_KEY: "sk-openai" }))).toEqual({
367+expect(
368+withEnv({ OPENAI_API_KEY: "sk-openai" }, () =>
369+resolveWindowsProviderAuth({ provider: "openai" }),
370+),
371+).toEqual({
388372apiKeyEnv: "OPENAI_API_KEY",
389373apiKeyValue: "sk-openai",
390374authChoice: "openai-api-key",
@@ -393,11 +377,12 @@ console.log(JSON.stringify(result));
393377});
394378395379expect(
396-JSON.parse(
397-runTsEval(source, {
380+withEnv(
381+{
398382OPENAI_API_KEY: "sk-openai",
399383OPENCLAW_PARALLELS_WINDOWS_OPENAI_MODEL: "openai/custom-windows",
400-}),
384+},
385+() => resolveWindowsProviderAuth({ provider: "openai" }),
401386),
402387).toEqual({
403388apiKeyEnv: "OPENAI_API_KEY",
@@ -575,7 +560,7 @@ console.log(JSON.stringify(result));
575560expect(macos).toContain('const guestOpenClaw = "openclaw"');
576561expect(macos).toContain('const guestNode = "node"');
577562expect(macos).toContain('const guestNpm = "npm"');
578-expect(macos).toContain('$(npm root -g)/openclaw/openclaw.mjs');
563+expect(macos).toContain("$(npm root -g)/openclaw/openclaw.mjs");
579564expect(macos).toContain("guestOpenClawEntryExec");
580565expect(macos).not.toContain('const guestOpenClaw = "/opt/homebrew/bin/openclaw"');
581566expect(macos).not.toContain('const guestNode = "/opt/homebrew/bin/node"');
@@ -609,17 +594,15 @@ console.log(JSON.stringify(result));
609594});
610595611596it("returns timed-out host command status when check is disabled", () => {
612-const result = JSON.parse(
613-runTsEval(`
614-import { run } from "./${TS_PATHS.hostCommand}";
615-const result = run(process.execPath, ["-e", "process.stdout.write('partial'); setTimeout(() => {}, 1000);"], {
616- check: false,
617- quiet: true,
618- timeoutMs: 50,
619-});
620-console.log(JSON.stringify(result));
621-`),
622-) as { status: number; stdout: string };
597+const result = run(
598+process.execPath,
599+["-e", "process.stdout.write('partial'); setTimeout(() => {}, 1000);"],
600+{
601+check: false,
602+quiet: true,
603+timeoutMs: 50,
604+},
605+);
623606624607expect(result.status).toBe(124);
625608expect(result.stdout).toBeTypeOf("string");
@@ -692,15 +675,11 @@ console.log(JSON.stringify(result));
692675});
693676694677it("gives GPT-5.5 enough Parallels model time on slower desktop guests", () => {
695-const source = `
696-import { resolveParallelsModelTimeoutSeconds } from "./${TS_PATHS.common}";
697-console.log(JSON.stringify({
698- macos: resolveParallelsModelTimeoutSeconds("macos"),
699- windows: resolveParallelsModelTimeoutSeconds("windows"),
700- linux: resolveParallelsModelTimeoutSeconds("linux"),
701-}));
702-`;
703-expect(JSON.parse(runTsEval(source))).toEqual({
678+expect({
679+linux: resolveParallelsModelTimeoutSeconds("linux"),
680+macos: resolveParallelsModelTimeoutSeconds("macos"),
681+windows: resolveParallelsModelTimeoutSeconds("windows"),
682+}).toEqual({
704683linux: 900,
705684macos: 1800,
706685windows: 1800,
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。