Skip to content

Commit 2e60830

Browse files
committed
test(agentic-db): add combined unifiedSearch + nearbyPlaces ORM test; bump graphile-settings to 4.21.0
1 parent 97ef3d8 commit 2e60830

5 files changed

Lines changed: 372 additions & 27 deletions

File tree

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -311,12 +311,12 @@ const ranked = await db.memory
311311

312312
The server compiles that into a single SQL statement: a `unified_search(...)` ranked CTE joined against an `EXISTS (… ST_DWithin(memory.location_geo, place.location_geo, 5000) …)` subquery. No GeoJSON goes over the wire on the spatial side, and the text-search score comes back as `searchScore`.
313313

314-
Each half of that example is exercised by an integration test:
314+
That exact combined shape is exercised end-to-end by an integration test: [`packages/agentic-db/__tests__/unified-spatial-combined.test.ts`](packages/agentic-db/__tests__/unified-spatial-combined.test.ts). It boots a real deploy of the agentic-db pgpm package, seeds three memories and two market-category places at known coordinates, runs the same `memory.findMany({ where: { unifiedSearch, nearbyPlaces } })` call through the generated SDK, and asserts that only the positive-match memory comes back with a non-null `searchScore`.
315315

316-
- **Spatial half** — the `nearbyPlaces: { distance, some: { … } }` filter shape is proven in [`packages/integration-tests/__tests__/orm.test.ts`](packages/integration-tests/__tests__/orm.test.ts) under the `RelationSpatial via ORM` describe block (5 cases covering all 5 spatial relations declared in the blueprint).
317-
- **Unified-search half** — the `unifiedSearch` filter field and `searchScore` ranking are proven in [`packages/agentic-db/__tests__/rag-unified-search.test.ts`](packages/agentic-db/__tests__/rag-unified-search.test.ts), against pre-baked `nomic-embed-text` fixtures so no Ollama instance is needed.
316+
Supporting single-axis coverage lives alongside:
318317

319-
The combined `where` shape above is valid against the exported GraphQL schema (see `unifiedSearch: String` on `MemoryFilter` in [`sdk/schemas/agentic-db.graphql`](sdk/schemas/agentic-db.graphql)) and is served end-to-end by the Constructive server plugin stack.
318+
- **Spatial-only**`nearbyPlaces` / `nearbyContacts` / `nearbyVenues` / `nearbyMemories` are each exercised in [`packages/integration-tests/__tests__/orm.test.ts`](packages/integration-tests/__tests__/orm.test.ts) under the `RelationSpatial via ORM` describe block (all 5 relations declared in the blueprint).
319+
- **Unified-search only**`unifiedSearch` + `searchScore` ranking behavior is covered by [`packages/agentic-db/__tests__/rag-unified-search.test.ts`](packages/agentic-db/__tests__/rag-unified-search.test.ts) against pre-baked `nomic-embed-text` fixtures (no Ollama required).
320320

321321
Full ORM reference in the [`orm-default` skill](skills/orm-default/SKILL.md).
322322

packages/agentic-db/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -311,12 +311,12 @@ const ranked = await db.memory
311311

312312
The server compiles that into a single SQL statement: a `unified_search(...)` ranked CTE joined against an `EXISTS (… ST_DWithin(memory.location_geo, place.location_geo, 5000) …)` subquery. No GeoJSON goes over the wire on the spatial side, and the text-search score comes back as `searchScore`.
313313

314-
Each half of that example is exercised by an integration test:
314+
That exact combined shape is exercised end-to-end by an integration test: [`packages/agentic-db/__tests__/unified-spatial-combined.test.ts`](__tests__/unified-spatial-combined.test.ts). It boots a real deploy of the agentic-db pgpm package, seeds three memories and two market-category places at known coordinates, runs the same `memory.findMany({ where: { unifiedSearch, nearbyPlaces } })` call through the generated SDK, and asserts that only the positive-match memory comes back with a non-null `searchScore`.
315315

316-
- **Spatial half** — the `nearbyPlaces: { distance, some: { … } }` filter shape is proven in [`packages/integration-tests/__tests__/orm.test.ts`](../integration-tests/__tests__/orm.test.ts) under the `RelationSpatial via ORM` describe block (5 cases covering all 5 spatial relations declared in the blueprint).
317-
- **Unified-search half** — the `unifiedSearch` filter field and `searchScore` ranking are proven in [`packages/agentic-db/__tests__/rag-unified-search.test.ts`](__tests__/rag-unified-search.test.ts), against pre-baked `nomic-embed-text` fixtures so no Ollama instance is needed.
316+
Supporting single-axis coverage lives alongside:
318317

