mcp-briven-ask.test.ts182 lines · main
| 1 | import { describe, expect, test } from 'bun:test'; |
| 2 | |
| 3 | import { |
| 4 | BRIVEN_AREA_GUIDES, |
| 5 | BRIVEN_ASK_TOOLS, |
| 6 | DOCS_INDEX, |
| 7 | matchBrivenGuides, |
| 8 | redactSecrets, |
| 9 | topicKey, |
| 10 | } from './mcp-briven-ask.js'; |
| 11 | import { coerceGroundedAnswer, validateStoredAnswer } from './mcp-answer-writer.js'; |
| 12 | |
| 13 | describe('briven_ask reception desk — pure helpers', () => { |
| 14 | test('storage question matches the storage guide first', () => { |
| 15 | const guides = matchBrivenGuides('how do I upload files and get a public share link?'); |
| 16 | expect(guides.length).toBeGreaterThan(0); |
| 17 | expect(guides[0]!.id).toBe('storage'); |
| 18 | }); |
| 19 | |
| 20 | test('realtime question matches the realtime guide', () => { |
| 21 | const ids = matchBrivenGuides('can my app subscribe to live updates via websocket?').map( |
| 22 | (g) => g.id, |
| 23 | ); |
| 24 | expect(ids).toContain('realtime'); |
| 25 | }); |
| 26 | |
| 27 | test('migration question matches the migration guide', () => { |
| 28 | const ids = matchBrivenGuides('we want to migrate from supabase, how?').map((g) => g.id); |
| 29 | expect(ids).toContain('migration'); |
| 30 | }); |
| 31 | |
| 32 | test('rate-limit question matches usage-limits', () => { |
| 33 | const ids = matchBrivenGuides('we keep hitting the rate limit on bulk insert').map( |
| 34 | (g) => g.id, |
| 35 | ); |
| 36 | expect(ids).toContain('usage-limits'); |
| 37 | }); |
| 38 | |
| 39 | test('completely foreign question matches nothing (→ filed path)', () => { |
| 40 | expect(matchBrivenGuides('kubernetes ingress annotations for istio')).toEqual([]); |
| 41 | }); |
| 42 | |
| 43 | test('THE CONTRACT: every guide has all three parts + docs citation', () => { |
| 44 | for (const g of BRIVEN_AREA_GUIDES) { |
| 45 | expect(g.howBrivenWorksHere.length).toBeGreaterThan(40); |
| 46 | expect(g.whatOurToolsGiveYou.length).toBeGreaterThan(0); |
| 47 | expect(g.whatYouBuildInYourProject.length).toBeGreaterThan(0); |
| 48 | expect(g.docs).toStartWith('https://docs.briven.tech'); |
| 49 | } |
| 50 | }); |
| 51 | |
| 52 | test('guides and docs index never name internal vendors', () => { |
| 53 | const blob = (JSON.stringify(BRIVEN_AREA_GUIDES) + JSON.stringify(DOCS_INDEX)).toLowerCase(); |
| 54 | expect(blob).not.toContain('mittera'); |
| 55 | expect(blob).not.toContain('stripe'); |
| 56 | expect(blob).not.toContain('polar'); |
| 57 | }); |
| 58 | |
| 59 | test('docs index urls resolve to the docs host', () => { |
| 60 | for (const d of DOCS_INDEX) expect(d.slug).toStartWith('/'); |
| 61 | }); |
| 62 | |
| 63 | test('BRIVEN_ASK_TOOLS exports exactly briven_ask', () => { |
| 64 | expect([...BRIVEN_ASK_TOOLS]).toEqual(['briven_ask']); |
| 65 | }); |
| 66 | |
| 67 | // The exact class of question the Unleashed agent got wrong (probed for a |
| 68 | // raw-SQL endpoint, concluded "no path"). The new card must catch it. |
| 69 | test('a "read tables from a python app" question matches app-data-access', () => { |
| 70 | const ids = matchBrivenGuides('how do I read my database tables from a python app?').map( |
| 71 | (g) => g.id, |
| 72 | ); |
| 73 | expect(ids).toContain('app-data-access'); |
| 74 | }); |
| 75 | |
| 76 | test('a "call briven from go with my server key" question matches app-data-access', () => { |
| 77 | const ids = matchBrivenGuides('how do I connect from my go backend using my server key?').map( |
| 78 | (g) => g.id, |
| 79 | ); |
| 80 | expect(ids).toContain('app-data-access'); |
| 81 | }); |
| 82 | |
| 83 | test('setup vs connect question matches cli-setup-connect', () => { |
| 84 | const ids = matchBrivenGuides( |
| 85 | 'should I use briven setup or briven connect for an existing project?', |
| 86 | ).map((g) => g.id); |
| 87 | expect(ids).toContain('cli-setup-connect'); |
| 88 | }); |
| 89 | |
| 90 | test('doltgres database question still matches database guide', () => { |
| 91 | const ids = matchBrivenGuides('is the database Doltgres or stock Postgres?').map((g) => g.id); |
| 92 | expect(ids).toContain('database'); |
| 93 | }); |
| 94 | |
| 95 | test('passkey / Face ID auth question matches auth guide', () => { |
| 96 | const ids = matchBrivenGuides( |
| 97 | 'passkey Face ID returns 404 on generate authenticate options', |
| 98 | ).map((g) => g.id); |
| 99 | expect(ids).toContain('auth'); |
| 100 | }); |
| 101 | |
| 102 | test('auth guide documents GET passkey options not POST', () => { |
| 103 | const auth = BRIVEN_AREA_GUIDES.find((g) => g.id === 'auth')!; |
| 104 | expect(auth.howBrivenWorksHere).toContain('generate-authenticate-options'); |
| 105 | expect(auth.howBrivenWorksHere.toLowerCase()).toContain('post = 404'); |
| 106 | }); |
| 107 | }); |
| 108 | |
| 109 | describe('briven_ask self-growing KB — helpers', () => { |
| 110 | test('topicKey is invariant to word order, filler words, and casing', () => { |
| 111 | const a = topicKey('How do I read my tables from a Python app?'); |
| 112 | const b = topicKey('read tables in a python app'); |
| 113 | expect(a).toBe(b); |
| 114 | }); |
| 115 | |
| 116 | test('topicKey is deterministic and non-empty even for all-filler input', () => { |
| 117 | expect(topicKey('how do I do this?').length).toBeGreaterThan(0); |
| 118 | expect(topicKey('python tables')).toBe(topicKey('tables python')); |
| 119 | }); |
| 120 | |
| 121 | test('coerceGroundedAnswer accepts a well-formed grounded object', () => { |
| 122 | const answer = coerceGroundedAnswer({ |
| 123 | grounded: true, |
| 124 | howBrivenWorksHere: 'apps call functions; there is no raw sql endpoint by design.', |
| 125 | whatOurToolsGiveYou: ['POST /v1/projects/<id>/functions/<name> with a brk_ key', ''], |
| 126 | whatYouBuildInYourProject: ['write a saveRun function and call it over http'], |
| 127 | docs: 'https://docs.briven.tech/functions', |
| 128 | }); |
| 129 | expect(answer).not.toBeNull(); |
| 130 | expect(answer!.whatOurToolsGiveYou).toHaveLength(1); // blank stripped |
| 131 | expect(answer!.docs).toBe('https://docs.briven.tech/functions'); |
| 132 | }); |
| 133 | |
| 134 | test('coerceGroundedAnswer rejects a decline (grounded:false)', () => { |
| 135 | expect(coerceGroundedAnswer({ grounded: false })).toBeNull(); |
| 136 | }); |
| 137 | |
| 138 | test('coerceGroundedAnswer rejects a substanceless answer', () => { |
| 139 | expect( |
| 140 | coerceGroundedAnswer({ grounded: true, howBrivenWorksHere: 'no', whatOurToolsGiveYou: [] }), |
| 141 | ).toBeNull(); |
| 142 | }); |
| 143 | |
| 144 | test('topicKey stays non-empty for a degenerate all-short-token question', () => { |
| 145 | // "I a b" has no multi-char word tokens; must still yield a stable key. |
| 146 | expect(topicKey('I a b').length).toBeGreaterThan(0); |
| 147 | }); |
| 148 | |
| 149 | test('validateStoredAnswer accepts a stored answer (no grounded flag) and rejects a blank one', () => { |
| 150 | const good = validateStoredAnswer({ |
| 151 | howBrivenWorksHere: 'apps call functions; there is no raw sql endpoint by design.', |
| 152 | whatOurToolsGiveYou: ['POST /v1/projects/<id>/functions/<name>'], |
| 153 | whatYouBuildInYourProject: [], |
| 154 | docs: 'https://docs.briven.tech/functions', |
| 155 | }); |
| 156 | expect(good).not.toBeNull(); |
| 157 | // A drifted/hand-seeded blank row must be treated as a miss, not served. |
| 158 | expect( |
| 159 | validateStoredAnswer({ howBrivenWorksHere: '', whatOurToolsGiveYou: [] }), |
| 160 | ).toBeNull(); |
| 161 | }); |
| 162 | |
| 163 | test('redactSecrets strips briven keys and long opaque tokens from a stored question', () => { |
| 164 | const scrubbed = redactSecrets( |
| 165 | 'why does brk_01ABCDEF2345 fail and my token pk_briven_mcp_ZZZ9998887 too?', |
| 166 | ); |
| 167 | expect(scrubbed).not.toContain('01ABCDEF2345'); |
| 168 | expect(scrubbed).not.toContain('ZZZ9998887'); |
| 169 | expect(scrubbed).toContain('[redacted]'); |
| 170 | }); |
| 171 | |
| 172 | test('coerceGroundedAnswer repairs a missing/invalid docs url', () => { |
| 173 | const answer = coerceGroundedAnswer({ |
| 174 | grounded: true, |
| 175 | howBrivenWorksHere: 'this is a sufficiently long grounded explanation of the platform.', |
| 176 | whatOurToolsGiveYou: ['use the mcp data tools'], |
| 177 | whatYouBuildInYourProject: [], |
| 178 | docs: 'not-a-url', |
| 179 | }); |
| 180 | expect(answer!.docs).toBe('https://docs.briven.tech'); |
| 181 | }); |
| 182 | }); |