Skip to content
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- **`verify` auto-publishes by default** — successful runs upload to My Verifications as `private`; opt-out with `--no-publish`; pass `--vis unlisted` or `--vis public` to widen visibility. (#425)
- **`verify` auto-publishes by default** — successful runs upload to My Verifications as `private`; opt-out with `--local-only`; pass `--vis unlisted` or `--vis public` to widen visibility. (#425)
- **Per-attachment assets in `verify` output** — `verify-response.json` now includes an `attachments` map (pageImages, originalDownload) alongside `verifications` when any attachment returned assets. (#430)
- **CDN blink animation synced with React** — CDN now imports `BLINK_*` constants from the same source as the React component, fixing a 180 ms vs 120 ms enter-duration discrepancy; implements the full three-stage enter animation with a `cancelBlink()` guard. (#427)
- **Billing copy** — quota-exceeded messages and the dashboard description now reflect subscription tiers (Standard 20/week at $20/mo, Pro unlimited at $200/mo). (#427)
Expand Down
47 changes: 39 additions & 8 deletions src/__tests__/citationParser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,30 @@ ${CITATION_DATA_END_DELIMITER}`;
expect(result.citations[0].source_context).toContain("Line one");
});

it("repairs literal newlines inside JSON string values", () => {
const response = `Multi-line content [1].

${CITATION_DATA_START_DELIMITER}
[
{
"id": 1,
"attachment_id": "doc789",
"source_context": "Line one
Line two
Line three",
"source_match": "Line two",
"page_id": "page_number_1_index_0"
}
]
${CITATION_DATA_END_DELIMITER}`;

const result = parseCitationData(response);

expect(result.success).toBe(true);
expect(result.citations.length).toBe(1);
expect(result.citations[0].source_context).toContain("Line one");
});

it("handles multiple citations in single sentence", () => {
const response = `Revenue was $1B [1] with profit of $100M [2] in Q4 [3].

Expand Down Expand Up @@ -162,6 +186,19 @@ ${CITATION_DATA_START_DELIMITER}
expect(result.citations.length).toBe(1);
});

it("handles malformed end delimiter variant", () => {
const response = `Test [1].

${CITATION_DATA_START_DELIMITER}
[{"id": 1, "attachment_id": "a", "source_context": "test", "source_match": "test"}]
<<</CITATION_DATA>>>`;

const result = parseCitationData(response);

expect(result.success).toBe(true);
expect(result.citations.length).toBe(1);
});

it("handles empty citation block", () => {
const response = `No citations here.

Expand All @@ -175,19 +212,13 @@ ${CITATION_DATA_END_DELIMITER}`;
expect(result.citations.length).toBe(0);
});

it("fails when citation block is present but whitespace-only", () => {
// A bare <<<CITATION_DATA>>> block with no content is an authoring mistake
// (e.g. unfilled template placeholder), not a legitimate empty result.
// An empty array `[]` is fine; whitespace-only is a failure.
// See plans/noble-skipping-wolf.md for the parallel-agent merge failure this caught.
it("treats whitespace-only citation blocks as recoverable empties", () => {
const response = `Body text.\n\n${CITATION_DATA_START_DELIMITER}\n\n${CITATION_DATA_END_DELIMITER}\n`;

const result = parseCitationData(response);

expect(result.success).toBe(false);
expect(result.error).toMatch(/empty/i);
expect(result.success).toBe(true);
expect(result.citations.length).toBe(0);
// visibleText is still extracted even on failure
expect(result.visibleText).toBe("Body text.");
});

Expand Down
16 changes: 16 additions & 0 deletions src/__tests__/cite.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ describe("extractMarkersFromBody", () => {
expect(markers[0].claimText).toBe("Discount Rate");
});

it("preserves alternate labels for a reused cite id", () => {
const body = "[Horizontal Boundaries](cite:1) and [Vertical Boundaries](cite:1)";
const markers = extractMarkersFromBody(body);
expect(markers).toHaveLength(1);
expect(markers[0].claimText).toBe("Horizontal Boundaries");
expect(markers[0].claimTextVariants).toEqual(["Vertical Boundaries"]);
});

it("extracts double-quoted anchor hint", () => {
const body = '[terminates](cite:3 "automatically terminate")';
const markers = extractMarkersFromBody(body);
Expand Down Expand Up @@ -108,6 +116,14 @@ describe("extractMarkersFromBody", () => {
expect(markers).toHaveLength(1);
expect(markers[0].claimText).toBe("Rate");
});

it("preserves alternate labels for reused bold markers", () => {
const body = "**Horizontal Boundaries** [1] and **Vertical Boundaries** [1]";
const markers = extractMarkersFromBody(body);
expect(markers).toHaveLength(1);
expect(markers[0].claimText).toBe("Horizontal Boundaries");
expect(markers[0].claimTextVariants).toEqual(["Vertical Boundaries"]);
});
});

// ── getAllLines ───────────────────────────────────────────────────
Expand Down
Loading
Loading