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
90 changes: 90 additions & 0 deletions src/lib/actions/sandbox/sessions/download-verify.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { assertDownloadedFile } from "./download-verify";

describe("assertDownloadedFile", () => {
let dir: string;

beforeEach(() => {
dir = fs.mkdtempSync(path.join(os.tmpdir(), "nc-7367-verify-"));
});

afterEach(() => {
fs.rmSync(dir, { recursive: true, force: true });
});

it("passes when the download reported success and wrote a non-empty file", () => {
const target = path.join(dir, "bundle.tgz");
fs.writeFileSync(target, "payload");
expect(() =>
assertDownloadedFile({ status: 0 }, target, {
remoteLabel: "/sandbox/x.tgz",
sandboxName: "alpha",
requireNonEmpty: true,
}),
).not.toThrow();
});

it("rejects a non-zero exit status with the exit code in the message", () => {
const target = path.join(dir, "bundle.tgz");
fs.writeFileSync(target, "payload");
expect(() =>
assertDownloadedFile({ status: 1 }, target, {
remoteLabel: "/sandbox/x.tgz",
sandboxName: "alpha",
}),
).toThrow(/Failed to download '\/sandbox\/x\.tgz' from sandbox 'alpha' \(exit 1\)\./);
});

// The #7367 core: openshell can exit 0 while writing nothing. Trusting the
// exit code alone would record the rejected download as a valid bundle.
it("rejects exit 0 when no file was written", () => {
const target = path.join(dir, "missing.tgz");
expect(() =>
assertDownloadedFile({ status: 0 }, target, {
remoteLabel: "/sandbox/x.tgz",
sandboxName: "alpha",
requireNonEmpty: true,
}),
).toThrow(/reported success \(exit 0\) but no file was written to/);
});

it("rejects exit 0 when the destination is a directory, not a regular file", () => {
const target = path.join(dir, "adir");
fs.mkdirSync(target);
expect(() =>
assertDownloadedFile({ status: 0 }, target, {
remoteLabel: "/sandbox/x.tgz",
sandboxName: "alpha",
}),
).toThrow(/reported success \(exit 0\) but '.*' is not a regular file/);
});

it("rejects exit 0 with an empty file when requireNonEmpty is set", () => {
const target = path.join(dir, "empty.tgz");
fs.writeFileSync(target, "");
expect(() =>
assertDownloadedFile({ status: 0 }, target, {
remoteLabel: "/sandbox/x.tgz",
sandboxName: "alpha",
requireNonEmpty: true,
}),
).toThrow(/reported success \(exit 0\) but wrote an empty file/);
});

it("allows an empty file when requireNonEmpty is not set (per-session files)", () => {
const target = path.join(dir, "session.jsonl");
fs.writeFileSync(target, "");
expect(() =>
assertDownloadedFile({ status: 0 }, target, {
remoteLabel: "/sandbox/.openclaw/agents/main/sessions/session.jsonl",
sandboxName: "alpha",
}),
).not.toThrow();
});
});
77 changes: 77 additions & 0 deletions src/lib/actions/sandbox/sessions/download-verify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import fs from "node:fs";

export interface DownloadOutcome {
status: number | null;
}

export interface VerifyDownloadedFileOptions {
/** Sandbox-side source label used in error messages (e.g. the remote path). */
remoteLabel: string;
/** Sandbox name used in error messages. */
sandboxName: string;
/**
* Require the artifact to be non-empty. Set for a bundle that is never
* legitimately empty (a gzip tarball of at least one file); leave off for
* individual session files and for the hermes export, whose size we do not
* want to constrain (a zero-session hermes export can be legitimately empty).
*/
requireNonEmpty?: boolean;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

/**
* Confirm that an `openshell sandbox download` of a single file both reported
* success AND actually produced the artifact on the host.
*
* The exit status alone cannot be trusted: `openshell sandbox download` has a
* process-exit race that can report success (exit 0) even when the transfer
* was rejected or failed and no file was written (NVIDIA/OpenShell; NemoClaw
* #7367). Trusting exit 0 alone would let a rejected or partial download be
* recorded as a valid session bundle, so re-check the outcome against the file
* system before treating the download as complete.
*
* `hostPath` must be a path that did not exist before the download — a fresh
* per-export staging path, published to its real destination only after this
* check passes. The check can only establish that SOMETHING exists at
* `hostPath`; run against a reused destination it would accept a stale
* artifact left by an earlier export and mask the exit-0/no-write race it
* exists to catch.
*
* @throws if the download reported a non-zero status, wrote no file, wrote a
* non-regular file, or (when `requireNonEmpty`) wrote an empty file.
*/
export function assertDownloadedFile(
download: DownloadOutcome,
hostPath: string,
options: VerifyDownloadedFileOptions,
): void {
const { remoteLabel, sandboxName, requireNonEmpty = false } = options;
const prefix = `Failed to download '${remoteLabel}' from sandbox '${sandboxName}'`;

if (download.status !== 0) {
throw new Error(`${prefix} (exit ${download.status}).`);
}

let stat: fs.Stats;
try {
stat = fs.statSync(hostPath);
} catch {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
throw new Error(
`${prefix}: openshell reported success (exit 0) but no file was written to '${hostPath}'.`,
);
}

if (!stat.isFile()) {
throw new Error(
`${prefix}: openshell reported success (exit 0) but '${hostPath}' is not a regular file.`,
);
}

if (requireNonEmpty && stat.size === 0) {
throw new Error(
`${prefix}: openshell reported success (exit 0) but wrote an empty file to '${hostPath}'.`,
);
}
}
Loading
Loading