diff --git a/backend/__tests__/unit/config/nativeAgents.scoutLinearCard.test.js b/backend/__tests__/unit/config/nativeAgents.scoutLinearCard.test.js new file mode 100644 index 0000000000..44213c81fe --- /dev/null +++ b/backend/__tests__/unit/config/nativeAgents.scoutLinearCard.test.js @@ -0,0 +1,39 @@ +/** + * Scout posts Linear list results as a kind:'ui' card (TASK-573, D98). + * + * The prompt is the only thing that tells Scout the card exists, so these tests + * pin three things: the catalogue tuple it quotes is the live revision file (not + * a copy that can drift), the example source it teaches passes the same + * validator a real write goes through, and the rule is present at all. + */ +const { scoutApp } = require('../../../config/native-agents/scout'); +const revision = require('../../../config/uiCardCatalogueRevision.json'); +const { validateAgentCardPayload } = require('../../../services/uiCardValidation'); + +const prompt = scoutApp.systemPrompt; + +// The example copied from the prompt, so a changed example fails here too. +const EXAMPLE_SOURCE = 'root = Card("Linear issues", [Table([Col("Issue"), Col("Status")], ' + + '[["LIN-12 Fix login", "In progress"]])])'; + +describe('Scout Linear list results as a card', () => { + it('teaches the rule with the live catalogue tuple', () => { + expect(prompt).toContain('LINEAR LIST RESULTS'); + expect(prompt).toContain(`"catalogId":"${revision.catalogId}"`); + expect(prompt).toContain(`"catalogRev":${revision.catalogRev}`); + expect(prompt).toContain(`"catalogHash":"${revision.catalogHash}"`); + }); + + it('teaches an example the card validator accepts', () => { + expect(prompt).toContain(EXAMPLE_SOURCE); + const payload = { + ...revision, kind: 'ui', v: 1, lang: 'openui@1', source: EXAMPLE_SOURCE, + }; + expect(() => validateAgentCardPayload('card', payload, 'LIN-12 Fix login (In progress)')).not.toThrow(); + }); + + it('names the card as the path for list results, with a text fallback', () => { + expect(prompt).toContain('messageType "card"'); + expect(prompt).toContain('fallback every reader sees'); + }); +}); diff --git a/backend/config/native-agents/scout.ts b/backend/config/native-agents/scout.ts index 3b63a15c02..afe953b422 100644 --- a/backend/config/native-agents/scout.ts +++ b/backend/config/native-agents/scout.ts @@ -1,5 +1,6 @@ import type { NativeAgentDefinition } from './types'; import { composePrompt } from './housePreamble'; +import UI_CARD_REVISION from '../uiCardCatalogueRevision.json'; /** * Scout — the per-user onboarding admin agent (retention plan D4; named @@ -294,7 +295,20 @@ export const scoutApp = { + '- They want Commonly to run an agent for them, or would rather not ' + 'install anything → commonly_offer_hosted_agent.\n' + '- commonly_read_context before answering questions about what happened ' - + 'here.', + + 'here.\n' + + '\n' + + 'LINEAR LIST RESULTS: when a linear.* list tool (for example linear.list_issues) ' + + 'returns issues, answer with one card, not bullet prose. Call commonly_post_message ' + + 'with messageType "card". Its content is a short plain-text list of the same ' + + 'issues, which is the fallback every reader sees. Its payload is ' + + `{"kind":"ui","v":1,"lang":"openui@1","catalogId":"${UI_CARD_REVISION.catalogId}",` + + `"catalogRev":${UI_CARD_REVISION.catalogRev},"catalogHash":"${UI_CARD_REVISION.catalogHash}",` + + '"source":""}. The source uses only the catalogue components ' + + 'Card, Table, Col and Text, in one statement set, for example: ' + + 'root = Card("Linear issues", [Table([Col("Issue"), Col("Status")], ' + + '[["LIN-12 Fix login", "In progress"]])]). Every row needs one cell per Col. ' + + 'Show at most 10 rows and say how many more there are in a Text line. Keep each ' + + 'cell under 80 characters. Copy the catalogue fields exactly.', 'Never claim the user\'s own agent is connected or working — the ' + 'connect page verifies that, not you. Point them there instead. And ' + 'remember you are the product\'s first impression: an invented product '