test: move custom onboarding edge cases down-stack

This commit is contained in:
Peter Steinberger
2026-04-17 07:54:39 +01:00
parent 24ef516879
commit e4f04d92a3
2 changed files with 45 additions and 117 deletions

View File

@@ -1130,22 +1130,6 @@ async function runOnboardingAndReadConfig(
return readJsonFile<ProviderAuthConfigSnapshot>(env.configPath);
}
const CUSTOM_LOCAL_BASE_URL = "https://models.custom.local/v1";
const CUSTOM_LOCAL_MODEL_ID = "local-large";
async function runCustomLocalNonInteractive(
runtime: NonInteractiveRuntime,
overrides: Record<string, unknown> = {},
): Promise<void> {
await runNonInteractiveSetupWithDefaults(runtime, {
authChoice: "custom-api-key",
customBaseUrl: CUSTOM_LOCAL_BASE_URL,
customModelId: CUSTOM_LOCAL_MODEL_ID,
skipSkills: true,
...overrides,
});
}
async function expectApiKeyProfile(params: {
profileId: string;
provider: string;
@@ -1565,77 +1549,4 @@ describe("onboard (non-interactive): provider auth", () => {
},
);
});
it("fails fast for custom provider ref mode when --custom-api-key is set but CUSTOM_API_KEY env is missing", async () => {
await withOnboardEnv("openclaw-onboard-custom-provider-ref-flag-", async ({ runtime }) => {
const providedSecret = "custom-inline-key-should-not-leak"; // pragma: allowlist secret
await withEnvAsync({ CUSTOM_API_KEY: undefined }, async () => {
let thrown: Error | undefined;
try {
await runCustomLocalNonInteractive(runtime, {
secretInputMode: "ref", // pragma: allowlist secret
customApiKey: providedSecret,
});
} catch (error) {
thrown = error as Error;
}
expect(thrown).toBeDefined();
const message = thrown?.message ?? "";
expect(message).toContain(
"--custom-api-key cannot be used with --secret-input-mode ref unless CUSTOM_API_KEY is set in env.",
);
expect(message).toContain(
"Set CUSTOM_API_KEY in env and omit --custom-api-key, or use --secret-input-mode plaintext.",
);
expect(message).not.toContain(providedSecret);
});
});
});
it("fails custom provider auth when compatibility is invalid", async () => {
await withOnboardEnv(
"openclaw-onboard-custom-provider-invalid-compat-",
async ({ runtime }) => {
await expect(
runNonInteractiveSetupWithDefaults(runtime, {
authChoice: "custom-api-key",
customBaseUrl: "https://models.custom.local/v1",
customModelId: "local-large",
customCompatibility: "xmlrpc",
skipSkills: true,
}),
).rejects.toThrow('Invalid --custom-compatibility (use "openai" or "anthropic").');
},
);
});
it("fails custom provider auth when explicit provider id is invalid", async () => {
await withOnboardEnv("openclaw-onboard-custom-provider-invalid-id-", async ({ runtime }) => {
await expect(
runNonInteractiveSetupWithDefaults(runtime, {
authChoice: "custom-api-key",
customBaseUrl: "https://models.custom.local/v1",
customModelId: "local-large",
customProviderId: "!!!",
skipSkills: true,
}),
).rejects.toThrow(
"Invalid custom provider config: Custom provider ID must include letters, numbers, or hyphens.",
);
});
});
it("fails inferred custom auth when required flags are incomplete", async () => {
await withOnboardEnv(
"openclaw-onboard-custom-provider-missing-required-",
async ({ runtime }) => {
await expect(
runNonInteractiveSetupWithDefaults(runtime, {
customApiKey: "custom-test-key", // pragma: allowlist secret
skipSkills: true,
}),
).rejects.toThrow('Auth choice "custom-api-key" requires a base URL and model ID.');
},
);
});
});

View File

@@ -60,37 +60,54 @@ describe("resolveNonInteractiveApiKey", () => {
expect(runtime.exit).not.toHaveBeenCalled();
});
it("rejects flag input in secret-ref mode without broad env discovery", async () => {
const runtime = createRuntime();
resolveEnvApiKey.mockReturnValue(null);
const previousXaiApiKey = process.env.XAI_API_KEY;
delete process.env.XAI_API_KEY;
it.each([
{
provider: "xai",
flagValue: "xai-flag-key",
flagName: "--xai-api-key",
envVar: "XAI_API_KEY",
},
{
provider: "custom-models-custom-local",
flagValue: "custom-inline-key-should-not-leak",
flagName: "--custom-api-key",
envVar: "CUSTOM_API_KEY",
},
])(
"rejects $flagName input in secret-ref mode without broad env discovery",
async ({ provider, flagValue, flagName, envVar }) => {
const runtime = createRuntime();
resolveEnvApiKey.mockReturnValue(null);
const previousValue = process.env[envVar];
delete process.env[envVar];
try {
const result = await resolveNonInteractiveApiKey({
provider: "xai",
cfg: {},
flagValue: "xai-flag-key",
flagName: "--xai-api-key",
envVar: "XAI_API_KEY",
runtime: runtime as never,
secretInputMode: "ref",
});
try {
const result = await resolveNonInteractiveApiKey({
provider,
cfg: {},
flagValue,
flagName,
envVar,
runtime: runtime as never,
secretInputMode: "ref",
});
expect(result).toBeNull();
expect(resolveEnvApiKey).not.toHaveBeenCalled();
expect(runtime.exit).toHaveBeenCalledWith(1);
expect(runtime.error).toHaveBeenCalledWith(
expect.stringContaining("--secret-input-mode ref"),
);
} finally {
if (previousXaiApiKey === undefined) {
delete process.env.XAI_API_KEY;
} else {
process.env.XAI_API_KEY = previousXaiApiKey;
const errorText = runtime.error.mock.calls.map(([message]) => String(message)).join("\n");
expect(result).toBeNull();
expect(resolveEnvApiKey).not.toHaveBeenCalled();
expect(runtime.exit).toHaveBeenCalledWith(1);
expect(errorText).toContain(flagName);
expect(errorText).toContain(envVar);
expect(errorText).not.toContain(flagValue);
} finally {
if (previousValue === undefined) {
delete process.env[envVar];
} else {
process.env[envVar] = previousValue;
}
}
}
});
},
);
it("returns explicit env fallback keys when provider env discovery misses", async () => {
const runtime = createRuntime();