fix(hub): fence close must reject info strings per CommonMark spec

The closing fence regex was not checking for an empty info string,
allowing e.g. ```python to incorrectly close an open fence. Also
adds missing test for tool_execution_update passthrough.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
yushen 2026-02-05 22:23:55 +08:00
parent fc0c1781c3
commit efb1495326
3 changed files with 27 additions and 2 deletions

View file

@ -223,6 +223,23 @@ describe("BlockChunker", () => {
expect(chunk + remainder).toBe(text);
});
it("does not treat fence with info string as closing fence", () => {
const chunker = new BlockChunker(cfg({ minChars: 10, maxChars: 500 }));
// The second ```python should NOT close the first fence (CommonMark: closing fence has no info string)
const text = "Before.\n\n```python\ncode line 1\n```python\ncode line 2\n```\n\nAfter text here.";
const result = chunker.tryChunk(text);
expect(result).not.toBeNull();
const chunk = result!.chunk;
// The split should not land between ```python and ```python (inside fence)
// It should either be before the fence or after the closing ```
const fenceOpens = (chunk.match(/```python/g) || []).length;
const fenceCloses = (chunk.match(/^```$/gm) || []).length;
if (fenceOpens > 0) {
// If chunk includes opening fences, it must include the real close
expect(fenceCloses).toBeGreaterThanOrEqual(1);
}
});
it("handles multiple sequential code blocks", () => {
const chunker = new BlockChunker(cfg({ minChars: 10, maxChars: 500 }));
const text = "```js\nfoo()\n```\n\n```py\nbar()\n```\n\nEnd.";

View file

@ -51,8 +51,8 @@ function detectFenceAt(text: string, upTo: number): FenceInfo | null {
if (openFence === null) {
// Opening a new fence
openFence = { marker, lang };
} else if (markerChar === openFence.marker[0] && marker.length >= openFence.marker.length) {
// Closing the current fence (same char, at least as many chars)
} else if (markerChar === openFence.marker[0] && marker.length >= openFence.marker.length && lang === "") {
// Closing the current fence (same char, at least as many chars, no info string per CommonMark)
openFence = null;
}
// Otherwise: different char or shorter marker, not a close — ignore

View file

@ -112,6 +112,14 @@ describe("MessageAggregator", () => {
expect(onBlock).not.toHaveBeenCalled();
});
it("passes through tool_execution_update immediately", () => {
const agg = new MessageAggregator(smallConfig(), onBlock, onPassthrough);
const event = { type: "tool_execution_update", toolCallId: "tool-1", content: "output" } as unknown as AgentEvent;
agg.handleEvent(event);
expect(onPassthrough).toHaveBeenCalledWith(event);
expect(onBlock).not.toHaveBeenCalled();
});
it("passes through compaction_start immediately", () => {
const agg = new MessageAggregator(smallConfig(), onBlock, onPassthrough);
const event = makeCompactionStart();