refactor(core): dedupe command, hook, and cron fixtures

This commit is contained in:
Peter Steinberger
2026-03-02 21:30:58 +00:00
parent 5f0cbd0edc
commit 91dd89313a
16 changed files with 325 additions and 330 deletions

View File

@@ -128,6 +128,28 @@ function emitJsonPayload(params: {
return true;
}
async function resolveConfigAndTargetAgentIdOrExit(params: {
runtime: RuntimeEnv;
agentInput: string | undefined;
}): Promise<{
cfg: NonNullable<Awaited<ReturnType<typeof requireValidConfig>>>;
agentId: string;
} | null> {
const cfg = await requireValidConfig(params.runtime);
if (!cfg) {
return null;
}
const agentId = resolveTargetAgentIdOrExit({
cfg,
runtime: params.runtime,
agentInput: params.agentInput,
});
if (!agentId) {
return null;
}
return { cfg, agentId };
}
export async function agentsBindingsCommand(
opts: AgentsBindingsListOptions,
runtime: RuntimeEnv = defaultRuntime,
@@ -186,15 +208,14 @@ export async function agentsBindCommand(
opts: AgentsBindOptions,
runtime: RuntimeEnv = defaultRuntime,
) {
const cfg = await requireValidConfig(runtime);
if (!cfg) {
return;
}
const agentId = resolveTargetAgentIdOrExit({ cfg, runtime, agentInput: opts.agent });
if (!agentId) {
const resolved = await resolveConfigAndTargetAgentIdOrExit({
runtime,
agentInput: opts.agent,
});
if (!resolved) {
return;
}
const { cfg, agentId } = resolved;
const parsed = resolveParsedBindingsOrExit({
runtime,
@@ -264,15 +285,14 @@ export async function agentsUnbindCommand(
opts: AgentsUnbindOptions,
runtime: RuntimeEnv = defaultRuntime,
) {
const cfg = await requireValidConfig(runtime);
if (!cfg) {
return;
}
const agentId = resolveTargetAgentIdOrExit({ cfg, runtime, agentInput: opts.agent });
if (!agentId) {
const resolved = await resolveConfigAndTargetAgentIdOrExit({
runtime,
agentInput: opts.agent,
});
if (!resolved) {
return;
}
const { cfg, agentId } = resolved;
if (opts.all && (opts.bind?.length ?? 0) > 0) {
runtime.error("Use either --all or --bind, not both.");
runtime.exit(1);

View File

@@ -44,6 +44,26 @@ function createPromptSpies(params?: { confirmResult?: boolean; textResult?: stri
return { confirm, note, text };
}
async function ensureMinimaxApiKey(params: {
confirm: WizardPrompter["confirm"];
text: WizardPrompter["text"];
setCredential: Parameters<typeof ensureApiKeyFromEnvOrPrompt>[0]["setCredential"];
config?: Parameters<typeof ensureApiKeyFromEnvOrPrompt>[0]["config"];
secretInputMode?: Parameters<typeof ensureApiKeyFromEnvOrPrompt>[0]["secretInputMode"];
}) {
return await ensureApiKeyFromEnvOrPrompt({
config: params.config ?? {},
provider: "minimax",
envLabel: "MINIMAX_API_KEY",
promptMessage: "Enter key",
normalize: (value) => value.trim(),
validate: () => undefined,
prompter: createPrompter({ confirm: params.confirm, text: params.text }),
secretInputMode: params.secretInputMode,
setCredential: params.setCredential,
});
}
async function runEnsureMinimaxApiKeyFlow(params: { confirmResult: boolean; textResult: string }) {
process.env.MINIMAX_API_KEY = "env-key";
delete process.env.MINIMAX_OAUTH_TOKEN;
@@ -53,15 +73,9 @@ async function runEnsureMinimaxApiKeyFlow(params: { confirmResult: boolean; text
textResult: params.textResult,
});
const setCredential = vi.fn(async () => undefined);
const result = await ensureApiKeyFromEnvOrPrompt({
config: {},
provider: "minimax",
envLabel: "MINIMAX_API_KEY",
promptMessage: "Enter key",
normalize: (value) => value.trim(),
validate: () => undefined,
prompter: createPrompter({ confirm, text }),
const result = await ensureMinimaxApiKey({
confirm,
text,
setCredential,
});
@@ -164,14 +178,9 @@ describe("ensureApiKeyFromEnvOrPrompt", () => {
});
const setCredential = vi.fn(async () => undefined);
const result = await ensureApiKeyFromEnvOrPrompt({
config: {},
provider: "minimax",
envLabel: "MINIMAX_API_KEY",
promptMessage: "Enter key",
normalize: (value) => value.trim(),
validate: () => undefined,
prompter: createPrompter({ confirm, text }),
const result = await ensureMinimaxApiKey({
confirm,
text,
secretInputMode: "ref",
setCredential,
});
@@ -195,14 +204,9 @@ describe("ensureApiKeyFromEnvOrPrompt", () => {
const setCredential = vi.fn(async () => undefined);
await expect(
ensureApiKeyFromEnvOrPrompt({
config: {},
provider: "minimax",
envLabel: "MINIMAX_API_KEY",
promptMessage: "Enter key",
normalize: (value) => value.trim(),
validate: () => undefined,
prompter: createPrompter({ confirm, text }),
ensureMinimaxApiKey({
confirm,
text,
secretInputMode: "ref",
setCredential,
}),

View File

@@ -29,6 +29,19 @@ function createHuggingfacePrompter(params: {
return createWizardPrompter(overrides, { defaultSelect: "" });
}
type ApplyHuggingfaceParams = Parameters<typeof applyAuthChoiceHuggingface>[0];
async function runHuggingfaceApply(
params: Omit<ApplyHuggingfaceParams, "authChoice" | "setDefaultModel"> &
Partial<Pick<ApplyHuggingfaceParams, "setDefaultModel">>,
) {
return await applyAuthChoiceHuggingface({
authChoice: "huggingface-api-key",
setDefaultModel: params.setDefaultModel ?? true,
...params,
});
}
describe("applyAuthChoiceHuggingface", () => {
const lifecycle = createAuthTestLifecycle([
"OPENCLAW_STATE_DIR",
@@ -75,12 +88,10 @@ describe("applyAuthChoiceHuggingface", () => {
const prompter = createHuggingfacePrompter({ text, select });
const runtime = createExitThrowingRuntime();
const result = await applyAuthChoiceHuggingface({
authChoice: "huggingface-api-key",
const result = await runHuggingfaceApply({
config: {},
prompter,
runtime,
setDefaultModel: true,
});
expect(result).not.toBeNull();
@@ -132,12 +143,10 @@ describe("applyAuthChoiceHuggingface", () => {
const prompter = createHuggingfacePrompter({ text, select, confirm });
const runtime = createExitThrowingRuntime();
const result = await applyAuthChoiceHuggingface({
authChoice: "huggingface-api-key",
const result = await runHuggingfaceApply({
config: {},
prompter,
runtime,
setDefaultModel: true,
opts: {
tokenProvider,
token,
@@ -167,12 +176,10 @@ describe("applyAuthChoiceHuggingface", () => {
const prompter = createHuggingfacePrompter({ text, select, note });
const runtime = createExitThrowingRuntime();
const result = await applyAuthChoiceHuggingface({
authChoice: "huggingface-api-key",
const result = await runHuggingfaceApply({
config: {},
prompter,
runtime,
setDefaultModel: true,
});
expect(result).not.toBeNull();

View File

@@ -1,16 +1,15 @@
import { describe, expect, it, vi } from "vitest";
import { withTempHomeConfig } from "../config/test-helpers.js";
const { noteSpy } = vi.hoisted(() => ({
noteSpy: vi.fn(),
}));
import { note } from "../terminal/note.js";
vi.mock("../terminal/note.js", () => ({
note: noteSpy,
note: vi.fn(),
}));
import { loadAndMaybeMigrateDoctorConfig } from "./doctor-config-flow.js";
const noteSpy = vi.mocked(note);
describe("doctor include warning", () => {
it("surfaces include confinement hint for escaped include paths", async () => {
await withTempHomeConfig({ $include: "/etc/passwd" }, async () => {

View File

@@ -1,13 +1,10 @@
import { describe, expect, it, vi } from "vitest";
import { note } from "../terminal/note.js";
import { withEnvAsync } from "../test-utils/env.js";
import { runDoctorConfigWithInput } from "./doctor-config-flow.test-utils.js";
const { noteSpy } = vi.hoisted(() => ({
noteSpy: vi.fn(),
}));
vi.mock("../terminal/note.js", () => ({
note: noteSpy,
note: vi.fn(),
}));
vi.mock("./doctor-legacy-config.js", async (importOriginal) => {
@@ -23,6 +20,8 @@ vi.mock("./doctor-legacy-config.js", async (importOriginal) => {
import { loadAndMaybeMigrateDoctorConfig } from "./doctor-config-flow.js";
const noteSpy = vi.mocked(note);
describe("doctor missing default account binding warning", () => {
it("emits a doctor warning when named accounts have no valid account-scoped bindings", async () => {
await withEnvAsync(

View File

@@ -2,20 +2,19 @@ import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { note } from "../terminal/note.js";
import { withEnvAsync } from "../test-utils/env.js";
import { runDoctorConfigWithInput } from "./doctor-config-flow.test-utils.js";
const { noteSpy } = vi.hoisted(() => ({
noteSpy: vi.fn(),
}));
vi.mock("../terminal/note.js", () => ({
note: noteSpy,
note: vi.fn(),
}));
import { loadAndMaybeMigrateDoctorConfig } from "./doctor-config-flow.js";
describe("doctor config flow safe bins", () => {
const noteSpy = vi.mocked(note);
beforeEach(() => {
noteSpy.mockClear();
});

View File

@@ -65,6 +65,20 @@ async function runStateIntegrity(cfg: OpenClawConfig) {
return confirmSkipInNonInteractive;
}
function writeSessionStore(
cfg: OpenClawConfig,
sessions: Record<string, { sessionId: string; updatedAt: number }>,
) {
setupSessionState(cfg, process.env, process.env.HOME ?? "");
const storePath = resolveStorePath(cfg.session?.store, { agentId: "main" });
fs.writeFileSync(storePath, JSON.stringify(sessions, null, 2));
}
async function runStateIntegrityText(cfg: OpenClawConfig): Promise<string> {
await noteStateIntegrity(cfg, { confirmSkipInNonInteractive: vi.fn(async () => false) });
return stateIntegrityText();
}
describe("doctor state integrity oauth dir checks", () => {
let envSnapshot: EnvSnapshot;
let tempHome = "";
@@ -146,25 +160,13 @@ describe("doctor state integrity oauth dir checks", () => {
it("prints openclaw-only verification hints when recent sessions are missing transcripts", async () => {
const cfg: OpenClawConfig = {};
setupSessionState(cfg, process.env, process.env.HOME ?? "");
const storePath = resolveStorePath(cfg.session?.store, { agentId: "main" });
fs.writeFileSync(
storePath,
JSON.stringify(
{
"agent:main:main": {
sessionId: "missing-transcript",
updatedAt: Date.now(),
},
},
null,
2,
),
);
await noteStateIntegrity(cfg, { confirmSkipInNonInteractive: vi.fn(async () => false) });
const text = stateIntegrityText();
writeSessionStore(cfg, {
"agent:main:main": {
sessionId: "missing-transcript",
updatedAt: Date.now(),
},
});
const text = await runStateIntegrityText(cfg);
expect(text).toContain("recent sessions are missing transcripts");
expect(text).toMatch(/openclaw sessions --store ".*sessions\.json"/);
expect(text).toMatch(/openclaw sessions cleanup --store ".*sessions\.json" --dry-run/);
@@ -177,25 +179,13 @@ describe("doctor state integrity oauth dir checks", () => {
it("ignores slash-routing sessions for recent missing transcript warnings", async () => {
const cfg: OpenClawConfig = {};
setupSessionState(cfg, process.env, process.env.HOME ?? "");
const storePath = resolveStorePath(cfg.session?.store, { agentId: "main" });
fs.writeFileSync(
storePath,
JSON.stringify(
{
"agent:main:telegram:slash:6790081233": {
sessionId: "missing-slash-transcript",
updatedAt: Date.now(),
},
},
null,
2,
),
);
await noteStateIntegrity(cfg, { confirmSkipInNonInteractive: vi.fn(async () => false) });
const text = stateIntegrityText();
writeSessionStore(cfg, {
"agent:main:telegram:slash:6790081233": {
sessionId: "missing-slash-transcript",
updatedAt: Date.now(),
},
});
const text = await runStateIntegrityText(cfg);
expect(text).not.toContain("recent sessions are missing transcripts");
});
});