Files
redefined-designs/backend/tests/unit/draftPrompt.test.ts
T
bermudalambandClaude Opus 5 ef5968383b feat(intake): tell the model to describe rather than invent (#223)
Pure and separately tested, because this is where the correctness of every draft is decided. Nothing downstream can distinguish an observed detail from an invented one — the description arrives as prose either way — so the instruction is the only place that distinction can be enforced, and the tests assert it is actually present.

On a one-of-a-kind item an invented "1930s hand-thrown stoneware" is not a cosmetic error but a false claim on a public shop, and the shop answers for it rather than the model. Visible damage is called out for the same reason in reverse: a buyer finding a chip on arrival is worse than reading about it beforehand.

Categories and tags are listed rather than described, so the model chooses from what exists instead of inventing a taxonomy the storefront filters know nothing about, and declining is explicitly allowed so a model with no matching option does not pick the closest wrong one.

The note is quoted and labelled as the sender's rather than merged into the instruction: it is untrusted text from an unauthenticated stranger and should read as evidence to weigh, not as something the shop asserts. A whitespace-only note counts as no note.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 19:29:05 -05:00

72 lines
2.6 KiB
TypeScript

import { buildSystemPrompt, buildUserContent } from '../../src/intake/draftPrompt';
describe('buildSystemPrompt', () => {
const prompt = buildSystemPrompt(['Ceramics', 'Textiles'], ['vintage', 'blue']);
// The whole reason this function is tested rather than inlined. On a
// one-of-a-kind item an invented age or maker is a false claim on a
// storefront, and nothing downstream can tell an invented detail from an
// observed one.
it('forbids inventing what is neither visible nor in the note', () => {
expect(prompt).toMatch(/do not (state|invent)/i);
expect(prompt).toMatch(/material/i);
expect(prompt).toMatch(/age/i);
expect(prompt).toMatch(/maker/i);
expect(prompt).toMatch(/provenance/i);
});
it('offers the categories it may choose from', () => {
expect(prompt).toContain('Ceramics');
expect(prompt).toContain('Textiles');
});
it('offers the tags it may choose from', () => {
expect(prompt).toContain('vintage');
expect(prompt).toContain('blue');
});
// Otherwise a model with no matching option picks the closest wrong one.
it('permits declining a category', () => {
expect(prompt).toMatch(/null/i);
});
it('survives a shop with no categories or tags yet', () => {
expect(() => buildSystemPrompt([], [])).not.toThrow();
});
});
describe('buildUserContent', () => {
const photo = { mediaType: 'image/jpeg', base64: 'AAAA' };
type Block = { type: string; source?: { media_type: string; data: string } };
it('sends every photo as an image block', () => {
const images = (buildUserContent([photo, photo], 'a note') as Block[]).filter(
(block) => block.type === 'image'
);
expect(images).toHaveLength(2);
expect(images[0]?.source?.media_type).toBe('image/jpeg');
expect(images[0]?.source?.data).toBe('AAAA');
});
it('includes the note verbatim', () => {
const text = JSON.stringify(buildUserContent([photo], 'Chipped base, bought 1998'));
expect(text).toContain('Chipped base, bought 1998');
});
// A submission with no note is ordinary — the field is optional — and the
// model has to be told so rather than left to read an empty string as a fact
// about the item.
it('says so when there is no note', () => {
const text = JSON.stringify(buildUserContent([photo], null));
expect(text).toMatch(/no note/i);
});
// A whitespace-only note is the same thing as no note. Sending it would let
// the model treat an accidental space as something the sender meant to say.
it('treats a blank note as no note', () => {
const text = JSON.stringify(buildUserContent([photo], ' '));
expect(text).toMatch(/no note/i);
});
});