openclaw/scripts/lib/vitest-process.mts
Peter Steinberger 762063f08f
chore(deps): update cooled Oxfmt and harden declaration fixtures (#137004)
* chore(deps): update cooled Oxfmt and isolate declaration fixtures

Select Oxfmt 0.65.0 and its 19 native bindings at the seven-day cutoff, preserving all other dependency pins and security policy. Apply its canonical formatter output and split over-limit UI presentation owners without adding lint or assertion exemptions.

Expose only the real packages needed by declaration fixtures instead of the entire repository dependency namespace. Preserve compiler mutation, cache, publication and cleanup guards and existing deadlines. Retain the fallback boundary assertion that the final reply outcome is failed.

Local proof: 100 declaration and sibling tests; 48 Chromium form-boundary tests; focused UI type/lint/ownership checks; inspected synthetic before/after UI parity; full formatter check; clean bounded Codex review. Current-main integration and exact-head CI follow before landing.

* test(ci): cover unified declarations on Windows

Exercise both declaration fixture modes in the existing second Windows lane. Preserve its serial execution, worker count, and deadlines. Inventory and workflow guards pass; native execution is required in PR CI.

* fix(build): keep native Node steps shell-free on Windows

Preserve literal Node arguments, including encoded import URLs, when the build orchestrator delegates to managed execution. Share native invocation construction across ordinary steps and no-pnpm fallbacks, while retaining pnpm shim handling and all shell-safety and process-cleanup guards.

Four boundary regressions fail before and pass after the repair. Final local launcher and combined declaration runs pass 204 and 21 tests respectively. The original native Windows failure and unrelated local readiness diagnostic are retained in PR evidence.

* style: apply Oxfmt to integrated main changes

Apply only verified canonical formatter output to five newly integrated files. The reviewed Windows launcher repair remains byte-identical. Whole formatting, UI types/build, and the complete line-limit ratchet pass.

* test(ui): consolidate unresolved session workspace cases

* style: format refreshed main presentation changes

Canonical Oxfmt output only. Preserved reviewed Windows and fixture bytes; verified frozen install, all-tree formatting, 382 changed core/UI/package lint targets, 59 session-menu tests, UI types and the canonical UI build.

* style: format current main approval and profile code

* fix(gateway): distinguish policy denials from pending approval

Correct session-host picker guidance after hot-reloaded node command denials. Evaluate Gateway policy independently from pairing approval while keeping commands and capabilities bounded by actual admission. Preserve live connections across deny and reallow. Follow-up to the hot-reload integration in https://github.com/openclaw/openclaw/pull/137160. Verified with registry-backed regressions, 127 owner and sibling tests, 14 UI tests, and a real Gateway/browser deny-reallow flow.

* style: format the updated dashboard gallery

* test(telegram): synchronize abort with real probe backoff

* style: format the current Devices action menu

* fix(ui): keep sidebar invitation layout stable

Keep deployment policy and browser dismissal authoritative while deferring the invitation until active sidebar interaction ends. Remove its independent idle-loaded component, preserve internal focus handoffs and organizer drag ownership, and keep artwork loading geometry-neutral.

Protect the original layout race and delayed policy, pointer, keyboard, drag, touch-input, disablement, and dismissal paths. Full owner proof passed 35 browser and 344 unit cases; four negative controls failed as intended. Final lint, styles, invitation cases and existing build budgets pass. Document the first-appearance behavior.

Independent scoped P0 review found no actionable issue. Follow-up for https://github.com/openclaw/openclaw/pull/137004.

* style: format integrated connected-account views

Canonical Oxfmt output for current main account controls. Executable AST and literal values remain unchanged; profile/account unit tests, browser flows, real-Gateway policy flow, and scoped checks pass. Independent P0 review found no actionable issue. The separately reported startup-JS budget excess remains a pre-land blocker.

* perf(ui): defer connected-account English copy

Load the 50 unchanged account-specific strings with their existing UI consumers rather than every initial page. Preserve shared navigation copy, the complete ordered catalog, and English fallback while registering the fragment in host generation and source hashing.

The explicit startup gate changes from 350191 B failing to 349242 B passing: 949 B removed with no budget, baseline, boot-manifest, or translation-memory changes. The canonical catalog is byte-identical; 150 unit tests, 16 browser cases, classified checks, and independent scoped P0 review pass. Follow-up for https://github.com/openclaw/openclaw/pull/137004.

* test(ui): scope reconnect headings to the page

Assert the selected agent heading inside the New Session surface, not the first heading anywhere in the document. The sidebar invitation is a separate valid heading. Preserve reconnect ordering, draft and request assertions, and all existing timeouts.

Both original assertions reproduce the CI mismatch locally; the complete 15-case file and classified checks pass with the scoped selectors. Independent P0 review is clean. Production code is unchanged.

* fix(test): keep source compiler cache in owned test namespaces

Preserve the tooling shared-cache opt-out while allowing source subprocesses to reuse transforms under the Vitest-owned temporary root. Keep worker IPC safeguards and Doctor deadlines unchanged. Real cache regression fails before the fix; Doctor and tooling boundary suites pass.

* fix(plugin-sdk): keep cold facade loading on canonical boundaries

Load Slack QA operations asynchronously without changing their Promise contracts. Restore prepared workspace aliases for cold activation and resolve bundled tracking metadata without eagerly loading activation policy. Preserve actual activation checks and registry fallback. Use the canonical record guard and shrink the old assertion baseline. Cold-source regression fails with the old alias override; unchanged Slack QA and sibling suites pass.

* test(gateway): keep cancellation fixtures live through cold setup

Give intended running descendants real run-context ownership before awaited setup. The unchanged real sweeper must not classify the fixture as an orphan. The deterministic pre-fix sweep reproduces the missed drain; the corrected fixture passes the same probe, all six cases and 33 admission siblings. No production or deadline changes.

* fix(plugin-sdk): activate browser cleanup asynchronously

Use the existing async activated-facade loader in already-async browser cleanup and remove the duplicate synchronous activation check. This avoids blocking cold source-hosted session resets while preserving activation rechecks, optional cleanup warnings and the public Promise contract. Both unchanged Gateway regressions pass; production shrinks by ten lines. Browser bridge throwing-loader migration is a separate follow-up.
2026-09-03 13:31:30 -07:00

152 lines
6.1 KiB
TypeScript

import { spawn, type SpawnOptions } from "node:child_process";
import fs from "node:fs";
import { homedir, tmpdir } from "node:os";
import path from "node:path";
import { waitForever } from "../../src/cli/wait.ts";
import { createTempDirTracker } from "../../test/helpers/temp-dir.ts";
import {
resolveTestBrowserCache,
resolveTestCorepackHome,
readTestHomeSource,
writeTestHomeSource,
} from "../../test/test-home-context.mts";
import {
assertTestHomeSelection,
LIVE_TEST_TRIGGER_ENV_KEYS,
resolveTestHomePolicy,
type TestHomeSelection,
} from "../../test/test-home-policy.mts";
import {
createVitestProcessCompletion,
shouldUseDetachedVitestProcessGroup,
} from "../vitest-process-group.mts";
import { runWithFailedTrailer, writeFailedTrailer } from "./failed-trailer.mts";
import { signalExitCode } from "./managed-child-process.mts";
import {
createVitestResourceOwner,
findVitestResourceOwner,
} from "./vitest-resource-ownership.mts";
/** Own temporary files until the Vitest child, its group, and its pipes have joined. */
export function spawnOwnedVitestProcess(spec: {
command: string;
args: string[];
options: SpawnOptions;
// Preparatory tools share lifetime ownership, but are not Vitest home consumers.
homeMode?: TestHomeSelection | "tooling";
}) {
const env = spec.options.env ?? process.env;
const mode = spec.homeMode ?? "unknown";
if (mode !== "tooling") {
assertTestHomeSelection(env, mode);
}
const policy = resolveTestHomePolicy(env, mode === "tooling" ? "live-aware" : mode);
const tempDirs = createTempDirTracker();
const detached = spec.options.detached ?? shouldUseDetachedVitestProcessGroup();
const verifiedGroup = detached && shouldUseDetachedVitestProcessGroup();
let tempRoot: string | undefined;
let owner: ReturnType<typeof createVitestResourceOwner> | undefined;
let parent: { root: string; release: () => void } | undefined;
const dispose = () => {
owner?.assertReleased();
tempDirs.cleanup();
parent?.release();
};
let child;
try {
const containingRoot = fs.realpathSync(env.TMPDIR || env.TMP || env.TEMP || tmpdir());
// An intermediate runner can die before publishing its own cleanup result.
// Its containing owner must already hold the obligation before allocation.
const containingOwner = findVitestResourceOwner(containingRoot);
if (containingOwner) {
parent = { root: containingOwner.root, release: containingOwner.claim() };
}
tempRoot = tempDirs.make("oc-vt-", containingRoot);
owner = createVitestResourceOwner(tempRoot);
const childEnv: NodeJS.ProcessEnv = { ...env, TMPDIR: tempRoot, TMP: tempRoot, TEMP: tempRoot };
if (mode !== "tooling") {
// The tooling shim avoids the shared tsx cache. Test children have this owned
// temp namespace, so source subprocesses can reuse transforms until cleanup.
delete childEnv.TSX_DISABLE_CACHE;
}
if (mode !== "tooling" && !(policy.live && policy.allowRealHome)) {
const nativeHome = path.join(tempRoot, "home");
fs.mkdirSync(nativeHome);
const callerHome = env.HOME ?? env.USERPROFILE ?? homedir();
childEnv.COREPACK_HOME = resolveTestCorepackHome(env, callerHome);
childEnv.PLAYWRIGHT_BROWSERS_PATH = resolveTestBrowserCache(env, callerHome);
// Set the actual process environment before config imports and Worker creation.
// Worker-local process.env and restored os.homedir mocks cannot retarget libuv.
if (!policy.hermetic) {
const sourceHome =
policy.live || policy.loadProfileEnv ? readTestHomeSource(env) : undefined;
writeTestHomeSource(tempRoot, sourceHome ?? callerHome);
}
childEnv.HOME = nativeHome;
childEnv.USERPROFILE = nativeHome;
}
if (policy.hermetic) {
for (const key of [...LIVE_TEST_TRIGGER_ENV_KEYS, "OPENCLAW_LIVE_USE_REAL_HOME"]) {
delete childEnv[key];
}
}
const options = { ...spec.options, detached, env: childEnv };
child = spawn(spec.command, spec.args, options);
} catch (error) {
tempDirs.cleanup();
parent?.release();
throw error;
}
const completion = (async () => {
try {
const result = await createVitestProcessCompletion({ child, detached });
if (verifiedGroup) {
dispose();
} else {
// Keep the containing claim too: leader exit cannot certify descendants.
console.error(
`[vitest] retained temporary namespace ${tempRoot}; descendant completion is unverified on this non-group launch. Stop the remaining writers before removing this exact directory.`,
);
}
return result;
} catch (error) {
// A failed parent receipt can follow successful child disposal. Report
// the still-owned ancestor, not a child directory already removed.
const retainedRoot = tempRoot && tempDirs.dirs.has(tempRoot) ? tempRoot : parent?.root;
// No PID means spawn failed; otherwise unverified writers still own the files.
if (!child.pid) {
dispose();
} else if (retainedRoot) {
throw Object.assign(
new Error(
`[vitest] retained temporary namespace ${retainedRoot}; child/group or nested resource completion was not verified. Stop the remaining writers before removing this exact directory.`,
{ cause: error },
),
{ processTreeState: "indeterminate" },
);
}
throw error;
}
})();
return { child, completion };
}
export async function exitVitestBySignal(signal: NodeJS.Signals): Promise<void> {
process.kill(process.pid, signal);
// Dependency signal handlers may finish cleanup and re-raise asynchronously.
// A numeric return must not win that race.
await waitForever();
}
/** Only public invocations report; internal children propagate their settled outcome. */
export function runVitestCli(
tool: string,
run: (exitBySignal: typeof exitVitestBySignal) => Promise<void>,
): Promise<void> {
return runWithFailedTrailer(tool, () =>
run(async (signal) => {
writeFailedTrailer(tool, signalExitCode(signal));
await exitVitestBySignal(signal);
}),
);
}