Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
119 changes: 115 additions & 4 deletions test/e2e-risk-signal-reporter.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
// 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 os from "node:os";
import path from "node:path";

import { describe, expect, it } from "vitest";
import type { TestModule } from "vitest/node";
import { describe, expect, it, vi } from "vitest";
import type { TestModule, Vitest } from "vitest/node";
import {
classifyLiveTestOutcome,
configuredLiveTestOutcomeFile,
Expand All @@ -18,6 +19,7 @@ import {
} from "../tools/e2e/live-test-outcome.mts";
import {
configuredEnvironment,
default as E2eRiskSignalReporter,
outcomeForRun,
RISK_SIGNAL_FILE,
type RiskSignalEnvironment,
Expand All @@ -32,7 +34,26 @@ function moduleWithStates(states: Array<"passed" | "failed" | "skipped" | "pendi
return {
children: {
*allTests() {
for (const state of states) yield { result: () => ({ state }) };
for (const [index, state] of states.entries()) {
yield { fullName: `test ${index}`, result: () => ({ state }) };
}
},
},
} as unknown as TestModule;
}

function moduleWithNamedStates(
tests: Array<{
fullName: string;
state: "passed" | "failed" | "skipped" | "pending";
}>,
): TestModule {
return {
children: {
*allTests() {
for (const { fullName, state } of tests) {
yield { fullName, result: () => ({ state }) };
}
},
},
} as unknown as TestModule;
Expand All @@ -42,7 +63,7 @@ function moduleWithFailedError(error: unknown): TestModule {
return {
children: {
*allTests() {
yield { result: () => ({ state: "failed", errors: [error] }) };
yield { fullName: "failed test", result: () => ({ state: "failed", errors: [error] }) };
},
},
} as unknown as TestModule;
Expand Down Expand Up @@ -127,6 +148,96 @@ describe("E2E risk signal reporter", () => {
}
});

it("applies the configured name pattern through the reporter lifecycle", () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-risk-signal-"));
try {
const testedSha = execFileSync("git", ["rev-parse", "--verify", "HEAD"], {
encoding: "utf8",
}).trim();
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
vi.stubEnv("E2E_ARTIFACT_DIR", dir);
vi.stubEnv("E2E_TARGET_ID", "network-policy");
vi.stubEnv("NEMOCLAW_E2E_EXPECTED_SHA", testedSha);
vi.stubEnv("NEMOCLAW_E2E_PLAN_HASH", PLAN_HASH);
vi.stubEnv("NEMOCLAW_E2E_CORRELATION_ID", CORRELATION_ID);
vi.stubEnv("NEMOCLAW_E2E_SHARD", "live-probes");

const reporter = new E2eRiskSignalReporter();
reporter.onInit({
config: { testNamePattern: /^network-policy:.+probes$/u },
} as Vitest);
reporter.onTestRunEnd(
[
moduleWithNamedStates([
{
fullName: "network-policy: restricted sandbox enforces live allow/deny policy probes",
state: "passed",
},
{
fullName:
"network-policy: default restricted OpenClaw onboard leaves policy-list with zero active presets",
state: "skipped",
},
]),
],
[],
"passed",
);

const signal = JSON.parse(
fs.readFileSync(path.join(dir, RISK_SIGNAL_FILE), "utf8"),
) as Record<string, unknown>;
expect(signal).toMatchObject({ passed: 1, failed: 0, skipped: 0, pending: 0 });
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});

it("counts a selected test that skips", () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-risk-signal-"));
try {
const signal = writeRiskSignal(
environment(dir),
[
moduleWithNamedStates([
{
fullName: "network-policy: restricted sandbox enforces live allow/deny policy probes",
state: "skipped",
},
{
fullName:
"network-policy: default restricted OpenClaw onboard leaves policy-list with zero active presets",
state: "skipped",
},
]),
],
[],
"passed",
/^network-policy:.+probes$/u,
);

expect(signal).toMatchObject({ passed: 0, failed: 0, skipped: 1, pending: 0 });
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});

it("emits no passing evidence when the name pattern matches no tests", () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-risk-signal-"));
try {
const signal = writeRiskSignal(
environment(dir),
[moduleWithNamedStates([{ fullName: "selected elsewhere", state: "skipped" }])],
[],
"passed",
/^missing test$/u,
);

expect(signal).toMatchObject({ passed: 0, failed: 0, skipped: 0, pending: 0 });
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});

it("refuses a symlinked prior signal without modifying its target", () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-risk-signal-"));
const target = path.join(dir, "target.json");
Expand Down
22 changes: 18 additions & 4 deletions test/e2e/risk-signal-reporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { execFileSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";

import type { TestModule } from "vitest/node";
import type { TestModule, Vitest } from "vitest/node";
import type { Reporter, TestRunEndReason } from "vitest/reporters";
import {
classifyLiveTestOutcome,
Expand Down Expand Up @@ -79,10 +79,18 @@ export function configuredEnvironment(
return { ...values, testedSha };
}

function counts(testModules: ReadonlyArray<TestModule>) {
function matchesNamePattern(fullName: string, pattern: RegExp | undefined): boolean {
if (!pattern) return true;
const stablePattern = new RegExp(pattern.source, pattern.flags);
// Vitest joins suite names with spaces when it applies testNamePattern.
return stablePattern.test(fullName.replaceAll(" > ", " "));
}

function counts(testModules: ReadonlyArray<TestModule>, testNamePattern?: RegExp) {
const result = { passed: 0, failed: 0, skipped: 0, pending: 0 };
for (const module of testModules) {
for (const test of module.children.allTests()) {
if (!matchesNamePattern(test.fullName, testNamePattern)) continue;
result[test.result().state] += 1;
}
}
Expand Down Expand Up @@ -158,6 +166,7 @@ export function writeRiskSignal(
testModules: ReadonlyArray<TestModule>,
unhandledErrors: ReadonlyArray<unknown>,
runReason: TestRunEndReason,
testNamePattern?: RegExp,
): E2eRiskSignal {
const signal: E2eRiskSignal = {
version: 1,
Expand All @@ -167,7 +176,7 @@ export function writeRiskSignal(
testedSha: environment.testedSha,
planHash: environment.planHash,
correlationId: environment.correlationId,
...counts(testModules),
...counts(testModules, testNamePattern),
unhandledErrors: unhandledErrors.length,
runReason,
};
Expand All @@ -181,13 +190,18 @@ export function writeRiskSignal(
export default class E2eRiskSignalReporter implements Reporter {
private readonly environment: RiskSignalEnvironment | null;
private readonly outcomeFile: string | null;
private testNamePattern: RegExp | undefined;
private processTimedOut = false;

constructor() {
this.environment = configuredEnvironment(process.env);
this.outcomeFile = configuredLiveTestOutcomeFile(process.env);
}

onInit(vitest: Vitest): void {
this.testNamePattern = vitest.config.testNamePattern;
}

onTestRunStart(): void {
this.processTimedOut = false;
if (!this.outcomeFile) return;
Expand All @@ -207,7 +221,7 @@ export default class E2eRiskSignalReporter implements Reporter {
reason: TestRunEndReason,
): void {
if (this.environment) {
writeRiskSignal(this.environment, testModules, unhandledErrors, reason);
writeRiskSignal(this.environment, testModules, unhandledErrors, reason, this.testNamePattern);
}
if (this.outcomeFile) {
writeLiveTestOutcome(
Expand Down
Loading