Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 1 addition & 2 deletions ci/test-file-size-budget.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
"test/onboard-messaging.test.ts": 2122,
"test/onboard-selection.test.ts": 7757,
"test/onboard.test.ts": 4887,
"test/policies.test.ts": 3147,
"test/sandbox-connect-inference.test.ts": 1577
"test/policies.test.ts": 3143
}
}
11 changes: 11 additions & 0 deletions src/lib/core/version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@ function isPackageInfo(value: PackageInfo | null): value is { version: string }
return typeof value?.version === "string";
}

function gitEnvForRoot(): NodeJS.ProcessEnv {
const env: NodeJS.ProcessEnv = {};
for (const [key, value] of Object.entries(process.env)) {
if (!key.startsWith("GIT_") && value !== undefined) {
env[key] = value;
}
}
return env;
}

export interface VersionOptions {
/** Override the repo root directory. */
rootDir?: string;
Expand All @@ -35,6 +45,7 @@ export function getVersion(opts: VersionOptions = {}): string {
const raw = execFileSync("git", ["describe", "--tags", "--match", "v*"], {
cwd: root,
encoding: "utf-8",
env: gitEnvForRoot(),
stdio: ["ignore", "pipe", "ignore"],
}).trim();
if (raw) return raw.replace(/^v/, "");
Expand Down
2 changes: 2 additions & 0 deletions src/lib/onboard/config-sync.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ describe("sandbox config sync helpers", () => {
try {
const nemoclawDir = path.join(homeDir, ".nemoclaw");
fs.mkdirSync(nemoclawDir, { mode: 0o755 });
fs.chmodSync(nemoclawDir, 0o755);
const script = buildSandboxConfigSyncScript({
endpointType: "custom",
endpointUrl: "https://inference.local/v1",
Expand All @@ -128,6 +129,7 @@ describe("sandbox config sync helpers", () => {
try {
const nemoclawDir = path.join(homeDir, ".nemoclaw");
fs.mkdirSync(nemoclawDir, { mode: 0o755 });
fs.chmodSync(nemoclawDir, 0o755);
writeFakeCommand(fakeBin, "id", "1234");
writeFakeCommand(fakeBin, "stat", "0");
const script = buildSandboxConfigSyncScript({
Expand Down
34 changes: 23 additions & 11 deletions src/lib/sandbox-base-image.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,34 @@ import {
const tmpRoots: string[] = [];
const emptyGitConfigDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-empty-gitconfig-"));
const emptyGitConfig = path.join(emptyGitConfigDir, "gitconfig");
const emptyGitHooksDir = path.join(emptyGitConfigDir, "hooks");
const emptyGitConfigFd = fs.openSync(emptyGitConfig, "wx", 0o600);
fs.closeSync(emptyGitConfigFd);
fs.mkdirSync(emptyGitHooksDir, { mode: 0o700 });

function buildGitEnv(): NodeJS.ProcessEnv {
const env: NodeJS.ProcessEnv = {};
for (const [key, value] of Object.entries(process.env)) {
if (!key.startsWith("GIT_") && value !== undefined) {
env[key] = value;
}
}
return {
...env,
GIT_CONFIG_GLOBAL: emptyGitConfig,
GIT_CONFIG_NOSYSTEM: "1",
GIT_TERMINAL_PROMPT: "0",
GIT_AUTHOR_NAME: "Test User",
GIT_AUTHOR_EMAIL: "test@example.com",
GIT_COMMITTER_NAME: "Test User",
GIT_COMMITTER_EMAIL: "test@example.com",
};
}

const gitEnv = {
...process.env,
GIT_CONFIG_GLOBAL: emptyGitConfig,
GIT_CONFIG_NOSYSTEM: "1",
GIT_TERMINAL_PROMPT: "0",
GIT_AUTHOR_NAME: "Test User",
GIT_AUTHOR_EMAIL: "test@example.com",
GIT_COMMITTER_NAME: "Test User",
GIT_COMMITTER_EMAIL: "test@example.com",
};
const gitEnv = buildGitEnv();

function git(root: string, args: string[]) {
const result = spawnSync("git", ["-C", root, ...args], {
const result = spawnSync("git", ["-c", `core.hooksPath=${emptyGitHooksDir}`, "-C", root, ...args], {
encoding: "utf-8",
env: gitEnv,
});
Expand Down
20 changes: 13 additions & 7 deletions src/lib/state/config-io.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ function makeTempDir(): string {
return dir;
}

function writeFileWithMode(filePath: string, contents: string, mode: number) {
fs.writeFileSync(filePath, contents, { mode });
fs.chmodSync(filePath, mode);
}

afterEach(() => {
for (const dir of tmpDirs.splice(0)) {
fs.rmSync(dir, { recursive: true, force: true });
Expand Down Expand Up @@ -75,6 +80,7 @@ describe("config-io", () => {
it("tightens pre-existing weak directory permissions to 0o700", () => {
const dir = path.join(makeTempDir(), "config");
fs.mkdirSync(dir, { mode: 0o755 });
fs.chmodSync(dir, 0o755);

ensureConfigDir(dir);

Expand Down Expand Up @@ -154,7 +160,7 @@ describe("config-io", () => {
const dir = makeTempDir();
fs.chmodSync(dir, 0o700);
const file = path.join(dir, "config.json");
fs.writeFileSync(file, JSON.stringify({ tight: true }), { mode: 0o644 });
writeFileWithMode(file, JSON.stringify({ tight: true }), 0o644);

const result = readConfigFile(file, null);

Expand Down Expand Up @@ -186,7 +192,7 @@ describe("config-io", () => {
"usage-notice.json",
];
for (const name of siblings) {
fs.writeFileSync(path.join(dir, name), "stale", { mode: 0o644 });
writeFileWithMode(path.join(dir, name), "stale", 0o644);
}

readConfigFile(target, null);
Expand Down Expand Up @@ -214,11 +220,11 @@ describe("config-io", () => {
fs.writeFileSync(target, JSON.stringify({ ok: true }), { mode: 0o600 });

const sibling = path.join(dir, "should-be-healed.json");
fs.writeFileSync(sibling, "stale", { mode: 0o644 });
writeFileWithMode(sibling, "stale", 0o644);

const outsideDir = makeTempDir();
const outside = path.join(outsideDir, "target");
fs.writeFileSync(outside, "outside", { mode: 0o644 });
writeFileWithMode(outside, "outside", 0o644);
const linkPath = path.join(dir, "rogue-link");
fs.symlinkSync(outside, linkPath);

Expand All @@ -242,7 +248,7 @@ describe("config-io", () => {

const outsideDir = makeTempDir();
const outside = path.join(outsideDir, "target.json");
fs.writeFileSync(outside, JSON.stringify({ outside: true }), { mode: 0o644 });
writeFileWithMode(outside, JSON.stringify({ outside: true }), 0o644);
const symlinkPath = path.join(dir, "config.json");
fs.symlinkSync(outside, symlinkPath);

Expand Down Expand Up @@ -280,7 +286,7 @@ describe("config-io", () => {
const target = path.join(unrelatedDir, "config.json");
fs.writeFileSync(target, JSON.stringify({ ok: true }), { mode: 0o600 });
const sibling = path.join(unrelatedDir, "other.json");
fs.writeFileSync(sibling, "stale", { mode: 0o644 });
writeFileWithMode(sibling, "stale", 0o644);

readConfigFile(target, null);

Expand All @@ -301,7 +307,7 @@ describe("config-io", () => {
const target = path.join(hostDir, "sandboxes.json");
fs.writeFileSync(target, JSON.stringify({ ok: true }), { mode: 0o600 });
const sibling = path.join(hostDir, "onboard-session.json");
fs.writeFileSync(sibling, "stale", { mode: 0o644 });
writeFileWithMode(sibling, "stale", 0o644);

readConfigFile(target, null);

Expand Down
34 changes: 15 additions & 19 deletions test/policies.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1041,41 +1041,37 @@ exit 1
}
});

// The assertion must fire BEFORE any temp dir/file creation. With a real
// `process.exit(1)` the matching `finally` does not run, so a temp dir
// created before the exit gets orphaned in $TMPDIR. A mocked exit (which
// throws) doesn't reproduce that — `finally` still runs and cleans up. To
// catch the real-world bug, snapshot $TMPDIR at the *moment* of exit:
// if the assertion fires before mkdtempSync, no nemoclaw-policy-* dir
// should exist yet.
// A mocked process.exit still runs finally cleanup, so record same-process
// mkdtempSync calls at exit instead of counting shared $TMPDIR state.
it("applyPreset does not create temp dirs before the openshell resolvability check", () => {
const beforeCount = fs
.readdirSync(os.tmpdir())
.filter((entry) => entry.startsWith("nemoclaw-policy-")).length;
let countAtExit = -1;
const policyTempPrefix = path.join(os.tmpdir(), "nemoclaw-policy-");
const originalMkdtempSync = fs.mkdtempSync.bind(fs);
let policyTempDirsCreated = 0;
let policyTempDirsCreatedAtExit = -1;

const resolveSpy = vi
.spyOn(resolveOpenshellModule, "resolveOpenshell")
.mockReturnValue(null);
const mkdtempSpy = vi.spyOn(fs, "mkdtempSync").mockImplementation((prefix, options) => {
if (prefix === policyTempPrefix) {
policyTempDirsCreated += 1;
}
return originalMkdtempSync(prefix, options);
});
const errSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
const logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined);
const exitSpy = vi.spyOn(process, "exit").mockImplementation(((_code?: number) => {
countAtExit = fs
.readdirSync(os.tmpdir())
.filter((entry) => entry.startsWith("nemoclaw-policy-")).length;
policyTempDirsCreatedAtExit = policyTempDirsCreated;
throw new Error("__test_exit__");
}) as never);

try {
// Apply a real built-in preset so applyPresetContent runs end-to-end
// up to the resolvability check.
expect(() => policies.applyPreset("my-assistant", "npm")).toThrow(/__test_exit__/);
expect(exitSpy).toHaveBeenCalledWith(1);
// No `nemoclaw-policy-*` temp dir should have been created before
// the resolvability check exited.
expect(countAtExit).toBe(beforeCount);
expect(policyTempDirsCreatedAtExit).toBe(0);
} finally {
resolveSpy.mockRestore();
mkdtempSpy.mockRestore();
errSpy.mockRestore();
logSpy.mockRestore();
exitSpy.mockRestore();
Expand Down
20 changes: 14 additions & 6 deletions test/release-latest-tag.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,18 @@ const planScriptPath = path.join(repoRoot, "scripts", "release-plan.ts");
const tsxPath = path.join(repoRoot, "node_modules", ".bin", "tsx");
const tempRoots: string[] = [];

function baseEnv(extra: NodeJS.ProcessEnv = {}): NodeJS.ProcessEnv {
const env: NodeJS.ProcessEnv = {};
for (const [key, value] of Object.entries(process.env)) {
if (!key.startsWith("GIT_") && value !== undefined) {
env[key] = value;
}
}
return { ...env, ...extra };
}

function testEnv(extra: NodeJS.ProcessEnv = {}): NodeJS.ProcessEnv {
return {
...process.env,
return baseEnv({
GIT_AUTHOR_NAME: "Release Test",
GIT_AUTHOR_EMAIL: "release-test@example.com",
GIT_COMMITTER_NAME: "Release Test",
Expand All @@ -34,7 +43,7 @@ function testEnv(extra: NodeJS.ProcessEnv = {}): NodeJS.ProcessEnv {
GIT_CONFIG_KEY_0: "tag.gpgSign",
GIT_CONFIG_VALUE_0: "false",
...extra,
};
});
}

function run(
Expand Down Expand Up @@ -134,8 +143,7 @@ function runReleaseLatestWithoutIdentity(
const xdgConfigHome = path.join(fixture.root, "empty-xdg-config");
fs.mkdirSync(home);
fs.mkdirSync(xdgConfigHome);
const env: NodeJS.ProcessEnv = {
...process.env,
const env = baseEnv({
GIT_CONFIG_COUNT: "2",
GIT_CONFIG_KEY_0: "user.useConfigOnly",
GIT_CONFIG_VALUE_0: "true",
Expand All @@ -146,7 +154,7 @@ function runReleaseLatestWithoutIdentity(
RELEASE_TAG: releaseTag,
REMOTE_NAME: "origin",
XDG_CONFIG_HOME: xdgConfigHome,
};
});
delete env.GIT_AUTHOR_NAME;
delete env.GIT_AUTHOR_EMAIL;
delete env.GIT_COMMITTER_NAME;
Expand Down
Loading
Loading