|
| 1 | +/** |
| 2 | + * @vitest-environment node |
| 3 | + */ |
| 4 | +import { describe, expect, it } from 'vitest' |
| 5 | +import { CrunchbaseBlock } from '@/blocks/blocks/crunchbase' |
| 6 | + |
| 7 | +/** |
| 8 | + * Every assertion here runs against `{ ...inputs, ...buildParams(inputs) }`, the |
| 9 | + * shape the generic tool handler actually forwards. A key the mapper omits is |
| 10 | + * *not* dropped by that merge — the raw subBlock value survives — so asserting |
| 11 | + * on the mapper's return alone would prove nothing about what the tool receives. |
| 12 | + */ |
| 13 | +describe('CrunchbaseBlock', () => { |
| 14 | + const buildParams = CrunchbaseBlock.tools.config!.params! |
| 15 | + const selectTool = CrunchbaseBlock.tools.config!.tool! |
| 16 | + |
| 17 | + const resolve = (inputs: Record<string, unknown>) => ({ ...inputs, ...buildParams(inputs) }) |
| 18 | + |
| 19 | + const operationIds = |
| 20 | + CrunchbaseBlock.subBlocks |
| 21 | + .find((subBlock) => subBlock.id === 'operation') |
| 22 | + ?.options?.map((option) => (option as { id: string }).id) ?? [] |
| 23 | + |
| 24 | + it('maps every dropdown operation onto a registered tool', () => { |
| 25 | + expect(operationIds).toHaveLength(14) |
| 26 | + expect(new Set(operationIds.map((id) => selectTool({ operation: id })))).toEqual( |
| 27 | + new Set(CrunchbaseBlock.tools.access) |
| 28 | + ) |
| 29 | + }) |
| 30 | + |
| 31 | + it('rejects an operation the dropdown does not offer', () => { |
| 32 | + expect(() => selectTool({ operation: 'search_unicorns' })).toThrow( |
| 33 | + /Invalid Crunchbase operation/ |
| 34 | + ) |
| 35 | + }) |
| 36 | + |
| 37 | + it('gives every subblock a unique id', () => { |
| 38 | + const ids = CrunchbaseBlock.subBlocks.map((subBlock) => subBlock.id) |
| 39 | + expect(ids).toHaveLength(new Set(ids).size) |
| 40 | + }) |
| 41 | + |
| 42 | + it('shows a subblock for every operation that owns it', () => { |
| 43 | + const conditionsFor = (id: string) => { |
| 44 | + const condition = CrunchbaseBlock.subBlocks.find((subBlock) => subBlock.id === id)?.condition |
| 45 | + const value = (condition as { value?: unknown } | undefined)?.value |
| 46 | + return new Set(Array.isArray(value) ? value.map(String) : [String(value)]) |
| 47 | + } |
| 48 | + |
| 49 | + expect(conditionsFor('searchQuery')).toEqual( |
| 50 | + new Set([ |
| 51 | + 'search_organizations', |
| 52 | + 'search_people', |
| 53 | + 'search_funding_rounds', |
| 54 | + 'search_acquisitions', |
| 55 | + 'search_entities', |
| 56 | + ]) |
| 57 | + ) |
| 58 | + expect(conditionsFor('entityId')).toEqual( |
| 59 | + new Set([ |
| 60 | + 'get_organization', |
| 61 | + 'get_person', |
| 62 | + 'get_funding_round', |
| 63 | + 'get_acquisition', |
| 64 | + 'get_entity', |
| 65 | + 'get_entity_card', |
| 66 | + ]) |
| 67 | + ) |
| 68 | + }) |
| 69 | + |
| 70 | + it('never hides a required field behind advanced mode', () => { |
| 71 | + const advancedRequired = CrunchbaseBlock.subBlocks |
| 72 | + .filter((subBlock) => subBlock.mode === 'advanced' && subBlock.required) |
| 73 | + .map((subBlock) => subBlock.id) |
| 74 | + |
| 75 | + expect(advancedRequired).toEqual([]) |
| 76 | + }) |
| 77 | + |
| 78 | + /* |
| 79 | + * The mapper's whole job is dropping keys that belong to another operation. |
| 80 | + * `shouldSerializeSubBlock` short-circuits on `mode: 'advanced'` before it |
| 81 | + * evaluates a condition, so a hidden advanced field still reaches `inputs` — |
| 82 | + * only an explicit `undefined` removes it from what goes out on the wire. |
| 83 | + */ |
| 84 | + it('drops a previous operation’s leftovers when the operation changes', () => { |
| 85 | + const params = resolve({ |
| 86 | + operation: 'autocomplete', |
| 87 | + apiKey: 'key', |
| 88 | + autocompleteQuery: 'airbnb', |
| 89 | + searchQuery: '[{"type":"predicate","field_id":"categories"}]', |
| 90 | + searchFieldIds: '["identifier"]', |
| 91 | + entityId: 'tesla-motors', |
| 92 | + fieldIds: '["identifier","name"]', |
| 93 | + cardIds: '["founders"]', |
| 94 | + order: '[{"field_id":"rank_org","sort":"asc"}]', |
| 95 | + afterId: 'stale-cursor', |
| 96 | + collection: 'organizations', |
| 97 | + }) |
| 98 | + |
| 99 | + expect(params.query).toBe('airbnb') |
| 100 | + expect(params.entityId).toBeUndefined() |
| 101 | + expect(params.fieldIds).toBeUndefined() |
| 102 | + expect(params.cardIds).toBeUndefined() |
| 103 | + expect(params.order).toBeUndefined() |
| 104 | + expect(params.afterId).toBeUndefined() |
| 105 | + expect(params.collection).toBeUndefined() |
| 106 | + expect(params.searchQuery).toBeUndefined() |
| 107 | + expect(params.autocompleteQuery).toBeUndefined() |
| 108 | + }) |
| 109 | + |
| 110 | + it('routes the predicate query to a search and the text query to autocomplete', () => { |
| 111 | + const search = resolve({ |
| 112 | + operation: 'search_organizations', |
| 113 | + apiKey: 'key', |
| 114 | + searchQuery: '[{"type":"predicate","field_id":"categories","operator_id":"includes"}]', |
| 115 | + autocompleteQuery: 'airbnb', |
| 116 | + }) |
| 117 | + expect(search.query).toBe( |
| 118 | + '[{"type":"predicate","field_id":"categories","operator_id":"includes"}]' |
| 119 | + ) |
| 120 | + |
| 121 | + const autocomplete = resolve({ |
| 122 | + operation: 'autocomplete', |
| 123 | + apiKey: 'key', |
| 124 | + searchQuery: '[{"type":"predicate","field_id":"categories","operator_id":"includes"}]', |
| 125 | + autocompleteQuery: 'airbnb', |
| 126 | + }) |
| 127 | + expect(autocomplete.query).toBe('airbnb') |
| 128 | + }) |
| 129 | + |
| 130 | + it('feeds the generic search its own required field list', () => { |
| 131 | + const generic = resolve({ |
| 132 | + operation: 'search_entities', |
| 133 | + apiKey: 'key', |
| 134 | + collection: 'events', |
| 135 | + searchQuery: '[]', |
| 136 | + searchFieldIds: '["identifier","short_description"]', |
| 137 | + fieldIds: '["identifier","name"]', |
| 138 | + }) |
| 139 | + expect(generic.fieldIds).toBe('["identifier","short_description"]') |
| 140 | + expect(generic.searchFieldIds).toBeUndefined() |
| 141 | + |
| 142 | + const specific = resolve({ |
| 143 | + operation: 'search_organizations', |
| 144 | + apiKey: 'key', |
| 145 | + searchQuery: '[]', |
| 146 | + searchFieldIds: '["identifier","short_description"]', |
| 147 | + fieldIds: '["identifier","name"]', |
| 148 | + }) |
| 149 | + expect(specific.fieldIds).toBe('["identifier","name"]') |
| 150 | + }) |
| 151 | + |
| 152 | + it('picks the collection each operation actually asks for', () => { |
| 153 | + const card = resolve({ |
| 154 | + operation: 'get_entity_card', |
| 155 | + apiKey: 'key', |
| 156 | + entityId: 'sequoia-capital', |
| 157 | + cardId: 'participated_investments', |
| 158 | + cardCollection: 'organizations', |
| 159 | + collection: 'events', |
| 160 | + deletedCollection: 'people', |
| 161 | + }) |
| 162 | + expect(card.collection).toBe('organizations') |
| 163 | + expect(card.cardCollection).toBeUndefined() |
| 164 | + |
| 165 | + const deleted = resolve({ |
| 166 | + operation: 'list_deleted_entities', |
| 167 | + apiKey: 'key', |
| 168 | + cardCollection: 'organizations', |
| 169 | + collection: 'events', |
| 170 | + deletedCollection: 'people', |
| 171 | + }) |
| 172 | + expect(deleted.collection).toBe('people') |
| 173 | + expect(deleted.deletedCollection).toBeUndefined() |
| 174 | + }) |
| 175 | +}) |
0 commit comments