mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-14 10:41:23 +00:00
237 lines
6.5 KiB
TypeScript
237 lines
6.5 KiB
TypeScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
import { createSuiteTempRootTracker } from "../test-helpers/temp-dir.js";
|
|
import {
|
|
appendConfigAuditRecord,
|
|
createConfigWriteAuditRecordBase,
|
|
finalizeConfigWriteAuditRecord,
|
|
formatConfigOverwriteLogMessage,
|
|
resolveConfigAuditLogPath,
|
|
} from "./io.audit.js";
|
|
|
|
function createRenameAuditRecord(home: string) {
|
|
return finalizeConfigWriteAuditRecord({
|
|
base: createConfigWriteAuditRecordBase({
|
|
configPath: path.join(home, ".openclaw", "openclaw.json"),
|
|
env: {} as NodeJS.ProcessEnv,
|
|
existsBefore: true,
|
|
previousHash: "prev-hash",
|
|
nextHash: "next-hash",
|
|
previousBytes: 12,
|
|
nextBytes: 24,
|
|
previousMetadata: {
|
|
dev: "10",
|
|
ino: "11",
|
|
mode: 0o600,
|
|
nlink: 1,
|
|
uid: 501,
|
|
gid: 20,
|
|
},
|
|
changedPathCount: 1,
|
|
hasMetaBefore: true,
|
|
hasMetaAfter: true,
|
|
gatewayModeBefore: "local",
|
|
gatewayModeAfter: "local",
|
|
suspicious: [],
|
|
now: "2026-04-07T08:00:00.000Z",
|
|
}),
|
|
result: "rename",
|
|
nextMetadata: {
|
|
dev: "12",
|
|
ino: "13",
|
|
mode: 0o600,
|
|
nlink: 1,
|
|
uid: 501,
|
|
gid: 20,
|
|
},
|
|
});
|
|
}
|
|
|
|
function readAuditLog(home: string): unknown[] {
|
|
const auditPath = path.join(home, ".openclaw", "logs", "config-audit.jsonl");
|
|
return fs
|
|
.readFileSync(auditPath, "utf-8")
|
|
.trim()
|
|
.split("\n")
|
|
.map((line) => JSON.parse(line));
|
|
}
|
|
|
|
describe("config io audit helpers", () => {
|
|
const suiteRootTracker = createSuiteTempRootTracker({ prefix: "openclaw-config-audit-" });
|
|
|
|
beforeAll(async () => {
|
|
await suiteRootTracker.setup();
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await suiteRootTracker.cleanup();
|
|
});
|
|
|
|
it('ignores literal "undefined" home env values when choosing the audit log path', async () => {
|
|
const home = await suiteRootTracker.make("home");
|
|
const auditPath = resolveConfigAuditLogPath(
|
|
{
|
|
HOME: "undefined",
|
|
USERPROFILE: "null",
|
|
OPENCLAW_HOME: "undefined",
|
|
} as NodeJS.ProcessEnv,
|
|
() => home,
|
|
);
|
|
expect(auditPath).toBe(path.join(home, ".openclaw", "logs", "config-audit.jsonl"));
|
|
expect(auditPath.startsWith(path.resolve("undefined"))).toBe(false);
|
|
});
|
|
|
|
it("formats overwrite warnings with hash transition and backup path", () => {
|
|
expect(
|
|
formatConfigOverwriteLogMessage({
|
|
configPath: "/tmp/openclaw.json",
|
|
previousHash: "prev-hash",
|
|
nextHash: "next-hash",
|
|
changedPathCount: 3,
|
|
}),
|
|
).toBe(
|
|
"Config overwrite: /tmp/openclaw.json (sha256 prev-hash -> next-hash, backup=/tmp/openclaw.json.bak, changedPaths=3)",
|
|
);
|
|
});
|
|
|
|
it("captures watch markers and next stat metadata for successful writes", () => {
|
|
const base = createConfigWriteAuditRecordBase({
|
|
configPath: "/tmp/openclaw.json",
|
|
env: {
|
|
OPENCLAW_WATCH_MODE: "1",
|
|
OPENCLAW_WATCH_SESSION: "watch-session-1",
|
|
OPENCLAW_WATCH_COMMAND: "gateway --force",
|
|
} as NodeJS.ProcessEnv,
|
|
existsBefore: true,
|
|
previousHash: "prev-hash",
|
|
nextHash: "next-hash",
|
|
previousBytes: 12,
|
|
nextBytes: 24,
|
|
previousMetadata: {
|
|
dev: "10",
|
|
ino: "11",
|
|
mode: 0o600,
|
|
nlink: 1,
|
|
uid: 501,
|
|
gid: 20,
|
|
},
|
|
changedPathCount: 2,
|
|
hasMetaBefore: false,
|
|
hasMetaAfter: true,
|
|
gatewayModeBefore: null,
|
|
gatewayModeAfter: "local",
|
|
suspicious: ["missing-meta-before-write"],
|
|
now: "2026-04-07T08:00:00.000Z",
|
|
processInfo: {
|
|
pid: 101,
|
|
ppid: 99,
|
|
cwd: "/work",
|
|
argv: ["node", "openclaw"],
|
|
execArgv: ["--loader"],
|
|
},
|
|
});
|
|
const record = finalizeConfigWriteAuditRecord({
|
|
base,
|
|
result: "rename",
|
|
nextMetadata: {
|
|
dev: "12",
|
|
ino: "13",
|
|
mode: 0o600,
|
|
nlink: 1,
|
|
uid: 501,
|
|
gid: 20,
|
|
},
|
|
});
|
|
|
|
expect(record.watchMode).toBe(true);
|
|
expect(record.watchSession).toBe("watch-session-1");
|
|
expect(record.watchCommand).toBe("gateway --force");
|
|
expect(record.nextHash).toBe("next-hash");
|
|
expect(record.nextBytes).toBe(24);
|
|
expect(record.nextDev).toBe("12");
|
|
expect(record.nextIno).toBe("13");
|
|
expect(record.result).toBe("rename");
|
|
});
|
|
|
|
it("drops next-file metadata and preserves error details for failed writes", () => {
|
|
const base = createConfigWriteAuditRecordBase({
|
|
configPath: "/tmp/openclaw.json",
|
|
env: {} as NodeJS.ProcessEnv,
|
|
existsBefore: true,
|
|
previousHash: "prev-hash",
|
|
nextHash: "next-hash",
|
|
previousBytes: 12,
|
|
nextBytes: 24,
|
|
previousMetadata: {
|
|
dev: "10",
|
|
ino: "11",
|
|
mode: 0o600,
|
|
nlink: 1,
|
|
uid: 501,
|
|
gid: 20,
|
|
},
|
|
changedPathCount: 1,
|
|
hasMetaBefore: true,
|
|
hasMetaAfter: true,
|
|
gatewayModeBefore: "local",
|
|
gatewayModeAfter: "local",
|
|
suspicious: [],
|
|
now: "2026-04-07T08:00:00.000Z",
|
|
});
|
|
const err = Object.assign(new Error("disk full"), { code: "ENOSPC" });
|
|
const record = finalizeConfigWriteAuditRecord({
|
|
base,
|
|
result: "failed",
|
|
err,
|
|
});
|
|
|
|
expect(record.result).toBe("failed");
|
|
expect(record.nextHash).toBeNull();
|
|
expect(record.nextBytes).toBeNull();
|
|
expect(record.nextDev).toBeNull();
|
|
expect(record.errorCode).toBe("ENOSPC");
|
|
expect(record.errorMessage).toBe("disk full");
|
|
});
|
|
|
|
it("appends JSONL audit entries to the resolved audit path", async () => {
|
|
const home = await suiteRootTracker.make("append");
|
|
const record = createRenameAuditRecord(home);
|
|
|
|
await appendConfigAuditRecord({
|
|
fs,
|
|
env: {} as NodeJS.ProcessEnv,
|
|
homedir: () => home,
|
|
record,
|
|
});
|
|
|
|
const records = readAuditLog(home);
|
|
expect(records).toHaveLength(1);
|
|
expect(records[0]).toMatchObject({
|
|
event: "config.write",
|
|
result: "rename",
|
|
nextHash: "next-hash",
|
|
});
|
|
});
|
|
|
|
it("also accepts flattened audit record params from legacy call sites", async () => {
|
|
const home = await suiteRootTracker.make("append-flat");
|
|
const record = createRenameAuditRecord(home);
|
|
|
|
await appendConfigAuditRecord({
|
|
fs,
|
|
env: {} as NodeJS.ProcessEnv,
|
|
homedir: () => home,
|
|
...record,
|
|
});
|
|
|
|
const records = readAuditLog(home);
|
|
expect(records).toHaveLength(1);
|
|
expect(records[0]).toMatchObject({
|
|
event: "config.write",
|
|
result: "rename",
|
|
nextHash: "next-hash",
|
|
});
|
|
});
|
|
});
|