@consiliency/agent-board-schema 1.55.0 → 1.57.0

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.
@@ -2,19 +2,49 @@
2
2
  --
3
3
  -- Mutation controls (the assertion names are part of the evidence contract):
4
4
  -- * removing the owner/admin gate fails RC-32 (member denial);
5
- -- * filtering active state before latest-row selection fails RC-38 (stale row);
6
- -- * removing the id DESC tiebreak fails RC-39 (equal timestamp);
7
5
  -- * omitting role from v_board_memberships fails RC-12 (catalog ordinal);
8
- -- * removing the roster board predicate fails RC-37 (cross-board isolation).
6
+ -- * removing the roster board predicate fails RC-37 (cross-board isolation);
7
+ -- * dropping the MBROWID 3a active-first key from the AUTHORIZATION site fails
8
+ -- RC-45/RC-47/RC-49 (an active owner/admin with a newer removed/left/requested
9
+ -- row is refused), message-board#542 -- and, since those callers then capture
10
+ -- nothing, every board-C state check after them;
11
+ -- * dropping it from the PROJECTION site fails RC-38/RC-39 and RC-46/RC-48/
12
+ -- RC-50/RC-51 (that member is omitted) while RC-45/RC-47/RC-49 still pass;
13
+ -- * splitting authorization into "has an active row" AND "has an owner/admin
14
+ -- row in any state" fails RC-55/RC-56 (a member/observer with a newer
15
+ -- non-active owner/admin row is admitted);
16
+ -- * dropping the projection's active-state filter fails RC-25/RC-28/RC-51/RC-52
17
+ -- (non-active agents listed); dropping the authorization's active-state check
18
+ -- fails RC-53 (an owner with no active row admitted).
9
19
  --
