fix(paseo-agent): support empty model-provider setup

Allow catalog provider setup to persist credentials before explicit model overrides exist, while keeping unrelated provider snapshots warmable after Paseo Agent config changes.
This commit is contained in:
Mohamed Boudra
2026-07-02 13:12:23 +02:00
parent d6e4e5f3ce
commit 5d67032ef6
17 changed files with 441 additions and 209 deletions
+88 -9
View File
@@ -18,7 +18,7 @@ interface RecordingClientInput {
setProvider?: (input: {
name: string;
providerType: string;
options: { apiKey?: string; models: Array<{ id: string }> };
options: { apiKey?: string; models?: Array<{ id: string }> };
}) => Promise<unknown>;
startOAuth?: (name: string) => Promise<unknown>;
completeOAuth?: (name: string) => Promise<unknown>;
@@ -40,7 +40,7 @@ function createClient(input: RecordingClientInput) {
setPaseoAgentProvider: async (providerInput: {
name: string;
providerType: string;
options: { apiKey?: string; models: Array<{ id: string }> };
options: { apiKey?: string; models?: Array<{ id: string }> };
}) => {
if (input.setProvider) {
return input.setProvider(providerInput);
@@ -51,7 +51,7 @@ function createClient(input: RecordingClientInput) {
provider: {
name: providerInput.name,
providerType: providerInput.providerType,
models: providerInput.options.models,
models: providerInput.options.models ?? [],
auth: { kind: "api_key", configured: true, source: "literal" },
available: true,
error: null,
@@ -159,7 +159,7 @@ describe("provider add", () => {
provider: {
name: input.name,
providerType: input.providerType,
models: input.options.models,
models: input.options.models ?? [],
auth: { kind: "api_key", configured: true, source: "literal" },
available: true,
error: null,
@@ -211,7 +211,7 @@ describe("provider add", () => {
provider: {
name: input.name,
providerType: input.providerType,
models: input.options.models,
models: input.options.models ?? [],
auth: { kind: "api_key", configured: false, source: "env" },
available: false,
error: null,
@@ -258,7 +258,7 @@ describe("provider add", () => {
provider: {
name: input.name,
providerType: input.providerType,
models: input.options.models,
models: input.options.models ?? [],
auth: { kind: "api_key", configured: true, source: "literal" },
available: true,
error: null,
@@ -281,6 +281,57 @@ describe("provider add", () => {
]);
});
it("configures an API-key provider without model defaults", async () => {
const setCalls: unknown[] = [];
const result = await runAddCommand("alpha-key", { apiKeyStdin: true }, {} as never, {
readStdin: async () => "stdin-secret\n",
promptSecret: async () => {
throw new Error("prompt should not be used with --api-key-stdin");
},
promptText: async () => {
throw new Error("text prompt should not be used");
},
write: () => {},
connectDaemon: async () =>
createClient({
catalog: [apiKeyEntry({ models: [] })],
setProvider: async (input) => {
setCalls.push(input);
return {
requestId: "set-1",
success: true,
provider: {
name: input.name,
providerType: input.providerType,
models: [],
auth: { kind: "api_key", configured: true, source: "literal" },
available: true,
error: null,
},
error: null,
};
},
}),
});
expect(setCalls).toEqual([
{
name: "alpha-key",
providerType: "alpha-key",
options: {
apiKey: "stdin-secret",
},
},
]);
expect(result.data).toMatchObject({
name: "alpha-key",
auth: "Connected",
available: "yes",
models: "-",
});
});
it("runs browser OAuth locally and pushes the credential to the selected daemon", async () => {
const order: string[] = [];
const stored: unknown[] = [];
@@ -322,7 +373,7 @@ describe("provider add", () => {
provider: {
name: input.name,
providerType: input.providerType,
models: input.options.models,
models: input.options.models ?? [],
auth: { kind: "oauth", configured: false },
available: false,
error: null,
@@ -493,7 +544,7 @@ describe("provider add", () => {
provider: {
name: input.name,
providerType: input.providerType,
models: input.options.models,
models: input.options.models ?? [],
auth: { kind: "api_key", configured: true, source: "literal" },
available: true,
error: null,
@@ -524,6 +575,34 @@ describe("provider add", () => {
]);
});
it("mentions known provider ids for an unknown catalog id", async () => {
await expect(
runAddCommand("missing-key", {}, {} as never, {
promptSecret: async () => {
throw new Error("prompt should not run");
},
promptText: async () => {
throw new Error("text prompt should not run");
},
readStdin: async () => {
throw new Error("stdin should not run");
},
write: () => {},
connectDaemon: async () =>
createClient({
catalog: [apiKeyEntry({ id: "alpha-key" }), apiKeyEntry({ id: "gamma-key" })],
setProvider: async () => {
throw new Error("set should not run");
},
}),
}),
).rejects.toMatchObject({
code: "UNKNOWN_PROVIDER",
message:
'Unknown model provider type "missing-key". Known provider ids: alpha-key, gamma-key.',
});
});
it("requires the catalog feature flag before reading the catalog", async () => {
const calls: string[] = [];
@@ -601,7 +680,7 @@ describe("provider add", () => {
provider: {
name: input.name,
providerType: input.providerType,
models: input.options.models,
models: input.options.models ?? [],
auth: { kind: "api_key", configured: true, source: "literal" },
available: true,
error: null,
+8 -23
View File
@@ -13,6 +13,7 @@ import { loginOAuthBrowser } from "@getpaseo/server";
import { connectToDaemon } from "../../utils/client.js";
import { collectMultiple } from "../../utils/command-options.js";
import { openBrowserUrl } from "../../utils/open-browser.js";
import { requirePaseoAgentCatalogFeature } from "./feature.js";
import type {
CommandError,
CommandOptions,
@@ -127,18 +128,6 @@ async function promptSecret(message: string): Promise<string> {
}
}
function requirePaseoAgentCatalogFeature(
client: Pick<DaemonClient, "getLastServerInfoMessage">,
): void {
if (client.getLastServerInfoMessage()?.features?.paseoAgentCatalog === true) {
return;
}
throw {
code: "HOST_UPDATE_REQUIRED",
message: "Update the Paseo daemon to use this command.",
} satisfies CommandError;
}
function authField(entry: PaseoAgentCatalogEntry, field: string): string | undefined {
const auth = entry.auth;
const value = auth[field];
@@ -185,10 +174,10 @@ function catalogModels(entry: PaseoAgentCatalogEntry): ProviderModelInput[] {
}));
}
function requireModels(
function resolveModels(
entry: PaseoAgentCatalogEntry,
options: ProviderAddOptions,
): ProviderModelInput[] {
): ProviderModelInput[] | undefined {
const modelIds = normalizeModels(options.model);
if (modelIds.length > 0) {
return modelIds.map((id) => ({ id }));
@@ -198,12 +187,7 @@ function requireModels(
if (models.length > 0) {
return models;
}
throw {
code: "MISSING_MODELS",
message: `At least one model is required for ${entry.label}.`,
details:
"Pass --model <model-id>. Repeat --model to configure more than one; comma-separated values are also accepted.",
} satisfies CommandError;
return undefined;
}
async function selectCatalogEntry(
@@ -249,9 +233,10 @@ async function resolveEntry(
return entry;
}
const knownIds = catalog.map((candidate) => candidate.id).join(", ");
throw {
code: "UNKNOWN_PROVIDER",
message: `Unknown model provider type "${id}".`,
message: `Unknown model provider type "${id}". Known provider ids: ${knownIds}.`,
} satisfies CommandError;
}
@@ -427,7 +412,7 @@ async function configureProvider(
options: ProviderAddOptions,
dependencies: ProviderAddDependencies,
): Promise<RedactedPaseoAgentProviderConfig> {
const models = requireModels(entry, options);
const models = resolveModels(entry, options);
const apiKey =
entry.auth.kind === "api_key" ? await resolveApiKey(entry, options, dependencies) : undefined;
const result = await client.setPaseoAgentProvider({
@@ -435,7 +420,7 @@ async function configureProvider(
providerType: entry.id,
options: {
...(apiKey ? { apiKey } : {}),
models,
...(models ? { models } : {}),
},
});
if (!result.success || !result.provider) {
@@ -0,0 +1,17 @@
import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
import type { CommandError } from "../../output/index.js";
export interface PaseoAgentCatalogFeatureClient extends Pick<
DaemonClient,
"getLastServerInfoMessage"
> {}
export function requirePaseoAgentCatalogFeature(client: PaseoAgentCatalogFeatureClient): void {
if (client.getLastServerInfoMessage()?.features?.paseoAgentCatalog === true) {
return;
}
throw {
code: "HOST_UPDATE_REQUIRED",
message: "Update the Paseo daemon to use this command.",
} satisfies CommandError;
}
@@ -13,6 +13,30 @@ function createServerInfo() {
}
describe("provider ls", () => {
it("renders an empty configured-provider table with headers", async () => {
const result = await runLsCommand({ host: "localhost:7777" }, {} as never, {
connectDaemon: async () => ({
getLastServerInfoMessage: createServerInfo,
getPaseoAgentCatalog: async () => ({
requestId: "catalog-1",
catalog: [],
error: null,
}),
getPaseoAgentProviders: async () => ({
requestId: "providers-1",
defaultModel: null,
providers: [],
error: null,
}),
close: async () => {},
}),
});
expect(result.data).toEqual([]);
expect(render(result, { format: "table", noColor: true })).toContain("NAME");
expect(render(result, { format: "json" })).toBe("[]");
});
it("lists configured model providers with catalog labels and auth states", async () => {
const result = await runLsCommand({ host: "localhost:7777" }, {} as never, {
connectDaemon: async (options) => {
+14 -13
View File
@@ -1,11 +1,18 @@
import type { Command } from "commander";
import type { CommandOptions, ListResult, OutputSchema } from "../../output/index.js";
import {
renderTable,
renderTableHeader,
type CommandOptions,
type ListResult,
type OutputSchema,
} from "../../output/index.js";
import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
import type {
PaseoAgentCatalogEntry,
RedactedPaseoAgentProviderConfig,
} from "@getpaseo/protocol/messages";
import { connectToDaemon } from "../../utils/client.js";
import { requirePaseoAgentCatalogFeature } from "./feature.js";
export interface ProviderListItem {
name: string;
@@ -39,6 +46,12 @@ export const providerLsSchema: OutputSchema<ProviderListItem> = {
{ header: "AVAILABLE", field: "available", width: 10 },
{ header: "MODELS", field: "models", width: 30 },
],
renderHuman: (result, options) => {
if (result.type === "list" && result.data.length === 0) {
return options.noHeaders ? "" : renderTableHeader(providerLsSchema, options);
}
return renderTable(result, options);
},
};
export type ProviderLsResult = ListResult<ProviderListItem>;
@@ -47,18 +60,6 @@ export interface ProviderLsOptions extends CommandOptions {
host?: string;
}
function requirePaseoAgentCatalogFeature(
client: Pick<DaemonClient, "getLastServerInfoMessage">,
): void {
if (client.getLastServerInfoMessage()?.features?.paseoAgentCatalog === true) {
return;
}
throw {
code: "HOST_UPDATE_REQUIRED",
message: "Update the Paseo daemon to use this command.",
};
}
function authState(provider: RedactedPaseoAgentProviderConfig): string {
if (!provider.auth) {
return "not configured";
+1 -12
View File
@@ -8,6 +8,7 @@ import type {
OutputSchema,
SingleResult,
} from "../../output/index.js";
import { requirePaseoAgentCatalogFeature } from "./feature.js";
interface ProviderRmOptions extends CommandOptions {
host?: string;
@@ -39,18 +40,6 @@ export const providerRemoveSchema: OutputSchema<ProviderRemoveItem> = {
],
};
function requirePaseoAgentCatalogFeature(
client: Pick<DaemonClient, "getLastServerInfoMessage">,
): void {
if (client.getLastServerInfoMessage()?.features?.paseoAgentCatalog === true) {
return;
}
throw {
code: "HOST_UPDATE_REQUIRED",
message: "Update the Paseo daemon to use this command.",
} satisfies CommandError;
}
export async function runRmCommand(
name: string,
options: ProviderRmOptions,
+146 -137
View File
@@ -3,15 +3,18 @@
/**
* Phase 15: Provider Command Tests
*
* Tests provider commands for listing providers and models.
* Provider ls data is static, while provider models are fetched via daemon integration.
* This test uses an isolated daemon to avoid coupling to a user's long-running daemon.
* Tests provider commands for configured Paseo Agent model providers and agent
* provider model listing. This test uses an isolated daemon to avoid coupling to
* a user's long-running daemon.
*
* Tests:
* - provider --help shows subcommands
* - provider ls lists all providers
* - provider ls --json outputs valid JSON
* - provider ls --quiet outputs provider names only
* - provider ls on a fresh daemon prints an empty configured-provider table
* - provider add stores an API-key model provider without network validation
* - repeated provider add updates the same provider instance
* - provider add rejects unknown catalog ids with known ids
* - provider rm removes a configured model provider
* - provider add uses catalog default models when present
* - provider models claude lists claude models
* - provider models codex lists codex models
* - provider models opencode lists opencode models
@@ -20,14 +23,7 @@
*/
import assert from "node:assert";
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import {
createE2ETestContext,
createTempDirs,
runPaseoCli,
startTestDaemon,
} from "./helpers/test-daemon.ts";
import { createE2ETestContext } from "./helpers/test-daemon.ts";
console.log("=== Provider Commands ===\n");
@@ -38,10 +34,12 @@ interface ProviderModel {
}
interface ProviderListRow {
provider: string;
name: string;
providerType: string;
label: string;
status: string;
enabled: string;
auth: string;
available: string;
models: string;
}
const EXPECTED_CLAUDE_MODELS = [
@@ -131,6 +129,24 @@ async function runProviderModelsJson(provider: string): Promise<ProviderModel[]>
return attemptRun(1);
}
function parseProviderListJson(stdout: string): ProviderListRow[] {
const data = JSON.parse(stdout.trim()) as ProviderListRow[];
assert(Array.isArray(data), "provider ls --json output should be an array");
return data;
}
async function getProviderRows(): Promise<ProviderListRow[]> {
const result = await ctx.paseo(["provider", "ls", "--json"]);
assert.strictEqual(result.exitCode, 0, "provider ls --json should exit 0");
return parseProviderListJson(result.stdout);
}
function assertProviderTableHeader(stdout: string): void {
for (const header of ["NAME", "TYPE", "LABEL", "AUTH", "AVAILABLE", "MODELS"]) {
assert(stdout.includes(header), `provider ls table should include ${header}`);
}
}
function assertClaudeModels(data: ProviderModel[]): void {
assert.strictEqual(
data.length,
@@ -167,142 +183,135 @@ try {
const result = await ctx.paseo(["provider", "--help"]);
assert.strictEqual(result.exitCode, 0, "provider --help should exit 0");
assert(result.stdout.includes("ls"), "help should mention ls");
assert(result.stdout.includes("add"), "help should mention add");
assert(result.stdout.includes("rm"), "help should mention rm");
assert(result.stdout.includes("models"), "help should mention models");
console.log("✓ provider --help shows subcommands\n");
}
// Test 2: provider ls lists all providers
// Test 2: provider ls on a fresh daemon shows no configured providers
{
console.log("Test 2: provider ls lists all providers");
console.log("Test 2: provider ls on a fresh daemon shows no configured providers");
const result = await ctx.paseo(["provider", "ls"]);
assert.strictEqual(result.exitCode, 0, "provider ls should exit 0");
assert(result.stdout.includes("claude"), "output should include claude");
assert(result.stdout.includes("codex"), "output should include codex");
assert(result.stdout.includes("opencode"), "output should include opencode");
assert(result.stdout.includes("ENABLED"), "output should include ENABLED column");
assert(result.stdout.includes("Enabled"), "output should show enabled providers");
assert(
result.stdout.includes("available") ||
result.stdout.includes("loading") ||
result.stdout.includes("unavailable"),
"output should show a provider status",
);
console.log("✓ provider ls lists all providers\n");
assertProviderTableHeader(result.stdout);
assert(!result.stdout.includes("OpenRouter"), "fresh output should have no provider rows");
const jsonResult = await ctx.paseo(["provider", "ls", "--json"]);
assert.strictEqual(jsonResult.exitCode, 0, "provider ls --json should exit 0");
assert.deepStrictEqual(parseProviderListJson(jsonResult.stdout), []);
console.log("✓ provider ls on a fresh daemon shows no configured providers\n");
}
// Test 3: provider ls --json outputs valid JSON
// Test 3: provider add openrouter stores a dummy key without network validation
{
console.log("Test 3: provider ls --json outputs valid JSON");
const result = await ctx.paseo(["provider", "ls", "--json"]);
assert.strictEqual(result.exitCode, 0, "should exit 0");
const data = JSON.parse(result.stdout.trim());
assert(Array.isArray(data), "output should be an array");
assert(data.length >= 3, `should have at least 3 providers, got ${data.length}`);
assert(
data.some((p: { provider: string }) => p.provider === "claude"),
"should include claude",
);
assert(
data.some((p: { provider: string }) => p.provider === "codex"),
"should include codex",
);
assert(
data.some((p: { provider: string }) => p.provider === "opencode"),
"should include opencode",
);
const rows = data as ProviderListRow[];
for (const provider of ["claude", "codex", "opencode"] as const) {
const row = rows.find((p) => p.provider === provider);
assert(row, `should include ${provider}`);
assert.strictEqual(row.enabled, "Enabled", `${provider} should report Enabled`);
}
console.log("Test 3: provider add openrouter stores a dummy key without network validation");
const result = await ctx.paseo(["provider", "add", "openrouter", "--api-key-stdin"], {
stdin: "dummy-openrouter-key\n",
});
assert.strictEqual(result.exitCode, 0, `provider add should exit 0\n${result.stderr}`);
assert(result.stdout.includes("openrouter"), "add output should include the instance name");
assert(result.stdout.includes("OpenRouter"), "add output should include the catalog label");
assert(result.stdout.includes("Connected"), "add output should show connected auth state");
assert(result.stdout.includes("yes"), "add output should show the provider as available");
const omp = rows.find((p) => p.provider === "omp");
assert(omp, "should include omp");
assert.strictEqual(omp.enabled, "Disabled", "omp should report Disabled by default");
console.log("✓ provider ls --json outputs valid JSON\n");
const rows = await getProviderRows();
assert.strictEqual(rows.length, 1, "provider ls should show exactly one configured instance");
assert.deepStrictEqual(rows[0], {
name: "openrouter",
providerType: "openrouter",
label: "OpenRouter",
auth: "Connected",
available: "yes",
models: "-",
});
console.log("✓ provider add openrouter stores a dummy key without network validation\n");
}
// Test 4: provider ls includes disabled providers
// Test 4: provider add is idempotent for the same instance name
{
console.log("Test 4: provider ls includes disabled providers");
const { paseoHome, workDir } = await createTempDirs();
await writeFile(
join(paseoHome, "config.json"),
JSON.stringify(
{
version: 1,
agents: {
providers: {
claude: {
enabled: false,
},
},
},
},
null,
2,
) + "\n",
console.log("Test 4: provider add is idempotent for the same instance name");
const result = await ctx.paseo(["provider", "add", "openrouter", "--api-key-stdin"], {
stdin: "dummy-openrouter-key-2\n",
});
assert.strictEqual(result.exitCode, 0, `provider add should exit 0\n${result.stderr}`);
const rows = await getProviderRows();
assert.strictEqual(rows.length, 1, "re-running add should not create another instance");
assert.strictEqual(rows[0]?.name, "openrouter");
assert.strictEqual(rows[0]?.label, "OpenRouter");
console.log("✓ provider add is idempotent for the same instance name\n");
}
// Test 5: provider add rejects unknown catalog ids with known ids
{
console.log("Test 5: provider add rejects unknown catalog ids with known ids");
const result = await ctx.paseo(["provider", "add", "nonsense-id", "--api-key-stdin"], {
stdin: "dummy-key\n",
});
assert.notStrictEqual(result.exitCode, 0, "provider add should fail for unknown ids");
const output = result.stdout + result.stderr;
assert(output.includes("nonsense-id"), "error should mention the requested id");
assert(output.includes("Known provider ids"), "error should mention known provider ids");
assert(output.includes("openrouter"), "known ids should include openrouter");
assert(output.includes("kimi"), "known ids should include kimi");
console.log("✓ provider add rejects unknown catalog ids with known ids\n");
}
// Test 6: provider rm removes a configured provider
{
console.log("Test 6: provider rm removes a configured provider");
const result = await ctx.paseo(["provider", "rm", "openrouter"]);
assert.strictEqual(result.exitCode, 0, `provider rm should exit 0\n${result.stderr}`);
assert(result.stdout.includes("openrouter"), "rm output should include the instance name");
assert(result.stdout.includes("yes"), "rm output should report removal");
const rows = await getProviderRows();
assert.deepStrictEqual(rows, [], "provider ls should be empty after removing openrouter");
const table = await ctx.paseo(["provider", "ls"]);
assert.strictEqual(table.exitCode, 0, "provider ls should stay successful after removal");
assertProviderTableHeader(table.stdout);
console.log("✓ provider rm removes a configured provider\n");
}
// Test 7: provider add uses catalog default models when present
{
console.log("Test 7: provider add uses catalog default models when present");
const result = await ctx.paseo(["provider", "add", "kimi", "--api-key-stdin"], {
stdin: "dummy-kimi-key\n",
});
assert.strictEqual(result.exitCode, 0, `provider add kimi should exit 0\n${result.stderr}`);
assert(result.stdout.includes("kimi"), "add output should include the instance name");
assert(result.stdout.includes("Kimi Coding Plan"), "add output should include the label");
const rows = await getProviderRows();
const kimi = rows.find((row) => row.name === "kimi");
assert(kimi, "provider ls should include the kimi instance");
assert.strictEqual(kimi.label, "Kimi Coding Plan");
assert.strictEqual(kimi.auth, "Connected");
assert.strictEqual(kimi.available, "yes");
assert.notStrictEqual(kimi.models, "-", "kimi should expose catalog-derived default models");
assert(
kimi.models
.split(",")
.map((model) => model.trim())
.filter(Boolean).length > 0,
"kimi should list at least one catalog-derived model id",
);
const disabledCtx = await startTestDaemon({ paseoHome, workDir, timeout: 120000 });
try {
const result = await runPaseoCli(disabledCtx, ["provider", "ls", "--json"]);
assert.strictEqual(result.exitCode, 0, "provider ls should exit 0");
const data = JSON.parse(result.stdout.trim()) as ProviderListRow[];
const claude = data.find((p) => p.provider === "claude");
assert(claude, "disabled claude provider should stay in provider ls");
assert.strictEqual(claude.enabled, "Disabled", "disabled provider should report Disabled");
const opencode = data.find((p) => p.provider === "opencode");
assert(opencode, "enabled opencode provider should stay in provider ls");
assert.strictEqual(opencode.enabled, "Enabled", "enabled provider should report Enabled");
const modelsResult = await runPaseoCli(disabledCtx, ["provider", "models", "claude"]);
assert.notStrictEqual(
modelsResult.exitCode,
0,
"provider models should fail for disabled providers",
);
const output = modelsResult.stdout + modelsResult.stderr;
assert(
output.includes("Provider claude is disabled"),
"provider models should surface the daemon disabled error",
);
assert(
!output.includes("claude-sonnet"),
"provider models should not print fallback models for disabled providers",
);
} finally {
await disabledCtx.stop();
}
console.log("✓ provider ls includes disabled providers\n");
console.log("✓ provider add uses catalog default models when present\n");
}
// Test 5: provider ls --quiet outputs provider names only
// Test 8: provider models claude lists canonical model aliases
{
console.log("Test 5: provider ls --quiet outputs provider names only");
const result = await ctx.paseo(["provider", "ls", "--quiet"]);
assert.strictEqual(result.exitCode, 0, "should exit 0");
const lines = result.stdout.trim().split("\n");
assert(lines.length >= 3, `should have at least 3 lines, got ${lines.length}`);
assert(lines.includes("claude"), "should include claude");
assert(lines.includes("codex"), "should include codex");
assert(lines.includes("opencode"), "should include opencode");
console.log("✓ provider ls --quiet outputs provider names only\n");
}
// Test 6: provider models claude lists canonical model aliases
{
console.log("Test 6: provider models claude lists canonical model aliases");
console.log("Test 8: provider models claude lists canonical model aliases");
const data = await runProviderModelsJson("claude");
assertClaudeModels(data);
console.log("✓ provider models claude lists canonical model aliases\n");
}
// Test 7: provider models codex includes concrete codex model IDs
// Test 9: provider models codex includes concrete codex model IDs
{
console.log("Test 7: provider models codex includes concrete codex model IDs");
console.log("Test 9: provider models codex includes concrete codex model IDs");
const data = await runProviderModelsJson("codex");
assert(data.length >= 1, "codex model list should not be empty");
const ids = data.map((m) => m.id);
@@ -322,9 +331,9 @@ try {
console.log("✓ provider models codex includes concrete codex model IDs\n");
}
// Test 8: provider models opencode returns namespaced model IDs
// Test 10: provider models opencode returns namespaced model IDs
{
console.log("Test 8: provider models opencode returns namespaced model IDs");
console.log("Test 10: provider models opencode returns namespaced model IDs");
const data = await runProviderModelsJson("opencode");
assert(data.length >= 1, "opencode model list should not be empty");
const ids = data.map((m) => m.id);
@@ -343,9 +352,9 @@ try {
console.log("✓ provider models opencode returns namespaced model IDs\n");
}
// Test 9: provider models unknown fails with error
// Test 11: provider models unknown fails with error
{
console.log("Test 9: provider models unknown fails with error");
console.log("Test 11: provider models unknown fails with error");
const result = await ctx.paseo(["provider", "models", "unknown"]);
assert.notStrictEqual(result.exitCode, 0, "should fail for unknown provider");
const output = result.stdout + result.stderr;
@@ -356,9 +365,9 @@ try {
console.log("✓ provider models unknown fails with error\n");
}
// Test 10: provider models --json outputs valid JSON
// Test 12: provider models --json outputs valid JSON
{
console.log("Test 10: provider models --json outputs valid JSON");
console.log("Test 12: provider models --json outputs valid JSON");
const data = await runProviderModelsJson("claude");
assert(Array.isArray(data), "output should be an array");
assert(
@@ -371,9 +380,9 @@ try {
console.log("✓ provider models --json outputs valid JSON\n");
}
// Test 11: provider models --quiet outputs model IDs only
// Test 13: provider models --quiet outputs model IDs only
{
console.log("Test 11: provider models --quiet outputs model IDs only");
console.log("Test 13: provider models --quiet outputs model IDs only");
assert(
claudeModelIdsFromJson.length > 0,
"claude model IDs should be captured from --json output",
+8 -3
View File
@@ -336,6 +336,7 @@ export async function runPaseoCli(
timeout?: number;
cwd?: string;
env?: NodeJS.ProcessEnv;
stdin?: string;
},
): Promise<{ exitCode: number; stdout: string; stderr: string }> {
const timeout = options?.timeout ?? 60000;
@@ -354,7 +355,7 @@ export async function runPaseoCli(
...options?.env,
},
cwd,
stdio: ["ignore", "pipe", "pipe"],
stdio: [options?.stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"],
detached: process.platform !== "win32",
});
@@ -369,6 +370,10 @@ export async function runPaseoCli(
appendOutputCapture(stderr, data);
});
if (options?.stdin !== undefined) {
proc.stdin?.end(options.stdin);
}
const timeoutId = setTimeout(() => {
if (proc.pid) {
signalProcessTree(proc.pid, "SIGKILL");
@@ -406,7 +411,7 @@ export async function createE2ETestContext(options?: {
/** Run a paseo CLI command against this daemon */
paseo: (
args: string[],
opts?: { timeout?: number; cwd?: string; env?: NodeJS.ProcessEnv },
opts?: { timeout?: number; cwd?: string; env?: NodeJS.ProcessEnv; stdin?: string },
) => Promise<{
exitCode: number;
stdout: string;
@@ -418,7 +423,7 @@ export async function createE2ETestContext(options?: {
const paseo = (
args: string[],
opts?: { timeout?: number; cwd?: string; env?: NodeJS.ProcessEnv },
opts?: { timeout?: number; cwd?: string; env?: NodeJS.ProcessEnv; stdin?: string },
) => runPaseoCli(ctx, args, opts);
return {