Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,7 @@ Use these NemoClaw precedents for durable evidence shape, not as inherited concl
- `docs/security/openclaw-2026.6.10-dependency-review.md` and
`test/openclaw-dependency-review.test.ts` for a tracked dependency review with contract tests;
- `docs/security/openshell-0.0.72-compatibility-review.mdx` for a runtime compatibility boundary;
- `scripts/checks/dependency-pins.ts` and `test/dependency-pins-check.test.ts` for selector
- `scripts/checks/dependency-pins.mts` and `test/dependency-pins-check.test.ts` for selector
coherence; and
- `scripts/check-installer-hash.sh` and `test/installer-hash-check.test.ts` for independently
trusted release manifests and consumed artifacts.
Expand Down
2 changes: 1 addition & 1 deletion .github/actions/ci-cli-coverage-shard/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ runs:
exit 0
;;
esac
npx tsx scripts/checks/e2e-mock-parity.ts --base "$base" --head "$head"
npx tsx scripts/checks/e2e-mock-parity.mts --base "$base" --head "$head"

- name: Build TypeScript plugin
shell: bash
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ jobs:
# covered without executing a mutable replacement action.
- name: Validate changed live E2E mock parity (bootstrap)
if: ${{ steps.trusted-shard-capabilities.outputs.e2e-support != 'true' && matrix.shard == 1 }}
run: npx tsx scripts/checks/e2e-mock-parity.ts --base HEAD^1 --head HEAD^2
run: npx tsx scripts/checks/e2e-mock-parity.mts --base HEAD^1 --head HEAD^2

