diff --git a/packages/opencode/test/provider/harmoniqs-no-tools.test.ts b/packages/opencode/test/provider/harmoniqs-no-tools.test.ts index b3b25aeec..2227ef6e5 100644 --- a/packages/opencode/test/provider/harmoniqs-no-tools.test.ts +++ b/packages/opencode/test/provider/harmoniqs-no-tools.test.ts @@ -99,25 +99,33 @@ describe("LLMRequestPrep.prepare - harmoniqs idempotency", () => { } } + // The per-provider header union carries the static shape only — harmoniqs + // injects Idempotency-Key at runtime (#374) — so tests index through a + // widened record rather than the narrowed type. + async function prepareHeaders(model: any) { + const result = await Effect.runPromise(LLMRequestPrep.prepare(baseInput(model))) + return result.headers as Record + } + test("sends Idempotency-Key header for harmoniqs provider", async () => { - const result = await Effect.runPromise(LLMRequestPrep.prepare(baseInput(harmoniqsModel))) - expect(result.headers["Idempotency-Key"]).toBeDefined() - expect(result.headers["Idempotency-Key"]).toMatch(/^amicode:/) + const headers = await prepareHeaders(harmoniqsModel) + expect(headers["Idempotency-Key"]).toBeDefined() + expect(headers["Idempotency-Key"]).toMatch(/^amicode:/) }) test("sends X-Session-Id header for harmoniqs provider", async () => { - const result = await Effect.runPromise(LLMRequestPrep.prepare(baseInput(harmoniqsModel))) - expect(result.headers["X-Session-Id"]).toBe(sessionID) + const headers = await prepareHeaders(harmoniqsModel) + expect(headers["X-Session-Id"]).toBe(sessionID) }) test("does not send Idempotency-Key for non-harmoniqs providers", async () => { - const result = await Effect.runPromise(LLMRequestPrep.prepare(baseInput(anthropicModel))) - expect(result.headers["Idempotency-Key"]).toBeUndefined() + const headers = await prepareHeaders(anthropicModel) + expect(headers["Idempotency-Key"]).toBeUndefined() }) test("generates unique Idempotency-Key per request", async () => { - const a = await Effect.runPromise(LLMRequestPrep.prepare(baseInput(harmoniqsModel))) - const b = await Effect.runPromise(LLMRequestPrep.prepare(baseInput(harmoniqsModel))) - expect(a.headers["Idempotency-Key"]).not.toBe(b.headers["Idempotency-Key"]) + const a = await prepareHeaders(harmoniqsModel) + const b = await prepareHeaders(harmoniqsModel) + expect(a["Idempotency-Key"]).not.toBe(b["Idempotency-Key"]) }) }) diff --git a/packages/ui/src/context/marked-math.test.ts b/packages/ui/src/context/marked-math.test.ts index 05338bcb2..313cd6e2b 100644 --- a/packages/ui/src/context/marked-math.test.ts +++ b/packages/ui/src/context/marked-math.test.ts @@ -17,6 +17,25 @@ test("renders tight and loose-inner single-$ math", async () => { expect(await parse("set $\\delta/2\\pi = 0.1$ MHz")).toContain('class="katex"') }) +test("renders digit-led single-$ inline math", async () => { + // #375 — the open-side digit guard rejected any math starting with a digit, + // which is the standard physics reporting shape ($3.53\times10^{-5}$) + expect(await parse("nice determinism check ($3.53\\times10^{-5}$ — free)")).toContain('class="katex"') + expect(await parse("floor through the mock seam ($3.1\\times10^{-3}$, T1-dominated)")).toContain( + 'class="katex"', + ) + expect(await parse("infidelity $10^{-5}$ at best")).toContain('class="katex"') + expect(await parse("$0.99$-style fidelity")).toContain('class="katex"') +}) + +test("keeps tight digit-pair currency literal under the close-side guard", async () => { + // Pandoc rule: the closing $ must not be followed by a digit — the first $ + // in "$30-and-$50" closes against "3", so the pair stays literal + const html = await parse("prices: $30-and-$50 pairs") + expect(html).not.toContain('class="katex"') + expect(html).toContain("$30-and-$50") +}) + test("keeps currency and env-var dollars literal", async () => { const prices = await parse("costs $5 and $10 total") expect(prices).not.toContain('class="katex"') @@ -66,6 +85,10 @@ test("renderMathInText renders single-$ inline and spares prose dollars", () => expect(prices).toContain("$5") expect(prices).toContain("$10") + // #375 — digit-led math through the global replace, tight pairs still literal + expect(renderMathInText("check ($3.53\\times10^{-5}$ — free)")).toContain('class="katex"') + expect(renderMathInText("prices: $30-and-$50 pairs")).not.toContain('class="katex"') + expect(renderMathInText("$$\n\\hat H\n$$")).toContain("katex-display") expect(renderMathInText("inline \\(\\Omega_x\\) math")).toContain('class="katex"') }) diff --git a/packages/ui/src/context/marked.tsx b/packages/ui/src/context/marked.tsx index e4a00db7d..47fda7846 100644 --- a/packages/ui/src/context/marked.tsx +++ b/packages/ui/src/context/marked.tsx @@ -420,12 +420,15 @@ registerCustomTheme("OpenCode", () => Promise.resolve(OpenCodeTheme)) // Single-$ inline math — restored after #34850 removed it for currency false // positives. Pandoc-style tight delimiters (no whitespace just inside either -// $), no digit-led content (so $5, and $30-and-$50 pairs, stay literal), no -// $$ adjacency, no escaped \$. One regex family, three shapes: the tokenizer -// start hint, the anchored tokenizer match, and the global replace below. +// $): the opening $ drops the digit-led guard so $3.53\times10^{-5}$ renders; +// instead the closing $ must not be followed by a digit (Pandoc's actual +// currency rule — $30-and-$50 pairs stay literal because the first closing $ +// abuts a digit). No $$ adjacency, no escaped \$. One regex family, three +// shapes: the tokenizer start hint, the anchored tokenizer match, and the +// global replace below. const singleDollarStartRegex = /(?