openclaw/test/scripts/run-tsgo-worktree.test.ts
Peter Steinberger 4e3ab7babc
Some checks are pending
ClawSweeper Dispatch / dispatch (push) Waiting to run
CodeQL / Security High (actions) (push) Waiting to run
CodeQL / Security High (process-exec-boundary) (push) Waiting to run
Control UI Locale Refresh / resolve-base (push) Waiting to run
Control UI Locale Refresh / Verify generated PR App permissions (push) Blocked by required conditions
Control UI Locale Refresh / Refresh (push) Blocked by required conditions
Control UI Locale Refresh / Commit control UI locale refresh (push) Blocked by required conditions
Docs Sync Publish Repo / sync-publish-repo (push) Waiting to run
Docs / docs (push) Waiting to run
Native App Locale Refresh / Refresh native fr (push) Blocked by required conditions
Native App Locale Refresh / Refresh native hi (push) Blocked by required conditions
Native App Locale Refresh / Refresh native id (push) Blocked by required conditions
Plugin NPM Release / Seal prepared plugin npm release (push) Blocked by required conditions
Plugin NPM Release / Trusted publisher OIDC exchange (push) Blocked by required conditions
Plugin NPM Release / approve_plugins_npm_release (push) Blocked by required conditions
CodeQL / Security High (channel-runtime-boundary) (push) Waiting to run
CodeQL / Security High (core-auth-secrets) (push) Waiting to run
CodeQL / Security High (mcp-process-tool-boundary) (push) Waiting to run
CodeQL / Security High (network-ssrf-boundary) (push) Waiting to run
CodeQL / Security High (plugin-trust-boundary) (push) Waiting to run
Native App Locale Refresh / Refresh native pl (push) Blocked by required conditions
Native App Locale Refresh / Refresh native pt-BR (push) Blocked by required conditions
Native App Locale Refresh / Refresh native ru (push) Blocked by required conditions
Native App Locale Refresh / Refresh native sv (push) Blocked by required conditions
Native App Locale Refresh / Refresh native th (push) Blocked by required conditions
Native App Locale Refresh / Refresh native tr (push) Blocked by required conditions
Native App Locale Refresh / Refresh native uk (push) Blocked by required conditions
Native App Locale Refresh / Refresh native vi (push) Blocked by required conditions
Native App Locale Refresh / Refresh native zh-CN (push) Blocked by required conditions
Native App Locale Refresh / Refresh native zh-TW (push) Blocked by required conditions
Native App Locale Refresh / Commit native locale refresh (push) Blocked by required conditions
Native App Locale Refresh / resolve-base (push) Waiting to run
Native App Locale Refresh / Verify generated PR App permissions (push) Blocked by required conditions
Native App Locale Refresh / Refresh native ar (push) Blocked by required conditions
Native App Locale Refresh / Refresh native de (push) Blocked by required conditions
Native App Locale Refresh / Refresh native es (push) Blocked by required conditions
Native App Locale Refresh / Refresh native fa (push) Blocked by required conditions
Native App Locale Refresh / Refresh native it (push) Blocked by required conditions
Native App Locale Refresh / Refresh native ja-JP (push) Blocked by required conditions
Native App Locale Refresh / Refresh native ko (push) Blocked by required conditions
Native App Locale Refresh / Refresh native nl (push) Blocked by required conditions
Node Runtime Conformance / scope (push) Waiting to run
Node Runtime Conformance / TypeScript contracts (push) Blocked by required conditions
Node Runtime Conformance / Rust workspace (push) Blocked by required conditions
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Waiting to run
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Blocked by required conditions
Plugin Init Scaffold Validation / Validate provider scaffold (push) Blocked by required conditions
Plugin Init Scaffold Validation / scope (push) Waiting to run
Plugin NPM Release / preview_plugins_npm (push) Waiting to run
Plugin NPM Release / Validate release publish approval (push) Blocked by required conditions
Plugin NPM Release / preview_plugin_pack (push) Blocked by required conditions
Plugin NPM Release / Preflight plugin npm package () (push) Blocked by required conditions
Plugin NPM Release / Publish plugin npm package () (push) Blocked by required conditions
Vitest Cache Warm / dependencies (push) Waiting to run
Plugin NPM Release / verify_plugins_npm (push) Blocked by required conditions
Vitest Cache Warm / warm (push) Waiting to run
Workflow Sanity / actionlint (push) Waiting to run
Workflow Sanity / generated-doc-baselines (push) Waiting to run
test(scripts): copy only the native compiler parts each fixture uses (#163374)
materializeNativeCompiler copied the whole native typescript package into
every fixture. 482 of its 493 files are dist/ (the typescript/unstable/*
JavaScript API) and vendor/ (that API's JSON-RPC transport), which only
toolchain identity hashing and fixture-resolved API imports read.

Per consumer:
- changed-lanes: root-test lint is boundary-free and parses config with the
  repository's own API, so the copy was never read; drop it.
- write-plugin-sdk-entry-dts / write-unified-entry-dts: createFixture already
  copies the same package with its platform binary since the TypeScript 7
  cutover; drop the redundant overlay.
- run-tsgo-worktree, ownership --version preflight, Windows long-path
  resolution: launcher-only copies (javaScriptApi: false).
- interceptor fixtures that exec a real compiler: exec the installed binary
  (resolveInstalledNativeCompiler); the lint-consumption case keeps its full
  fixture-local install and binary because preparation hashes it and admits
  default-library reads inside the checkout.
- shard concurrency: its synthetic compiler needs no install.

Build-artifact, extension-boundary, native-declaration, prepare/check
boundary, and tsdown-config fixtures keep the full copy. Copies stay on the
filtered close-on-exec cpSync path; assertions and timeouts are unchanged.
2026-10-02 10:13:53 -07:00

228 lines
9.1 KiB
TypeScript

import { spawnSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { collectRuntimeImportClosure } from "../../scripts/lib/runtime-import-closure.mts";
import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
import {
materializeNativeCompiler,
overrideNativeFixtureExecutable,
resolveInstalledNativeCompiler,
writeNativeFixtureFile,
} from "./native-boundary-fixture.js";
const roots = useAutoCleanupTempDirTracker(afterEach);
const sourceRoot = process.cwd();
// Commit only the wrapper's runtime closure: staging all of scripts/lib (~480
// files) exceeded the fixture's Git budget on loaded hosts. The CLI shim loads
// the implementation and its tsx loader by path, so they are explicit inputs.
const WRAPPER_FILES = collectRuntimeImportClosure(
sourceRoot,
["scripts/run-tsgo.mjs", "scripts/run-tsgo.mts", "scripts/tsx.mjs"],
{ includeDynamicImports: true },
);
function createLinkedCheckoutFixture() {
const directory = fs.realpathSync.native(roots.make("native-wrapper-worktree-"));
const primary = path.join(directory, "primary");
const root = path.join(directory, "linked");
fs.mkdirSync(primary);
// Isolate fixture Git writes from hook-owned indexes/objects. Wrapper commands
// below still inherit the untouched environment and exercise the real bootstrap.
const gitEnv = { ...process.env };
for (const key of [
"GIT_DIR",
"GIT_COMMON_DIR",
"GIT_WORK_TREE",
"GIT_INDEX_FILE",
"GIT_OBJECT_DIRECTORY",
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
"GIT_QUARANTINE_PATH",
]) {
delete gitEnv[key];
}
const git = (args: string[]) => {
const result = spawnSync(
"git",
[
`--git-dir=${path.join(primary, ".git")}`,
`--work-tree=${primary}`,
"-c",
`core.hooksPath=${path.join(directory, "unused-hooks")}`,
"-c",
"commit.gpgsign=false",
"-c",
"user.name=Fixture",
"-c",
"user.email=fixture@example.invalid",
...args,
],
{ cwd: primary, env: gitEnv, encoding: "utf8", timeout: 10_000 },
);
expect(result.error).toBeUndefined();
expect(result.status, result.stdout + result.stderr).toBe(0);
};
git(["init", "-q"]);
writeNativeFixtureFile(primary, "package.json", '{"private":true,"type":"module"}\n');
writeNativeFixtureFile(primary, "pnpm-workspace.yaml", "packages: []\n");
writeNativeFixtureFile(primary, ".gitignore", "node_modules/\n.artifacts/\n");
for (const file of WRAPPER_FILES) {
const target = path.join(primary, file);
fs.mkdirSync(path.dirname(target), { recursive: true });
fs.copyFileSync(path.join(sourceRoot, file), target);
}
git(["add", "."]);
git(["commit", "-qm", "Synthetic compiler wrapper fixture"]);
git(["worktree", "add", "--detach", "-q", root]);
// An ancestor install must not let the worktree's own preload skip its Git fallback.
expect(path.relative(primary, root)).toBe(`..${path.sep}linked`);
expect(fs.lstatSync(path.join(root, ".git")).isFile()).toBe(true);
return { primary, root, git };
}
function linkCheckoutTools(root: string) {
for (const name of ["tsx", "@openclaw/fs-safe"]) {
const target = path.join(root, "node_modules", name);
fs.mkdirSync(path.dirname(target), { recursive: true });
fs.symlinkSync(path.join(sourceRoot, "node_modules", name), target, "junction");
}
}
function installCheckoutTools(root: string) {
const native = materializeNativeCompiler(root, { javaScriptApi: false });
linkCheckoutTools(root);
return native;
}
function runTsgoEntry(
root: string,
args: string[],
options: { cwd?: string; env?: NodeJS.ProcessEnv } = {},
) {
return spawnSync(process.execPath, [path.join(root, "scripts/run-tsgo.mjs"), ...args], {
cwd: options.cwd ?? root,
env: options.env ?? process.env,
encoding: "utf8",
timeout: 25_000,
killSignal: "SIGKILL",
});
}
describe("run-tsgo linked worktree entry", () => {
it("selects its own root compiler from src while preserving relative project semantics", () => {
const { primary, root } = createLinkedCheckoutFixture();
installCheckoutTools(primary);
linkCheckoutTools(root);
const native = resolveInstalledNativeCompiler();
const write = (file: string, text: string) => writeNativeFixtureFile(root, file, text);
const compilerOptions = {
module: "NodeNext",
target: "ES2023",
types: [],
strict: true,
noEmit: true,
};
write("tsconfig.json", JSON.stringify({ compilerOptions, files: ["wrong-entry.ts"] }));
write("wrong-entry.ts", 'export const value: number = "wrong project";\n');
write("src/tsconfig.json", JSON.stringify({ compilerOptions, files: ["entry.ts"] }));
write("src/entry.ts", "export const value: number = 1;\n");
const selectionPath = path.join(root, "compiler-selection.json");
// Record the selected executable and cwd, then let the unchanged native compiler
// parse -p and check the real project. No bootstrap or compiler result is mocked.
write(
"native-compiler.mjs",
`#!/usr/bin/env node
import fs from "node:fs";
import { spawnSync } from "node:child_process";
fs.writeFileSync(${JSON.stringify(selectionPath)}, JSON.stringify({ cwd: process.cwd(), args: process.argv.slice(2) }));
const result = spawnSync(${JSON.stringify(native)}, process.argv.slice(2), { stdio: "inherit" });
if (result.error) throw result.error;
process.exitCode = result.status ?? 1;
`,
);
fs.chmodSync(path.join(root, "native-compiler.mjs"), 0o755);
const launcher = path.join(root, "node_modules/.bin/tsgo");
fs.mkdirSync(path.dirname(launcher), { recursive: true });
fs.symlinkSync("../../native-compiler.mjs", launcher, "file");
if (process.platform === "win32") {
write("node_modules/.bin/tsgo.cmd", '@node "%~dp0tsgo" %*\r\n');
}
overrideNativeFixtureExecutable(root, launcher);
const cwd = path.join(root, "src");
const result = runTsgoEntry(root, ["-p", "tsconfig.json"], {
cwd,
env: { ...process.env, OPENCLAW_CI_STATIC_EVIDENCE: "1" },
});
expect(result.error).toBeUndefined();
expect(result.status, result.stdout + result.stderr).toBe(0);
const selected: { cwd: string; args: string[] } = JSON.parse(
fs.readFileSync(selectionPath, "utf8"),
);
expect(selected.cwd).toBe(cwd);
expect(selected.args.slice(0, 2)).toEqual(["-p", "tsconfig.json"]);
if (process.platform !== "win32") {
const receipts = result.stdout.trim().split("\n");
expect(receipts).toHaveLength(2);
const leaf = JSON.parse(receipts[0]!.slice(receipts[0]!.indexOf(" ") + 1));
expect(leaf).toEqual({
version: 1,
id: expect.any(String),
config: "tsconfig.json",
exitCode: 0,
stdout: "",
stderr: "",
});
expect(JSON.parse(receipts[1]!.slice(receipts[1]!.indexOf(" ") + 1))).toEqual({
version: 1,
id: expect.any(String),
planned: 1,
completed: 1,
leaves: [leaf.id],
});
}
expect(fs.lstatSync(path.join(cwd, "node_modules"), { throwIfNoEntry: false })).toBeUndefined();
}, 30_000);
it("refuses a missing local install through its own wrapper without creating dependency links", () => {
const { primary, root } = createLinkedCheckoutFixture();
installCheckoutTools(primary);
const installed = runTsgoEntry(primary, ["--version"]);
expect(installed.error).toBeUndefined();
expect(installed.status, installed.stdout + installed.stderr).toBe(0);
expect(installed.stdout).toMatch(/^Version \d/m);
const modules = path.join(root, "node_modules");
expect(fs.lstatSync(modules, { throwIfNoEntry: false })).toBeUndefined();
const result = runTsgoEntry(root, ["--version"]);
expect(result.error).toBeUndefined();
expect(result.status, result.stdout + result.stderr).toBe(1);
expect(result.stdout).not.toMatch(/^Version \d/m);
expect(result.stderr.trim().split("\n").at(-1)).toBe("[tsgo] FAILED (exit 1)");
expect(fs.lstatSync(modules, { throwIfNoEntry: false })).toBeUndefined();
}, 30_000);
it("visibly skips a missing sparse project through its own wrapper before creating dependencies or outputs", () => {
const { primary, root, git } = createLinkedCheckoutFixture();
installCheckoutTools(primary);
git(["config", "core.sparseCheckout", "true"]);
const result = runTsgoEntry(
root,
[
"-p",
"test/tsconfig/tsconfig.core.test.json",
"--tsBuildInfoFile",
".artifacts/should-not-exist.tsbuildinfo",
],
{ env: { ...process.env, OPENCLAW_TSGO_SPARSE_SKIP: "1", OPENCLAW_CI_STATIC_EVIDENCE: "1" } },
);
expect(result.error).toBeUndefined();
expect(result.status, result.stdout + result.stderr).toBe(0);
expect(result.stderr).toContain("skipping sparse-missing project");
expect(result.stderr).toContain("OPENCLAW_TSGO_SPARSE_SKIP=1");
expect(result.stdout).not.toContain("[ci-static:tsgo:");
expect(
fs.lstatSync(path.join(root, "node_modules"), { throwIfNoEntry: false }),
).toBeUndefined();
expect(fs.existsSync(path.join(root, ".artifacts"))).toBe(false);
}, 30_000);
});