@bongos/core 1.19.249 → 1.19.250

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,250 @@
1
+ // tests/copy_desk_flags_db.mjs — the copy_desk_flags schema + SQL against a REAL
2
+ // Postgres (task 1003113 / R02 of goal 1000074).
3
+ //
4
+ // WHY THIS EXISTS SEPARATELY FROM modules/copy-desk/tests/. Those three files are
5
+ // DB-free by contract and cover the PURE layer — the validators, the queue
6
+ // composition, the no-CMS shape. They cannot cover the half of this feature that
7
+ // lives in the schema, and that half is where the load-bearing rules are:
8
+ //
9
+ // * the REASON FLOOR is a CHECK constraint, not just a route check. The whole
10
+ // argument for putting it there is that no future write path can get around
11
+ // it — which is a claim about Postgres, and can only be tested against
12
+ // Postgres.
13
+ // * the ONE-OPEN-FLAG-PER-(builder, target) rule is a PARTIAL unique index. Its
14
+ // partiality is the design (two builders flagging one sentence is signal; the
15
+ // same builder twice is a double-click; and a closed flag must not block a
16
+ // re-flag when the copy regresses). A fake client cannot exercise that.
17
+ // * the CLOSE IS GUARDED on `status='open'` inside the UPDATE so two people
18
+ // closing at once cannot both win. That is a race, and a race needs a real
19
+ // transaction engine to demonstrate.
20
+ // * the FK on resolved_task_id is what turns a typo'd task number into a named
21
+ // 400 instead of an opaque 500.
22
+ //
23
+ // It SELF-SKIPS (exit 0) when no Postgres is reachable, so it is safe on a bare
24
+ // worktree, and it is listed in run-unit-tests.js's INTEGRATION set so the
25
+ // default DB-free lane never runs it.
26
+ //
27
+ // Run: DATABASE_URL=postgres://... node tests/copy_desk_flags_db.mjs
28
+ //
29
+ // It creates its OWN minimal `builders`/`tasks` stand-ins if they are absent, so
30
+ // it works against a throwaway container as well as a full migrated database, and
31
+ // it cleans up every row it writes.
32
+
33
+ import { createRequire } from 'node:module';
34
+ import { strict as assert } from 'node:assert';
35
+ import fs from 'node:fs';
36
+ import path from 'node:path';
37
+ import url from 'node:url';
38
+
39
+ const require = createRequire(import.meta.url);
40
+ const { Pool } = require('pg');
41
+
42
+ const ROOT = path.resolve(path.dirname(url.fileURLToPath(import.meta.url)), '..');
43
+ const pool = new Pool({ connectionString: process.env.DATABASE_URL || undefined });
44
+
45
+ try {
46
+ await pool.query('SELECT 1');
47
+ } catch (e) {
48
+ console.log(`copy_desk_flags_db.mjs SKIPPED — no reachable Postgres (${e.code || e.message}). Set DATABASE_URL to run.`);
49
+ await pool.end().catch(() => {});
50
+ process.exit(0);
51
+ }
52
+
53
+ let passed = 0;
54
+ let failed = 0;
55
+ async function test(name, fn) {
56
+ try { await fn(); passed++; console.log(` ok ${name}`); }
57
+ catch (err) { failed++; console.log(` FAIL ${name}`); console.log(` ${err.message}`); }
58
+ }
59
+
60
+ // --- fixtures -------------------------------------------------------------
61
+ // Minimal stand-ins ONLY if the real tables are absent (throwaway container).
62
+ // Against a fully migrated DB these are no-ops and the real tables are used.
63
+ await pool.query(`
64
+ CREATE TABLE IF NOT EXISTS schema_migrations (version text PRIMARY KEY);
65
+ CREATE TABLE IF NOT EXISTS builders (id bigserial PRIMARY KEY, github_login text, display_name text);
66
+ CREATE TABLE IF NOT EXISTS tasks (id bigserial PRIMARY KEY);
67
+ `);
68
+
69
+ // Two throwaway builders + one task, at ids far above any real row.
70
+ const A = 900000001;
71
+ const B = 900000002;
72
+ const T = 900000003;
73
+ await pool.query('INSERT INTO builders (id, github_login, display_name) VALUES ($1,$2,$3),($4,$5,$6) ON CONFLICT (id) DO NOTHING',
74
+ [A, 'cdtest-a', 'Copy Desk Test A', B, 'cdtest-b', 'Copy Desk Test B']);
75
+ await pool.query('INSERT INTO tasks (id) VALUES ($1) ON CONFLICT (id) DO NOTHING', [T]);
76
+
77
+ // Apply the migration verbatim — this also proves it is idempotent, since a
78
+ // migrated DB has already run it.
79
+ const MIGRATION = fs.readFileSync(path.join(ROOT, 'modules/copy-desk/migrations/copy_desk_001_flags.sql'), 'utf8');
80
+ await pool.query(MIGRATION);
81
+
82
+ // Every row this test writes carries a surface nothing else uses, so cleanup is
83
+ // exact and it can never delete a real flag.
84
+ const SURF = '__copy_desk_test__';
85
+ const clean = () => pool.query('DELETE FROM copy_desk_flags WHERE surface = $1', [SURF]);
86
+ await clean();
87
+
88
+ const REASON = 'Reads like a stack trace, not a sentence a person would say.';
89
+ // `string_id` uses an `in` check, not `??` — an explicit null is the POINT of
90
+ // several cases below, and `??` would silently substitute the default.
91
+ function ins(o = {}) {
92
+ return pool.query(
93
+ `INSERT INTO copy_desk_flags
94
+ (scope, surface, string_id, reason, kind, text_at_flag, file_at_flag, line_at_flag, registry_schema, flagged_by)
95
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) RETURNING *`,
96
+ [o.scope ?? 'string', SURF, ('string_id' in o ? o.string_id : 'abc123'), o.reason ?? REASON,
97
+ o.kind ?? 'other', 'Could not save.', 'a.js', 1, 1, o.by ?? A]
98
+ );
99
+ }
100
+
101
+ console.log('copy_desk_flags — the constraints hold in Postgres, not just in JS');
102
+
103
+ await test('the migration is idempotent (re-running is clean)', async () => {
104
+ await pool.query(MIGRATION);
105
+ });
106
+
107
+ await test('a well-formed flag inserts and defaults to open + unstamped', async () => {
108
+ const { rows } = await ins();
109
+ assert.equal(rows[0].status, 'open');
110
+ assert.equal(rows[0].resolved_at, null);
111
+ assert.equal(rows[0].resolved_by, null);
112
+ assert.ok(rows[0].flagged_at);
113
+ });
114
+
115
+ await test('THE REASON FLOOR fires in the DB, not only in the route', async () => {
116
+ await assert.rejects(() => ins({ reason: 'bad', string_id: 'r1' }), (e) => e.code === '23514');
117
+ });
118
+
119
+ await test('a whitespace-only reason is refused (btrim in the CHECK)', async () => {
120
+ await assert.rejects(() => ins({ reason: ' ', string_id: 'r2' }), (e) => e.code === '23514');
121
+ });
122
+
123
+ await test("scope 'string' with a NULL string_id is refused — it points nowhere", async () => {
124
+ await assert.rejects(() => ins({ scope: 'string', string_id: null }), (e) => e.code === '23514');
125
+ });
126
+
127
+ await test("scope 'surface' WITH a string_id is refused — it would be silently narrower than it claims", async () => {
128
+ await assert.rejects(() => ins({ scope: 'surface', string_id: 'x1' }), (e) => e.code === '23514');
129
+ });
130
+
131
+ await test("scope 'surface' with a NULL string_id inserts", async () => {
132
+ const { rows } = await ins({ scope: 'surface', string_id: null, by: B });
133
+ assert.equal(rows[0].scope, 'surface');
134
+ assert.equal(rows[0].string_id, null);
135
+ });
136
+
137
+ await test('an unknown kind is refused', async () => {
138
+ await assert.rejects(() => ins({ kind: 'vibes', string_id: 'k1' }), (e) => e.code === '23514');
139
+ });
140
+
141
+ await test('THE PARTIAL UNIQUE INDEX: the same builder on the same target twice → 23505', async () => {
142
+ await ins({ string_id: 'dup-target', by: A });
143
+ await assert.rejects(() => ins({ string_id: 'dup-target', by: A }), (e) => e.code === '23505');
144
+ });
145
+
146
+ await test('a DIFFERENT builder may flag the same target — that agreement is signal', async () => {
147
+ const { rows } = await ins({ string_id: 'dup-target', by: B });
148
+ assert.ok(rows[0].id);
149
+ });
150
+
151
+ await test('the guarded close stamps exactly one row', async () => {
152
+ const { rows: [f] } = await ins({ string_id: 'close-me', by: A });
153
+ const upd = await pool.query(
154
+ `UPDATE copy_desk_flags SET status=$2, resolution_note=$3, resolved_task_id=$4, resolved_by=$5, resolved_at=now()
155
+ WHERE id=$1 AND status='open' RETURNING *`,
156
+ [f.id, 'resolved', 'Rewritten in the ship below.', T, B]);
157
+ assert.equal(upd.rowCount, 1);
158
+ assert.equal(upd.rows[0].status, 'resolved');
159
+ assert.equal(String(upd.rows[0].resolved_task_id), String(T));
160
+ assert.ok(upd.rows[0].resolved_at);
161
+ });
162
+
163
+ await test('A SECOND CLOSE LOSES THE RACE — 0 rows, never an overwrite of the first verdict', async () => {
164
+ const { rows: [f] } = await ins({ string_id: 'race', by: A });
165
+ const first = await pool.query(
166
+ `UPDATE copy_desk_flags SET status='resolved', resolution_note='first verdict stands', resolved_by=$2, resolved_at=now()
167
+ WHERE id=$1 AND status='open' RETURNING *`, [f.id, A]);
168
+ const second = await pool.query(
169
+ `UPDATE copy_desk_flags SET status='dismissed', resolution_note='second must not win', resolved_by=$2, resolved_at=now()
170
+ WHERE id=$1 AND status='open' RETURNING *`, [f.id, B]);
171
+ assert.equal(first.rowCount, 1);
172
+ assert.equal(second.rowCount, 0);
173
+ const { rows } = await pool.query('SELECT resolution_note FROM copy_desk_flags WHERE id=$1', [f.id]);
174
+ assert.equal(rows[0].resolution_note, 'first verdict stands');
175
+ });
176
+
177
+ await test('after a close, the SAME builder may flag that target again (the index is partial)', async () => {
178
+ const { rows } = await ins({ string_id: 'close-me', by: A });
179
+ assert.ok(rows[0].id, 'a sentence that regresses must be re-flaggable by the person who caught it before');
180
+ });
181
+
182
+ await test('a closed row with no resolved_at is refused — "resolved by nobody at no time" is impossible', async () => {
183
+ const { rows: [f] } = await ins({ string_id: 'halfclosed', by: A });
184
+ await assert.rejects(
185
+ () => pool.query("UPDATE copy_desk_flags SET status='resolved' WHERE id=$1", [f.id]),
186
+ (e) => e.code === '23514');
187
+ });
188
+
189
+ await test('THE FK: a nonexistent resolved_task_id raises 23503 — the route\'s named 400', async () => {
190
+ const { rows: [f] } = await ins({ string_id: 'fk-test', by: A });
191
+ await assert.rejects(
192
+ () => pool.query(
193
+ `UPDATE copy_desk_flags SET status='resolved', resolution_note='names a task that does not exist',
194
+ resolved_by=$2, resolved_at=now(), resolved_task_id=999999999 WHERE id=$1`, [f.id, A]),
195
+ (e) => e.code === '23503');
196
+ });
197
+
198
+ console.log("\nthe module's own SQL functions (flags.js), not hand-written queries");
199
+
200
+ // Point the doorway's shared pool at THIS connection so flags.js runs its real
201
+ // queries against the test database.
202
+ const api = require(path.join(ROOT, 'src/module-api.js'));
203
+ api.pool.query = (...a) => pool.query(...a);
204
+ const flagsLib = require(path.join(ROOT, 'modules/copy-desk/flags.js'));
205
+
206
+ await test('insertFlag writes and projects the full row shape', async () => {
207
+ const row = await flagsLib.insertFlag({
208
+ scope: 'string', surface: SURF, string_id: 'lib-1', reason: 'Says nothing a reader can act on.',
209
+ kind: 'unclear', text_at_flag: 'Enter.', file_at_flag: 'i.html', line_at_flag: 4, registry_schema: 1,
210
+ }, A);
211
+ assert.equal(row.status, 'open');
212
+ assert.equal(row.kind, 'unclear');
213
+ assert.equal(row.string_id, 'lib-1');
214
+ assert.ok('resolution_note' in row, 'the RETURNING projection matches the read projection');
215
+ });
216
+
217
+ await test('listFlags resolves the flagger join and sorts open ahead of closed', async () => {
218
+ const rows = await flagsLib.listFlags({ status: 'all', surface: SURF });
219
+ assert.ok(rows.length > 0);
220
+ assert.ok(rows.some((r) => r.flagged_by_login === 'cdtest-a'), 'the builders join resolves a display name');
221
+ const firstClosed = rows.findIndex((r) => r.status !== 'open');
222
+ if (firstClosed !== -1) {
223
+ assert.ok(rows.slice(firstClosed).every((r) => r.status !== 'open'), 'open rows sort ahead of closed ones');
224
+ }
225
+ });
226
+
227
+ await test('listFlags honours the status + surface filters', async () => {
228
+ const open = await flagsLib.listFlags({ status: 'open', surface: SURF });
229
+ assert.ok(open.length > 0);
230
+ assert.ok(open.every((r) => r.status === 'open' && r.surface === SURF));
231
+ });
232
+
233
+ await test('closeFlag returns null on a second call — the route answers 409 from that', async () => {
234
+ const row = await flagsLib.insertFlag({
235
+ scope: 'string', surface: SURF, string_id: 'lib-close', reason: 'Off voice for the front door.',
236
+ kind: 'off-voice', text_at_flag: 'x', file_at_flag: 'i.html', line_at_flag: 9, registry_schema: 1,
237
+ }, B);
238
+ const first = await flagsLib.closeFlag(row.id, { status: 'dismissed', resolution_note: 'Deliberate house style.', resolved_task_id: null }, A);
239
+ assert.ok(first, 'the first close returns the row');
240
+ const second = await flagsLib.closeFlag(row.id, { status: 'resolved', resolution_note: 'must not win', resolved_task_id: null }, B);
241
+ assert.equal(second, null);
242
+ });
243
+
244
+ // --- cleanup --------------------------------------------------------------
245
+ await clean();
246
+ await pool.query('DELETE FROM builders WHERE id = ANY($1)', [[A, B]]).catch(() => {});
247
+
248
+ console.log(`\n${passed} passed, ${failed} failed`);
249
+ await pool.end();
250
+ process.exit(failed ? 1 : 0);
@@ -131,7 +131,13 @@ test('sidebar groups read by audience, in order', () => {
131
131
  // naming the top rank was wrong), then → 'Government' by ADR 0174: a header
132
132
  // reading "Archon" over items a Metic can open would misdescribe the ladder.
133
133
  assert.deepEqual(nav.map((g) => g.label), ['', 'You', 'Community', 'Docs', 'Government']);
134
- assert.deepEqual(nav[0].items.map((i) => i.id), ['home', 'roadmap', 'goals', 'work'],
134
+ // task 1003113 (R02 of goal 1000074): 'copy-desk' joins the unlabeled group,
135
+ // after Tasks. It belongs with the project's own work and not in Docs — it is a
136
+ // QUEUE somebody acts on, not reference material — and not in You or Community,
137
+ // whose subject is a person rather than the product. Deliberate addition to
138
+ // this oracle: the group is pinned exactly so a new tab in the hall's most
139
+ // valuable nav real estate is a reviewed decision, not a drift.
140
+ assert.deepEqual(nav[0].items.map((i) => i.id), ['home', 'roadmap', 'goals', 'work', 'copy-desk'],
135
141
  'the unlabeled group is the project itself (task 1002425 order)');
136
142
  assert.deepEqual(nav[1].items.map((i) => i.id), ['standing', 'profile'], 'You = your own numbers');
137
143
  // People (task 1002577): the /scouting directory graduated into the hall.
@@ -173,7 +173,10 @@ test('the live tree discovers the extracted modules cleanly (BV1.R43/R48/R47/R49
173
173
  // configurable roles/permissions module; and agents (ADR 0083 / goal 1000038) the
174
174
  // declarative agent registry — the first module to ship default:false, which is
175
175
  // why it is DISCOVERED here (discovery is enablement-independent) while staying
176
- // off in enabledModules(). All are discovered; none has validation
176
+ // off in enabledModules(). And copy-desk (task 1003113 / R02 of goal 1000074) —
177
+ // the copy half of the design contract: the flag + the artist's queue over
178
+ // user-facing text, default-on because every instance shows a person words.
179
+ // All are discovered; none has validation
177
180
  // errors or coreVersion mismatches. Order is the readdir sort.
178
181
  //
179
182
  // NB this is the CORE package tree: game / art-pipeline / character-anim / demo are
@@ -197,7 +200,7 @@ test('the live tree discovers the extracted modules cleanly (BV1.R43/R48/R47/R49
197
200
  // That is precisely how the agents regression reached an open PR (task 1002485).
198
201
  loader._reset();
199
202
  const snap = loader.loaderSnapshot();
200
- assert.deepEqual(snap.loaded, ['agents', 'autonomy', 'builder-settings', 'dev-box', 'discord', 'economy', 'government', 'grading', 'hall-ui', 'ideas', 'lifecycle', 'memory', 'onboarding', 'platform-identity', 'provisioning', 'security', 'sessions', 'status-ui']);
203
+ assert.deepEqual(snap.loaded, ['agents', 'autonomy', 'builder-settings', 'copy-desk', 'dev-box', 'discord', 'economy', 'government', 'grading', 'hall-ui', 'ideas', 'lifecycle', 'memory', 'onboarding', 'platform-identity', 'provisioning', 'security', 'sessions', 'status-ui']);
201
204
  assert.deepEqual(snap.errors, []);
202
205
  assert.deepEqual(snap.skipped, []);
203
206
  loader._reset();