Skip to content

Commit df65aa5

Browse files
authored
Merge pull request #85 from DevelopmentFinanceObservatory/release/2026-10-29
release: website 24e53dc to production
2 parents 9f71c47 + 24e53dc commit df65aa5

121 files changed

Lines changed: 15364 additions & 918 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import path from 'node:path';
2+
import assert from 'node:assert/strict';
3+
import test from 'node:test';
4+
import { drizzle } from 'drizzle-orm/node-postgres';
5+
import { searchConversations } from '../../src/lib/chat/conversationSearch.ts';
6+
import { applyMigrations } from '../apply-migrations.ts';
7+
import { conversation, user } from '../schema.ts';
8+
import { createTestDatabase } from './harness.ts';
9+
10+
const MIGRATIONS_FOLDER = path.resolve(import.meta.dirname, '../migrations');
11+
12+
/**
13+
* Conversation search on Postgres reads each saved message's text and the
14+
* title of the card a user message names, and leaves out the approved plan
15+
* stored beside a reply.
16+
*/
17+
test('conversation search matches message text and card titles, not stored plans', async (t) => {
18+
const database = await createTestDatabase();
19+
t.after(() => database.teardown());
20+
21+
await applyMigrations(database.migratorPool(), {
22+
migrationsFolder: MIGRATIONS_FOLDER,
23+
runtimeDbUser: database.runtimeUser,
24+
migratorDbUser: database.migratorUser,
25+
});
26+
27+
const db = drizzle(database.runtimePool());
28+
29+
await db.insert(user).values([{ id: 'owner', name: 'Owner', email: 'owner@example.org' }]);
30+
await db.insert(conversation).values({
31+
id: 'conv-1',
32+
userId: 'owner',
33+
title: 'Aid question',
34+
data: {
35+
messages: [
36+
{ role: 'user', text: 'How much aid did Ghana get?' },
37+
{
38+
role: 'bot',
39+
text: 'About 1 billion dollars.',
40+
plan: { title: 'Net disbursements to Zanzibar', steps: [], choices: [] },
41+
},
42+
{ role: 'user', text: 'Make it a bar chart', card: { title: 'Health aid trend' } },
43+
],
44+
},
45+
});
46+
47+
const hits = async (q: string) =>
48+
(await searchConversations(db, 'owner', q)).map((hit) => [hit.id, hit.snippet]);
49+
50+
assert.deepEqual(await hits('ghana'), [['conv-1', 'How much aid did Ghana get?']]);
51+
assert.deepEqual(await hits('health aid'), [['conv-1', 'Health aid trend']]);
52+
assert.deepEqual(await hits('zanzibar'), []);
53+
});

‎db/test/harness.ts‎

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,16 @@ export async function createTestDatabase(): Promise<TestDatabase> {
7878
});
7979

8080
const pools: Pool[] = [];
81+
const closed: Promise<void>[] = [];
82+
83+
const open = (user: string, password: string): Pool => {
84+
const pool = poolFor(databaseName, user, password);
85+
pool.on('connect', (client) => {
86+
closed.push(new Promise((resolve) => client.once('end', resolve)));
87+
});
88+
pools.push(pool);
89+
return pool;
90+
};
8191