10
- -- HAND-COUNTED plan: RC-01..RC-44 = 44 assertions. The runner independently
20
+ -- CURRENT STANDING IS ACTIVE-FIRST (message-board#542, migration 20261006000100).
21
+ -- Each agent's current row is chosen `case when membership_state in
22
+ -- ('joined','subscribed') then 0 else 1 end, created_at desc, id desc`, as every
23
+ -- other acting membership reader does (MBROWID 3a). The partial active unique
24
+ -- index allows at most ONE active row per (board, agent), so the result is "the
25
+ -- agent's active row, if any". Two consequences for this file:
26
+ -- * RC-38/RC-39 used to assert that a newer left/removed row HID an older
27
+ -- joined row. That was the #542 defect written down as a requirement; they
28
+ -- now assert the opposite, on board C.
29
+ -- * the id DESC tiebreak is no longer observable through this function: with
30
+ -- at most one active row it only orders non-active rows, which are never
31
+ -- listed and never authorize. It is pinned structurally by assertion 99 of
32
+ -- agent_board_rowid.test.sql (both list_board_members sites carry
33
+ -- `created_at desc, id desc`); the key itself is a class property in
34
+ -- agent_board_membership_current_row_class.test.sql.
35
+ --
36
+ -- EMISSION ORDER differs from label order: in the #542 section the captures
37
+ -- RC-45/47/49 run first, then RC-46/48/50/51, RC-38, RC-39, RC-52, RC-57,
38
+ -- RC-55, RC-56, RC-53, RC-54. Cite assertions by LABEL, not by TAP number.
39
+ --
40
+ -- HAND-COUNTED plan: RC-01..RC-57 = 57 assertions. The runner independently
11
41
  -- verifies the plan and emitted assertion counts, so a truncated file cannot pass.
12
42
  create extension if not exists pgtap with schema extensions;
13
43
  set search_path = extensions, public, agent_board;
14
44
 
15
45
  begin;
16
46
 
17
- select plan(44);
47
+ select plan(57);
18
48
 
19
49
  -- ---------------------------------------------------------------------------
20
50
  -- Catalog contract: exact signature, projection, posture, privileges, and view.
@@ -158,6 +188,20 @@ create temp table _roomcontract_fixture (
158
188
  stale_agent uuid,
159
189
  tie_agent uuid,
160
190
  board_b_agent uuid,
191
+ board_c uuid,
192
+ c_owner_removed uuid,
193
+ c_admin_left uuid,
194
+ c_owner_requested uuid,
195
+ c_inactive_only uuid,
196
+ c_removed_owner uuid,
197
+ c_member_was_owner uuid,
198
+ c_observer_was_admin uuid,
199
+ c_member_was_owner_user uuid,
200
+ c_observer_was_admin_user uuid,
201
+ c_owner_removed_user uuid,
202
+ c_admin_left_user uuid,
203
+ c_owner_requested_user uuid,
204
+ c_removed_owner_user uuid,
161
205
  creator_user uuid,
162
206
  owner_user uuid,
163
207
  admin_user uuid,
@@ -177,6 +221,12 @@ declare
177
221
  v_observer_user uuid;
178
222
  v_nonmember_user uuid;
179
223
  v_suspended_user uuid;
224
+ v_c_owner_removed_user uuid;
225
+ v_c_admin_left_user uuid;
226
+ v_c_owner_requested_user uuid;
227
+ v_c_removed_owner_user uuid;
228
+ v_c_member_was_owner_user uuid;
229
+ v_c_observer_was_admin_user uuid;
180
230
  begin
181
231
  v_creator_user := public.ensure_github_user('rc-creator', 'rc-creator@example.test', 'rc-creator', 'RC Creator');
182
232
  v_owner_user := public.ensure_github_user('rc-owner', 'rc-owner@example.test', 'rc-owner', 'RC Owner');
@@ -185,6 +235,12 @@ begin
185
235
  v_observer_user := public.ensure_github_user('rc-observer', 'rc-observer@example.test', 'rc-observer', 'RC Observer');
186
236
  v_nonmember_user := public.ensure_github_user('rc-nonmember', 'rc-nonmember@example.test', 'rc-nonmember', 'RC Nonmember');
187
237
  v_suspended_user := public.ensure_github_user('rc-suspended', 'rc-suspended@example.test', 'rc-suspended', 'RC Suspended');
238
+ v_c_owner_removed_user := public.ensure_github_user('rc-c-owner-removed', 'rc-c-owner-removed@example.test', 'rc-c-owner-removed', 'RC C Owner Removed');
239
+ v_c_admin_left_user := public.ensure_github_user('rc-c-admin-left', 'rc-c-admin-left@example.test', 'rc-c-admin-left', 'RC C Admin Left');
240
+ v_c_owner_requested_user := public.ensure_github_user('rc-c-owner-requested', 'rc-c-owner-requested@example.test', 'rc-c-owner-requested', 'RC C Owner Requested');
241
+ v_c_removed_owner_user := public.ensure_github_user('rc-c-removed-owner', 'rc-c-removed-owner@example.test', 'rc-c-removed-owner', 'RC C Removed Owner');
242
+ v_c_member_was_owner_user := public.ensure_github_user('rc-c-member-was-owner', 'rc-c-member-was-owner@example.test', 'rc-c-member-was-owner', 'RC C Member Was Owner');
243
+ v_c_observer_was_admin_user := public.ensure_github_user('rc-c-observer-was-admin', 'rc-c-observer-was-admin@example.test', 'rc-c-observer-was-admin', 'RC C Observer Was Admin');
188
244
 
189
245
  insert into agent_board.agents(
190
246
  id, agent_type, handle, display_name, status, auth_user_id, owner_user_id
@@ -201,12 +257,20 @@ begin
201
257
  ('73000000-0000-4000-8000-00000000000a', 'agent', 'rc-suspended', 'RC Suspended', 'suspended', v_suspended_user, v_suspended_user),
202
258
  ('73000000-0000-4000-8000-00000000000b', 'agent', 'rc-stale', 'RC Stale', 'active', null, null),
203
259
  ('73000000-0000-4000-8000-00000000000c', 'agent', 'rc-tie', 'RC Tie', 'active', null, null),
204
- ('73000000-0000-4000-8000-00000000000d', 'agent', 'rc-board-b', 'RC Board B', 'active', null, null);
260
+ ('73000000-0000-4000-8000-00000000000d', 'agent', 'rc-board-b', 'RC Board B', 'active', null, null),
261
+ ('73000000-0000-4000-8000-00000000000e', 'human', 'rc-c-owner-removed', 'RC C Owner Removed', 'active', v_c_owner_removed_user, v_c_owner_removed_user),
262
+ ('73000000-0000-4000-8000-00000000000f', 'human', 'rc-c-admin-left', 'RC C Admin Left', 'active', v_c_admin_left_user, v_c_admin_left_user),
263
+ ('73000000-0000-4000-8000-000000000010', 'human', 'rc-c-owner-requested', 'RC C Owner Requested', 'active', v_c_owner_requested_user, v_c_owner_requested_user),
264
+ ('73000000-0000-4000-8000-000000000011', 'agent', 'rc-c-inactive-only', 'RC C Inactive Only', 'active', null, null),
265
+ ('73000000-0000-4000-8000-000000000012', 'human', 'rc-c-removed-owner', 'RC C Removed Owner', 'active', v_c_removed_owner_user, v_c_removed_owner_user),
266
+ ('73000000-0000-4000-8000-000000000013', 'human', 'rc-c-member-was-owner', 'RC C Member Was Owner', 'active', v_c_member_was_owner_user, v_c_member_was_owner_user),
267
+ ('73000000-0000-4000-8000-000000000014', 'human', 'rc-c-observer-was-admin', 'RC C Observer Was Admin', 'active', v_c_observer_was_admin_user, v_c_observer_was_admin_user);
205
268
 
206
269
  insert into agent_board.boards(id, slug, name, created_by_agent_id, status)
207
270
  values
208
271
  ('73010000-0000-4000-8000-000000000001', 'roomcontract-a', 'ROOMCONTRACT A', '73000000-0000-4000-8000-000000000001', 'active'),
209
- ('73010000-0000-4000-8000-000000000002', 'roomcontract-b', 'ROOMCONTRACT B', '73000000-0000-4000-8000-000000000001', 'active');
272
+ ('73010000-0000-4000-8000-000000000002', 'roomcontract-b', 'ROOMCONTRACT B', '73000000-0000-4000-8000-000000000001', 'active'),
273
+ ('73010000-0000-4000-8000-000000000003', 'roomcontract-c', 'ROOMCONTRACT C', '73000000-0000-4000-8000-000000000001', 'active');
210
274
 
211
275
  -- Board A current active roster: owner, admin, four members, observer.
212
276
  insert into agent_board.board_memberships(board_id, agent_id, membership_state, role, created_by_agent_id)
@@ -219,26 +283,59 @@ begin
219
283
  ('73010000-0000-4000-8000-000000000001', '73000000-0000-4000-8000-000000000007', 'joined', 'member', '73000000-0000-4000-8000-000000000002'),
220
284
  ('73010000-0000-4000-8000-000000000001', '73000000-0000-4000-8000-000000000008', 'joined', 'observer', '73000000-0000-4000-8000-000000000002');
221
285
 
222
- -- The board creator has historical owner standing but a newer departed row.
286
+ -- The board creator once held owner standing and has NO active row now: a
287
+ -- removed owner row and a newer left row. Creator identity is not authority.
223
288
  insert into agent_board.board_memberships(
224
289
  id, board_id, agent_id, membership_state, role, created_by_agent_id, left_at, created_at, updated_at
225
290
  ) values
226
- ('73020000-0000-4000-8000-000000000001', '73010000-0000-4000-8000-000000000001', '73000000-0000-4000-8000-000000000001', 'joined', 'owner', '73000000-0000-4000-8000-000000000001', null, '2026-09-01 00:00:00+00', '2026-09-01 00:00:00+00'),
291
+ ('73020000-0000-4000-8000-000000000001', '73010000-0000-4000-8000-000000000001', '73000000-0000-4000-8000-000000000001', 'removed', 'owner', '73000000-0000-4000-8000-000000000001', '2026-09-01 00:00:00+00', '2026-09-01 00:00:00+00', '2026-09-01 00:00:00+00'),
227
292
  ('73020000-0000-4000-8000-000000000002', '73010000-0000-4000-8000-000000000001', '73000000-0000-4000-8000-000000000001', 'left', 'member', '73000000-0000-4000-8000-000000000001', '2026-09-02 00:00:00+00', '2026-09-02 00:00:00+00', '2026-09-02 00:00:00+00');
228
293
 
229
- -- Stale-row control: selecting active rows before choosing latest resurrects this actor.
294
+ -- Board C (message-board#542): an active row coexisting with NEWER non-active
295
+ -- rows. The partial active unique index admits every pair below.
296
+ --
297
+ -- Stale-row control: an older joined row next to a newer left row is CURRENT.
230
298
  insert into agent_board.board_memberships(
231
299
  id, board_id, agent_id, membership_state, role, created_by_agent_id, left_at, created_at, updated_at
232
300
  ) values
233
- ('73020000-0000-4000-8000-000000000003', '73010000-0000-4000-8000-000000000001', '73000000-0000-4000-8000-00000000000b', 'joined', 'member', '73000000-0000-4000-8000-000000000002', null, '2026-09-03 00:00:00+00', '2026-09-03 00:00:00+00'),
234
- ('73020000-0000-4000-8000-000000000004', '73010000-0000-4000-8000-000000000001', '73000000-0000-4000-8000-00000000000b', 'left', 'member', '73000000-0000-4000-8000-000000000002', '2026-09-04 00:00:00+00', '2026-09-04 00:00:00+00', '2026-09-04 00:00:00+00');
301
+ ('73020000-0000-4000-8000-000000000003', '73010000-0000-4000-8000-000000000003', '73000000-0000-4000-8000-00000000000b', 'joined', 'member', '73000000-0000-4000-8000-000000000002', null, '2026-09-03 00:00:00+00', '2026-09-03 00:00:00+00'),
302
+ ('73020000-0000-4000-8000-000000000004', '73010000-0000-4000-8000-000000000003', '73000000-0000-4000-8000-00000000000b', 'left', 'member', '73000000-0000-4000-8000-000000000002', '2026-09-04 00:00:00+00', '2026-09-04 00:00:00+00', '2026-09-04 00:00:00+00');
235
303
 
236
- -- Total-order control: timestamps tie, so the larger removed-row id must win.
304
+ -- Equal-timestamp control: the removed row has the LARGER id, so a bare
305
+ -- `created_at desc, id desc` would pick it; active-first picks the joined row.
237
306
  insert into agent_board.board_memberships(
238
307
  id, board_id, agent_id, membership_state, role, created_by_agent_id, left_at, created_at, updated_at
239
308
  ) values
240
- ('73020000-0000-4000-8000-000000000010', '73010000-0000-4000-8000-000000000001', '73000000-0000-4000-8000-00000000000c', 'joined', 'member', '73000000-0000-4000-8000-000000000002', null, '2026-09-05 00:00:00+00', '2026-09-05 00:00:00+00'),
241
- ('73020000-0000-4000-8000-000000000011', '73010000-0000-4000-8000-000000000001', '73000000-0000-4000-8000-00000000000c', 'removed', 'member', '73000000-0000-4000-8000-000000000002', '2026-09-05 00:00:00+00', '2026-09-05 00:00:00+00', '2026-09-05 00:00:00+00');
309
+ ('73020000-0000-4000-8000-000000000010', '73010000-0000-4000-8000-000000000003', '73000000-0000-4000-8000-00000000000c', 'joined', 'member', '73000000-0000-4000-8000-000000000002', null, '2026-09-05 00:00:00+00', '2026-09-05 00:00:00+00'),
310
+ ('73020000-0000-4000-8000-000000000011', '73010000-0000-4000-8000-000000000003', '73000000-0000-4000-8000-00000000000c', 'removed', 'member', '73000000-0000-4000-8000-000000000002', '2026-09-05 00:00:00+00', '2026-09-05 00:00:00+00', '2026-09-05 00:00:00+00');
311
+
312
+ -- The three callers #542 names: an active owner with a newer REMOVED row, an
313
+ -- active admin with a newer LEFT row, an active owner with a newer REQUESTED row.
314
+ -- Then an agent whose ONLY rows are non-active, and an owner with no active row.
315
+ insert into agent_board.board_memberships(
316
+ id, board_id, agent_id, membership_state, role, created_by_agent_id, left_at, created_at, updated_at
317
+ ) values
318
+ ('73020000-0000-4000-8000-000000000020', '73010000-0000-4000-8000-000000000003', '73000000-0000-4000-8000-00000000000e', 'joined', 'owner', '73000000-0000-4000-8000-000000000001', null, '2026-09-10 00:00:00+00', '2026-09-10 00:00:00+00'),
319
+ ('73020000-0000-4000-8000-000000000021', '73010000-0000-4000-8000-000000000003', '73000000-0000-4000-8000-00000000000e', 'removed', 'owner', '73000000-0000-4000-8000-000000000001', '2026-09-11 00:00:00+00', '2026-09-11 00:00:00+00', '2026-09-11 00:00:00+00'),
320
+ ('73020000-0000-4000-8000-000000000022', '73010000-0000-4000-8000-000000000003', '73000000-0000-4000-8000-00000000000f', 'subscribed', 'admin', '73000000-0000-4000-8000-000000000001', null, '2026-09-10 00:00:00+00', '2026-09-10 00:00:00+00'),
321
+ ('73020000-0000-4000-8000-000000000023', '73010000-0000-4000-8000-000000000003', '73000000-0000-4000-8000-00000000000f', 'left', 'admin', '73000000-0000-4000-8000-000000000001', '2026-09-12 00:00:00+00', '2026-09-12 00:00:00+00', '2026-09-12 00:00:00+00'),
322
+ ('73020000-0000-4000-8000-000000000024', '73010000-0000-4000-8000-000000000003', '73000000-0000-4000-8000-000000000010', 'joined', 'owner', '73000000-0000-4000-8000-000000000001', null, '2026-09-10 00:00:00+00', '2026-09-10 00:00:00+00'),
323
+ ('73020000-0000-4000-8000-000000000025', '73010000-0000-4000-8000-000000000003', '73000000-0000-4000-8000-000000000010', 'requested', 'member', '73000000-0000-4000-8000-000000000001', null, '2026-09-13 00:00:00+00', '2026-09-13 00:00:00+00'),
324
+ ('73020000-0000-4000-8000-000000000026', '73010000-0000-4000-8000-000000000003', '73000000-0000-4000-8000-000000000011', 'left', 'member', '73000000-0000-4000-8000-000000000001', '2026-09-10 00:00:00+00', '2026-09-10 00:00:00+00', '2026-09-10 00:00:00+00'),
325
+ ('73020000-0000-4000-8000-000000000027', '73010000-0000-4000-8000-000000000003', '73000000-0000-4000-8000-000000000011', 'removed', 'member', '73000000-0000-4000-8000-000000000001', '2026-09-11 00:00:00+00', '2026-09-11 00:00:00+00', '2026-09-11 00:00:00+00'),
326
+ ('73020000-0000-4000-8000-000000000028', '73010000-0000-4000-8000-000000000003', '73000000-0000-4000-8000-000000000012', 'left', 'owner', '73000000-0000-4000-8000-000000000001', '2026-09-10 00:00:00+00', '2026-09-10 00:00:00+00', '2026-09-10 00:00:00+00'),
327
+ ('73020000-0000-4000-8000-000000000029', '73010000-0000-4000-8000-000000000003', '73000000-0000-4000-8000-000000000012', 'removed', 'owner', '73000000-0000-4000-8000-000000000001', '2026-09-11 00:00:00+00', '2026-09-11 00:00:00+00', '2026-09-11 00:00:00+00');
328
+
329
+ -- The NARROWING direction (round-1 review): an ACTIVE member/observer row
330
+ -- next to a NEWER non-active OWNER/ADMIN row. Standing comes from the active
331
+ -- row, so the historical owner/admin role confers nothing.
332
+ insert into agent_board.board_memberships(
333
+ id, board_id, agent_id, membership_state, role, created_by_agent_id, left_at, created_at, updated_at
334
+ ) values
335
+ ('73020000-0000-4000-8000-000000000030', '73010000-0000-4000-8000-000000000003', '73000000-0000-4000-8000-000000000013', 'joined', 'member', '73000000-0000-4000-8000-000000000001', null, '2026-09-10 00:00:00+00', '2026-09-10 00:00:00+00'),
336
+ ('73020000-0000-4000-8000-000000000031', '73010000-0000-4000-8000-000000000003', '73000000-0000-4000-8000-000000000013', 'removed', 'owner', '73000000-0000-4000-8000-000000000001', '2026-09-14 00:00:00+00', '2026-09-14 00:00:00+00', '2026-09-14 00:00:00+00'),
337
+ ('73020000-0000-4000-8000-000000000032', '73010000-0000-4000-8000-000000000003', '73000000-0000-4000-8000-000000000014', 'subscribed', 'observer', '73000000-0000-4000-8000-000000000001', null, '2026-09-10 00:00:00+00', '2026-09-10 00:00:00+00'),
338
+ ('73020000-0000-4000-8000-000000000033', '73010000-0000-4000-8000-000000000003', '73000000-0000-4000-8000-000000000014', 'left', 'admin', '73000000-0000-4000-8000-000000000001', '2026-09-14 00:00:00+00', '2026-09-14 00:00:00+00', '2026-09-14 00:00:00+00');
242
339
 
243
340
  insert into agent_board.board_memberships(board_id, agent_id, membership_state, role, created_by_agent_id)
244
341
  values
@@ -255,6 +352,16 @@ begin
255
352
  '73000000-0000-4000-8000-00000000000b',
256
353
  '73000000-0000-4000-8000-00000000000c',
257
354
  '73000000-0000-4000-8000-00000000000d',
355
+ '73010000-0000-4000-8000-000000000003',
356
+ '73000000-0000-4000-8000-00000000000e',
357
+ '73000000-0000-4000-8000-00000000000f',
358
+ '73000000-0000-4000-8000-000000000010',
359
+ '73000000-0000-4000-8000-000000000011',
360
+ '73000000-0000-4000-8000-000000000012',
361
+ '73000000-0000-4000-8000-000000000013',
362
+ '73000000-0000-4000-8000-000000000014',
363
+ v_c_member_was_owner_user, v_c_observer_was_admin_user,
364
+ v_c_owner_removed_user, v_c_admin_left_user, v_c_owner_requested_user, v_c_removed_owner_user,
258
365
  v_creator_user, v_owner_user, v_admin_user, v_member_user, v_observer_user,
259
366
  v_nonmember_user, v_suspended_user
260
367
  );
@@ -401,7 +508,7 @@ set local role authenticated;
401
508
  select throws_ok(
402
509
  (select format($q$select agent_board.list_board_members(%L::uuid)$q$, board_a) from _roomcontract_fixture),
403
510
  '42501', 'only a board owner or admin may list board members',
404
- 'RC-36: board creator identity is not authority after its latest membership departed');
511
+ 'RC-36: board creator identity is not authority when no membership row of its own is active');
405
512
 
406
513
  reset role;
407
514
 
@@ -416,18 +523,149 @@ select is(
416
523
  where agent_id = (select board_b_agent from _roomcontract_fixture)),
417
524
  0,
418
525
  'RC-37: board B membership is absent from board A roster (cross-board isolation)');
526
+ reset role;
527
+
528
+ -- ---------------------------------------------------------------------------
529
+ -- message-board#542: current standing is ACTIVE-FIRST (MBROWID 3a), on board C.
530
+ -- Each allow is lives_ok over a CAPTURE (`create temp table … as select * from
531
+ -- list_board_members(…)`), then SEPARATE state checks read the captured rows
532
+ -- (AGENTS.md rule 3, #113). A bare `is((select … from list_board_members(…)))`
533
+ -- would ABORT the file on a 42501 instead of failing one named assertion:
534
+ -- measured while writing this section — with the authorization key mutated
535
+ -- away, the first bare form killed the transaction and only 38 of 54 ran.
536
+ -- ---------------------------------------------------------------------------
537
+ select set_config('request.jwt.claim.sub', (select c_owner_removed_user::text from _roomcontract_fixture), true);
538
+ set local role authenticated;
539
+ select lives_ok(
540
+ (select format($q$create temp table _rc542_owner_removed on commit drop as select * from agent_board.list_board_members(%L::uuid)$q$, board_c) from _roomcontract_fixture),
541
+ 'RC-45: an active owner with a NEWER removed row may list the roster (#542)');
542
+ reset role;
543
+ select set_config('request.jwt.claim.sub', (select c_admin_left_user::text from _roomcontract_fixture), true);
544
+ set local role authenticated;
545
+ select lives_ok(
546
+ (select format($q$create temp table _rc542_admin_left on commit drop as select * from agent_board.list_board_members(%L::uuid)$q$, board_c) from _roomcontract_fixture),
547
+ 'RC-47: an active admin with a NEWER left row may list the roster (#542)');
548
+ reset role;
549
+ select set_config('request.jwt.claim.sub', (select c_owner_requested_user::text from _roomcontract_fixture), true);
550
+ set local role authenticated;
551
+ select lives_ok(
552
+ (select format($q$create temp table _rc542_owner_requested on commit drop as select * from agent_board.list_board_members(%L::uuid)$q$, board_c) from _roomcontract_fixture),
553
+ 'RC-49: an active owner with a NEWER requested row may list the roster (#542)');
554
+ reset role;
555
+
556
+ -- A capture that never ran leaves no table; to_regclass keeps each state check
557
+ -- a named `not ok` (have NULL) instead of a missing-relation abort.
558
+ create temp table _rc542_rows (
559
+ caller text,
560
+ membership_id uuid,
561
+ board_id uuid,
562
+ agent_id uuid,
563
+ handle text,
564
+ display_name text,
565
+ agent_type text,
566
+ membership_state text,
567
+ role text,
568
+ created_by_agent_id uuid,
569
+ membership_created_at timestamptz,
570
+ membership_updated_at timestamptz
571
+ ) on commit drop;
572
+ do $$
573
+ begin
574
+ if to_regclass('pg_temp._rc542_owner_removed') is not null then
575
+ insert into _rc542_rows select 'owner_removed', r.* from _rc542_owner_removed r;
576
+ end if;
577
+ if to_regclass('pg_temp._rc542_admin_left') is not null then
578
+ insert into _rc542_rows select 'admin_left', r.* from _rc542_admin_left r;
579
+ end if;
580
+ if to_regclass('pg_temp._rc542_owner_requested') is not null then
581
+ insert into _rc542_rows select 'owner_requested', r.* from _rc542_owner_requested r;
582
+ end if;
583
+ end
584
+ $$;
585
+
586
+ select is(
587
+ (select string_agg(r.membership_state || '/' || r.role, ',')
588
+ from _rc542_rows r, _roomcontract_fixture f
589
+ where r.caller = 'owner_removed' and r.agent_id = f.c_owner_removed and r.board_id = f.board_c),
590
+ 'joined/owner',
591
+ 'RC-46: that owner is listed once, by its ACTIVE owner row, not the newer removed row');
592
+ select is(
593
+ (select string_agg(r.membership_state || '/' || r.role, ',')
594
+ from _rc542_rows r, _roomcontract_fixture f
595
+ where r.caller = 'admin_left' and r.agent_id = f.c_admin_left and r.board_id = f.board_c),
596
+ 'subscribed/admin',
597
+ 'RC-48: that admin is listed once, by its ACTIVE admin row, not the newer left row');
598
+ select is(
599
+ (select string_agg(r.membership_state || '/' || r.role, ',')
600
+ from _rc542_rows r, _roomcontract_fixture f
601
+ where r.caller = 'owner_requested' and r.agent_id = f.c_owner_requested and r.board_id = f.board_c),
602
+ 'joined/owner',
603
+ 'RC-50: that owner is listed once, by its ACTIVE owner row, not the newer requested row');
604
+ select is(
605
+ (select array_agg(r.handle order by r.handle collate "C")
606
+ from _rc542_rows r, _roomcontract_fixture f
607
+ where r.caller = 'owner_removed' and r.board_id = f.board_c),
608
+ array['rc-c-admin-left', 'rc-c-member-was-owner', 'rc-c-observer-was-admin', 'rc-c-owner-removed', 'rc-c-owner-requested', 'rc-stale', 'rc-tie']::text[],
609
+ 'RC-51: board C roster is exactly the seven agents holding an active row, whatever newer rows they have');
419
610
  select is(
420
611
  (select count(*)::int
421
- from agent_board.list_board_members((select board_a from _roomcontract_fixture))
422
- where agent_id = (select stale_agent from _roomcontract_fixture)),
423
- 0,
424
- 'RC-38: latest left row excludes an older joined row (choose latest before filtering state)');
612
+ from _rc542_rows r, _roomcontract_fixture f
613
+ where r.caller = 'owner_removed' and r.agent_id = f.stale_agent and r.board_id = f.board_c
614
+ and r.membership_state = 'joined'),
615
+ 1,
616
+ 'RC-38: an older joined row outranks a newer LEFT row (active-first before recency)');
425
617
  select is(
426
618
  (select count(*)::int
427
- from agent_board.list_board_members((select board_a from _roomcontract_fixture))
428
- where agent_id = (select tie_agent from _roomcontract_fixture)),
619
+ from _rc542_rows r, _roomcontract_fixture f
620
+ where r.caller = 'owner_removed' and r.agent_id = f.tie_agent and r.board_id = f.board_c
621
+ and r.membership_id = '73020000-0000-4000-8000-000000000010'::uuid),
622
+ 1,
623
+ 'RC-39: at an equal created_at the joined row outranks the larger-id removed row');
624
+ select is(
625
+ (select count(*)::int
626
+ from _rc542_rows r, _roomcontract_fixture f
627
+ where r.caller = 'owner_removed' and r.agent_id = f.c_inactive_only and r.board_id = f.board_c),
429
628
  0,
430
- 'RC-39: equal created_at rows use id DESC, so the larger removed row excludes the older joined row');
629
+ 'RC-52: an agent whose only rows are left/removed is not listed');
630
+
631
+ select is(
632
+ (select string_agg(r.handle || '=' || r.membership_state || '/' || r.role, ',' order by r.handle collate "C")
633
+ from _rc542_rows r, _roomcontract_fixture f
634
+ where r.caller = 'owner_removed' and r.board_id = f.board_c
635
+ and r.agent_id in (f.c_member_was_owner, f.c_observer_was_admin)),
636
+ 'rc-c-member-was-owner=joined/member,rc-c-observer-was-admin=subscribed/observer',
637
+ 'RC-57: a member/observer with a NEWER non-active owner/admin row is listed with its ACTIVE role');
638
+
639
+ select set_config('request.jwt.claim.sub', (select c_member_was_owner_user::text from _roomcontract_fixture), true);
640
+ set local role authenticated;
641
+ select throws_ok(
642
+ (select format($q$select agent_board.list_board_members(%L::uuid)$q$, board_c) from _roomcontract_fixture),
643
+ '42501', 'only a board owner or admin may list board members',
644
+ 'RC-55: an active MEMBER with a newer removed OWNER row is refused (the old role confers nothing)');
645
+ reset role;
646
+
647
+ select set_config('request.jwt.claim.sub', (select c_observer_was_admin_user::text from _roomcontract_fixture), true);
648
+ set local role authenticated;
649
+ select throws_ok(
650
+ (select format($q$select agent_board.list_board_members(%L::uuid)$q$, board_c) from _roomcontract_fixture),
651
+ '42501', 'only a board owner or admin may list board members',
652
+ 'RC-56: an active OBSERVER with a newer left ADMIN row is refused');
653
+ reset role;
654
+
655
+ select set_config('request.jwt.claim.sub', (select c_removed_owner_user::text from _roomcontract_fixture), true);
656
+ set local role authenticated;
657
+ select throws_ok(
658
+ (select format($q$select agent_board.list_board_members(%L::uuid)$q$, board_c) from _roomcontract_fixture),
659
+ '42501', 'only a board owner or admin may list board members',
660
+ 'RC-53: an owner whose rows are all left/removed (no active row) is refused');
661
+ reset role;
662
+
663
+ select set_config('request.jwt.claim.sub', (select owner_user::text from _roomcontract_fixture), true);
664
+ set local role authenticated;
665
+ select throws_ok(
666
+ (select format($q$select agent_board.list_board_members(%L::uuid)$q$, board_c) from _roomcontract_fixture),
667
+ '42501', 'only a board owner or admin may list board members',
668
+ 'RC-54: board A''s owner holds no row on board C and is refused there (standing is board-scoped)');
431
669
  reset role;
432
670
 
433
671
  -- ---------------------------------------------------------------------------
@@ -456,7 +694,7 @@ select set_config('request.jwt.claim.sub', '', true);
456
694
  \else
457
695
 
458
696
  select * from skip(
459
- 28,
697
+ 41,
460
698
  'ROOMCONTRACT runtime checks require list_board_members and v_board_memberships.role'
461
699
  );
462
700
 
@@ -19,7 +19,7 @@ set search_path = extensions, public, agent_board;
19
19
 
20
20
  begin;
21
21
 
22
- select plan(98);
22
+ select plan(100);
23
23
 
24
24
  create temp view _rowid_order_sites as
25
25
  with defs as (
@@ -29,7 +29,7 @@ with defs as (
29
29
  -- LITERALS STRIPPED, and re-derived rather than assumed. The first version of
30
30
  -- this line kept literals "because some ORDER BY operands are literals"; that
31
31
  -- was measured and is false for THIS view -- the derived checks over it match
32
- -- `created_at desc` / `id desc`, which are code, and the whole file is 98/98
32
+ -- `created_at desc` / `id desc`, which are code, and the whole file was 98/98 when measured
33
33
  -- with literals stripped. Keeping them left a real, if narrow, hole: the site
34
34
  -- set is built by splitting `def` on `;`, so a literal containing a semicolon
35
35
  -- re-splits a statement and can drop it out of the set.
@@ -506,9 +506,10 @@ select is(
506
506
  true,
507
507
  'control: fixture and structural block really runs as service_role');
508
508
 
509
- -- 2. Catalog-derived inventory: eight membership readers plus the two task/message idempotency
510
- -- readers. request_join_board joined the set in MBCONSENT-B(iii), and list_board_members joined
511
- -- it in ROOMCONTRACT; both read board_memberships with the created_at DESC + id DESC tiebreak.
509
+ -- 2. Catalog-derived inventory: nine membership readers plus the two task/message idempotency
510
+ -- readers. request_join_board joined the set in MBCONSENT-B(iii), list_board_members joined
511
+ -- it in ROOMCONTRACT, and list_my_board_requests joined it in ONBOARD (v11); all read
512
+ -- board_memberships with the created_at DESC + id DESC tiebreak.
512
513
  select is(
513
514
  (select array_agg(proname order by proname)
514
515
  from _rowid_function_order_counts),
@@ -519,12 +520,13 @@ select is(
519
520
  'join_board',
520
521
  'list_board_members',
521
522
  'list_board_pending_memberships',
523
+ 'list_my_board_requests',
522
524
  'post_board_message',
523
525
  'request_join_board',
524
526
  'resolve_board_request',
525
527
  'update_board_membership'
526
528
  ]::text[],
527
- 'live catalog exposes eight membership readers plus two task/message idempotency readers');
529
+ 'live catalog exposes nine membership readers plus two task/message idempotency readers');
528
530
 
529
531
  -- 3-10. COUNT-EQUALITY, never substring containment.
530
532
  select is(
@@ -1300,6 +1302,30 @@ select is(
1300
1302
  select set_config('request.jwt.claim.role', '', true);
1301
1303
  select set_config('request.jwt.claim.sub', '', true);
1302
1304
 
1305
+ -- 99. list_board_members (ROOMCONTRACT, re-created active-first by #542) was in
1306
+ -- the assertion-2 inventory but had no count-equality row of its own, so a
1307
+ -- dropped `bm.id desc` at either site went unnoticed here; through the roster
1308
+ -- itself the tiebreak is unobservable (it only orders non-active rows). Pinned as
1309
+ -- the exact pair (created_at DESC sites, of them with id DESC) so a site that
1310
+ -- disappears is not read as agreement 0 = 0.
1311
+ select is(
1312
+ (select array[created_at_desc_count, total_order_count]
1313
+ from _rowid_function_order_counts
1314
+ where proname = 'list_board_members'),
1315
+ array[2, 2],
1316
+ 'list_board_members: BOTH created_at DESC sites (authorization + projection) have the id DESC tiebreak');
1317
+
1318
+ -- 100. list_my_board_requests (ONBOARD v11, 20261007000100): one statement, two created_at
1319
+ -- DESC sites -- the lateral that picks the caller's current membership row (active-first),
1320
+ -- and the result order -- and both carry the id DESC tiebreak. The exact pair, as for 99,
1321
+ -- so a site that disappears is not read as agreement 0 = 0.
1322
+ select is(
1323
+ (select array[created_at_desc_count, total_order_count]
1324
+ from _rowid_function_order_counts
1325
+ where proname = 'list_my_board_requests'),
1326
+ array[2, 2],
1327
+ 'list_my_board_requests: BOTH created_at DESC sites (current row + result order) have the id DESC tiebreak');
1328
+
1303
1329
  select finish();
1304
1330
 
1305
1331
  rollback;
@@ -1036,6 +1036,15 @@
1036
1036
  "authenticated": true
1037
1037
  }
1038
1038
  },
1039
+ {
1040
+ "name": "invite_board_for_display",
1041
+ "signature": "invite_board_for_display(p_code_sha256 text)",
1042
+ "executable_by": {
1043
+ "anon": false,
1044
+ "service_role": true,
1045
+ "authenticated": false
1046
+ }
1047
+ },
1039
1048
  {
1040
1049
  "name": "invite_board_member",
1041
1050
  "signature": "invite_board_member(p_board_id uuid, p_agent_id uuid, p_created_by_agent_id uuid, p_metadata jsonb)",
@@ -1090,6 +1099,15 @@
1090
1099
  "authenticated": true
1091
1100
  }
1092
1101
  },
1102
+ {
1103
+ "name": "list_my_board_requests",
1104
+ "signature": "list_my_board_requests()",
1105
+ "executable_by": {
1106
+ "anon": false,
1107
+ "service_role": false,
1108
+ "authenticated": true
1109
+ }
1110
+ },
1093
1111
  {
1094
1112
  "name": "mark_read",
1095
1113
  "signature": "mark_read(p_thread_id uuid, p_agent_id uuid, p_last_read_message_sequence bigint)",
@@ -461,6 +461,10 @@
461
461
  "signature": "heartbeat(p_agent_id uuid, p_thread_id uuid, p_availability text, p_activity_summary text, p_expires_in interval, p_metadata jsonb)",
462
462
  "declares_p_metadata": true
463
463
  },
464
+ {
465
+ "signature": "invite_board_for_display(p_code_sha256 text)",
466
+ "declares_p_metadata": false
467
+ },
464
468
  {
465
469
  "signature": "invite_board_member(p_board_id uuid, p_agent_id uuid, p_created_by_agent_id uuid, p_metadata jsonb)",
466
470
  "declares_p_metadata": true
@@ -485,6 +489,10 @@
485
489
  "signature": "list_board_pending_memberships(p_board_id uuid)",
486
490
  "declares_p_metadata": false
487
491
  },
492
+ {
493
+ "signature": "list_my_board_requests()",
494
+ "declares_p_metadata": false
495
+ },
488
496
  {
489
497
  "signature": "mark_read(p_thread_id uuid, p_agent_id uuid, p_last_read_message_sequence bigint)",
490
498
  "declares_p_metadata": false