Skip to content

Commit 36c2ea5

Browse files
committed
fix(workflows): skip dynamically bound selectors in id validation
Tier-2 selector validation is a static id-existence check against the workspace, so it cannot evaluate a value whose id only arrives at execution time. A `<block.output>` or `{{ENV_VAR}}` binding written into a selector field was therefore reported as a resource that does not exist, on every graph write. `collectSelectorFields` now skips those values via the existing `containsReference`, and splits multi-select values with `splitOutsideReferences` so a reference containing a comma is not torn into fragments that each get validated as an id. Filtering is per entry rather than on the whole string: a multi-select can mix literal ids with dynamic ones, and testing `<a.b>,kb_real,<c.d>` as a whole would drop `kb_real` along with the references. Verified end to end against a local dev server: the three reference forms drop from one unresolved-reference finding each to zero, while a literal id that does not resolve still reports one.
1 parent 83d361e commit 36c2ea5

3 files changed

Lines changed: 451 additions & 5 deletions

File tree

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/**
2+
* @vitest-environment node
3+
*
4+
* Integration coverage for the Tier-2 reference guard, against the REAL block registry.
5+
*
6+
* `validation.test.ts` exercises the same guard with hand-written block fixtures, which cannot
7+
* catch a fixture that has drifted from the shipped block config. This file unmocks the registry
8+
* so the canonical pair, its `mode`s and its sub-block types come from `knowledge.ts` itself.
9+
* Only the database lookup is mocked - it is the one dependency the guard deliberately protects.
10+
*/
11+
import { beforeEach, describe, expect, it, vi } from 'vitest'
12+
13+
vi.unmock('@/blocks/registry')
14+
15+
const { mockValidateSelectorIds } = vi.hoisted(() => ({
16+
mockValidateSelectorIds: vi.fn(),
17+
}))
18+
19+
vi.mock('@/lib/workflows/editing/selector-validator', () => ({
20+
validateSelectorIds: mockValidateSelectorIds,
21+
}))
22+
23+
import { collectUnresolvedReferences } from '@/lib/workflows/editing/validation'
24+
import { getBlock } from '@/blocks/registry'
25+
26+
const CTX = { userId: 'user-1', workspaceId: 'workspace-1' }
27+
28+
/** The real basic member of the knowledge block's `knowledgeBaseId` canonical pair. */
29+
const KB_SELECTOR_ID = 'knowledgeBaseSelector'
30+
31+
function knowledgeGraph(value: string) {
32+
return {
33+
blocks: {
34+
kb1: {
35+
type: 'knowledge',
36+
name: 'KB',
37+
subBlocks: { [KB_SELECTOR_ID]: { value } },
38+
},
39+
},
40+
}
41+
}
42+
43+
describe('Tier-2 reference guard (real block registry)', () => {
44+
beforeEach(() => {
45+
vi.clearAllMocks()
46+
mockValidateSelectorIds.mockResolvedValue({ valid: [], invalid: [] })
47+
})
48+
49+
it('the fixture matches the shipped knowledge block, so the cases below are meaningful', () => {
50+
const config = getBlock('knowledge')
51+
const member = config?.subBlocks?.find((s) => s.id === KB_SELECTOR_ID)
52+
expect(member).toBeDefined()
53+
expect(member?.type).toBe('knowledge-base-selector')
54+
expect(member?.canonicalParamId).toBe('knowledgeBaseId')
55+
expect(member?.mode).toBe('basic')
56+
})
57+
58+
it.each([
59+
['a block-output reference', '<start.kbId>'],
60+
['an env-var reference', '{{KB_ID}}'],
61+
['a partially templated value', 'kb_<start.suffix>'],
62+
])('never hits the database for %s', async (_label, value) => {
63+
// Seeded so `toHaveLength(0)` is load-bearing: with a permissive mock it would pass
64+
// whether or not the guard ran.
65+
mockValidateSelectorIds.mockResolvedValue({ valid: [], invalid: [value] })
66+
const refs = await collectUnresolvedReferences(knowledgeGraph(value), CTX)
67+
expect(mockValidateSelectorIds).not.toHaveBeenCalled()
68+
expect(refs).toHaveLength(0)
69+
})
70+
71+
it('still reports a literal id that does not resolve', async () => {
72+
mockValidateSelectorIds.mockResolvedValue({ valid: [], invalid: ['kb_gone'] })
73+
const refs = await collectUnresolvedReferences(knowledgeGraph('kb_gone'), CTX)
74+
expect(mockValidateSelectorIds).toHaveBeenCalledWith('knowledge-base-selector', 'kb_gone', CTX)
75+
expect(refs).toHaveLength(1)
76+
expect(refs[0]).toMatchObject({ blockId: 'kb1', field: KB_SELECTOR_ID, kind: 'resource' })
77+
})
78+
})

0 commit comments

Comments
 (0)