refactor(core): extract shared dedup helpers

This commit is contained in:
Peter Steinberger
2026-03-07 10:40:49 +00:00
parent 14c61bb33f
commit 3c71e2bd48
114 changed files with 3400 additions and 2040 deletions

View File

@@ -1,6 +1,5 @@
import type { Command } from "commander";
import type { CronJob } from "../../cron/types.js";
import { danger } from "../../globals.js";
import { sanitizeAgentId } from "../../routing/session-key.js";
import { defaultRuntime } from "../../runtime.js";
import type { GatewayRpcOpts } from "../gateway-rpc.js";
@@ -8,9 +7,11 @@ import { addGatewayClientOptions, callGatewayFromCli } from "../gateway-rpc.js";
import { parsePositiveIntOrUndefined } from "../program/helpers.js";
import {
getCronChannelOptions,
handleCronCliError,
parseAt,
parseCronStaggerMs,
parseDurationMs,
printCronJson,
printCronList,
warnIfCronSchedulerDisabled,
} from "./shared.js";
@@ -24,10 +25,9 @@ export function registerCronStatusCommand(cron: Command) {
.action(async (opts) => {
try {
const res = await callGatewayFromCli("cron.status", opts, {});
defaultRuntime.log(JSON.stringify(res, null, 2));
printCronJson(res);
} catch (err) {
defaultRuntime.error(danger(String(err)));
defaultRuntime.exit(1);
handleCronCliError(err);
}
}),
);
@@ -46,14 +46,13 @@ export function registerCronListCommand(cron: Command) {
includeDisabled: Boolean(opts.all),
});
if (opts.json) {
defaultRuntime.log(JSON.stringify(res, null, 2));
printCronJson(res);
return;
}
const jobs = (res as { jobs?: CronJob[] } | null)?.jobs ?? [];
printCronList(jobs, defaultRuntime);
} catch (err) {
defaultRuntime.error(danger(String(err)));
defaultRuntime.exit(1);
handleCronCliError(err);
}
}),
);
@@ -273,11 +272,10 @@ export function registerCronAddCommand(cron: Command) {
};
const res = await callGatewayFromCli("cron.add", opts, params);
defaultRuntime.log(JSON.stringify(res, null, 2));
printCronJson(res);
await warnIfCronSchedulerDisabled(opts);
} catch (err) {
defaultRuntime.error(danger(String(err)));
defaultRuntime.exit(1);
handleCronCliError(err);
}
}),
);

View File

@@ -1,8 +1,7 @@
import type { Command } from "commander";
import { danger } from "../../globals.js";
import { defaultRuntime } from "../../runtime.js";
import { addGatewayClientOptions, callGatewayFromCli } from "../gateway-rpc.js";
import { warnIfCronSchedulerDisabled } from "./shared.js";
import { handleCronCliError, printCronJson, warnIfCronSchedulerDisabled } from "./shared.js";
function registerCronToggleCommand(params: {
cron: Command;
@@ -21,11 +20,10 @@ function registerCronToggleCommand(params: {
id,
patch: { enabled: params.enabled },
});
defaultRuntime.log(JSON.stringify(res, null, 2));
printCronJson(res);
await warnIfCronSchedulerDisabled(opts);
} catch (err) {
defaultRuntime.error(danger(String(err)));
defaultRuntime.exit(1);
handleCronCliError(err);
}
}),
);
@@ -43,10 +41,9 @@ export function registerCronSimpleCommands(cron: Command) {
.action(async (id, opts) => {
try {
const res = await callGatewayFromCli("cron.remove", opts, { id });
defaultRuntime.log(JSON.stringify(res, null, 2));
printCronJson(res);
} catch (err) {
defaultRuntime.error(danger(String(err)));
defaultRuntime.exit(1);
handleCronCliError(err);
}
}),
);
@@ -79,10 +76,9 @@ export function registerCronSimpleCommands(cron: Command) {
id,
limit,
});
defaultRuntime.log(JSON.stringify(res, null, 2));
printCronJson(res);
} catch (err) {
defaultRuntime.error(danger(String(err)));
defaultRuntime.exit(1);
handleCronCliError(err);
}
}),
);
@@ -102,12 +98,11 @@ export function registerCronSimpleCommands(cron: Command) {
id,
mode: opts.due ? "due" : "force",
});
defaultRuntime.log(JSON.stringify(res, null, 2));
printCronJson(res);
const result = res as { ok?: boolean; ran?: boolean } | undefined;
defaultRuntime.exit(result?.ok && result?.ran ? 0 : 1);
} catch (err) {
defaultRuntime.error(danger(String(err)));
defaultRuntime.exit(1);
handleCronCliError(err);
}
}),
);