319-
The combined `where` shape above is valid against the exported GraphQL schema (see `unifiedSearch: String` on `MemoryFilter` in [`sdk/schemas/agentic-db.graphql`](../../sdk/schemas/agentic-db.graphql)) and is served end-to-end by the Constructive server plugin stack.
318+
- **Spatial-only**`nearbyPlaces` / `nearbyContacts` / `nearbyVenues` / `nearbyMemories` are each exercised in [`packages/integration-tests/__tests__/orm.test.ts`](../integration-tests/__tests__/orm.test.ts) under the `RelationSpatial via ORM` describe block (all 5 relations declared in the blueprint).
319+
- **Unified-search only**`unifiedSearch` + `searchScore` ranking behavior is covered by [`packages/agentic-db/__tests__/rag-unified-search.test.ts`](__tests__/rag-unified-search.test.ts) against pre-baked `nomic-embed-text` fixtures (no Ollama required).
320320

321321
Full ORM reference in the [`orm-default` skill](../../skills/orm-default/SKILL.md).
322322

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
/**
2+
* Combined Unified Search + RelationSpatial test — ORM.
3+
*
4+
* Proves the exact query shape documented in the root README's
5+
* "Use the SDK (ORM)" section: a single `where:` clause that
6+
* composes the unified-search filter (`unifiedSearch: '...'`) with
7+
* a cross-table PostGIS spatial relation (`nearbyPlaces: { distance,
8+
* some: { ... } }`).
9+
*
10+
* Harness: `@constructive-io/graphql-test`'s `getConnections()` boots
11+
* the full `ConstructivePreset` plugin stack — which includes both
12+
* `UnifiedSearchPreset` (graphile-search) AND
13+
* `PostgisSpatialRelationsPlugin` — against a real deploy of the
14+
* agentic-db pgpm package. Same stack that `cnc server` serves in
15+
* production, so this test exercises the documented query shape
16+
* end-to-end through the generated SDK.
17+
*
18+
* Fixture data is seeded via raw SQL at known coordinates so the
19+
* distance half is deterministic and the text half has a clean
20+
* positive + two negatives.
21+
*/
22+
jest.setTimeout(300000);
23+
process.env.LOG_SCOPE = '@constructive-io/graphql-test';
24+
25+
import { getConnections, GraphQLTestAdapter } from '@constructive-io/graphql-test';
26+
import type { GraphQLQueryFn } from '@constructive-io/graphql-test';
27+
import { createClient } from '@agentic-db/sdk';
28+
import {
29+
createAppJobsStub,
30+
grantAnonymousAccess,
31+
} from '../test-utils/helpers';
32+
33+
const SCHEMAS = ['agentic_db_app_public'];
34+
35+
// Deterministic UUIDs so the assertions can name-check matches / negatives.
36+
const AGENT_ID = '00000000-0000-0000-0000-0000000000a1';
37+
const MEMORY_SF = '00000000-0000-0000-0000-0000000000b1';
38+
const MEMORY_OAKLAND = '00000000-0000-0000-0000-0000000000b2';
39+
const MEMORY_NYC = '00000000-0000-0000-0000-0000000000b3';
40+
const PLACE_FERRY = '00000000-0000-0000-0000-0000000000c1';
41+
const PLACE_TOKYO = '00000000-0000-0000-0000-0000000000c2';
42+
43+
let db: any;
44+
let pg: any;
45+
let query: GraphQLQueryFn;
46+
let teardown: () => Promise<void>;
47+
48+
beforeAll(async () => {
49+
const connections = await getConnections({
50+
schemas: SCHEMAS,
51+
authRole: 'anonymous',
52+
});
53+
({ db, pg, query, teardown } = connections);
54+
55+
await grantAnonymousAccess(pg);
56+
await createAppJobsStub(pg);
57+
});
58+
59+
afterAll(async () => {
60+
if (teardown) await teardown();
61+
});
62+
63+
// Each test runs in its own transaction (begun here, rolled back
64+
// after the test). Seeding happens inside the transaction so the
65+
// ORM query in `it(...)` sees the inserted rows.
66+
beforeEach(() => db.beforeEach());
67+
afterEach(() => db.afterEach());
68+
69+
describe('Unified search + RelationSpatial composition via ORM', () => {
70+
beforeEach(async () => {
71+
// Minimal agent row so memories have a valid agent_id FK.
72+
await pg.query(
73+
`
74+
INSERT INTO agentic_db_app_public.agents (id, name)
75+
VALUES ($1, 'test-agent')
76+
ON CONFLICT (id) DO NOTHING
77+
`,
78+
[AGENT_ID]
79+
);
80+
81+
// Memories — raw SQL so location_geo can be set as a geography
82+
// Point in one statement. Title/content chosen so only MEMORY_SF
83+
// has meaningful text overlap with the query term
84+
// "Ferry Building coffee".
85+
await pg.query(
86+
`
87+
INSERT INTO agentic_db_app_public.memories
88+
(id, agent_id, title, content, location_geo)
89+
VALUES
90+
(
91+
$1, $4,
92+
'Ferry Building keynote recap',
93+
'Met a collaborator over coffee near the Ferry Building after the retrieval keynote.',
94+
ST_SetSRID(ST_MakePoint(-122.4194, 37.7749), 4326)::geography
95+
),
96+
(
97+
$2, $4,
98+
'Oakland lunch',
99+
'Reviewed an unrelated benchmark over lunch.',
100+
ST_SetSRID(ST_MakePoint(-122.2712, 37.8044), 4326)::geography
101+
),
102+
(
103+
$3, $4,
104+
'NYC meetup',
105+
'Caught up with the east-coast team; had pasta.',
106+
ST_SetSRID(ST_MakePoint(-74.0060, 40.7128), 4326)::geography
107+
)
108+
ON CONFLICT (id) DO NOTHING
109+
`,
110+
[MEMORY_SF, MEMORY_OAKLAND, MEMORY_NYC, AGENT_ID]
111+
);
112+
113+
// Places — one matches the `category='market'` predicate and is
114+
// ~200 m from MEMORY_SF, ~13 km from MEMORY_OAKLAND, ~4100 km
115+
// from MEMORY_NYC. The Tokyo row exists to make sure the
116+
// spatial predicate actually filters (no memory is within 5 km
117+
// of Tokyo).
118+
await pg.query(
119+
`
120+
INSERT INTO agentic_db_app_public.places
121+
(id, name, category, location_geo)
122+
VALUES
123+
(
124+
$1,
125+
'Ferry Building Marketplace',
126+
'market',
127+
ST_SetSRID(ST_MakePoint(-122.3937, 37.7956), 4326)::geography
128+
),
129+
(
130+
$2,
131+
'Tsukiji Outer Market',
132+
'market',
133+
ST_SetSRID(ST_MakePoint(139.7700, 35.6655), 4326)::geography
134+
)
135+
ON CONFLICT (id) DO NOTHING
136+
`,
137+
[PLACE_FERRY, PLACE_TOKYO]
138+
);
139+
140+
await db.publish();
141+
});
142+
143+
it('memory.findMany(unifiedSearch + nearbyPlaces): composes text + spatial in one where', async () => {
144+
const sdk = createClient({ adapter: new GraphQLTestAdapter(query) });
145+
146+
const result = await sdk.memory
147+
.findMany({
148+
where: {
149+
// Text half — matches on title/content via any of FTS,
150+
// BM25 or trgm depending on what's configured on the
151+
// underlying columns. MEMORY_SF contains both "Ferry
152+
// Building" and "coffee", so it scores strongly.
153+
unifiedSearch: 'Ferry Building coffee',
154+
// Spatial half — @spatialRelation smart tag on
155+
// memory.location_geo (declared in
156+
// packages/provision/src/schemas/spatial-relations.ts)
157+
// exposes `nearbyPlaces` on MemoryFilter. Body uses the
158+
// plugin's `{ distance, some: { …PlaceFilter… } }` shape.
159+
nearbyPlaces: {
160+
distance: 5000,
161+
some: { category: { equalTo: 'market' } },
162+
},
163+
},
164+
first: 10,
165+
select: {
166+
id: true,
167+
title: true,
168+
searchScore: true,
169+
},
170+
})
171+
.execute();
172+
173+
if (!result.ok) {
174+
throw new Error(
175+
`combined unifiedSearch+nearbyPlaces query failed: ${JSON.stringify(result.errors, null, 2)}`
176+
);
177+
}
178+
179+
const nodes = result.data.memories.nodes;
180+
const ids = nodes.map((n: any) => n.id);
181+
182+
// MEMORY_SF passes BOTH halves: its text matches the query and
183+
// it's within 5 km of the Ferry Building Marketplace (market).
184+
expect(ids).toContain(MEMORY_SF);
185+
186+
// MEMORY_OAKLAND fails BOTH halves: text doesn't overlap, and
187+
// it's ~13 km from any market-category place (Ferry Building is
188+
// 13 km away; Tsukiji is across the Pacific).
189+
expect(ids).not.toContain(MEMORY_OAKLAND);
190+
191+
// MEMORY_NYC fails BOTH halves: "pasta" doesn't overlap the
192+
// query, and NYC is ~4100 km from the nearest market.
193+
expect(ids).not.toContain(MEMORY_NYC);
194+
195+
// The unified-search plugin populates `searchScore` as a
196+
// 0..1 blended relevance signal when any text algorithm fires.
197+
const sf = nodes.find((n: any) => n.id === MEMORY_SF);
198+
expect(sf).toBeDefined();
199+
expect(typeof sf.searchScore).toBe('number');
200+
expect(sf.searchScore).toBeGreaterThan(0);
201+
expect(sf.searchScore).toBeLessThanOrEqual(1);
202+
});
203+
});

packages/agentic-db/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
"@agentic-db/sdk": "workspace:*",
3939
"@agentic-kit/ollama": "^1.0.3",
4040
"@constructive-io/graphql-test": "^4.9.10",
41-
"graphile-settings": "4.18.5",
41+
"graphile-settings": "4.21.0",
4242
"pgsql-test": "^4.7.6"
4343
}
4444
}

0 commit comments

Comments
 (0)