Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .specs/features/web-console/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Setup and usage analytics currently require the terminal, while review already s
- The setup wizard allows typed harness:model values through src/cli/picker-state.ts:113-141 and keeps configured models that are absent from the catalog in src/cli/commands/setup.ts:499-518. It also preserves a role's binding and effort when their screens are skipped; first-run skips produce an empty agents object sentinel. Tests are in tests/setup-wizard.test.ts:579-592, :757-764, and :1179-1183.
- The setup command resolves explicit and active profiles in src/cli/commands/setup.ts:831-853. An active profile that does not exist raises SetupUsageError. An explicit profile without a saved snapshot starts from profile defaults with agents: {}.
- SetupEnvelope in src/cli/commands/setup.ts:1000-1036 uses top-level fields proposta, validacoes, mudancas, and resultado. runSetupBatch reads config without replacing invalid JSON and returns code 14 for invalid config or 15 for a read error at :1369-1387. It validates only winning --bind entries at :1404 and :1444-1459. loadConfig in src/config/config.ts:531-540 swallows read and parse errors, so web setup must use readConfigForSetup.
- getBatchModels in src/core/models.ts:276-389 supports allowNetwork:false for cached reads and refresh:true for network discovery, with a default 12,000 ms timeout. If discovery is incomplete or a requested harness reports an error, it discards partial network results and returns the cache fallback with status and discoveryError.
- getBatchModels in src/core/models.ts:276-389 supports allowNetwork:false for cached reads and refresh:true for network discovery, with a default 25,000 ms timeout (`CATALOG_DISCOVERY_TIMEOUT_MS`). If discovery is incomplete or a requested harness reports an error, it discards partial network results and returns the cache fallback with status and discoveryError.
- Custom orchestrator parallelism accepts positive finite numbers in src/cli/commands/setup.ts:319-345 and :361-394. README.md:149-186 currently describes setup as a terminal picker.

