fix(test): preserve private temporary fixture ownership (#154759)

This commit is contained in:
Peter Steinberger 2026-09-21 06:02:30 -07:00 • committed by GitHub
parent 659b4e8c71
commit 33ecdbb346
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 99 additions and 2 deletions

View file

@ -4,6 +4,7 @@ import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { execNodeEvalSync } from "../test-utils/node-process.js";
import { withTempDirSync, withTestDir } from "./temp-dir.js";
const parentRoots: string[] = [];
@ -74,3 +75,50 @@ describe("withTestDir", () => {
await expect(fs.readdir(parentDir)).resolves.toStrictEqual([]);
});
});
describe.skipIf(process.platform === "win32")("private temporary case directories", () => {
it.each(["async", "sync", "suite"])(
"admits a private workspace under a group-writable umask (%s)",
async (kind) => {
const parentDir = await makeParentRoot();
// umask is process-wide and cannot be changed in a Vitest worker thread.
const stdout = execNodeEvalSync(
`import fs from "node:fs";
import { tempWorkspaceSync } from "@openclaw/fs-safe/temp";
import { withTestDir, withTempDirSync, createSuiteTempRootTracker } from ${JSON.stringify(new URL("./temp-dir.ts", import.meta.url).href)};
process.umask(0o002);
const options = { prefix: "private-case-", parentDir: ${JSON.stringify(parentDir)} };
function exercise(dir) {
const workspace = tempWorkspaceSync({ rootDir: dir, prefix: "private-workspace-" });
try {
workspace.write("marker", "private");
return workspace.read("marker").toString();
} finally {
workspace.cleanup();
}
}
let result;
if (${JSON.stringify(kind)} === "async") {
result = await withTestDir({ ...options, subdir: "nested/leaf" }, async (dir) => exercise(dir));
} else if (${JSON.stringify(kind)} === "sync") {
result = withTempDirSync({ ...options, subdir: "nested/leaf" }, exercise);
} else {
const tracker = createSuiteTempRootTracker(options);
await tracker.setup();
try {
result = exercise(await tracker.make("nested/case"));
} finally {
await tracker.cleanup();
}
}
console.log(JSON.stringify({ result, remaining: fs.readdirSync(options.parentDir) }));`,
{
imports: [new URL("../../scripts/tsx.mjs", import.meta.url).href],
timeout: 10_000,
killSignal: "SIGKILL",
},
);
expect(JSON.parse(stdout)).toEqual({ result: "private", remaining: [] });
},
);
});

View file

@ -7,6 +7,12 @@ import { createDeferred } from "../../test/helpers/promise.js";
import { runExclusiveSessionStoreWrite } from "../config/sessions/store-writer.js";
import { resolveEffectiveHomeDir } from "../infra/home-dir.js";
import { withTempHomeCore } from "../plugin-sdk/test-helpers/temp-home.js";
import {
captureOpenClawStateDatabaseReadAdmission,
closeOpenClawStateDatabaseByPathAsync,
registerOpenClawStateDatabaseAsyncResource,
} from "../state/openclaw-state-db-cache.js";
import { resolveOpenClawStateSqlitePath } from "../state/openclaw-state-db.paths.js";
import { captureEnv, captureFullEnv, withEnvAsync } from "./env.js";
import { createTempHomeEnv } from "./temp-home.js";
@ -21,6 +27,46 @@ async function expectPathMissing(targetPath: string): Promise<void> {
}
describe("createTempHomeEnv", () => {
it("restores the environment and retains home after resource drainage fails", async () => {
const envKeys = [
"HOME",
"USERPROFILE",
"HOMEDRIVE",
"HOMEPATH",
"OPENCLAW_HOME",
"OPENCLAW_STATE_DIR",
];
const environment = captureEnv(envKeys);
const previous = Object.fromEntries(envKeys.map((key) => [key, process.env[key]]));
const temporary = await createTempHomeEnv("openclaw-temp-home-drain-");
const stateDir = path.join(temporary.home, ".openclaw");
const databasePath = resolveOpenClawStateSqlitePath({ OPENCLAW_STATE_DIR: stateDir });
const admission = captureOpenClawStateDatabaseReadAdmission(databasePath);
const failure = new Error("fixture resource still owns its state");
let rejectClose = true;
const unregister = registerOpenClawStateDatabaseAsyncResource({
close: async (identity) => {
if (identity?.key === admission.identity.key && rejectClose) {
throw failure;
}
},
});
const marker = path.join(temporary.home, "owned.txt");
try {
await fs.writeFile(marker, "retained fixture");
await expect(temporary.restore()).rejects.toBe(failure);
expect(Object.fromEntries(envKeys.map((key) => [key, process.env[key]]))).toEqual(previous);
expect(await fs.readFile(marker, "utf8")).toBe("retained fixture");
} finally {
// Release the deliberately retained owner before disposing its fixture.
rejectClose = false;
await closeOpenClawStateDatabaseByPathAsync(databasePath);
unregister();
environment.restore();
await fs.rm(temporary.home, { recursive: true, force: true });
}
});
it.each(["directory", "environment"])(
"rolls back failed %s acquisition without removing a sibling home",
async (stage) => {

View file

@ -77,8 +77,11 @@ export async function createTempHomeEnv(prefix: string): Promise<TempHomeEnv> {
return {
home,
restore: async () => {
await cleanupSessionStateForTest({ stateDir }).catch(() => undefined);
snapshot.restore();
try {
await cleanupSessionStateForTest({ stateDir });
} finally {
snapshot.restore();
}
await fs.rm(home, { recursive: true, force: true });
},
};