diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 0000000..469e98c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,34 @@ +name: Bug report +description: Report a reproducible provider problem +title: "Bug: " +body: + - type: markdown + attributes: + value: "Do not paste API keys, auth files, prompts, private tool results, or full session exports. Use a synthetic reproduction." + - type: textarea + id: diagnostics + attributes: + label: Safe diagnostics + description: "Run /commandcode-diagnostics in the failing instance and paste its output. This report excludes prompts, credentials and local paths. If the extension does not load, give the plugin, Pi/OMP, Node/Bun and OS versions instead." + render: text + validations: + required: true + - type: dropdown + id: plan + attributes: + label: Plan + options: [GOAT, Pro, Max, Team, Provider, Go, Unknown] + validations: + required: true + - type: textarea + id: reproduction + attributes: + label: Minimal reproduction + description: "Steps, expected/actual behavior, exact model ID, and whether this followed a model switch, compaction, update or session resume. Use synthetic data." + validations: + required: true + - type: textarea + id: error + attributes: + label: Error and timing + description: "Share the error code and approximate timestamp. Review/redact any error text yourself; do not include entire request or response bodies." diff --git a/CHANGELOG.md b/CHANGELOG.md index 69ac314..2ad6bfa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Add content-free `/commandcode-diagnostics` reports, per-request body-chunk timing and error categories, explicit unknown-price warnings, and a structured GitHub bug form. Generate requests now honor the caller's fetch hook so both transports can be measured without retaining request bodies. + - Add `/commandcode-endpoint auto|responses|chat-completions` for persistent per-model Provider API routing. Choices survive catalog refreshes and restarts, are validated against the model wire, and are shown in `/commandcode-status` (#156). No automatic mid-stream endpoint switching is performed. ## 0.7.8 - 2026-10-15 diff --git a/README.md b/README.md index 4e9e2e0..1cc766a 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,7 @@ While pi is running, use these provider commands without restarting: - `/commandcode-refresh` fetches and re-registers the current model catalog. Overlapping refreshes are coalesced, and a failed refresh keeps the last valid catalog active. - `/commandcode-status` shows redacted discovery diagnostics, including the source, model count, timestamps, cache path, endpoint, and warning. If a Responses reasoning event has an unsupported shape, a separate stream warning remains visible until the extension is reloaded or the host restarts. The event is preserved unchanged; no reasoning text is included in the warning. +- `/commandcode-diagnostics` shows a shareable support report: plugin/host version, CLI snapshot, selected route and override, capability provenance, price status, and the latest request's HTTP status, attempts, outcome, first body-chunk latency and longest gap. It keeps only one request summary in memory. It never stores or exports prompts, tool content, credentials, raw errors, account information or local paths. Request IDs are included only when they match a bounded hexadecimal/UUID format. Timing is measured between body chunks, not tokens; first-chunk latency may include an error response. A request started most recently owns the report even if another request finishes later. Retry counts are scoped to that stream call, not host-level retries. Review the report before sharing it. - `/commandcode-quota` shows your Command Code account usage and quota in a dashboard-style layout: credits remaining and used with a percentage, monthly/purchased/free sources, the current plan, available usage totals, the API key name, and the 5-hour and weekly usage windows. The `commandcode-quota` command reads from the Command Code alpha usage endpoints (the same ones the `cmd` CLI `/usage` command uses): `whoami`, `billing/credits`, `billing/subscriptions`, and `usage/summary`. It authenticates with the same API key the provider already uses. If the command cannot reach those endpoints or an endpoint schema changes, unavailable sections are reported explicitly instead of being displayed as zero usage. Output is plain text (via `ui.notify`) so it works across pi and compatible hosts such as OMP. @@ -173,7 +174,7 @@ The legacy generate transport resolves image support from the host's `model.inpu The Command Code Provider API does not currently include prices in its model catalog. This extension therefore keeps a static table for models with known prices so pi and OMP can display estimated request costs. DeepSeek V4 uses time-dependent rates: the extension applies the documented 2x peak rates on weekdays from 01:00-04:00 and 06:00-10:00 UTC, and off-peak rates at all other times, including the entire weekend. The same request-time selection is used by the native Provider API path and the legacy generate fallback. -Models missing from that table display zero cost in pi. This does **not** mean that Command Code will bill the request at zero. The Command Code Usage page remains authoritative for each request. Check the current [Command Code pricing](https://commandcode.ai/docs/resources/pricing-limits) before relying on the displayed value. +Models missing from that table display zero cost in pi. The provider warns once per selected unpriced model per load, and diagnostics distinguishes an unknown price from an explicitly free model. An expired tracked promotion is marked `review-required`; the snapshot verification date is always shown. This does **not** mean that Command Code will bill the request at zero. The Command Code Usage page remains authoritative for each request. Check the current [Command Code pricing](https://commandcode.ai/docs/resources/pricing-limits) before relying on the displayed value. A read-only companion check, `npm run check:commandcode-pricing`, compares the live Provider API catalog and the official pricing page with the static table. It never writes prices: exit code `0` means every compared rate matches, `1` means drift or an expired promotion needs manual review, and `2` means the price could not be checked. The daily catalog workflow and applicable pull requests publish the Markdown report to the GitHub Actions summary. The pricing page has no public pricing API, so the check parses its private Next.js Flight payload; if that format changes the check fails loudly instead of reporting a false pass, and prices stay manually reviewed. diff --git a/index.ts b/index.ts index 89a2cc9..f6b16fa 100644 --- a/index.ts +++ b/index.ts @@ -15,6 +15,9 @@ import { type ProviderConfig, } from "@earendil-works/pi-coding-agent" import { join } from "node:path" +import { readFile } from "node:fs/promises" +import * as hostPackage from "@earendil-works/pi-coding-agent" +import { createRequestDiagnostics, formatDiagnostics, priceStatus } from "./src/diagnostics.ts" import { getConfiguredApiKey } from "./src/api-key.ts" import { pickCommandCodeApiKey, withResolvedCommandCodeApiKey } from "./src/converters.ts" @@ -197,6 +200,31 @@ function legacyApiBase(providerApiBase: string): string { } export default async function (pi: ExtensionAPI) { + const packageData: unknown = JSON.parse( + await readFile(new URL("./package.json", import.meta.url), "utf8"), + ) + const pluginVersion = + typeof packageData === "object" && + packageData !== null && + "version" in packageData && + typeof packageData.version === "string" + ? packageData.version + : "unknown" + const hostVersion = + "VERSION" in hostPackage && typeof hostPackage.VERSION === "string" + ? hostPackage.VERSION + : "unknown" + const diagnostics = createRequestDiagnostics() + const warnedPrices = new Set() + pi.on("before_agent_start", (_event, ctx) => { + if (ctx.model?.provider !== "commandcode" || warnedPrices.has(ctx.model.id)) return + if (priceStatus(ctx.model.id) === "known") return + warnedPrices.add(ctx.model.id) + ctx.ui.notify( + "Command Code display price is unknown or needs review. A zero estimate does not mean free usage; check /commandcode-diagnostics.", + "warning", + ) + }) // Hosts may select their built-in Command Code model before loading extensions. // Rebind that stale selection to our registered transport before the first turn. pi.on("session_start", async (_event, ctx) => { @@ -320,8 +348,20 @@ export default async function (pi: ExtensionAPI) { // custom api there so those calls reach the same transport. The registry // resolves no credentials for extension providers, so fall back to the // configured key when the caller passes none or a placeholder. + const measuredStream = ( + model: Parameters[0], + context: Parameters[1], + options?: Parameters[2], + ) => + diagnostics.stream( + transport.stream, + () => new AssistantMessageEventStream(), + model, + context, + resolveStreamOptions(options), + ) const compatStream: CompatStreamFunction = (model, context, options) => - transport.stream(model, context, resolveStreamOptions(options)) as AssistantMessageEventStream + measuredStream(model, context, options) as AssistantMessageEventStream registerCompatApiProvider(compatStream) pi.on("message_end", async (event, ctx) => { @@ -349,7 +389,25 @@ export default async function (pi: ExtensionAPI) { createProviderConfig: (models) => { catalogModels.clear() for (const model of models) catalogModels.set(model.id, model) - return createProviderConfig(models, apiBase, transport.stream) + return createProviderConfig(models, apiBase, measuredStream) + }, + getDiagnostics: (model) => { + let route = "not selected" + if (model?.provider === "commandcode") { + try { + route = `${transport.getTransport()}/${resolveModelApi(model.id)} (${routing[model.id] ?? "auto"})` + } catch { + route = "invalid routing configuration" + } + } + return formatDiagnostics({ + pluginVersion, + host: compatApiProviderRegistrar() ? "pi" : "omp-compatible", + hostVersion, + modelId: model?.provider === "commandcode" ? model.id : undefined, + route, + request: diagnostics.snapshot(), + }) }, getRoutingStatus: (model) => { if (routingWarning) return `routing warning: ${routingWarning}` diff --git a/package.json b/package.json index 9ad72cb..d48f544 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "LICENSE" ], "scripts": { - "test": "npm run typecheck && npm run test:routing && node --test tests/test-release.mjs tests/test-pi-child-cleanup.mjs && tsx tests/test-package-manifest.ts && tsx tests/test-api-key.ts && tsx tests/test-pure-functions.ts && tsx tests/test-models.ts && tsx tests/test-model-metadata-check.ts && tsx tests/test-runtime.ts && tsx tests/test-pricing.ts && tsx tests/test-pricing-check.ts && tsx tests/test-cost.ts && tsx tests/test-oauth.ts && tsx tests/test-abort.ts && tsx tests/test-overflow.ts && tsx tests/test-stream.ts && tsx tests/test-quota.ts && tsx tests/test-quota-command.ts && tsx tests/test-usage.ts && tsx tests/test-retry.ts && tsx tests/test-transport.ts && node tests/test-pi-isolated.mjs && node tests/test-pi-authenticated.mjs && node tests/test-pi-local.mjs && node tests/test-omp-compat.mjs", + "test": "npm run typecheck && npm run test:diagnostics && npm run test:routing && node --test tests/test-release.mjs tests/test-pi-child-cleanup.mjs && tsx tests/test-package-manifest.ts && tsx tests/test-api-key.ts && tsx tests/test-pure-functions.ts && tsx tests/test-models.ts && tsx tests/test-model-metadata-check.ts && tsx tests/test-runtime.ts && tsx tests/test-pricing.ts && tsx tests/test-pricing-check.ts && tsx tests/test-cost.ts && tsx tests/test-oauth.ts && tsx tests/test-abort.ts && tsx tests/test-overflow.ts && tsx tests/test-stream.ts && tsx tests/test-quota.ts && tsx tests/test-quota-command.ts && tsx tests/test-usage.ts && tsx tests/test-retry.ts && tsx tests/test-transport.ts && node tests/test-pi-isolated.mjs && node tests/test-pi-authenticated.mjs && node tests/test-pi-local.mjs && node tests/test-omp-compat.mjs", "typecheck": "tsc --noEmit", "format:check": "prettier --check '**/*.{ts,mjs,json,md}'", "format": "prettier --write '**/*.{ts,mjs,json,md}'", @@ -40,9 +40,10 @@ "sync:commandcode-catalog": "tsx .github/scripts/check-commandcode-model-metadata.ts command-code@latest --write", "test:quota": "tsx tests/test-quota.ts && tsx tests/test-quota-command.ts && tsx tests/test-usage.ts", "test:usage": "tsx tests/test-usage.ts", - "test:unit": "npm run test:routing && tsx tests/test-api-key.ts && tsx tests/test-pure-functions.ts && tsx tests/test-models.ts && tsx tests/test-model-metadata-check.ts && tsx tests/test-runtime.ts && tsx tests/test-pricing.ts && tsx tests/test-pricing-check.ts && tsx tests/test-cost.ts && tsx tests/test-oauth.ts && tsx tests/test-abort.ts && tsx tests/test-overflow.ts && tsx tests/test-stream.ts && tsx tests/test-quota.ts && tsx tests/test-quota-command.ts && tsx tests/test-usage.ts && tsx tests/test-retry.ts && tsx tests/test-transport.ts", + "test:unit": "npm run test:diagnostics && npm run test:routing && tsx tests/test-api-key.ts && tsx tests/test-pure-functions.ts && tsx tests/test-models.ts && tsx tests/test-model-metadata-check.ts && tsx tests/test-runtime.ts && tsx tests/test-pricing.ts && tsx tests/test-pricing-check.ts && tsx tests/test-cost.ts && tsx tests/test-oauth.ts && tsx tests/test-abort.ts && tsx tests/test-overflow.ts && tsx tests/test-stream.ts && tsx tests/test-quota.ts && tsx tests/test-quota-command.ts && tsx tests/test-usage.ts && tsx tests/test-retry.ts && tsx tests/test-transport.ts", "test:api-key": "tsx tests/test-api-key.ts", "test:models": "tsx tests/test-models.ts && tsx tests/test-model-metadata-check.ts", + "test:diagnostics": "tsx tests/test-diagnostics.ts", "test:routing": "tsx tests/test-routing.ts", "test:runtime": "tsx tests/test-runtime.ts", "test:pricing": "tsx tests/test-pricing.ts", diff --git a/src/core.ts b/src/core.ts index f58f856..789556b 100644 --- a/src/core.ts +++ b/src/core.ts @@ -630,7 +630,7 @@ export function createStreamCommandCode(deps: CoreDependencies) { try { try { - response = await fetchImpl(`${apiBase}/alpha/generate`, { + response = await (options?.fetch ?? fetchImpl)(`${apiBase}/alpha/generate`, { method: "POST", headers: requestHeaders, body: bodyStr, diff --git a/src/diagnostics.ts b/src/diagnostics.ts new file mode 100644 index 0000000..d4006f6 --- /dev/null +++ b/src/diagnostics.ts @@ -0,0 +1,205 @@ +import { + MODEL_INPUT_MODALITIES, + MODEL_REASONING, + MODEL_EFFORTS, + COMMAND_CODE_CLI_VERSION, +} from "./commandcode-catalog.ts" +import { MODEL_COSTS, PRICING_LAST_VERIFIED, TEMPORARY_PRICING } from "./pricing.ts" +import type { + AssistantMessageEventStreamLike, + ContextLike, + ModelLike, + StreamOptions, +} from "./types.ts" + +export interface RequestDiagnostic { + model: string + endpoint?: string + status?: number + requestId?: string + attempts: number + firstChunkMs?: number + longestChunkGapMs: number + durationMs?: number + outcome: "pending" | "done" | "error" | "aborted" + error?: + | "authentication" + | "permission" + | "rate_limit" + | "server" + | "context_length" + | "timeout" + | "request_failed" +} + +export function priceStatus( + modelId: string, + now = Date.now(), +): "known" | "unknown" | "review-required" { + if (!MODEL_COSTS[modelId]) return "unknown" + const day = new Date(now).toISOString().slice(0, 10) + return TEMPORARY_PRICING.some( + (promotion) => promotion.models.includes(modelId) && promotion.expiresOn < day, + ) + ? "review-required" + : "known" +} + +function safeIdentifier(value: string): string { + return /^[a-zA-Z0-9_./:+-]{1,160}$/.test(value) && !/\b(?:user|cc)_[a-zA-Z0-9_-]{8,}/.test(value) + ? value + : "[redacted]" +} + +function classify(status: number | undefined, message: string): RequestDiagnostic["error"] { + if (status === 401) return "authentication" + if (status === 403) return "permission" + if (status === 429) return "rate_limit" + if (status && status >= 500) return "server" + if (/context_length_exceeded/.test(message)) return "context_length" + if (/timed?\s*out/i.test(message)) return "timeout" + return "request_failed" +} + +const ENDPOINTS = [ + "/provider/v1/responses", + "/provider/v1/chat/completions", + "/provider/v1/messages", + "/alpha/generate", +] + +/** Memory-only, content-free measurements. Never retain URLs, headers, bodies or credentials. */ +export function createRequestDiagnostics(now: () => number = Date.now) { + let latest: RequestDiagnostic | undefined + return { + snapshot: () => (latest ? { ...latest } : undefined), + stream( + source: ( + model: ModelLike, + context: ContextLike, + options?: StreamOptions, + ) => AssistantMessageEventStreamLike, + createStream: () => AssistantMessageEventStreamLike, + model: ModelLike, + context: ContextLike, + options?: StreamOptions, + ): AssistantMessageEventStreamLike { + const started = now() + const record: RequestDiagnostic = { + model: safeIdentifier(model.id), + attempts: 0, + longestChunkGapMs: 0, + outcome: "pending", + } + latest = record + const output = createStream() + let lastChunk: number | undefined + const instrumented: StreamOptions = { + ...options, + fetch: async (input, init) => { + record.attempts += 1 + lastChunk = undefined + try { + const pathname = new URL( + typeof input === "string" ? input : input instanceof URL ? input.href : input.url, + ).pathname + record.endpoint = ENDPOINTS.find((path) => pathname === path) ?? "custom endpoint" + } catch { + record.endpoint = "custom endpoint" + } + const response = await (options?.fetch ?? fetch)(input, init) + record.status = response.status + const requestId = + response.headers.get("x-request-id") ?? response.headers.get("request-id") + record.requestId = + requestId && /^[a-f0-9-]{16,64}$/i.test(requestId) ? requestId : undefined + if (!response.body) return response + return new Response( + response.body.pipeThrough( + new TransformStream({ + transform(chunk, controller) { + const at = now() + if (record.firstChunkMs === undefined) record.firstChunkMs = at - started + if (lastChunk !== undefined) + record.longestChunkGapMs = Math.max(record.longestChunkGapMs, at - lastChunk) + lastChunk = at + controller.enqueue(chunk) + }, + }), + ), + { status: response.status, statusText: response.statusText, headers: response.headers }, + ) + }, + } + const run = async () => { + try { + for await (const event of source(model, context, instrumented)) { + if (event.type === "done") record.outcome = "done" + if (event.type === "error") { + record.outcome = event.reason === "aborted" ? "aborted" : "error" + record.error = classify(record.status, event.error.errorMessage ?? "") + } + output.push(event) + } + } catch (error) { + record.outcome = options?.signal?.aborted ? "aborted" : "error" + record.error = classify(record.status, error instanceof Error ? error.message : "") + output.push({ + type: "error", + reason: record.outcome === "aborted" ? "aborted" : "error", + error: { + role: "assistant", + content: [], + api: model.api, + provider: model.provider, + model: model.id, + timestamp: now(), + stopReason: record.outcome === "aborted" ? "aborted" : "error", + errorMessage: + "Command Code request failed before completing the stream; see /commandcode-diagnostics", + usage: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + totalTokens: 0, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, + }, + }, + }) + } finally { + record.durationMs = now() - started + output.end() + } + } + run().catch(() => output.end()) + return output + }, + } +} + +export function formatDiagnostics(input: { + pluginVersion: string + host: string + hostVersion: string + modelId?: string + route: string + request?: RequestDiagnostic +}): string { + const modelId = input.modelId + const known = + modelId && + (MODEL_INPUT_MODALITIES[modelId] || MODEL_REASONING[modelId] || MODEL_EFFORTS[modelId]) + return [ + `plugin: ${safeIdentifier(input.pluginVersion)}`, + `host: ${safeIdentifier(input.host)} ${safeIdentifier(input.hostVersion)}`, + `catalog CLI: ${COMMAND_CODE_CLI_VERSION}`, + `model: ${modelId ? safeIdentifier(modelId) : "none"}`, + // Caller supplies only a normalized routing description, never an error body. + `route: ${input.route}`, + `capabilities: ${known ? "bundled snapshot; host overrides may apply" : "not explicitly catalogued; conservative defaults"}`, + `price: ${modelId ? priceStatus(modelId) : "unknown"} (verified ${PRICING_LAST_VERIFIED}; estimate, not billing)`, + input.request ? `last request: ${JSON.stringify(input.request)}` : "last request: none", + "Timing measures received body chunks, not tokens. No prompts, tool content, credentials, account IDs or local paths are included.", + ].join("\n") +} diff --git a/src/runtime.ts b/src/runtime.ts index f7e5925..f1ee612 100644 --- a/src/runtime.ts +++ b/src/runtime.ts @@ -33,6 +33,7 @@ export interface CommandCodeRuntimeOptions { createProviderConfig: (models: readonly CommandCodeModel[]) => TProviderConfig getTransport?: () => "unknown" | "provider" | "generate" getRoutingStatus?: (model: CommandCodeCommandContext["model"]) => string + getDiagnostics?: (model: CommandCodeCommandContext["model"]) => string getStreamWarning?: () => string | undefined now?: () => number logWarning?: (message: string) => void @@ -289,6 +290,12 @@ export class CommandCodeRuntime { + ctx.ui.notify(this.options.getDiagnostics?.(ctx.model) ?? "Diagnostics unavailable", "info") + }, + }) this.pi.registerCommand("commandcode-refresh", { description: "Refresh the Command Code model catalog", handler: async (_args, ctx) => { @@ -313,7 +320,11 @@ export class CommandCodeRuntime { const status = this.getStatus() ctx.ui.notify( - [formatCommandCodeStatus(status), this.options.getRoutingStatus?.(ctx.model)] + [ + formatCommandCodeStatus(status), + this.options.getRoutingStatus?.(ctx.model), + this.options.getDiagnostics?.(ctx.model), + ] .filter(Boolean) .join("\n"), status.warning || status.streamWarning ? "warning" : "info", diff --git a/tests/test-diagnostics.ts b/tests/test-diagnostics.ts new file mode 100644 index 0000000..aa87b12 --- /dev/null +++ b/tests/test-diagnostics.ts @@ -0,0 +1,62 @@ +import assert from "node:assert/strict" +import { describe, it } from "node:test" +import { createRequestDiagnostics, formatDiagnostics, priceStatus } from "../src/diagnostics.ts" +import { + createTestDeps, + createTestEventStream, + collectEvents, + makeContext, + makeModel, +} from "./helpers.ts" + +describe("content-free diagnostics", () => { + it("distinguishes missing prices from explicit free models", () => { + assert.equal(priceStatus("unknown/model"), "unknown") + assert.equal(priceStatus("stealth/glyph-cluster:free"), "known") + }) + it("preserves streams and measures body chunks without retaining contents", async () => { + let now = 0 + const diagnostics = createRequestDiagnostics(() => now) + const { streamCommandCode } = createTestDeps() + const events = await collectEvents( + diagnostics.stream(streamCommandCode, createTestEventStream, makeModel(), makeContext(), { + apiKey: "synthetic-key", + fetch: async () => + new Response( + new ReadableStream({ + start(controller) { + now = 10 + controller.enqueue( + new TextEncoder().encode('{"type":"text-delta","text":"PRIVATE PROMPT"}\n'), + ) + setTimeout(() => { + now = 50 + controller.enqueue( + new TextEncoder().encode('{"type":"finish","finishReason":"stop"}\n'), + ) + controller.close() + }, 10) + }, + }), + { headers: { "x-request-id": "user_private_secret" } }, + ), + }), + ) + assert.equal(events.at(-1)?.type, "done") + const record = diagnostics.snapshot() + assert.equal(record?.attempts, 1) + assert.equal(record?.outcome, "done") + assert.equal(record?.firstChunkMs, 10) + assert.equal(record?.longestChunkGapMs, 40) + const report = formatDiagnostics({ + pluginVersion: "0.7.7", + host: "pi", + hostVersion: "1.1.0", + route: "provider/openai-responses (auto)", + modelId: "unknown/model", + request: record, + }) + assert.doesNotMatch(report, /PRIVATE PROMPT|synthetic-key|user_private_secret|Bearer/) + assert.match(report, /price: unknown/) + }) +}) diff --git a/tests/test-pi-local.mjs b/tests/test-pi-local.mjs index fdcf6a8..65004c6 100644 --- a/tests/test-pi-local.mjs +++ b/tests/test-pi-local.mjs @@ -704,6 +704,14 @@ async function runRpcExtensionCommands(timeoutMs = 30_000) { ) assert.match(statusAfter.message, /model endpoint: openai-completions.*user override/) + send({ id: "safe-diagnostics", type: "prompt", message: "/commandcode-diagnostics" }) + const report = await waitFor( + (event) => event.type === "extension_ui_request" && event.message?.startsWith("plugin:"), + ) + assert.match(report.message, /host: pi/) + assert.match(report.message, /catalog CLI:/) + assert.doesNotMatch(report.message, /cache path:|https?:|mock-key|Bearer/) + assert.ok(commandNames.includes("commandcode-diagnostics")) send({ id: "endpoint-auto", type: "prompt", diff --git a/tests/test-runtime.ts b/tests/test-runtime.ts index 8d2d441..39fbd20 100644 --- a/tests/test-runtime.ts +++ b/tests/test-runtime.ts @@ -1,3 +1,4 @@ +import "./test-diagnostics.ts" import assert from "node:assert/strict" import { describe, it } from "node:test" @@ -163,7 +164,10 @@ describe("Command Code runtime", () => { }) const initialization = runtime.initialize() - assert.deepEqual([...pi.commands.keys()], ["commandcode-refresh", "commandcode-status"]) + assert.deepEqual( + [...pi.commands.keys()], + ["commandcode-diagnostics", "commandcode-refresh", "commandcode-status"], + ) await Promise.resolve() assert.equal(runtime.getStatus().refreshing, true) assert.equal(runtime.getStatus().lastAttempt, now)