refactor(agents): dedupe config and truncation guards

This commit is contained in:
Peter Steinberger
2026-02-22 17:54:42 +00:00
parent 409a02691f
commit 3286791316
12 changed files with 325 additions and 318 deletions

View File

@@ -39,6 +39,25 @@ describe("shell env fallback", () => {
return { res, exec };
}
function makeUnsafeStartupEnv(): NodeJS.ProcessEnv {
return {
SHELL: "/bin/bash",
HOME: "/tmp/evil-home",
ZDOTDIR: "/tmp/evil-zdotdir",
BASH_ENV: "/tmp/evil-bash-env",
PS4: "$(touch /tmp/pwned)",
};
}
function expectSanitizedStartupEnv(receivedEnv: NodeJS.ProcessEnv | undefined) {
expect(receivedEnv).toBeDefined();
expect(receivedEnv?.BASH_ENV).toBeUndefined();
expect(receivedEnv?.PS4).toBeUndefined();
expect(receivedEnv?.ZDOTDIR).toBeUndefined();
expect(receivedEnv?.SHELL).toBeUndefined();
expect(receivedEnv?.HOME).toBe(os.homedir());
}
it("is disabled by default", () => {
expect(shouldEnableShellEnvFallback({} as NodeJS.ProcessEnv)).toBe(false);
expect(shouldEnableShellEnvFallback({ OPENCLAW_LOAD_SHELL_ENV: "0" })).toBe(false);
@@ -167,13 +186,7 @@ describe("shell env fallback", () => {
});
it("sanitizes startup-related env vars before shell fallback exec", () => {
const env: NodeJS.ProcessEnv = {
SHELL: "/bin/bash",
HOME: "/tmp/evil-home",
ZDOTDIR: "/tmp/evil-zdotdir",
BASH_ENV: "/tmp/evil-bash-env",
PS4: "$(touch /tmp/pwned)",
};
const env = makeUnsafeStartupEnv();
let receivedEnv: NodeJS.ProcessEnv | undefined;
const exec = vi.fn((_shell: string, _args: string[], options: { env: NodeJS.ProcessEnv }) => {
receivedEnv = options.env;
@@ -189,23 +202,12 @@ describe("shell env fallback", () => {
expect(res.ok).toBe(true);
expect(exec).toHaveBeenCalledTimes(1);
expect(receivedEnv).toBeDefined();
expect(receivedEnv?.BASH_ENV).toBeUndefined();
expect(receivedEnv?.PS4).toBeUndefined();
expect(receivedEnv?.ZDOTDIR).toBeUndefined();
expect(receivedEnv?.SHELL).toBeUndefined();
expect(receivedEnv?.HOME).toBe(os.homedir());
expectSanitizedStartupEnv(receivedEnv);
});
it("sanitizes startup-related env vars before login-shell PATH probe", () => {
resetShellPathCacheForTests();
const env: NodeJS.ProcessEnv = {
SHELL: "/bin/bash",
HOME: "/tmp/evil-home",
ZDOTDIR: "/tmp/evil-zdotdir",
BASH_ENV: "/tmp/evil-bash-env",
PS4: "$(touch /tmp/pwned)",
};
const env = makeUnsafeStartupEnv();
let receivedEnv: NodeJS.ProcessEnv | undefined;
const exec = vi.fn((_shell: string, _args: string[], options: { env: NodeJS.ProcessEnv }) => {
receivedEnv = options.env;
@@ -220,12 +222,7 @@ describe("shell env fallback", () => {
expect(result).toBe("/usr/local/bin:/usr/bin");
expect(exec).toHaveBeenCalledTimes(1);
expect(receivedEnv).toBeDefined();
expect(receivedEnv?.BASH_ENV).toBeUndefined();
expect(receivedEnv?.PS4).toBeUndefined();
expect(receivedEnv?.ZDOTDIR).toBeUndefined();
expect(receivedEnv?.SHELL).toBeUndefined();
expect(receivedEnv?.HOME).toBe(os.homedir());
expectSanitizedStartupEnv(receivedEnv);
});
it("returns null without invoking shell on win32", () => {

View File

@@ -106,7 +106,7 @@ describe("update-startup", () => {
suiteCase = 0;
});
async function runUpdateCheckAndReadState(channel: "stable" | "beta") {
function mockPackageUpdateStatus(tag = "latest", version = "2.0.0") {
vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue("/opt/openclaw");
vi.mocked(checkUpdateStatus).mockResolvedValue({
root: "/opt/openclaw",
@@ -114,9 +114,13 @@ describe("update-startup", () => {
packageManager: "npm",
} satisfies UpdateCheckResult);
vi.mocked(resolveNpmChannelTag).mockResolvedValue({
tag: "latest",
version: "2.0.0",
tag,
version,
});
}
async function runUpdateCheckAndReadState(channel: "stable" | "beta") {
mockPackageUpdateStatus("latest", "2.0.0");
const log = { info: vi.fn() };
await runGatewayUpdateCheck({
@@ -136,6 +140,13 @@ describe("update-startup", () => {
return { log, parsed };
}
function createAutoUpdateSuccessMock() {
return vi.fn().mockResolvedValue({
ok: true,
code: 0,
});
}
it.each([
{
name: "stable channel",
@@ -252,33 +263,25 @@ describe("update-startup", () => {
});
it("defers stable auto-update until rollout window is due", async () => {
vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue("/opt/openclaw");
vi.mocked(checkUpdateStatus).mockResolvedValue({
root: "/opt/openclaw",
installKind: "package",
packageManager: "npm",
} satisfies UpdateCheckResult);
vi.mocked(resolveNpmChannelTag).mockResolvedValue({
tag: "latest",
version: "2.0.0",
});
mockPackageUpdateStatus("latest", "2.0.0");
const runAutoUpdate = vi.fn().mockResolvedValue({
ok: true,
code: 0,
});
await runGatewayUpdateCheck({
cfg: {
update: {
channel: "stable",
auto: {
enabled: true,
stableDelayHours: 6,
stableJitterHours: 12,
},
const stableAutoConfig = {
update: {
channel: "stable" as const,
auto: {
enabled: true,
stableDelayHours: 6,
stableJitterHours: 12,
},
},
};
await runGatewayUpdateCheck({
cfg: stableAutoConfig,
log: { info: vi.fn() },
isNixMode: false,
allowInTests: true,
@@ -288,16 +291,7 @@ describe("update-startup", () => {
vi.setSystemTime(new Date("2026-01-18T07:00:00Z"));
await runGatewayUpdateCheck({
cfg: {
update: {
channel: "stable",
auto: {
enabled: true,
stableDelayHours: 6,
stableJitterHours: 12,
},
},
},
cfg: stableAutoConfig,
log: { info: vi.fn() },
isNixMode: false,
allowInTests: true,
@@ -313,21 +307,8 @@ describe("update-startup", () => {
});
it("runs beta auto-update checks hourly when enabled", async () => {
vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue("/opt/openclaw");
vi.mocked(checkUpdateStatus).mockResolvedValue({
root: "/opt/openclaw",
installKind: "package",
packageManager: "npm",
} satisfies UpdateCheckResult);
vi.mocked(resolveNpmChannelTag).mockResolvedValue({
tag: "beta",
version: "2.0.0-beta.1",
});
const runAutoUpdate = vi.fn().mockResolvedValue({
ok: true,
code: 0,
});
mockPackageUpdateStatus("beta", "2.0.0-beta.1");
const runAutoUpdate = createAutoUpdateSuccessMock();
await runGatewayUpdateCheck({
cfg: {
@@ -354,20 +335,8 @@ describe("update-startup", () => {
});
it("runs auto-update when checkOnStart is false but auto-update is enabled", async () => {
vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue("/opt/openclaw");
vi.mocked(checkUpdateStatus).mockResolvedValue({
root: "/opt/openclaw",
installKind: "package",
packageManager: "npm",
} satisfies UpdateCheckResult);
vi.mocked(resolveNpmChannelTag).mockResolvedValue({
tag: "beta",
version: "2.0.0-beta.1",
});
const runAutoUpdate = vi.fn().mockResolvedValue({
ok: true,
code: 0,
});
mockPackageUpdateStatus("beta", "2.0.0-beta.1");
const runAutoUpdate = createAutoUpdateSuccessMock();
await runGatewayUpdateCheck({
cfg: {
@@ -450,16 +419,7 @@ describe("update-startup", () => {
});
it("scheduleGatewayUpdateCheck returns a cleanup function", async () => {
vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue("/opt/openclaw");
vi.mocked(checkUpdateStatus).mockResolvedValue({
root: "/opt/openclaw",
installKind: "package",
packageManager: "npm",
} satisfies UpdateCheckResult);
vi.mocked(resolveNpmChannelTag).mockResolvedValue({
tag: "latest",
version: "2.0.0",
});
mockPackageUpdateStatus("latest", "2.0.0");
const stop = scheduleGatewayUpdateCheck({
cfg: { update: { channel: "stable" } },