test(intake): the second review pass, and CLAUDE.md for the rules it set
ci / lint-test (push) Successful in 45s
ci / lint-test (push) Successful in 45s
Six of the nine fixes have a test that fails against the previous tree: the metadata row's currency, the Create-path hold, two concurrent batches booking once, mrkdwn escaping including plain_text, the escaped currency, and candidate-row currency. Three do not, and the reason is the same each time — the window needs a crash or a lost race that a test cannot force. Asserted instead: a stale metadataId falls back to the live row rather than reporting a phantom write; two writes racing to create a metadata row both resolve to one; the create path records a baseline at all. All three passed before as well.
This commit is contained in:
@@ -234,7 +234,7 @@ describe("cards carry the C5 action ids and JSON values", () => {
|
||||
expect(ids).toContain('"action_id":"intake_meal"');
|
||||
});
|
||||
|
||||
it("mrkdwn escapes the names it did not write; the plain_text button keeps them raw", () => {
|
||||
it("every value the card did not write is escaped, plain_text included", () => {
|
||||
const hostile = { ...c, merchant: "Ben & Jerry's <fake>" };
|
||||
const { text, blocks } = candidatesCard(hostile, [
|
||||
{ transaction_id: 42, description: "SQ *BEN & JERRY <AU>", transaction_date: "2026-08-17", amount: "19.90" },
|
||||
@@ -242,24 +242,38 @@ describe("cards carry the C5 action ids and JSON values", () => {
|
||||
const sections = JSON.stringify(blocks.filter((b) => (b as Block).type === "section"));
|
||||
expect(sections).toContain("Ben & Jerry's <fake>");
|
||||
expect(sections).toContain("SQ *BEN & JERRY <AU>");
|
||||
expect(sections).not.toContain("<fake>");
|
||||
expect(text).toContain("Ben & Jerry's <fake>");
|
||||
// The Create button's label is plain_text — Slack shows entities literally
|
||||
// there, so escaping it would print "Ben & Jerry's" on the button.
|
||||
const actions = JSON.stringify(blocks.filter((b) => (b as Block).type === "actions"));
|
||||
expect(actions).toContain("Ben & Jerry's <fake>");
|
||||
// plain_text disables formatting, not entity decoding — &, < and > are
|
||||
// control characters everywhere in a Slack payload.
|
||||
const whole = JSON.stringify(blocks);
|
||||
expect(whole).not.toContain("<fake>");
|
||||
expect(whole).not.toContain("<AU>");
|
||||
});
|
||||
|
||||
it("a foreign receipt is not printed as dollars", () => {
|
||||
it("the currency is model output too, so it is escaped like any other", () => {
|
||||
// parseExtraction accepts any string for `currency`; unescaped, `<@U123>`
|
||||
// would be a live mention on every card the batch produces.
|
||||
const { text } = enrichedCard({ ...c, currency: "<@U123>", transactionId: 7 });
|
||||
expect(text).toContain("<@U123> 22.40");
|
||||
expect(text).not.toContain("<@U123>");
|
||||
});
|
||||
|
||||
it("a foreign receipt is not printed as dollars — header and candidate rows alike", () => {
|
||||
expect(enrichedCard({ ...c, currency: "USD", transactionId: 7 }).text).toBe(
|
||||
"Added 2 items to your USD 22.40 Uber Eats (Burgertory (Werribee)) order ✓"
|
||||
);
|
||||
expect(enrichedCard({ ...c, currency: "AUD", transactionId: 7 }).text).toContain("$22.40");
|
||||
expect(enrichedCard({ ...c, transactionId: 7 }).text).toContain("$22.40");
|
||||
// A candidate row is a ledger transaction and carries its own currency; it
|
||||
// used to print "$" beneath a header reading "USD 22.40".
|
||||
const { blocks } = candidatesCard({ ...c, currency: "USD" }, [
|
||||
{ transaction_id: 42, description: "Order - Burgertory", transaction_date: "2026-08-17", amount: "19.90", currency: "USD" },
|
||||
]);
|
||||
expect(JSON.stringify(blocks)).toContain("USD 19.90");
|
||||
});
|
||||
});
|
||||
|
||||
describe("liteLLMExtractor — only a finished reply is accepted", () => {
|
||||
describe("liteLLMExtractor — a finish_reason that is present must be `stop`", () => {
|
||||
const good = { merchant: "Burgertory (Werribee)", order_date: "2026-08-19", total: 22.4, subtotal: 28, currency: "AUD", items: [], adjustments: [] };
|
||||
const extractor = (finish_reason?: string) =>
|
||||
liteLLMExtractor({
|
||||
@@ -271,12 +285,13 @@ describe("liteLLMExtractor — only a finished reply is accepted", () => {
|
||||
})) as unknown as typeof fetch,
|
||||
});
|
||||
|
||||
it("accepts `stop`, and a reply that reports no reason at all", async () => {
|
||||
it("accepts `stop`, and — on purpose — a reply that reports no reason at all", async () => {
|
||||
expect((await extractor("stop")([{ kind: "text", text: "x", name: "x" }])).total).toBe(22.4);
|
||||
expect((await extractor()([{ kind: "text", text: "x", name: "x" }])).total).toBe(22.4);
|
||||
});
|
||||
|
||||
it("rejects any other reason, naming it — the strict schema only held for a reply that finished", async () => {
|
||||
// An absent reason is NOT rejected: see the comment in intake-extract.ts.
|
||||
await expect(extractor("length")([{ kind: "text", text: "x", name: "x" }])).rejects.toThrow(ExtractionError);
|
||||
await expect(extractor("length")([{ kind: "text", text: "x", name: "x" }])).rejects.toThrow(/too long/);
|
||||
await expect(extractor("content_filter")([{ kind: "text", text: "x", name: "x" }])).rejects.toThrow(/content_filter/);
|
||||
|
||||
Reference in New Issue
Block a user