Skip to content

Commit 9ea663b

Browse files
committed
fix(search): enforce retrieval budgets in integration coverage
1 parent 22985e6 commit 9ea663b

5 files changed

Lines changed: 78 additions & 51 deletions

File tree

.github/workflows/test-build.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,12 @@ jobs:
182182
lib/memory/message-provenance.postgres.test.ts
183183
executor/handlers/agent/memory-harness.postgres.test.ts
184184
185+
- name: Verify Search vector projection upgrade in PostgreSQL
186+
working-directory: packages/db
187+
env:
188+
KNOWLEDGE_ACL_TEST_DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/sim_auth_scim
189+
run: bunx vitest run script-migrations/0016_backfill_search_vectors.postgres.test.ts
190+
185191
- name: Verify Search progress, pagination, and outbox scheduling in PostgreSQL
186192
working-directory: apps/sim
187193
env:

apps/sim/lib/knowledge/__integration__/search-latency.integration.ts

Lines changed: 59 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ import {
3636
createKnowledgeAclFixtureIds,
3737
seedKnowledgeAclFixture,
3838
} from '@/lib/knowledge/__integration__/seed-source-access-fixture'
39-
import { searchScopedKnowledge } from '@/lib/knowledge/application/workspace-search'
4039
import {
4140
SearchBudget,
4241
SearchDeadlineError,
@@ -166,6 +165,9 @@ const diagnosticSchema = z
166165
surface: z.enum(['dashboard', 'copilot']),
167166
outcome: z.literal('success'),
168167
elapsedMs: z.number(),
168+
vectorBudgetMs: z.number().positive(),
169+
retrievalStatus: z.enum(['complete', 'partial']),
170+
timedOutLegs: z.array(z.enum(['vector', 'keyword', 'tags'])),
169171
toolResultBytes: z.number().int().nonnegative().optional(),
170172
passageBytes: z.number().int().nonnegative().optional(),
171173
maxPassageBytes: z.number().int().nonnegative().optional(),
@@ -217,6 +219,45 @@ async function search(
217219
)
218220
}
219221