## Out of Scope
Expand Down Expand Up @@ -56,7 +56,7 @@ Setup and usage analytics currently require the terminal, while review already s
| Token bootstrap redirect | A valid token GET to an HTML route sets the cookie and returns HTTP 303 to the same path without t. | The token is removed from the address bar after bootstrap. | No |
| Origin comparison | Require an HTTP Origin whose host and port match the accepted request Host and bound port on every POST; return HTTP 403 if absent or different. | This gives every action route one same-origin rule. | No |
| Home links | Render a link only when its page route is registered in the active route table. | P1 must not advertise Usage before P5 registers it. | No |
| Setup catalog source | GET calls getBatchModels with allowNetwork:false; protected refresh calls it with refresh:true, allowNetwork:true, and timeoutMs:12000. | This is the single catalog source and matches the batch helper's fallback behavior. | No |
| Setup catalog source | GET calls getBatchModels with allowNetwork:false; protected refresh calls it with refresh:true, allowNetwork:true, and timeoutMs:25000 (CATALOG_DISCOVERY_TIMEOUT_MS). | This is the single catalog source and matches the batch helper's fallback behavior. | No |
| Concurrent catalog refresh | Requests during one discovery share one in-flight refresh promise. | Duplicate provider requests do not improve the displayed result. | No |
| Setup --refresh behavior | On an interactive setup web launch, open /setup and request protected catalog refresh on page load. | This preserves the current flag while keeping discovery behind the action route. | No |
| Setup request size | Accept JSON bodies up to 64 KiB; return HTTP 400 for larger or malformed bodies. | A full role selection is small and a fixed limit bounds request parsing. | No |
Expand All @@ -65,7 +65,7 @@ Setup and usage analytics currently require the terminal, while review already s
| Usage --by with --web | Use --by as the initially selected breakdown; keep all available breakdown sections on the page. | This preserves the CLI grouping preference without hiding the other web sections. | No |
| Usage interval | Use a floor of 1 second and a numeric-conversion fallback of 2 seconds for web polling. | Commander supplies the default string "2", so the implementation cannot distinguish an omitted option from explicit --interval 2. | No |
| Aggregate usage --web --json | Start the browser page and print the token URL; --json affects aggregate output only when --web is absent. | The browser is the selected aggregate interface, while the positional run-id JSON contract remains unchanged. | No |
| Catalog refresh method | Use protected POST /api/setup/catalog/refresh with refresh:true, allowNetwork:true, and timeoutMs:12000. | Discovery may update the model cache, so it uses the guarded action path and the helper's current timeout. | No |
| Catalog refresh method | Use protected POST /api/setup/catalog/refresh with refresh:true, allowNetwork:true, and timeoutMs:25000 (CATALOG_DISCOVERY_TIMEOUT_MS). | Discovery may update the model cache, so it uses the guarded action path and the helper's current timeout. | No |
| Web apply catalog validation | Validate changed harness:model bindings against the cached catalog with getBatchModels({allowNetwork:false}); reject an off-catalog model with HTTP 422 unless the request includes explicit confirmation for that role. | This keeps apply offline and mirrors the wizard's second-Enter confirmation for typed models absent from its catalog. | Yes |
| Page behavior tests | Put state, rendering decisions, and polling transitions in TypeScript functions exported from each page module, then inject their source into its HTML string. | Node tests can exercise the same functions without installing a DOM implementation. | No |
| Usage origin dependency | Use the required byOrigin type already present in main at 80ec486 (#103); tolerate an absent field only when an older daemon process answers over IPC. | The implementation branch has the usage changes already, while an older running daemon can still use the earlier protocol shape. | Yes |
Expand Down
4 changes: 3 additions & 1 deletion src/core/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,9 @@ function maxAge(entries: Array<{ age: CacheAge }>): number | null {
return Math.max(...entries.map((entry) => entry.age.ageMs));
}

export const CATALOG_DISCOVERY_TIMEOUT_MS = 12_000;
// Covers the slowest driver budget: omp spends up to 10 s on `models refresh`
// and 12 s on `models --json`. Discovery returns as soon as every harness answers.
export const CATALOG_DISCOVERY_TIMEOUT_MS = 25_000;

async function discoverWithTimeout(
registry: DriverRegistry,
Expand Down
8 changes: 6 additions & 2 deletions src/drivers/opencode/driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,12 @@ export class OpencodeDriver extends SessionDriver {
if (!install.installed || !install.path) return [];

try {
const args = options?.refresh ? ["models", "--refresh"] : ["models"];
const stdout = await runCommandWithTimeout(install.path, args, { timeoutMs: 10000 });
const list = (args: string[]) => runCommandWithTimeout(install.path!, args, { timeoutMs: 10000 });
// OpenCode v2 dropped `models --refresh` and exits with its usage text,
// so a refresh falls back to the plain listing instead of an empty catalog.
const stdout = options?.refresh
? await list(["models", "--refresh"]).catch(() => list(["models"]))
: await list(["models"]);
const lines = stdout.split("\n");

const providerMap = new Map<string, ModelInfo[]>();
Expand Down
22 changes: 22 additions & 0 deletions tests/opencode-driver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,28 @@ afterEach(() => {
});

describe("OpencodeDriver", () => {
it("lists models on refresh when the binary rejects models --refresh", async () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "codedeck-opencode-models-"));
const binary = path.join(dir, "opencode");
fs.writeFileSync(
binary,
'#!/bin/sh\nif [ "$2" = "--refresh" ]; then echo "USAGE" >&2; exit 1; fi\nprintf "alibaba-token-plan/qwen3.8-max\\nopencode/gemini-3.8-flash\\n"\n',
{ mode: 0o755 },
);
mockedDetect.mockResolvedValue({ installed: true, path: binary, version: "opencode v2.0.15" });

try {
const providers = await new OpencodeDriver().listModels({ refresh: true });

expect(providers.map((provider) => [provider.provider, provider.models.map((model) => model.id)])).toEqual([
["alibaba-token-plan", ["alibaba-token-plan/qwen3.8-max"]],
["opencode", ["opencode/gemini-3.8-flash"]],
]);
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});

it("detects and launches the managed OpenCode v2 binary when PATH is stale", async () => {
const home = fs.mkdtempSync(path.join(os.tmpdir(), "codedeck-opencode-"));
const binary = path.join(home, ".opencode", "bin", "opencode");
Expand Down
2 changes: 1 addition & 1 deletion tests/setup-web.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ describe("setup catalog routes", () => {
cacheWriteFailed: false,
};
const getBatchModels = vi.fn(async (options: BatchModelsOptions) => {
expect(options).toEqual({ refresh: true, allowNetwork: true, timeoutMs: 12_000 });
expect(options).toEqual({ refresh: true, allowNetwork: true, timeoutMs: 25_000 });
return await new Promise<BatchModelsResult>((resolve) => { resolveRefresh = resolve; });
});
const handle = await makeServer({ getBatchModels });
Expand Down
2 changes: 1 addition & 1 deletion tests/web-cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ describe("ui route table", () => {
expect(await usage.json()).toEqual(emptyUsage);
expect(fetchUsageQuery).toHaveBeenCalledOnce();
expect(getBatchModels).toHaveBeenCalledWith({ allowNetwork: false });
expect(getBatchModels).toHaveBeenCalledWith({ refresh: true, allowNetwork: true, timeoutMs: 12_000 });
expect(getBatchModels).toHaveBeenCalledWith({ refresh: true, allowNetwork: true, timeoutMs: 25_000 });
});
});

Expand Down
Loading