- name: Run E2E support shard (bootstrap)
if: ${{ steps.trusted-shard-capabilities.outputs.e2e-support != 'true' }}
Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,13 @@
"test:coverage:cli": "npm run clean:cli && npm run build:cli && tsx scripts/check-dist-sourcemaps.mts dist && vitest run --project cli --project integration --coverage --coverage.reporter=text-summary --coverage.reporter=json-summary --coverage.reportsDirectory=coverage/cli --coverage.include=\"bin/**/*.js\" --coverage.include=\"src/**/*.ts\" --coverage.exclude=\"test/**/*.js\" --coverage.exclude=\"test/**/*.ts\" && tsx scripts/check-coverage-ratchet.mts coverage/cli/coverage-summary.json ci/coverage-threshold-cli.json \"CLI coverage\"",
"test:coverage:plugin": "vitest run --project plugin --coverage --coverage.reporter=text-summary --coverage.reporter=json-summary --coverage.reportsDirectory=coverage/plugin --coverage.include=\"nemoclaw/src/**/*.ts\" --coverage.include=\"nemoclaw/src/**/*.cts\" --coverage.exclude=\"**/*.test.ts\" && tsx scripts/check-coverage-ratchet.mts coverage/plugin/coverage-summary.json ci/coverage-threshold-plugin.json \"Plugin coverage\"",
"test:live-e2e": "npm run clean:cli && npm run build:cli && NEMOCLAW_RUN_LIVE_E2E=1 vitest run --project e2e-live",
"test:imports:check": "tsx scripts/checks/no-test-dist-imports.ts",
"test:projects:check": "tsx scripts/checks/vitest-project-overlap.ts",
"test:titles:check": "tsx scripts/checks/test-title-style.ts",
"test:imports:check": "tsx scripts/checks/no-test-dist-imports.mts",
"test:projects:check": "tsx scripts/checks/vitest-project-overlap.mts",
"test:titles:check": "tsx scripts/checks/test-title-style.mts",
"bench": "tsx scripts/bench/run.mts",
"check": "npx prek run --all-files --stage pre-commit && npx prek run --all-files --stage manual",
"check:diff": "npx prek run --from-ref origin/main --to-ref HEAD --stage pre-commit && npx commitlint --from origin/main --to HEAD && npx prek run --from-ref origin/main --to-ref HEAD --stage pre-push",
"checks": "tsx scripts/checks/run.ts",
"checks": "tsx scripts/checks/run.mts",
"lint": "npx @biomejs/biome lint . && npm run checks",
"lint:fix": "npx @biomejs/biome lint --write . && npm run checks",
"lint:ts": "cd nemoclaw && npm run check",
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import { readFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import * as ts from "typescript";
import { SUPPORTED_CREDENTIAL_ENV_NAMES } from "../../src/lib/security/credential-env";

const { SUPPORTED_CREDENTIAL_ENV_NAMES } = await import("../../src/lib/security/credential-env");
const CREDENTIAL_ENV_KEYS = SUPPORTED_CREDENTIAL_ENV_NAMES;

const MESSAGE =
Expand Down Expand Up @@ -251,7 +251,7 @@ function scriptKindForPath(filePath: string): ts.ScriptKind {
function main(): void {
const filePaths = process.argv.slice(2).filter((arg) => arg !== "--");
if (filePaths.length === 0) {
console.error("Usage: tsx scripts/checks/direct-credential-env.ts FILE...");
console.error("Usage: tsx scripts/checks/direct-credential-env.mts FILE...");
process.exitCode = 2;
return;
}
Expand Down
200 changes: 200 additions & 0 deletions scripts/checks/e2e-mock-parity.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { execFileSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";

import ts from "typescript";

const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
export const DEFAULT_PARITY_MANIFEST = "test/e2e/mock-parity.json";

export type MockParityEntry = {
live: string;
fast?: string[];
liveOnlyReason?: string;
};

export type MockParityManifest = {
version: 1;
entries: MockParityEntry[];
};

const LIVE_TEST = /^test\/e2e\/live\/.+\.test\.ts$/u;
const FAST_TESTS = [
/^src\/.+\.test\.ts$/u,
/^nemoclaw\/src\/.+\.test\.ts$/u,
/^test\/e2e\/support\/.+\.test\.ts$/u,
/^test\/(?!e2e\/|package-contract\/).+\.test\.(?:js|ts)$/u,
] as const;

function sourceTokens(source: string): string {
const sourceFile = ts.createSourceFile(
"source.ts",
source,
ts.ScriptTarget.Latest,
true,
ts.ScriptKind.TS,
);
const tokens: Array<[ts.SyntaxKind, string]> = [];
const visit = (node: ts.Node): void => {
const children = node.getChildren(sourceFile);
if (children.length === 0) {
if (node.kind !== ts.SyntaxKind.EndOfFileToken) {
tokens.push([node.kind, node.getText(sourceFile)]);
}
return;
}
for (const child of children) visit(child);
};
visit(sourceFile);
return JSON.stringify(tokens);
}

export function isMockParityRelevantSourceChange(
baseSource: string | null,
headSource: string | null,
): boolean {
if (baseSource === null || headSource === null) return true;
return sourceTokens(baseSource) !== sourceTokens(headSource);
}

function isSafeRepoPath(file: string): boolean {
return (
file.length > 0 &&
!path.posix.isAbsolute(file) &&
!file.includes("\\") &&
!file.split("/").includes("..")
);
}

function isFastPrTest(file: string): boolean {
return isSafeRepoPath(file) && FAST_TESTS.some((pattern) => pattern.test(file));
}

export function validateMockParity(options: {
manifest: MockParityManifest;
changedFiles: readonly string[];
fileExists?: (file: string) => boolean;
}): string[] {
const {
manifest,
changedFiles,
fileExists = (file) => fs.existsSync(path.join(REPO_ROOT, file)),
} = options;
const errors: string[] = [];

if (manifest.version !== 1 || !Array.isArray(manifest.entries)) {
return ["mock parity manifest must have version 1 and an entries array"];
}

const entries = new Map<string, MockParityEntry>();
for (const entry of manifest.entries) {
if (!entry || typeof entry !== "object" || typeof entry.live !== "string") {
errors.push("mock parity entries must be objects with a live path");
continue;
}
if (!isSafeRepoPath(entry.live) || !LIVE_TEST.test(entry.live)) {
errors.push(`${entry.live}: live path must be a test/e2e/live/**/*.test.ts file`);
continue;
}
if (entries.has(entry.live)) {
errors.push(`${entry.live}: duplicate mock parity entry`);
continue;
}
entries.set(entry.live, entry);

if (
entry.fast !== undefined &&
(!Array.isArray(entry.fast) || entry.fast.some((file) => typeof file !== "string"))
) {
errors.push(`${entry.live}: fast must be an array of test paths`);
continue;
}
if (entry.liveOnlyReason !== undefined && typeof entry.liveOnlyReason !== "string") {
errors.push(`${entry.live}: liveOnlyReason must be a string`);
continue;
}
const fast = entry.fast ?? [];
const liveOnlyReason = entry.liveOnlyReason?.trim() ?? "";
if (fast.length > 0 && liveOnlyReason) {
errors.push(`${entry.live}: choose fast tests or a live-only reason, not both`);
} else if (fast.length === 0 && !liveOnlyReason) {
errors.push(`${entry.live}: map at least one fast test or provide a live-only reason`);
}

if (!fileExists(entry.live)) errors.push(`${entry.live}: live test does not exist`);
for (const fastFile of new Set(fast)) {
if (!isFastPrTest(fastFile)) {
errors.push(`${entry.live}: ${fastFile} is not collected by a fast PR test project`);
} else if (!fileExists(fastFile)) {
errors.push(`${entry.live}: mapped fast test does not exist: ${fastFile}`);
}
}
}

for (const liveFile of [...new Set(changedFiles)].filter((file) => LIVE_TEST.test(file))) {
if (!entries.has(liveFile)) {
errors.push(`${liveFile}: changed live E2E needs an entry in ${DEFAULT_PARITY_MANIFEST}`);
}
}

return errors.sort();
}

function argument(name: string): string | undefined {
const index = process.argv.indexOf(name);
return index >= 0 ? process.argv[index + 1] : undefined;
}

function sourceAtRef(ref: string, file: string): string | null {
try {
return execFileSync("git", ["show", `${ref}:${file}`], {
cwd: REPO_ROOT,
encoding: "utf8",
maxBuffer: 10 * 1024 * 1024,
});
} catch {
return null;
}
}

function changedFiles(base: string, head: string): string[] {
const files = execFileSync(
"git",
["diff", "--name-only", "--diff-filter=ACMR", `${base}...${head}`],
{
cwd: REPO_ROOT,
encoding: "utf8",
},
)
.split(/\r?\n/u)
.filter(Boolean);
return files.filter(
(file) =>
!LIVE_TEST.test(file) ||
isMockParityRelevantSourceChange(sourceAtRef(base, file), sourceAtRef(head, file)),
);
}

export function main(): void {
const base = argument("--base");
const head = argument("--head") ?? "HEAD";
if (!base) throw new Error("usage: e2e-mock-parity.mts --base <git-ref> [--head <git-ref>]");

const manifestPath = path.join(REPO_ROOT, DEFAULT_PARITY_MANIFEST);
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")) as MockParityManifest;
const errors = validateMockParity({ manifest, changedFiles: changedFiles(base, head) });
if (errors.length > 0) {
console.error(
["E2E mock/live parity check failed:", ...errors.map((error) => `- ${error}`)].join("\n"),
);
process.exitCode = 1;
return;
}
console.log("E2E mock/live parity check passed.");
}

if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) main();
Loading
Loading