222+
async function searchDashboard() {
223+
const authenticate = vi.spyOn(internalSessionAuth, 'authenticate').mockResolvedValue({
224+
kind: 'session',
225+
userId: ids.aliceId,
226+
sessionId: 'fixture-dashboard',
227+
})
228+
try {
229+
const response = await searchRoute(
230+
new NextRequest('http://localhost/api/knowledge/search', {
231+
method: 'POST',
232+
headers: { 'content-type': 'application/json' },
233+
body: JSON.stringify({
234+
workspaceId: ids.workspaceId,
235+
query: 'Orion deployment',
236+
topK: 15,
237+
}),
238+
})
239+
)
240+
expect(response.status).toBe(200)
241+
return {
242+
success: true as const,
243+
data: workspaceKnowledgeSearchDataSchema.parse((await response.json()).data),
244+
}
245+
} finally {
246+
authenticate.mockRestore()
247+
}
248+
}
249+
250+
/** Allow either index or filtered plans, but require successful retrieval within the real surface budget. */
251+
function expectCompleteVectorSearch(diagnostics: z.infer<typeof diagnosticSchema>) {
252+
const budget = diagnostics.surface === 'dashboard' ? 3000 : 8000
253+
expect(diagnostics).toMatchObject({
254+
vectorBudgetMs: budget,
255+
retrievalStatus: 'complete',
256+
timedOutLegs: [],
257+
})
258+
expect(diagnostics.stages.vector.totalMs).toBeLessThanOrEqual(budget)
259+
}
260+
220261
async function sample(label: string, run: () => ReturnType<typeof search>) {
221262
captured.length = 0
222263
diagnosticLog?.mockClear()
@@ -614,11 +655,6 @@ describe.skipIf(!enabled)('Assistant search latency on a realistic indexed corpu
614655
'returns incomplete dashboard coverage when %s SQL branches exceed their deadline',
615656
async (delayedLegs) => {
616657
diagnosticLog?.mockClear()
617-
const authenticate = vi.spyOn(internalSessionAuth, 'authenticate').mockResolvedValue({
618-
kind: 'session',
619-
userId: ids.aliceId,
620-
sessionId: 'fixture-dashboard',
621-
})
622658
const query = SearchBudget.prototype.query
623659
const delayed = vi.spyOn(SearchBudget.prototype, 'query').mockImplementation(function <T>(
624660
this: SearchBudget,
@@ -632,26 +668,14 @@ describe.skipIf(!enabled)('Assistant search latency on a realistic indexed corpu
632668
}) as Promise<T>
633669
})
634670
try {
635-
const response = await searchRoute(
636-
new NextRequest('http://localhost/api/knowledge/search', {
637-
method: 'POST',
638-
headers: { 'content-type': 'application/json' },
639-
body: JSON.stringify({
640-
workspaceId: ids.workspaceId,
641-
query: 'Orion deployment',
642-
topK: 15,
643-
}),
644-
})
645-
)
646-
expect(response.status).toBe(200)
671+
const { data } = await searchDashboard()
647672
const completed = diagnosticLog?.mock.calls.find(
648673
([message]) => message === 'Knowledge search completed'
649674
)
650675
const diagnostics = diagnosticSchema.parse(completed?.[1])
651676
expect(diagnostics.vectorBudgetMs).toBe(3000)
652677
expect(diagnostics.stages.vector.totalMs).toBeGreaterThan(2500)
653678
expect(diagnostics.stages.vector.totalMs).toBeLessThan(4000)
654-
const data = workspaceKnowledgeSearchDataSchema.parse((await response.json()).data)
655679
expect(data.retrieval).toEqual({
656680
status: 'partial',
657681
timedOutLegs: delayedLegs === 'both' ? ['vector', 'keyword'] : ['vector'],
@@ -666,7 +690,6 @@ describe.skipIf(!enabled)('Assistant search latency on a realistic indexed corpu
666690
report[`dashboard.deadline.${delayedLegs}`] = { resultCount: data.results.length }
667691
} finally {
668692
delayed.mockRestore()
669-
authenticate.mockRestore()
670693
}
671694
},
672695
30_000
@@ -675,7 +698,8 @@ describe.skipIf(!enabled)('Assistant search latency on a realistic indexed corpu
675698
it('records first and repeated application searches with the actual SQL plans', async () => {
676699
const before = embeddingCalls
677700
for (let iteration = 0; iteration < 2; iteration++) {
678-
const { result, plans } = await sample(`broad.${iteration}`, () => search())
701+
const { result, plans, diagnostics } = await sample(`broad.${iteration}`, () => search())
702+
expectCompleteVectorSearch(diagnostics)
679703
expect(result.data.results).toHaveLength(15)
680704
expect(result.data.results.every((row) => row.knowledgeBaseId === ids.knowledgeBaseId)).toBe(
681705
true
@@ -703,9 +727,10 @@ describe.skipIf(!enabled)('Assistant search latency on a realistic indexed corpu
703727

704728
it('preserves exact-neighbor recall across different query vectors', async () => {
705729
for (const topic of [3, 11, 23]) {
706-
const { plans } = await sample(`topic.${topic}`, () =>
730+
const { plans, diagnostics } = await sample(`topic.${topic}`, () =>
707731
search(ids.aliceId, `Topic ${topic} deployment`)
708732
)
733+
expectCompleteVectorSearch(diagnostics)
709734
const candidates = plans.find((plan) => plan.kind === 'vector')!
710735
assertCompactCandidates(candidates.plan[0].Plan)
711736
const rerank = plans.find((plan) => plan.kind === 'rerank')!
@@ -723,19 +748,10 @@ describe.skipIf(!enabled)('Assistant search latency on a realistic indexed corpu
723748
}, 180_000)
724749

725750
it('compares the Search tab and Assistant with the same person, query and index', async () => {
726-
const dashboard = await sample('dashboard', async () => {
727-
const result = await searchScopedKnowledge.execute({
728-
principal: { kind: 'session', userId: ids.aliceId, sessionId: 'fixture-dashboard' },
729-
input: {
730-
workspaceId: ids.workspaceId,
731-
query: 'Orion deployment',
732-
topK: 15,
733-
surface: 'dashboard',
734-
},
735-
})
736-
return resultSchema.parse({ success: true, data: result })
737-
})
751+
const dashboard = await sample('dashboard', searchDashboard)
738752
const assistant = await sample('assistant.comparison', () => search())
753+
expectCompleteVectorSearch(dashboard.diagnostics)
754+
expectCompleteVectorSearch(assistant.diagnostics)
739755
expect(dashboard.diagnostics.surface).toBe('dashboard')
740756
expect(assistant.diagnostics.surface).toBe('copilot')
741757
expect(dashboard.diagnostics.stages.result_provenance).toBeUndefined()
@@ -767,21 +783,9 @@ describe.skipIf(!enabled)('Assistant search latency on a realistic indexed corpu
767783
for (const surface of ['copilot', 'dashboard'] as const) {
768784
const { result, plans, diagnostics } = await sample(
769785
`filtered-neighborhood.${surface}`,
770-
async () => {
771-
if (surface === 'copilot') return search()
772-
const data = await searchScopedKnowledge.execute({
773-
principal: { kind: 'session', userId: ids.aliceId, sessionId: 'fixture-dashboard' },
774-
input: {
775-
workspaceId: ids.workspaceId,
776-
query: 'Orion deployment',
777-
topK: 15,
778-
surface,
779-
},
780-
})
781-
return resultSchema.parse({ success: true, data })
782-
}
786+
surface === 'copilot' ? () => search() : searchDashboard
783787
)
784-
expect(diagnostics).toMatchObject({ retrievalStatus: 'complete', timedOutLegs: [] })
788+
expectCompleteVectorSearch(diagnostics)
785789
expect(result.data.results).toHaveLength(15)
786790
const rerank = plans.find((plan) => plan.kind === 'rerank')!
787791
expect(rerank).toBeDefined()
@@ -852,6 +856,7 @@ describe.skipIf(!enabled)('Assistant search latency on a realistic indexed corpu
852856
}, 180_000)
853857

854858
it('runs two independent Assistant searches concurrently', async () => {
859+
diagnosticLog?.mockClear()
855860
const start = performance.now()
856861
const results = await Promise.all([search(), search(ids.aliceId, 'Engineering operations')])
857862
report.concurrent = {
@@ -860,6 +865,12 @@ describe.skipIf(!enabled)('Assistant search latency on a realistic indexed corpu
860865
}
861866
saveReport()
862867
for (const result of results) expect(result.data.results).toHaveLength(15)
868+
const completed = diagnosticLog!.mock.calls.filter(
869+
([message]) => message === 'Knowledge search completed'
870+
)
871+
expect(completed).toHaveLength(2)
872+
for (const [, metadata] of completed)
873+
expectCompleteVectorSearch(diagnosticSchema.parse(metadata))
863874
}, 180_000)
864875

865876
it('checks live reader access on every search, including after revocation', async () => {

packages/db/script-migrations-paused-billing-attribution.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,7 @@ describe('script migration registry', () => {
452452
'0013_backfill_legacy_knowledge_base_workspaces',
453453
'0014_require_knowledge_base_owner',
454454
'0015_backfill_embedding_search',
455+
'0016_backfill_search_vectors',
455456
])
456457
})
457458
})

packages/db/script-migrations/0016_backfill_search_vectors.postgres.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ describe.runIf(Boolean(databaseUrl))('search projection upgrade in PostgreSQL',
1616
const url = new URL(databaseUrl!)
1717
if (
1818
!['localhost', '127.0.0.1'].includes(url.hostname) ||
19-
!url.pathname.startsWith('/sim_acl_test')
19+
(!url.pathname.startsWith('/sim_acl_test') && url.pathname !== '/sim_auth_scim')
2020
) {
21-
throw new Error('Projection tests require a disposable local sim_acl_test database')
21+
throw new Error('Projection tests require a disposable local integration database')
2222
}
2323
admin = postgres(url.toString(), { max: 1, onnotice: () => undefined })
2424
await admin.unsafe(`CREATE SCHEMA "${schemaName}"`)

scripts/test-knowledge-acls.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ try {
180180
env: environment,
181181
}
182182
)
183-
if (!scale && testFilters.length === 0)
183+
if (!scale && testFilters.length === 0) {
184184
run(
185185
'bunx',
186186
[
@@ -195,6 +195,15 @@ try {
195195
env: environment,
196196
}
197197
)
198+
run(
199+
'bunx',
200+
['vitest', 'run', 'script-migrations/0016_backfill_search_vectors.postgres.test.ts'],
201+
{
202+
cwd: path.join(root, 'packages/db'),
203+
env: environment,
204+
}
205+
)
206+
}
198207
logger.info(
199208
scale
200209
? 'Opt-in knowledge scale measurements passed'

0 commit comments

Comments
 (0)