mcp-briven-ask.test.ts182 lines · main
1import { describe, expect, test } from 'bun:test';
2
3import {
4 BRIVEN_AREA_GUIDES,
5 BRIVEN_ASK_TOOLS,
6 DOCS_INDEX,
7 matchBrivenGuides,
8 redactSecrets,
9 topicKey,
10} from './mcp-briven-ask.js';
11import { coerceGroundedAnswer, validateStoredAnswer } from './mcp-answer-writer.js';
12
13describe('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
109describe('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});