View File

@@ -2,6 +2,7 @@ import { listChannelPlugins } from "../../channels/plugins/index.js";
import { parseAbsoluteTimeMs } from "../../cron/parse.js";
import { resolveCronStaggerMs } from "../../cron/stagger.js";
import type { CronJob, CronSchedule } from "../../cron/types.js";
import { danger } from "../../globals.js";
import { formatDurationHuman } from "../../infra/format-time/format-duration.ts";
import { defaultRuntime } from "../../runtime.js";
import { colorize, isRich, theme } from "../../terminal/theme.js";
@@ -11,6 +12,15 @@ import { callGatewayFromCli } from "../gateway-rpc.js";
export const getCronChannelOptions = () =>
["last", ...listChannelPlugins().map((plugin) => plugin.id)].join("|");
export function printCronJson(value: unknown) {
defaultRuntime.log(JSON.stringify(value, null, 2));
}
export function handleCronCliError(err: unknown) {
defaultRuntime.error(danger(String(err)));
defaultRuntime.exit(1);
}
export async function warnIfCronSchedulerDisabled(opts: GatewayRpcOpts) {
try {
const res = (await callGatewayFromCli("cron.status", opts, {})) as {

View File

@@ -60,6 +60,8 @@ describe("memory cli", () => {
return JSON.parse(String(log.mock.calls[0]?.[0] ?? "null")) as Record<string, unknown>;
}
const inactiveMemorySecretDiagnostic = "agents.defaults.memorySearch.remote.apiKey inactive";
function expectCliSync(sync: ReturnType<typeof vi.fn>) {
expect(sync).toHaveBeenCalledWith(
expect.objectContaining({ reason: "cli", force: false, progress: expect.any(Function) }),
@@ -85,6 +87,25 @@ describe("memory cli", () => {
getMemorySearchManager.mockResolvedValueOnce({ manager });
}
function setupMemoryStatusWithInactiveSecretDiagnostics(close: ReturnType<typeof vi.fn>) {
resolveCommandSecretRefsViaGateway.mockResolvedValueOnce({
resolvedConfig: {},
diagnostics: [inactiveMemorySecretDiagnostic] as string[],
});
mockManager({
probeVectorAvailability: vi.fn(async () => true),
status: () => makeMemoryStatus({ workspaceDir: undefined }),
close,
});
}
function hasLoggedInactiveSecretDiagnostic(spy: ReturnType<typeof vi.spyOn>) {
return spy.mock.calls.some(
(call: unknown[]) =>
typeof call[0] === "string" && call[0].includes(inactiveMemorySecretDiagnostic),
);
}
async function runMemoryCli(args: string[]) {
const program = new Command();
program.name("test");
@@ -191,26 +212,12 @@ describe("memory cli", () => {
it("logs gateway secret diagnostics for non-json status output", async () => {
const close = vi.fn(async () => {});
resolveCommandSecretRefsViaGateway.mockResolvedValueOnce({
resolvedConfig: {},
diagnostics: ["agents.defaults.memorySearch.remote.apiKey inactive"] as string[],
});
mockManager({
probeVectorAvailability: vi.fn(async () => true),
status: () => makeMemoryStatus({ workspaceDir: undefined }),
close,
});
setupMemoryStatusWithInactiveSecretDiagnostics(close);
const log = spyRuntimeLogs();
await runMemoryCli(["status"]);
expect(
log.mock.calls.some(
(call) =>
typeof call[0] === "string" &&
call[0].includes("agents.defaults.memorySearch.remote.apiKey inactive"),
),
).toBe(true);
expect(hasLoggedInactiveSecretDiagnostic(log)).toBe(true);
});
it("prints vector error when unavailable", async () => {
@@ -410,15 +417,7 @@ describe("memory cli", () => {
it("routes gateway secret diagnostics to stderr for json status output", async () => {
const close = vi.fn(async () => {});
resolveCommandSecretRefsViaGateway.mockResolvedValueOnce({
resolvedConfig: {},
diagnostics: ["agents.defaults.memorySearch.remote.apiKey inactive"] as string[],
});
mockManager({
probeVectorAvailability: vi.fn(async () => true),
status: () => makeMemoryStatus({ workspaceDir: undefined }),
close,
});
setupMemoryStatusWithInactiveSecretDiagnostics(close);
const log = spyRuntimeLogs();
const error = spyRuntimeErrors();
@@ -426,13 +425,7 @@ describe("memory cli", () => {
const payload = firstLoggedJson(log);
expect(Array.isArray(payload)).toBe(true);
expect(
error.mock.calls.some(
(call) =>
typeof call[0] === "string" &&
call[0].includes("agents.defaults.memorySearch.remote.apiKey inactive"),
),
).toBe(true);
expect(hasLoggedInactiveSecretDiagnostic(error)).toBe(true);
});
it("logs default message when memory manager is missing", async () => {

View File

@@ -9,6 +9,8 @@ import {
type ExecSecurity,
maxAsk,
minSecurity,
normalizeExecAsk,
normalizeExecSecurity,
resolveExecApprovalsFromFile,
} from "../../infra/exec-approvals.js";
import { buildNodeShellCommand } from "../../infra/node-shell.js";
@@ -43,22 +45,6 @@ type ExecDefaults = {
safeBins?: string[];
};
function normalizeExecSecurity(value?: string | null): ExecSecurity | null {
const normalized = value?.trim().toLowerCase();
if (normalized === "deny" || normalized === "allowlist" || normalized === "full") {
return normalized;
}
return null;
}
function normalizeExecAsk(value?: string | null): ExecAsk | null {
const normalized = value?.trim().toLowerCase();
if (normalized === "off" || normalized === "on-miss" || normalized === "always") {
return normalized as ExecAsk;
}
return null;
}
function resolveExecDefaults(
cfg: ReturnType<typeof loadConfig>,
agentId: string | undefined,

View File

@@ -72,6 +72,32 @@ function createTailscaleRemoteRefConfig() {
};
}
function createDefaultSecretProvider() {
return {
providers: {
default: { source: "env" as const },
},
};
}
function createLocalGatewayConfigWithAuth(auth: Record<string, unknown>) {
return {
secrets: createDefaultSecretProvider(),
gateway: {
bind: "custom",
customBindHost: "gateway.local",
auth,
},
};
}
function createLocalGatewayPasswordRefAuth(secretId: string) {
return {
mode: "password",
password: { source: "env", provider: "default", id: secretId },
};
}
describe("registerQrCli", () => {
function createProgram() {
const program = new Command();
@@ -88,6 +114,23 @@ describe("registerQrCli", () => {
await expect(runQr(args)).rejects.toThrow("exit");
}
function parseLastLoggedQrJson() {
return JSON.parse(String(runtime.log.mock.calls.at(-1)?.[0] ?? "{}")) as {
setupCode?: string;
gatewayUrl?: string;
auth?: string;
urlSource?: string;
};
}
function mockTailscaleStatusLookup() {
runCommandWithTimeout.mockResolvedValue({
code: 0,
stdout: '{"Self":{"DNSName":"ts-host.tailnet.ts.net."}}',
stderr: "",
});
}
beforeEach(() => {
vi.clearAllMocks();
vi.stubEnv("OPENCLAW_GATEWAY_TOKEN", "");
@@ -157,21 +200,11 @@ describe("registerQrCli", () => {
});
it("skips local password SecretRef resolution when --token override is provided", async () => {
loadConfig.mockReturnValue({
secrets: {
providers: {
default: { source: "env" },
},
},
gateway: {
bind: "custom",
customBindHost: "gateway.local",
auth: {
mode: "password",
password: { source: "env", provider: "default", id: "MISSING_LOCAL_GATEWAY_PASSWORD" },
},
},
});
loadConfig.mockReturnValue(
createLocalGatewayConfigWithAuth(
createLocalGatewayPasswordRefAuth("MISSING_LOCAL_GATEWAY_PASSWORD"),
),
);
await runQr(["--setup-code-only", "--token", "override-token"]);
@@ -184,21 +217,11 @@ describe("registerQrCli", () => {
it("resolves local gateway auth password SecretRefs before setup code generation", async () => {
vi.stubEnv("QR_LOCAL_GATEWAY_PASSWORD", "local-password-secret");
loadConfig.mockReturnValue({
secrets: {
providers: {
default: { source: "env" },
},
},
gateway: {
bind: "custom",
customBindHost: "gateway.local",
auth: {
mode: "password",
password: { source: "env", provider: "default", id: "QR_LOCAL_GATEWAY_PASSWORD" },
},
},
});
loadConfig.mockReturnValue(
createLocalGatewayConfigWithAuth(
createLocalGatewayPasswordRefAuth("QR_LOCAL_GATEWAY_PASSWORD"),
),
);
await runQr(["--setup-code-only"]);
@@ -212,21 +235,11 @@ describe("registerQrCli", () => {
it("uses OPENCLAW_GATEWAY_PASSWORD without resolving local password SecretRef", async () => {
vi.stubEnv("OPENCLAW_GATEWAY_PASSWORD", "password-from-env");
loadConfig.mockReturnValue({
secrets: {
providers: {
default: { source: "env" },
},
},
gateway: {
bind: "custom",
customBindHost: "gateway.local",
auth: {
mode: "password",
password: { source: "env", provider: "default", id: "MISSING_LOCAL_GATEWAY_PASSWORD" },
},
},
});
loadConfig.mockReturnValue(
createLocalGatewayConfigWithAuth(
createLocalGatewayPasswordRefAuth("MISSING_LOCAL_GATEWAY_PASSWORD"),
),
);
await runQr(["--setup-code-only"]);
@@ -239,22 +252,13 @@ describe("registerQrCli", () => {
});
it("does not resolve local password SecretRef when auth mode is token", async () => {
loadConfig.mockReturnValue({
secrets: {
providers: {
default: { source: "env" },
},
},
gateway: {
bind: "custom",
customBindHost: "gateway.local",
auth: {
mode: "token",
token: "token-123",
password: { source: "env", provider: "default", id: "MISSING_LOCAL_GATEWAY_PASSWORD" },
},
},
});
loadConfig.mockReturnValue(
createLocalGatewayConfigWithAuth({
mode: "token",
token: "token-123",
password: { source: "env", provider: "default", id: "MISSING_LOCAL_GATEWAY_PASSWORD" },
}),
);
await runQr(["--setup-code-only"]);
@@ -268,20 +272,11 @@ describe("registerQrCli", () => {
it("resolves local password SecretRef when auth mode is inferred", async () => {
vi.stubEnv("QR_INFERRED_GATEWAY_PASSWORD", "inferred-password");
loadConfig.mockReturnValue({
secrets: {
providers: {
default: { source: "env" },
},
},
gateway: {
bind: "custom",
customBindHost: "gateway.local",
auth: {
password: { source: "env", provider: "default", id: "QR_INFERRED_GATEWAY_PASSWORD" },
},
},
});
loadConfig.mockReturnValue(
createLocalGatewayConfigWithAuth({
password: { source: "env", provider: "default", id: "QR_INFERRED_GATEWAY_PASSWORD" },
}),
);
await runQr(["--setup-code-only"]);
@@ -390,20 +385,11 @@ describe("registerQrCli", () => {
{ name: "when tailscale is configured", withTailscale: true },
])("reports gateway.remote.url as source in --remote json output ($name)", async (testCase) => {
loadConfig.mockReturnValue(createRemoteQrConfig({ withTailscale: testCase.withTailscale }));
runCommandWithTimeout.mockResolvedValue({
code: 0,
stdout: '{"Self":{"DNSName":"ts-host.tailnet.ts.net."}}',
stderr: "",
});
mockTailscaleStatusLookup();
await runQr(["--json", "--remote"]);
const payload = JSON.parse(String(runtime.log.mock.calls.at(-1)?.[0] ?? "{}")) as {
setupCode?: string;
gatewayUrl?: string;
auth?: string;
urlSource?: string;
};
const payload = parseLastLoggedQrJson();
expect(payload.gatewayUrl).toBe("wss://remote.example.com:444");
expect(payload.auth).toBe("token");
expect(payload.urlSource).toBe("gateway.remote.url");
@@ -416,20 +402,11 @@ describe("registerQrCli", () => {
resolvedConfig: createRemoteQrConfig(),
diagnostics: ["gateway.remote.password inactive"] as string[],
});
runCommandWithTimeout.mockResolvedValue({
code: 0,
stdout: '{"Self":{"DNSName":"ts-host.tailnet.ts.net."}}',
stderr: "",
});
mockTailscaleStatusLookup();
await runQr(["--json", "--remote"]);
const payload = JSON.parse(String(runtime.log.mock.calls.at(-1)?.[0] ?? "{}")) as {
setupCode?: string;
gatewayUrl?: string;
auth?: string;
urlSource?: string;
};
const payload = parseLastLoggedQrJson();
expect(payload.gatewayUrl).toBe("wss://remote.example.com:444");
expect(
runtime.error.mock.calls.some((call) =>