diff --git a/src/context/repoMap/symbolExtractor.ts b/src/context/repoMap/symbolExtractor.ts index 1435d3572..754677199 100644 --- a/src/context/repoMap/symbolExtractor.ts +++ b/src/context/repoMap/symbolExtractor.ts @@ -52,7 +52,8 @@ async function getQuery(language: NonNullable {}) + try { + await runAimlapiTopup({ email: 'user@example.com', amountUsd: '25', noOpen: true }) + const logged = output.mock.calls.flat().join('\n') + expect(logged).toContain('Balance topped up and provider configured.') + expect(logged).not.toContain('exchanged-key') + expect(logged).not.toContain('exchan') + expect(logged).not.toContain('exchanged-id') + } finally { + output.mockRestore() + } // The retained settled key finished the write — no account check/provisioning, // the persisted model is preserved, and the checkout state is cleared. diff --git a/src/integrations/aimlapi/topup.ts b/src/integrations/aimlapi/topup.ts index af016b917..b2f68923c 100644 --- a/src/integrations/aimlapi/topup.ts +++ b/src/integrations/aimlapi/topup.ts @@ -167,10 +167,6 @@ export function setAimlapiTopupTestDoubles( promptHiddenFn = doubles?.promptHidden ?? promptHidden } -function maskKey(key: string): string { - return key.length <= 10 ? '****' : `${key.slice(0, 6)}...${key.slice(-4)}` -} - // A session the provider reports as `exchanged` has already minted its one-shot // key; it can never be paid or exchanged again. Both the poll loop and the // resume path use this so neither opens a second, separately chargeable checkout @@ -262,7 +258,6 @@ export async function runAimlapiTopup(options: AimlapiTopupOptions): Promise { try { await rebuild } finally { - if (state.pending === rebuild) { + // Compare the in-flight operations themselves, not their resolved values. + if (Object.is(state.pending, rebuild)) { state.pending = null } } diff --git a/src/services/api/providerConfig.local.test.ts b/src/services/api/providerConfig.local.test.ts index a745fd2e8..8ddad51b0 100644 --- a/src/services/api/providerConfig.local.test.ts +++ b/src/services/api/providerConfig.local.test.ts @@ -137,6 +137,8 @@ test('partitions local openai-compatible model cache scope by credentials and he process.env.ANTHROPIC_CUSTOM_HEADERS = 'X-Route: first' const firstScope = getAdditionalModelOptionsCacheScope() + expect(getAdditionalModelOptionsCacheScope()).toBe(firstScope) + expect(firstScope).not.toContain('first-key') process.env.OPENAI_API_KEY = 'second-key' const secondScope = getAdditionalModelOptionsCacheScope() @@ -144,6 +146,8 @@ test('partitions local openai-compatible model cache scope by credentials and he process.env.OPENAI_API_KEY = 'first-key' process.env.ANTHROPIC_CUSTOM_HEADERS = 'X-Route: second' const thirdScope = getAdditionalModelOptionsCacheScope() + process.env.ANTHROPIC_CUSTOM_HEADERS = 'X-Route: first' + expect(getAdditionalModelOptionsCacheScope()).toBe(firstScope) delete process.env.OPENAI_API_KEY process.env.ANTHROPIC_CUSTOM_HEADERS = 'X-Route: first' diff --git a/src/services/api/providerConfig.ts b/src/services/api/providerConfig.ts index d0b4b2ec5..1ddda4b48 100644 --- a/src/services/api/providerConfig.ts +++ b/src/services/api/providerConfig.ts @@ -1,5 +1,5 @@ import { existsSync, readFileSync } from 'node:fs' -import { createHash } from 'node:crypto' +import { createHmac } from 'node:crypto' import { isIP } from 'node:net' import { homedir } from 'node:os' import { join } from 'node:path' @@ -210,8 +210,10 @@ type ModelDescriptor = { const LOCALHOST_HOSTNAMES = new Set(['localhost', '127.0.0.1', '::1']) function hashCacheScopePartition(value: unknown): string { - return createHash('sha256') - .update(JSON.stringify(value)) + // Credential-keyed fingerprint, not a password hash. Keep it deterministic + // because model discovery scopes are persisted across CLI runs. + return createHmac('sha256', JSON.stringify(value)) + .update('openclaude:additional-model-options:v1') .digest('hex') .slice(0, 16) } diff --git a/src/types/utils.types.test.ts b/src/types/utils.types.test.ts index ba520cb00..f63c218f2 100644 --- a/src/types/utils.types.test.ts +++ b/src/types/utils.types.test.ts @@ -57,7 +57,7 @@ function assertReadonlyCollectionTypes( readonlyTuple[0].a.push('mutates') // @ts-expect-error DeepImmutable preserves readonly tuple arity. - readonlyTuple[2] + void readonlyTuple[2] } void assertReadonlyCollectionTypes diff --git a/src/utils/diagnostics/issueReport.test.ts b/src/utils/diagnostics/issueReport.test.ts index c8c855478..aa4a7e0b0 100644 --- a/src/utils/diagnostics/issueReport.test.ts +++ b/src/utils/diagnostics/issueReport.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from "bun:test"; +import { marked } from "marked"; import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs"; import { homedir, tmpdir } from "node:os"; import { join } from "node:path"; @@ -113,7 +114,14 @@ describe("diagnostic issue report", () => { errors: [], }); + report.checks = [{ + label: "Path", + ok: true, + detail: String.raw`C:\tools\bin | next\|cell`, + }]; const markdown = formatIssueReportAsMarkdown(report); + const html = marked.parse(markdown) as string; + expect(html).toContain(String.raw`C:\tools\bin | next\|cell`); expect(markdown).toContain("# OpenClaude diagnostic report"); expect(markdown).toContain("## Summary"); diff --git a/src/utils/diagnostics/issueReport.ts b/src/utils/diagnostics/issueReport.ts index 39a683e49..74c1fc2c7 100644 --- a/src/utils/diagnostics/issueReport.ts +++ b/src/utils/diagnostics/issueReport.ts @@ -659,6 +659,7 @@ function formatStatus(ok: boolean): string { function tableEscape(value: string | number | boolean | null | undefined): string { return String(value ?? '') + .replace(/\\/g, '\\\\') .replace(/\|/g, '\\|') .replace(/\r?\n/g, ' ') } diff --git a/src/utils/permissions/planFilePath.test.ts b/src/utils/permissions/planFilePath.test.ts index a42fd6a91..369f8d24b 100644 --- a/src/utils/permissions/planFilePath.test.ts +++ b/src/utils/permissions/planFilePath.test.ts @@ -145,7 +145,7 @@ test('rejects unencoded-percent lookalikes the producer never emits', () => { const sub = join(PLANS, AGENT_PLANS_SUBDIR) for (const rawComponent of [ 'writer@100%', // canonical form is writer@100%25 - 'writer@a%2Fb'.replace('%2F', '%'), // writer@a%b -> canonical writer@a%25b + 'writer@a%b', // writer@a%b -> canonical writer@a%25b 'a%', // bare trailing percent 'a%zz', // percent not starting a known escape 'a%2', // truncated escape