8292
return {
8393
databaseName,
@@ -86,22 +96,21 @@ export async function createTestDatabase(): Promise<TestDatabase> {
8696
runtimeUser,
8797
runtimePassword,
8898
migratorPool(): Pool {
89-
const pool = poolFor(databaseName, migratorUser, migratorPassword);
90-
pools.push(pool);
91-
return pool;
99+
return open(migratorUser, migratorPassword);
92100
},
93101
runtimePool(): Pool {
94-
const pool = poolFor(databaseName, runtimeUser, runtimePassword);
95-
pools.push(pool);
96-
return pool;
102+
return open(runtimeUser, runtimePassword);
97103
},
98104
adminPool(): Pool {
99-
const pool = poolFor(databaseName, SUPERUSER_USER, SUPERUSER_PASSWORD);
100-
pools.push(pool);
101-
return pool;
105+
return open(SUPERUSER_USER, SUPERUSER_PASSWORD);
102106
},
103107
async teardown(): Promise<void> {
108+
// pool.end() resolves once it has asked its clients to close, not
109+
// once they have. A backend that has not yet read its client's
110+
// Terminate when the terminate below reaches it sends the client
111+
// a FATAL error, which the ended pool emits with no listener.
104112
await Promise.all(pools.map((pool) => pool.end()));
113+
await Promise.all(closed);
105114

106115
const admin = new Client({
107116
host: SUPERUSER_HOST,

‎docs/development.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -211,17 +211,17 @@ The `apis`, `ai` and `dcp` repositories run their own dev services. `apis` and `
211211

212212
## API client package
213213

214-
The Search, Datasets and Graph types come from `@developmentfinance/client`, vendored as `vendor/developmentfinance-client-0.1.0.tgz`. The tarball is the `typescript/` package of the sdk repository, built and packed. To refresh it, pack that package into `vendor/` and re-lock, so `pnpm-lock.yaml` carries the new tarball's integrity:
214+
The Search, Datasets and Graph types come from `@developmentfinance/client`, vendored as `vendor/developmentfinance-client-0.1.0.tgz`. The tarball is the `typescript/` package of the sdk repository, built and packed. To refresh it, pack that package into `vendor/` and re-lock:
215215

216216
```sh
217217
cd <sdk checkout>/typescript
218218
pnpm install && pnpm run build
219219
pnpm pack --pack-destination <website checkout>/vendor
220220
cd <website checkout>
221-
pnpm install
221+
just relock-client
222222
```
223223

224-
A new package version packs under a new file name. Point the `@developmentfinance/client` entry in `package.json` at it before re-locking. A rebuilt tarball keeps its file name, so a local re-vendor needs the client's directory under `node_modules/.pnpm` deleted before `pnpm install --frozen-lockfile` picks it up. Commit the tarball and the lockfile together.
224+
A new package version packs under a new file name. Point the `@developmentfinance/client` entry in `package.json` at it, delete the old tarball, then re-lock. `just relock-client` deletes the client's stale directory under `node_modules/.pnpm` (pnpm keeps an installed `file:` package of the same name and version, so a rebuilt tarball is otherwise ignored), has pnpm re-resolve the client's lockfile entries from the tarball with `pnpm install --lockfile-only --fix-lockfile`, runs `pnpm install --frozen-lockfile`, and imports the client to prove it loads. The re-lock needs the registry. It changes the client's entries and those of any dependency the new tarball adds. pnpm 10.5.2 also fills in deprecation notices and writes `eslint-plugin-import`'s peer suffix in one of two equivalent forms, so a re-lock can touch those few lines too. Commit the tarball and the lockfile together.
225225

226226
The tarball carries the statuses of the codes the proxy raises itself and not their descriptions. `src/lib/proxy/proxy-codes.json` holds the full file, copied from the spec-kit repository's `python/client-contract/src/dfo_client_contract/proxy-codes.json` and formatted with Prettier. Copy it again when the client contract changes it.
227227

‎justfile‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,16 @@ env:
4848
echo "ok Created {{ENV_FILE}} from {{ENV_SAMPLE}}."
4949
fi
5050
51+
# pnpm reuses an installed `file:` package whose name and version are
52+
# unchanged, so the stale install goes first.
53+
#
54+
# Re-lock the vendored API client tarball
55+
relock-client:
56+
rm -rf node_modules/.pnpm/@developmentfinance+client@*
57+
pnpm install --lockfile-only --fix-lockfile
58+
pnpm install --frozen-lockfile
59+
node --input-type=module -e "await import('@developmentfinance/client')"
60+
5161
# ── Development ───────────────────────────────────────
5262

5363
# LAN access (phone/tablet testing) needs DEV_ORIGINS set in {{ENV_FILE}}.

0 commit comments

Comments
 (0)