Skip to content
Merged
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
87 changes: 86 additions & 1 deletion test/e2e/fixtures/artifacts.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,92 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import fsSync from "node:fs";
import fs from "node:fs/promises";
import path from "node:path";

import { redactString } from "./redaction.ts";

export type TargetContract = string | readonly string[];

export type TargetMetadata<Extension extends object = Record<string, unknown>> = {
id: string;
contract?: TargetContract;
contracts?: readonly string[];
} & Extension;

export type TargetResult<Extension extends object = Record<string, unknown>> = {
id: string;
/**
* Optional for the normal success path: reaching `complete()` after the live
* assertions have passed records `passed`. Skipped or non-success evidence
* must set an explicit status at the call site. Omit the key to use the
* default; an explicit `undefined` value is rejected like any other invalid
* status payload.
*/
status?: string;
} & Extension;

type TargetEvidenceKind = "metadata" | "result";

function normalizeTargetEvidence(
kind: TargetEvidenceKind,
value: TargetMetadata | TargetResult,
): Record<string, unknown> {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new TypeError(`target ${kind} must be an object`);
}
if (typeof value.id !== "string" || value.id.trim() === "") {
throw new TypeError(`target ${kind} id must be a non-empty string`);
}
if (
kind === "result" &&
"status" in value &&
(typeof value.status !== "string" || value.status.trim() === "")
) {
throw new TypeError("target result status must be a non-empty string");
}

const record = { ...value } as Record<string, unknown>;
if (kind === "metadata") {
const singular = record.contract;
const plural = record.contracts;
if (singular !== undefined && plural !== undefined) {
throw new TypeError("target metadata must use either contract or contracts, not both");
}
const contracts = singular ?? plural;
if (contracts !== undefined) {
const normalized = typeof contracts === "string" ? [contracts] : contracts;
if (
!Array.isArray(normalized) ||
normalized.some((contract) => typeof contract !== "string")
) {
throw new TypeError("target contracts must be a string or an array of strings");
}
record.contracts = normalized;
}
delete record.contract;
}
if (kind === "result") record.status ??= "passed";
record.runner = "vitest";
return record;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

export class TargetEvidenceWriter {
constructor(private readonly artifacts: ArtifactSink) {}

async declare<Extension extends object>(metadata: TargetMetadata<Extension>): Promise<string> {
return this.artifacts.writeJson("target.json", normalizeTargetEvidence("metadata", metadata));
}

async complete<Extension extends object>(result: TargetResult<Extension>): Promise<string> {
return this.artifacts.writeJson(
"target-result.json",
normalizeTargetEvidence("result", result),
);
}
}

/**
* The publication boundary for live E2E evidence.
*
Expand All @@ -15,10 +96,14 @@ import { redactString } from "./redaction.ts";
*/
export class ArtifactSink {
readonly rootDir: string;
readonly target: TargetEvidenceWriter;
private readonly redactionValues = new Set<string>();

constructor(rootDir: string, redactionValues: Iterable<string> = []) {
this.rootDir = path.resolve(rootDir);
const resolvedRoot = path.resolve(rootDir);
fsSync.mkdirSync(resolvedRoot, { recursive: true });
this.rootDir = fsSync.realpathSync(resolvedRoot);
this.target = new TargetEvidenceWriter(this);
this.addRedactionValues(redactionValues);
}

Expand Down
2 changes: 1 addition & 1 deletion test/e2e/live/agent-turn-latency.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ test.skipIf(!shouldRunLiveE2E())(
async ({ artifacts, cleanup, host, sandbox, secrets }) => {
const apiKey = secrets.required("NVIDIA_INFERENCE_API_KEY");
const results: Record<string, unknown> = { model: MODEL, maxTurnSeconds: MAX_TURN_SECONDS };
await artifacts.writeJson("target.json", {
await artifacts.target.declare({
id: "agent-turn-latency",
boundary: "two real sandboxes + hosted inference + OpenClaw agent turn + Hermes API turn",
openclawSandbox: OPENCLAW_SANDBOX,
Expand Down
7 changes: 3 additions & 4 deletions test/e2e/live/bedrock-runtime-compatible-anthropic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1149,7 +1149,7 @@ async function skipPreContractEndpointValidationRateLimit(options: {
redactedStdoutTail: evidenceTail(options.onboarding.redactedStdout),
redactedStderrTail: evidenceTail(options.onboarding.redactedStderr),
});
await options.artifacts.writeJson("target-result.json", {
await options.artifacts.target.complete({
id: "bedrock-runtime-compatible-anthropic",
status: "skipped",
reason: BEDROCK_PRE_CONTRACT_ENDPOINT_VALIDATION_SKIP_REASON,
Expand Down Expand Up @@ -1253,9 +1253,8 @@ RUN_BEDROCK_TEST(
}
});

await artifacts.writeJson("target.json", {
await artifacts.target.declare({
id: "bedrock-runtime-compatible-anthropic",
runner: "vitest",
refs: ["#3767", "#5098"],
agent: AGENT,
sandboxName: SANDBOX_NAME,
Expand Down Expand Up @@ -1364,7 +1363,7 @@ RUN_BEDROCK_TEST(
redact: (text, extraValues) => secrets.redact(text, extraValues),
});

await artifacts.writeJson("target-result.json", {
await artifacts.target.complete({
id: "bedrock-runtime-compatible-anthropic",
agent: AGENT,
assertions: {
Expand Down
3 changes: 1 addition & 2 deletions test/e2e/live/brave-search.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,8 @@ test.skipIf(!shouldRunLiveE2E())(
const inferenceKey = secrets.required("NVIDIA_INFERENCE_API_KEY");
const redactionValues = [braveKey, inferenceKey];

await artifacts.writeJson("target.json", {
await artifacts.target.declare({
id: "brave-search",
runner: "vitest",
boundary:
"source CLI onboard + OpenShell policy/config + in-sandbox OpenClaw/Brave API calls",
sandboxName: SANDBOX_NAME,
Expand Down
3 changes: 1 addition & 2 deletions test/e2e/live/channels-add-remove.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -398,9 +398,8 @@ liveTest(
onboarding: "cloud-openclaw",
});

await artifacts.writeJson("target.json", {
await artifacts.target.declare({
id: "channels-add-remove",
runner: "vitest",
sandboxName: SANDBOX_NAME,
contract: [
"onboard creates an OpenClaw sandbox with no Telegram channel",
Expand Down
2 changes: 1 addition & 1 deletion test/e2e/live/channels-stop-start-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,7 @@ export async function runChannelsStopStartTarget({
});
const redactions = redactionValues(apiKey, tokens);

await artifacts.writeJson("target.json", {
await artifacts.target.declare({
id: "channels-stop-start",
boundary:
"install.sh messaging onboard + channels stop/start CLI + rebuild + sandbox config probes",
Expand Down
7 changes: 3 additions & 4 deletions test/e2e/live/cloud-inference.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ async function writePreContractExternalProviderSkip(
): Promise<void> {
const evidence = buildPreContractExternalProviderSkipEvidence(install, classification);
await artifacts.writeJson("transient-provider-validation.skip.json", evidence);
await artifacts.writeJson("target-result.json", evidence);
await artifacts.target.complete(evidence);
}

function testEnv(home: string, extra: NodeJS.ProcessEnv = {}): NodeJS.ProcessEnv {
Expand Down Expand Up @@ -243,9 +243,8 @@ test.skipIf(!shouldRunLiveE2E())(
`missing sandbox skill validator: ${SANDBOX_SKILL_VALIDATOR}`,
).toBe(true);

await artifacts.writeJson("target.json", {
await artifacts.target.declare({
id: "cloud-inference",
runner: "vitest",
boundary: "install-sh-onboard-sandbox-inference-local-skill-filesystem",
contracts: [
"Docker is running before install/onboard",
Expand Down Expand Up @@ -339,7 +338,7 @@ test.skipIf(!shouldRunLiveE2E())(
: "unknown";
expect(sandboxSkillStatus, resultText(sandboxSkills)).not.toBe("unknown");

await artifacts.writeJson("target-result.json", {
await artifacts.target.complete({
id: "cloud-inference",
status: "passed",
assertions: {
Expand Down
4 changes: 2 additions & 2 deletions test/e2e/live/cloud-onboard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ liveTest(
const installCwd = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-public-install-"));
const redactionValues = [hosted.apiKey];

await artifacts.writeJson("target.json", {
await artifacts.target.declare({
id: "cloud-onboard",
sandboxName: SANDBOX_NAME,
installUrl,
Expand Down Expand Up @@ -176,6 +176,6 @@ liveTest(
}

await cleanup(host, sandbox, { label: "final-cleanup", verify: true });
await artifacts.writeJson("target-result.json", { id: "cloud-onboard", status: "passed" });
await artifacts.target.complete({ id: "cloud-onboard", status: "passed" });
},
);
12 changes: 6 additions & 6 deletions test/e2e/live/common-egress-agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -574,7 +574,7 @@ describe.sequential("common-egress agent live targets", () => {
const hosted = await assertPrerequisites(host, secrets, skip);
const apiKey = hosted.apiKey;
const braveApiKey = secrets.required("BRAVE_API_KEY");
await artifacts.writeJson("target.json", {
await artifacts.target.declare({
id: "common-egress-agent",
case: "openclaw-balanced-weather",
sandboxName: OPENCLAW_BALANCED_SANDBOX,
Expand Down Expand Up @@ -687,7 +687,7 @@ After it returns, reply with only WEATHER_AGENT_OK. Do not fetch any other URL.`
);
expect(weatherProof.exitCode, text(weatherProof)).toBe(0);
expect(weatherProof.stdout.trim()).toMatch(/^[a-f0-9]{64}\s+/);
await artifacts.writeJson("target-result.json", {
await artifacts.target.complete({
id: "common-egress-agent",
case: "openclaw-balanced-weather",
status: "passed",
Expand All @@ -701,7 +701,7 @@ After it returns, reply with only WEATHER_AGENT_OK. Do not fetch any other URL.`
async ({ artifacts, cleanup, host, sandbox, secrets, skip }) => {
const hosted = await assertPrerequisites(host, secrets, skip);
const apiKey = hosted.apiKey;
await artifacts.writeJson("target.json", {
await artifacts.target.declare({
id: "common-egress-agent",
case: "openclaw-open-public-reference",
sandboxName: OPENCLAW_OPEN_SANDBOX,
Expand Down Expand Up @@ -733,7 +733,7 @@ After it returns, reply with only WEATHER_AGENT_OK. Do not fetch any other URL.`
https://www.wikidata.org/w/api.php?action=wbgetentities&ids=Q30&props=labels&languages=en&format=json
After web_fetch returns, reply exactly REFERENCE_AGENT_OK if the fetched response says entity Q30 has the English label United States. Do not fetch any other URL.`,
});
await artifacts.writeJson("target-result.json", {
await artifacts.target.complete({
id: "common-egress-agent",
case: "openclaw-open-public-reference",
status: "passed",
Expand All @@ -747,7 +747,7 @@ After web_fetch returns, reply exactly REFERENCE_AGENT_OK if the fetched respons
async ({ artifacts, cleanup, host, sandbox, secrets, skip }) => {
const hosted = await assertPrerequisites(host, secrets, skip);
const apiKey = hosted.apiKey;
await artifacts.writeJson("target.json", {
await artifacts.target.declare({
id: "common-egress-agent",
case: "hermes-open-public-reference",
sandboxName: HERMES_SANDBOX,
Expand Down Expand Up @@ -783,7 +783,7 @@ After web_fetch returns, reply exactly REFERENCE_AGENT_OK if the fetched respons
prompt: buildHermesReferencePrompt(),
sandboxName: HERMES_SANDBOX,
});
await artifacts.writeJson("target-result.json", {
await artifacts.target.complete({
id: "common-egress-agent",
case: "hermes-open-public-reference",
status: "passed",
Expand Down
5 changes: 2 additions & 3 deletions test/e2e/live/concurrent-gateway-ports.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,9 +297,8 @@ liveTest(
const fake = await startFakeOpenAiCompatibleServer({
port: Number(process.env.NEMOCLAW_E2E_FAKE_PORT ?? 0),
});
await artifacts.writeJson("target.json", {
await artifacts.target.declare({
id: "concurrent-gateway-ports",
runner: "vitest",
boundary: "direct-cli-docker-openshell-multiple-gateways-dashboard-forwards",
contract: [
"sandbox A onboards on the default NemoClaw gateway and dashboard port",
Expand Down Expand Up @@ -401,7 +400,7 @@ liveTest(
expect(["Ready", "Running"]).toContain(phaseAAfterDestroyB);
await expectPortListening(host, GATEWAY_PORT_A, "phase-4-gateway-port-a-still-listening");

await artifacts.writeJson("target-result.json", {
await artifacts.target.complete({
id: "concurrent-gateway-ports",
assertions: {
sandboxAOnboarded: onboardA.exitCode === 0,
Expand Down
5 changes: 2 additions & 3 deletions test/e2e/live/credential-migration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,9 +181,8 @@ runCredentialMigrationTest(
fs.rmSync(home, { recursive: true, force: true });
});

await artifacts.writeJson("target.json", {
await artifacts.target.declare({
id: "credential-migration",
runner: "vitest",
boundary: "real-onboard-openshell-gateway",
sandboxName: SANDBOX_NAME,
contracts: [
Expand Down Expand Up @@ -294,7 +293,7 @@ runCredentialMigrationTest(
expect(fs.existsSync(victimFile), "symlink target must remain present").toBe(true);
expect(fs.readFileSync(victimFile, "utf-8")).toBe(victimPayload);

await artifacts.writeJson("target-result.json", {
await artifacts.target.complete({
id: "credential-migration",
sandboxName: SANDBOX_NAME,
model: hostedInference.model || CREDENTIAL_MIGRATION_MODEL,
Expand Down
3 changes: 1 addition & 2 deletions test/e2e/live/credential-sanitization.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,9 +297,8 @@ runCredentialSanitizationTest(
"run `npm run build:cli` before live repo CLI targets",
).toBe(true);

await artifacts.writeJson("target.json", {
await artifacts.target.declare({
id: "credential-sanitization",
runner: "vitest",
boundary: "install-sh-onboard-and-sandbox-exec",
sandboxName: SANDBOX_NAME,
contracts: [
Expand Down
3 changes: 1 addition & 2 deletions test/e2e/live/cron-preflight-inference-local.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,9 +212,8 @@ test.skipIf(!shouldRunLiveE2E())(
const hosted = requireHostedInferenceConfig(secrets, process.env, { model: MODEL });
const apiKey = hosted.apiKey;

await artifacts.writeJson("target.json", {
await artifacts.target.declare({
id: "cron-preflight-inference-local",
runner: "vitest",
boundary: "install.sh + in-sandbox OpenClaw cron preflight runtime helper",
sandboxName: SANDBOX_NAME,
model: MODEL,
Expand Down
3 changes: 1 addition & 2 deletions test/e2e/live/dashboard-remote-bind.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,8 @@ runDashboardRemoteBindTest(
const dashboardPort = process.env.NEMOCLAW_DASHBOARD_PORT || "18789";
const remoteHost = remoteHostCandidate();

await artifacts.writeJson("target.json", {
await artifacts.target.declare({
id: "dashboard-remote-bind",
runner: "vitest",
boundary: "remote-dashboard-forward",
optIn: "NEMOCLAW_E2E_DASHBOARD_REMOTE_BIND=1",
sandboxName,
Expand Down
3 changes: 1 addition & 2 deletions test/e2e/live/device-auth-health.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,8 @@ test.skipIf(!shouldRunLiveE2E())(
model: INFERENCE_MODEL,
};

await artifacts.writeJson("target.json", {
await artifacts.target.declare({
id: "device-auth-health",
runner: "vitest",
boundary: "install.sh + OpenShell sandbox exec + NemoClaw status + host curl",
sandboxName: SANDBOX_NAME,
dashboardPort: DASHBOARD_PORT,
Expand Down
5 changes: 2 additions & 3 deletions test/e2e/live/diagnostics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,8 @@ runDiagnosticsTest(

const hosted = requireHostedInferenceConfig(secrets);
const apiKey = hosted.apiKey;
await artifacts.writeJson("target.json", {
await artifacts.target.declare({
id: "diagnostics",
runner: "vitest",
boundary: "debug-archive-install-sh-docker-openshell-sandbox-exec-credentials",
sandboxName: SANDBOX_NAME,
contracts: [
Expand Down Expand Up @@ -413,7 +412,7 @@ runDiagnosticsTest(
});
}

await artifacts.writeJson("target-result.json", {
await artifacts.target.complete({
id: "diagnostics",
sandboxName: SANDBOX_NAME,
model: hosted.model,
Expand Down
3 changes: 1 addition & 2 deletions test/e2e/live/docs-validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,8 @@ runDocsValidationTest(
"docs validation matches CLI help and local documentation links",
{ timeout: BUILD_TIMEOUT_MS + DOCS_CHECK_TIMEOUT_MS * 2 },
async ({ artifacts, host }) => {
await artifacts.writeJson("target.json", {
await artifacts.target.declare({
id: "docs-validation",
runner: "vitest",
boundary: "checkout-local-docs-checks",
phases: ["cli-docs-parity", "local-markdown-links"],
});
Expand Down
Loading
Loading