@consiliency/agent-board-schema 1.55.0 → 1.56.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.
- package/fixtures/already-applied.json +4 -0
- package/migrations/20261006000100_agent_board_room_roster_active_first.sql +118 -0
- package/package.json +1 -1
- package/schema-manifest.json +12 -3
- package/src/tooling.test.mjs +5 -2
- package/tests/agent_board_membership_current_row_class.test.sql +460 -0
- package/tests/agent_board_room_roster.test.sql +262 -24
- package/tests/agent_board_rowid.test.sql +15 -2
|
@@ -612,6 +612,10 @@
|
|
|
612
612
|
{
|
|
613
613
|
"id": "20261005000100_agent_board_enroll_token_single_use",
|
|
614
614
|
"sha256": "317d7b41432b36a2e38cc64940838e173e425014e3e256a96d832202b250e82d"
|
|
615
|
+
},
|
|
616
|
+
{
|
|
617
|
+
"id": "20261006000100_agent_board_room_roster_active_first",
|
|
618
|
+
"sha256": "637480feeca07dbb2fe7a4624dc6d063d0c1f23e6ea3c3ee9a946ad43de4e34e"
|
|
615
619
|
}
|
|
616
620
|
]
|
|
617
621
|
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
-- ROOMCONTRACT-ACTIVEFIRST (message-board#542): the owner/admin roster chooses current
|
|
2
|
+
-- standing ACTIVE-FIRST, as every other acting membership reader does (MBROWID 3a).
|
|
3
|
+
--
|
|
4
|
+
-- THE DEFECT. 20261003000100 chose the caller's standing (authorization) and each
|
|
5
|
+
-- member's inclusion (projection) from the LATEST board_memberships row alone:
|
|
6
|
+
-- `order by created_at desc, id desc`. The active-membership unique index is PARTIAL
|
|
7
|
+
-- (idx_agent_board_board_memberships_active_unique, WHERE membership_state in
|
|
8
|
+
-- ('joined','subscribed')), so one active row can coexist with newer non-active rows
|
|
9
|
+
-- (removed / left / requested / invited). MBROWID 3a (20260803000100) ruled that every
|
|
10
|
+
-- reader acting on "current standing" prefers the active row when one exists; join_board,
|
|
11
|
+
-- update_board_membership, _upsert_board_membership_state and
|
|
12
|
+
-- list_board_pending_memberships all do. The roster did not, so an active owner or admin
|
|
13
|
+
-- with a newer non-active row was refused with 42501 and omitted from every roster —
|
|
14
|
+
-- while the rest of MB (and Portal's self view, which reduces active-first as MBROWID
|
|
15
|
+
-- requires) treated them as active.
|
|
16
|
+
--
|
|
17
|
+
-- THE FIX. Both selection sites take the MBROWID 3a key, then the existing total order:
|
|
18
|
+
-- order by case when membership_state in ('joined','subscribed') then 0 else 1 end,
|
|
19
|
+
-- created_at desc, id desc
|
|
20
|
+
-- Everything else is unchanged: signature, projection, STABLE, SECURITY DEFINER,
|
|
21
|
+
-- search_path, the 42501 message, the result order, and the grants.
|
|
22
|
+
--
|
|
23
|
+
-- The class this belongs to is pinned from the catalog, not from this list:
|
|
24
|
+
-- tests/agent_board_membership_current_row_class.test.sql derives every statement that
|
|
25
|
+
-- picks ONE board_memberships row by recency and requires the active-first key, or a
|
|
26
|
+
-- named exemption whose filter it asserts exactly. This migration fixes the only two
|
|
27
|
+
-- sites that class found without the key; the exemptions are recorded there.
|
|
28
|
+
--
|
|
29
|
+
-- CREATE OR REPLACE keeps the ACL and the COMMENT, but both are restated below anyway:
|
|
30
|
+
-- the COMMENT's text described latest-row semantics and must change, and the grants are
|
|
31
|
+
-- restated so this file alone says what the function's posture is.
|
|
32
|
+
|
|
33
|
+
create or replace function agent_board.list_board_members(p_board_id uuid)
|
|
34
|
+
returns table(
|
|
35
|
+
membership_id uuid,
|
|
36
|
+
board_id uuid,
|
|
37
|
+
agent_id uuid,
|
|
38
|
+
handle text,
|
|
39
|
+
display_name text,
|
|
40
|
+
agent_type text,
|
|
41
|
+
membership_state text,
|
|
42
|
+
role text,
|
|
43
|
+
created_by_agent_id uuid,
|
|
44
|
+
membership_created_at timestamptz,
|
|
45
|
+
membership_updated_at timestamptz
|
|
46
|
+
)
|
|
47
|
+
language plpgsql
|
|
48
|
+
stable
|
|
49
|
+
security definer
|
|
50
|
+
set search_path = agent_board, public
|
|
51
|
+
as $$
|
|
52
|
+
declare
|
|
53
|
+
v_actor uuid := agent_board.current_agent_id();
|
|
54
|
+
begin
|
|
55
|
+
-- Authorization precedes disclosure. Creator identity is deliberately absent:
|
|
56
|
+
-- only the caller's current membership row can establish authority, and the
|
|
57
|
+
-- current row is the ACTIVE one when one exists (MBROWID 3a), else the latest.
|
|
58
|
+
if v_actor is null
|
|
59
|
+
or not exists (
|
|
60
|
+
select 1
|
|
61
|
+
from (
|
|
62
|
+
select bm.membership_state, bm.role
|
|
63
|
+
from agent_board.board_memberships bm
|
|
64
|
+
where bm.board_id = p_board_id
|
|
65
|
+
and bm.agent_id = v_actor
|
|
66
|
+
order by case when bm.membership_state in ('joined', 'subscribed') then 0 else 1 end,
|
|
67
|
+
bm.created_at desc, bm.id desc
|
|
68
|
+
limit 1
|
|
69
|
+
) actor_standing
|
|
70
|
+
where actor_standing.membership_state in ('joined', 'subscribed')
|
|
71
|
+
and actor_standing.role in ('owner', 'admin')
|
|
72
|
+
) then
|
|
73
|
+
raise exception 'only a board owner or admin may list board members'
|
|
74
|
+
using errcode = '42501';
|
|
75
|
+
end if;
|
|
76
|
+
|
|
77
|
+
-- Choose exactly one current row per agent (active-first, MBROWID 3a), then
|
|
78
|
+
-- keep only agents whose current row is active.
|
|
79
|
+
return query
|
|
80
|
+
select
|
|
81
|
+
latest.id,
|
|
82
|
+
latest.board_id,
|
|
83
|
+
latest.agent_id,
|
|
84
|
+
a.handle,
|
|
85
|
+
a.display_name,
|
|
86
|
+
a.agent_type,
|
|
87
|
+
latest.membership_state,
|
|
88
|
+
latest.role,
|
|
89
|
+
latest.created_by_agent_id,
|
|
90
|
+
latest.created_at,
|
|
91
|
+
latest.updated_at
|
|
92
|
+
from (
|
|
93
|
+
select distinct on (bm.agent_id) bm.*
|
|
94
|
+
from agent_board.board_memberships bm
|
|
95
|
+
where bm.board_id = p_board_id
|
|
96
|
+
order by bm.agent_id,
|
|
97
|
+
case when bm.membership_state in ('joined', 'subscribed') then 0 else 1 end,
|
|
98
|
+
bm.created_at desc, bm.id desc
|
|
99
|
+
) latest
|
|
100
|
+
join agent_board.agents a on a.id = latest.agent_id
|
|
101
|
+
where latest.membership_state in ('joined', 'subscribed')
|
|
102
|
+
order by
|
|
103
|
+
case latest.role
|
|
104
|
+
when 'owner' then 0
|
|
105
|
+
when 'admin' then 1
|
|
106
|
+
when 'member' then 2
|
|
107
|
+
when 'observer' then 3
|
|
108
|
+
end,
|
|
109
|
+
lower(a.handle),
|
|
110
|
+
latest.agent_id;
|
|
111
|
+
end;
|
|
112
|
+
$$;
|
|
113
|
+
|
|
114
|
+
comment on function agent_board.list_board_members(uuid) is
|
|
115
|
+
'Owner/admin-gated active board roster. Fails 42501 for a null actor (including service_role through this user-facing path), creator without current standing, or any caller whose current board membership is not active owner/admin. The current row is chosen active-first (MBROWID 3a): case when membership_state in (joined, subscribed) then 0 else 1 end, created_at desc, id desc — so an active row outranks a newer removed/left/requested/invited row. Returns each agent''s current row only when joined/subscribed, ordered owner/admin/member/observer then lower(handle), agent_id. Board-scoped and metadata-free.';
|
|
116
|
+
|
|
117
|
+
revoke all on function agent_board.list_board_members(uuid) from public, anon;
|
|
118
|
+
grant execute on function agent_board.list_board_members(uuid) to authenticated, service_role;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@consiliency/agent-board-schema",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.56.0",
|
|
4
4
|
"description": "Canonical, immutable, exact-pinnable agent_board schema bundle: ordered SQL migrations, digest + compatibility manifest, and deterministic materialize/plan/check tooling for adopters.",
|
|
5
5
|
"repository": "https://github.com/Consiliency/message-board.git",
|
|
6
6
|
"type": "module",
|
package/schema-manifest.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schema": "agent_board_schema_manifest.v1",
|
|
3
3
|
"interface": "IF-0-MBPLANE-BUNDLE-1",
|
|
4
4
|
"package": "@consiliency/agent-board-schema",
|
|
5
|
-
"packageVersion": "1.
|
|
5
|
+
"packageVersion": "1.56.0",
|
|
6
6
|
"schemaVersion": 8,
|
|
7
7
|
"minClientVersion": "1.1.0",
|
|
8
8
|
"source": {
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
"usage": "upsert (retention/monitoring defaults; on conflict do nothing)"
|
|
47
47
|
}
|
|
48
48
|
],
|
|
49
|
-
"migrationCount":
|
|
50
|
-
"aggregateSha256": "
|
|
49
|
+
"migrationCount": 154,
|
|
50
|
+
"aggregateSha256": "d0b74be9b41032a78ad45227706a48283f931b40cd6350c63c619a0e153c0cee",
|
|
51
51
|
"migrations": [
|
|
52
52
|
{
|
|
53
53
|
"order": 0,
|
|
@@ -1425,6 +1425,15 @@
|
|
|
1425
1425
|
"sourceRef": "message-board",
|
|
1426
1426
|
"bytes": 55964,
|
|
1427
1427
|
"sha256": "317d7b41432b36a2e38cc64940838e173e425014e3e256a96d832202b250e82d"
|
|
1428
|
+
},
|
|
1429
|
+
{
|
|
1430
|
+
"order": 153,
|
|
1431
|
+
"id": "20261006000100_agent_board_room_roster_active_first",
|
|
1432
|
+
"filename": "20261006000100_agent_board_room_roster_active_first.sql",
|
|
1433
|
+
"origin": "message-board-forward",
|
|
1434
|
+
"sourceRef": "message-board",
|
|
1435
|
+
"bytes": 5470,
|
|
1436
|
+
"sha256": "637480feeca07dbb2fe7a4624dc6d063d0c1f23e6ea3c3ee9a946ad43de4e34e"
|
|
1428
1437
|
}
|
|
1429
1438
|
],
|
|
1430
1439
|
"excludedBridge": {
|
package/src/tooling.test.mjs
CHANGED
|
@@ -38,9 +38,9 @@ describe('manifest integrity', () => {
|
|
|
38
38
|
expect(manifest.schemaVersion).toBe(8);
|
|
39
39
|
expect(manifest.minClientVersion).toMatch(/^\d+\.\d+\.\d+$/);
|
|
40
40
|
});
|
|
41
|
-
it('registers the four MBAUTHZ + four MBSENDAUTHZ + four MBSESSION + four MBDELEG + three MBFED + three MBHARDEN + two MBPORTALGATE + two MBPORTAL-PR32 + two ROOMOWN + two ROOMLINK + one ROOMDELEG + one LOCALAUTH + one ROOMUIAPI + one FEDSESSION + one FEDSESSION-AUTOJOIN + one GATENULL + one MEMBSTATE + one AGENTSDIR + one LEAVEIDEM + one ROLERESET + one MBROWID + one MBHYGIENE + one MBCONSENT-A + one MBCONSENT-B(a) + one MBCONSENT-B(i) + one MBCONSENT-B(ii) + one MBCONSENT-B(iii) + one MBHARDEN-#97 + one MBHARDEN-#118 + one INVITEPOLICY + one OBSGATE + one OBSGATE-2 + one OBSGATE-3 + one OBSGATE-4 + one FLEETMACHINERLS + one RESOLVENOOP + one ROOMDELEG-LANEB + one CLAIMKEY + two MINTHARDEN + one FLEETMACHINEHIJACK + one CLAIMGATE-SHADOW + one CLAIMGATE-ENFORCE + one FLEETMACHINEREACTIVATION + one DELEGCAPCEILING + one FLEETMACHINERECLAIM + one DELEGSOLEWRITER + one FLEETDOMAINRECLAIM + five ATOMICFLEETONBOARDING (onboarding + ceiling-lock + revocation-evidence + fedsession-gate + fedsession-lock-order) + one OUTBOXWAKE + one ENROLLLOCKORDER + one FEDPROVIDERABSENCE + one WAKERECOVERY + one CONSENTRECONCILE + one ENROLLRESUMPTION + one SIGNEDAUTHORITY + one SIGNERLIFECYCLE + one LEDGERAUTHORITY + one CEILINGLOCKCOMMENT + one LEDGERBOARDCREATE + one RECEIPTSUBJECTGATE + one TASKTHREADLOCKORDER + one CLAIMSUBJECTGATE + one FEDSESSIONIDENTITYGATE + one FEDIDENTITYSLUG + one INVITENODOWNGRADE + one SIGNEDWRITEFLAG + one SIGNEDWRITEMODEL + one RTCREDCLAIM + one ORIGINPOSTURE + one BOARDCLAIMPOLICY + one HEARTBEATPRESERVE + one LIVENESSDENOM + one OWNERLESSRENEWTTL + one OWNERLESSQUOTACENSUS + one FLEETDOMAINNODOWNGRADE + one OWNERLESSQUOTACONVERGE + one ADVISORYREVERTRESET + one CLAIMPOLICYRACE + one CLAIMPOLICYLOCKGRANT + one SESSIONCHALLENGESUPERSEDE + one ROOMCONTRACT + one CALLBACKCONSUMER + one ENROLLTOKENSINGLEUSE forward migrations (migrationCount = prior 113 + 5 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 =
|
|
41
|
+
it('registers the four MBAUTHZ + four MBSENDAUTHZ + four MBSESSION + four MBDELEG + three MBFED + three MBHARDEN + two MBPORTALGATE + two MBPORTAL-PR32 + two ROOMOWN + two ROOMLINK + one ROOMDELEG + one LOCALAUTH + one ROOMUIAPI + one FEDSESSION + one FEDSESSION-AUTOJOIN + one GATENULL + one MEMBSTATE + one AGENTSDIR + one LEAVEIDEM + one ROLERESET + one MBROWID + one MBHYGIENE + one MBCONSENT-A + one MBCONSENT-B(a) + one MBCONSENT-B(i) + one MBCONSENT-B(ii) + one MBCONSENT-B(iii) + one MBHARDEN-#97 + one MBHARDEN-#118 + one INVITEPOLICY + one OBSGATE + one OBSGATE-2 + one OBSGATE-3 + one OBSGATE-4 + one FLEETMACHINERLS + one RESOLVENOOP + one ROOMDELEG-LANEB + one CLAIMKEY + two MINTHARDEN + one FLEETMACHINEHIJACK + one CLAIMGATE-SHADOW + one CLAIMGATE-ENFORCE + one FLEETMACHINEREACTIVATION + one DELEGCAPCEILING + one FLEETMACHINERECLAIM + one DELEGSOLEWRITER + one FLEETDOMAINRECLAIM + five ATOMICFLEETONBOARDING (onboarding + ceiling-lock + revocation-evidence + fedsession-gate + fedsession-lock-order) + one OUTBOXWAKE + one ENROLLLOCKORDER + one FEDPROVIDERABSENCE + one WAKERECOVERY + one CONSENTRECONCILE + one ENROLLRESUMPTION + one SIGNEDAUTHORITY + one SIGNERLIFECYCLE + one LEDGERAUTHORITY + one CEILINGLOCKCOMMENT + one LEDGERBOARDCREATE + one RECEIPTSUBJECTGATE + one TASKTHREADLOCKORDER + one CLAIMSUBJECTGATE + one FEDSESSIONIDENTITYGATE + one FEDIDENTITYSLUG + one INVITENODOWNGRADE + one SIGNEDWRITEFLAG + one SIGNEDWRITEMODEL + one RTCREDCLAIM + one ORIGINPOSTURE + one BOARDCLAIMPOLICY + one HEARTBEATPRESERVE + one LIVENESSDENOM + one OWNERLESSRENEWTTL + one OWNERLESSQUOTACENSUS + one FLEETDOMAINNODOWNGRADE + one OWNERLESSQUOTACONVERGE + one ADVISORYREVERTRESET + one CLAIMPOLICYRACE + one CLAIMPOLICYLOCKGRANT + one SESSIONCHALLENGESUPERSEDE + one ROOMCONTRACT + one CALLBACKCONSUMER + one ENROLLTOKENSINGLEUSE + one ROOMCONTRACT-ACTIVEFIRST forward migrations (migrationCount = prior 113 + 5 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 = 154)', () => {
|
|
42
42
|
expect(manifest.migrationCount).toBe(manifest.migrations.length);
|
|
43
|
-
expect(manifest.migrationCount).toBe(
|
|
43
|
+
expect(manifest.migrationCount).toBe(154);
|
|
44
44
|
// MBSESSION switched enforcement on (scoped-send re-grant): the client-visible
|
|
45
45
|
// schema version bumped to 8. MBDELEG tightens delegation and MBFED adds only
|
|
46
46
|
// enrollment-only additive surface (identity_providers registry, enrollment audit,
|
|
@@ -340,6 +340,9 @@ describe('manifest integrity', () => {
|
|
|
340
340
|
'20261004000100_agent_board_callback_consumer_intent.sql',
|
|
341
341
|
// ENROLLTOKENSINGLEUSE (#521): a verified Portal token binds to at most one key.
|
|
342
342
|
'20261005000100_agent_board_enroll_token_single_use.sql',
|
|
343
|
+
// ROOMCONTRACT-ACTIVEFIRST (#542): list_board_members chooses current standing
|
|
344
|
+
// active-first (MBROWID 3a) at both its authorization and projection sites.
|
|
345
|
+
'20261006000100_agent_board_room_roster_active_first.sql',
|
|
343
346
|
]) {
|
|
344
347
|
expect(names.has(f)).toBe(true);
|
|
345
348
|
}
|
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
-- MBROWID 3a as a CLASS (message-board#542): every statement that picks ONE
|
|
2
|
+
-- board_memberships row must prefer the active row first.
|
|
3
|
+
--
|
|
4
|
+
-- WHY THIS FILE EXISTS. MBROWID (20260803000100) ruled that a reader acting on
|
|
5
|
+
-- "current standing" orders
|
|
6
|
+
-- case when membership_state in ('joined','subscribed') then 0 else 1 end,
|
|
7
|
+
-- created_at desc, id desc
|
|
8
|
+
-- and enumerated the readers of its day in a comment. ROOMCONTRACT
|
|
9
|
+
-- (20261003000100) then shipped list_board_members with bare
|
|
10
|
+
-- `created_at desc, id desc` at BOTH its selection sites, and nothing noticed:
|
|
11
|
+
-- the rule lived in a comment, and agent_board_rowid.test.sql pins the id DESC
|
|
12
|
+
-- tiebreak, not the active-first key. This file makes the key a class property,
|
|
13
|
+
-- so the next reader that forgets it fails here and NAMES itself.
|
|
14
|
+
--
|
|
15
|
+
-- THE CLASS, derived from the catalog (every agent_board function body, read
|
|
16
|
+
-- through agent_board_test.stripped_body so a comment cannot supply or hide a
|
|
17
|
+
-- site). A SITE is a query that picks ONE row from a SOURCE:
|
|
18
|
+
-- * SOURCE `direct`: agent_board.board_memberships or the unfiltered view
|
|
19
|
+
-- agent_board.v_board_memberships, at the query's own parenthesis level;
|
|
20
|
+
-- * SOURCE `outer` (form shown as `limit(outer)` etc.): a parenthesised group
|
|
21
|
+
-- -- a derived table or a CTE body -- that reads either one directly or one
|
|
22
|
+
-- group down, with the choosing ORDER BY outside the group;
|
|
23
|
+
-- and the ONE-ROW forms, each followed at the same level:
|
|
24
|
+
-- * `limit` : ORDER BY <list> LIMIT|FETCH;
|
|
25
|
+
-- * `distinct`: DISTINCT ON (...) ... ORDER BY <list>;
|
|
26
|
+
-- * `into` : a plpgsql SELECT ... INTO with an ORDER BY and no LIMIT, which
|
|
27
|
+
-- silently keeps the first row.
|
|
28
|
+
-- The ORDER BY operands are NOT inspected: a recency order spelled through an
|
|
29
|
+
-- output alias or a column ordinal is in the class the same as `created_at desc`,
|
|
30
|
+
-- and so is a non-recency order (see _grant_within_ceiling below).
|
|
31
|
+
-- A site is:
|
|
32
|
+
-- * `active-first` when the MBROWID 3a key is the FIRST ordering item (for
|
|
33
|
+
-- DISTINCT ON, the first after exactly the DISTINCT ON expressions) AND a
|
|
34
|
+
-- COMPLETE ascending item: `case when membership_state in ('joined',
|
|
35
|
+
-- 'subscribed') then 0 else 1 end`, then only [ASC] [NULLS FIRST|LAST], then
|
|
36
|
+
-- `,` or the end of the list (see the key pattern for why);
|
|
37
|
+
-- * `filtered:<set>` when the key is absent but the query's own WHERE pins
|
|
38
|
+
-- membership_state to an explicit literal set AT ITS OWN LEVEL (a
|
|
39
|
+
-- `membership_state in (...)` inside a nested subquery, on any table, is
|
|
40
|
+
-- consumed with that subquery and never read as this query's filter). The
|
|
41
|
+
-- row is then chosen among rows of that set only, and the set is asserted
|
|
42
|
+
-- EXACTLY per exemption below;
|
|
43
|
+
-- * `latest-row` otherwise -- the defect.
|
|
44
|
+
--
|
|
45
|
+
-- WHAT IS ASSERTED.
|
|
46
|
+
-- CRC-1 the exact site population (function / form / verdict). A new site --
|
|
47
|
+
-- compliant or not -- changes it and appears in the diff by name, so
|
|
48
|
+
-- reverse membership holds, and a site that silently leaves the class
|
|
49
|
+
-- (a matcher that stops matching) fails too.
|
|
50
|
+
-- CRC-2 no site is `latest-row`; the failure lists every offender by name.
|
|
51
|
+
-- CRC-3 each exemption, positively: the exact function and the exact state set
|
|
52
|
+
-- its WHERE pins. Widening one (resolve_board_request's pending read
|
|
53
|
+
-- admitting 'joined', say) fails here even though the site stays
|
|
54
|
+
-- "filtered".
|
|
55
|
+
-- CRC-4 the active-first members, exactly.
|
|
56
|
+
-- CRC-5.. behaviour rows: the SAME pattern objects over fixture text, with the
|
|
57
|
+
-- REJECT rows (key after another ordering item, a key naming the wrong
|
|
58
|
+
-- states, a key only in a comment, a `)` inside a literal) that every
|
|
59
|
+
-- defective matcher passes.
|
|
60
|
+
--
|
|
61
|
+
-- WHAT THIS CANNOT SEE, stated so the green is not over-read:
|
|
62
|
+
-- * views as SITES. v_board_directory's current_membership lateral is
|
|
63
|
+
-- active-first and is pinned BEHAVIOURALLY by agent_board_rowid.test.sql; a
|
|
64
|
+
-- deparsed viewdef spells the key `CASE WHEN (... = ANY (ARRAY[...]))`, which
|
|
65
|
+
-- this matcher does not read. (A function reading v_board_memberships IS a
|
|
66
|
+
-- site, above.)
|
|
67
|
+
-- * any other view or set-returning function that exposes membership rows,
|
|
68
|
+
-- read by a function: only the two relation names above are sources.
|
|
69
|
+
-- * an `outer` source nested more than two groups deep, and nesting deeper than
|
|
70
|
+
-- three parenthesis levels inside one ORDER BY or WHERE.
|
|
71
|
+
-- * dynamic SQL (EXECUTE of an assembled string), and a choice made by
|
|
72
|
+
-- max()/min() or a window function instead of ORDER BY.
|
|
73
|
+
-- * a filter widened by an OR elsewhere in the same WHERE level: the filter
|
|
74
|
+
-- check reads the first `membership_state in (...)` conjunct at that level,
|
|
75
|
+
-- not the boolean structure around it. An `outer` site's filter is read from
|
|
76
|
+
-- the span AFTER the group, so a filter inside the derived table does not
|
|
77
|
+
-- exempt it (fails closed).
|
|
78
|
+
-- * the key's SEMANTICS beyond its spelling. The matcher accepts one exact
|
|
79
|
+
-- spelling (arms 0/1, the literal pair in that order, ascending) and rejects
|
|
80
|
+
-- every other; an equivalent spelling (`in ('subscribed','joined')`,
|
|
81
|
+
-- `then 0 else 2`, a helper function returning the rank) is reported
|
|
82
|
+
-- `latest-row` and fails closed rather than being judged.
|
|
83
|
+
-- * a key spelled inside a DOLLAR-quoted string constant within the ORDER BY
|
|
84
|
+
-- list. Literals are kept (see _crc_bodies), so such a constant's text would
|
|
85
|
+
-- satisfy the key match. A single-quoted constant cannot: its doubled quotes
|
|
86
|
+
-- (`(''joined''`) break the `\(\s*'joined'` match. Ordering by a string
|
|
87
|
+
-- constant is not a shape any reader has a reason to write; it is named here
|
|
88
|
+
-- because the fixture rows cover the comment arm and not this one.
|
|
89
|
+
-- The shapes the second review round named (a derived table or CTE with the
|
|
90
|
+
-- ORDER BY ... LIMIT outside it, a read through v_board_memberships, an ORDER BY
|
|
91
|
+
-- alias or ordinal, a filter inside a nested subquery) are detected, and pinned
|
|
92
|
+
-- by CRC-15..CRC-19.
|
|
93
|
+
--
|
|
94
|
+
-- HAND-COUNTED plan: CRC-1..CRC-4 + 20 behaviour rows = 24.
|
|
95
|
+
create extension if not exists pgtap with schema extensions;
|
|
96
|
+
set search_path = extensions, public, agent_board;
|
|
97
|
+
|
|
98
|
+
begin;
|
|
99
|
+
|
|
100
|
+
select plan(24);
|
|
101
|
+
|
|
102
|
+
-- The composed patterns, built ONCE from the shared matcher family and read by
|
|
103
|
+
-- both the catalog scan and the behaviour rows, so the rows exercise the same
|
|
104
|
+
-- objects the scan compiles.
|
|
105
|
+
-- lit : a plain string literal, consumed whole ('' escapes), so a `)` or `;`
|
|
106
|
+
-- inside one cannot end a span;
|
|
107
|
+
-- bal : a parenthesised group, literal-aware, three levels deep;
|
|
108
|
+
-- item : one token of the SAME parenthesis level (a group is ONE token);
|
|
109
|
+
-- qn : a reference to agent_board.board_memberships OR to the unfiltered
|
|
110
|
+
-- view agent_board.v_board_memberships (bare admitted: every
|
|
111
|
+
-- agent_board function that could read either bare pins agent_board
|
|
112
|
+
-- on its search_path, and over-admitting only ADDS sites, which fails
|
|
113
|
+
-- closed);
|
|
114
|
+
-- grp : a parenthesised group (derived table, CTE body) that references qn
|
|
115
|
+
-- directly or one group down -- the `outer` source, whose ORDER BY
|
|
116
|
+
-- sits OUTSIDE the group that reads memberships.
|
|
117
|
+
create temp view _crc_frag as
|
|
118
|
+
with base as (
|
|
119
|
+
select
|
|
120
|
+
$l$'(?:[^']|'')*'$l$ as lit,
|
|
121
|
+
'\m' || agent_board_test.ci_re('order by') || '\M' as orderby,
|
|
122
|
+
'\m(?:' || agent_board_test.ci_re('limit') || '|' || agent_board_test.ci_re('fetch') || ')\M' as limitkw,
|
|
123
|
+
'(?:' || agent_board_test.qname_frag('board_memberships', true)
|
|
124
|
+
|| '|' || agent_board_test.qname_frag('v_board_memberships', true) || ')' as qn
|
|
125
|
+
),
|
|
126
|
+
bal as (
|
|
127
|
+
select b.*,
|
|
128
|
+
'\((?:[^()'']|' || b.lit || '|\((?:[^()'']|' || b.lit
|
|
129
|
+
|| '|\((?:[^()'']|' || b.lit || ')*\))*\))*\)' as bal
|
|
130
|
+
from base b
|
|
131
|
+
),
|
|
132
|
+
it as (
|
|
133
|
+
select b.*, '(?:[^;()'']|' || b.lit || '|' || b.bal || ')' as item,
|
|
134
|
+
'(?:[^()'']|' || b.lit || '|' || b.bal || ')' as gitem
|
|
135
|
+
from bal b
|
|
136
|
+
),
|
|
137
|
+
g1 as (
|
|
138
|
+
select i.*, '\(' || i.gitem || '*?' || i.qn || i.gitem || '*\)' as grp1
|
|
139
|
+
from it i
|
|
140
|
+
)
|
|
141
|
+
select
|
|
142
|
+
g.*,
|
|
143
|
+
'(?:' || g.grp1 || '|\(' || g.gitem || '*?' || g.grp1 || g.gitem || '*\))' as grp,
|
|
144
|
+
-- the MBROWID 3a key, literals included: its operands ARE the claim.
|
|
145
|
+
-- It must be a COMPLETE ASCENDING ORDER BY item, so the pattern pins three
|
|
146
|
+
-- things, not just the CASE text:
|
|
147
|
+
-- * the arms: exactly `then 0 else 1` -- `then 1 else 0` is the reversal
|
|
148
|
+
-- and does not match (`\s+0\s+` also refuses `0.5`, `00`, `0::int`);
|
|
149
|
+
-- * the CONTINUATION after END: only an optional ASC, then an optional
|
|
150
|
+
-- NULLS FIRST|LAST, then `,` or the END of the ORDER BY list. DESC (the
|
|
151
|
+
-- reversal), arithmetic (`* 0` erases the preference), a cast, COLLATE,
|
|
152
|
+
-- or anything else leaves the item unmatched, and the site falls through
|
|
153
|
+
-- to `latest-row` (fails closed). NULLS FIRST/LAST are admitted because
|
|
154
|
+
-- they are inert here: this CASE has an ELSE and literal arms, so it is
|
|
155
|
+
-- never NULL and a NULLS clause cannot reorder it;
|
|
156
|
+
-- * the START is checked by the classifier, not here: nothing but the
|
|
157
|
+
-- DISTINCT ON expressions may precede the key in the list.
|
|
158
|
+
agent_board_test.ci_re('case') || '\s+' || agent_board_test.ci_re('when') || '\s+'
|
|
159
|
+
|| agent_board_test.col_ref_frag('membership_state') || '\s+' || agent_board_test.ci_re('in')
|
|
160
|
+
|| $k$\s*\(\s*'joined'\s*,\s*'subscribed'\s*\)\s*$k$
|
|
161
|
+
|| agent_board_test.ci_re('then') || '\s+0\s+' || agent_board_test.ci_re('else') || '\s+1\s+'
|
|
162
|
+
|| agent_board_test.ci_re('end') || agent_board_test.trail_guard()
|
|
163
|
+
|| '(?:\s+' || agent_board_test.ci_re('asc') || agent_board_test.trail_guard() || ')?'
|
|
164
|
+
|| '(?:\s+' || agent_board_test.ci_re('nulls') || '\s+(?:' || agent_board_test.ci_re('first')
|
|
165
|
+
|| '|' || agent_board_test.ci_re('last') || ')' || agent_board_test.trail_guard() || ')?'
|
|
166
|
+
|| '\s*(?:,|$)' as key,
|
|
167
|
+
-- a `membership_state in ('<a>', '<b>', ...)` conjunct AT THE WHERE-SPAN'S OWN
|
|
168
|
+
-- LEVEL: every group before it is consumed whole, so a filter inside a nested
|
|
169
|
+
-- subquery (on any table) is never read as this query's filter.
|
|
170
|
+
'^(?:(?!' || agent_board_test.col_ref_frag('membership_state') || '\s+' || agent_board_test.ci_re('in') || '\s*\()'
|
|
171
|
+
|| g.item || ')*' || agent_board_test.col_ref_frag('membership_state') || '\s+' || agent_board_test.ci_re('in')
|
|
172
|
+
|| '\s*\(((?:\s*' || g.lit || '\s*,?)+)\)' as filter
|
|
173
|
+
from g1 g;
|
|
174
|
+
|
|
175
|
+
-- Six extraction patterns: three single-row FORMS x two SOURCES. The sources run
|
|
176
|
+
-- as SEPARATE patterns because a 'g' scan never returns overlapping matches: a
|
|
177
|
+
-- derived table with its own LIMIT inside and another outside would otherwise
|
|
178
|
+
-- lose one of the two sites.
|
|
179
|
+
-- form `limit` : <source> <where-span> ORDER BY <list> LIMIT|FETCH
|
|
180
|
+
-- form `distinct`: DISTINCT ON (<exprs>) ... <source> <where-span> ORDER BY <list> <end of level>
|
|
181
|
+
-- form `into` : SELECT ... INTO <var> ... <source> <where-span> ORDER BY <list>, no LIMIT
|
|
182
|
+
-- (`into` directly followed by the relation is an INSERT target)
|
|
183
|
+
-- Every pattern captures (1) the where-span, (2) the ORDER BY list; `distinct`
|
|
184
|
+
-- captures (3) the DISTINCT ON group.
|
|
185
|
+
create temp table _crc_re on commit drop as
|
|
186
|
+
with src(source, s, guard) as (
|
|
187
|
+
-- An OUTER match's where-span may not itself name the relation at its own
|
|
188
|
+
-- level: `exists (select … from memberships …) then … from memberships … order
|
|
189
|
+
-- by … limit 1` is the DIRECT site after the group, not a second, outer one.
|
|
190
|
+
select 'direct', f.qn, '' from _crc_frag f
|
|
191
|
+
union all
|
|
192
|
+
select 'outer', f.grp, '(?!' || f.qn || ')' from _crc_frag f
|
|
193
|
+
)
|
|
194
|
+
select
|
|
195
|
+
x.source,
|
|
196
|
+
agent_board_test.re(
|
|
197
|
+
x.s || '((?:(?!' || f.orderby || ')' || x.guard || f.item || ')*)' || f.orderby
|
|
198
|
+
|| '((?:(?!' || f.limitkw || ')' || f.item || ')*)(?=' || f.limitkw || ')'
|
|
199
|
+
) as limit_re,
|
|
200
|
+
agent_board_test.re(
|
|
201
|
+
'\m' || agent_board_test.ci_re('distinct on') || '\s*(' || f.bal || ')'
|
|
202
|
+
|| '(?:(?!' || f.orderby || ')' || f.item || ')*?' || x.s
|
|
203
|
+
|| '((?:(?!' || f.orderby || ')' || x.guard || f.item || ')*)' || f.orderby
|
|
204
|
+
|| '((?:(?!' || f.limitkw || ')' || f.item || ')*)(?=\s*(?:\)|;|$))'
|
|
205
|
+
) as distinct_re,
|
|
206
|
+
agent_board_test.re(
|
|
207
|
+
'\m' || agent_board_test.ci_re('into') || '\M(?!\s*' || f.qn || ')'
|
|
208
|
+
|| '(?:(?!' || f.orderby || ')' || f.item || ')*?' || x.s
|
|
209
|
+
|| '((?:(?!' || f.orderby || ')' || x.guard || f.item || ')*)' || f.orderby
|
|
210
|
+
|| '((?:(?!' || f.limitkw || ')' || f.item || ')*)(?=\s*(?:\)|;|$|\m'
|
|
211
|
+
|| agent_board_test.ci_re('for') || '\M|\m' || agent_board_test.ci_re('loop') || '\M))'
|
|
212
|
+
) as into_re,
|
|
213
|
+
agent_board_test.re(f.key) as key_re,
|
|
214
|
+
agent_board_test.re(f.filter) as filter_re,
|
|
215
|
+
agent_board_test.re(f.lit) as lit_re
|
|
216
|
+
from src x cross join _crc_frag f;
|
|
217
|
+
|
|
218
|
+
-- Bodies: every live agent_board function, plus the behaviour-row fixtures, in
|
|
219
|
+
-- ONE relation so both go through the same extraction.
|
|
220
|
+
-- stripped_body(..., true) KEEPS LITERALS on purpose: the key's operands
|
|
221
|
+
-- 'joined'/'subscribed' and the exemption filter sets ARE the claim, and a
|
|
222
|
+
-- literal-stripped body would make every key and every filter read identically.
|
|
223
|
+
-- Comments are still removed, so prose cannot supply a key.
|
|
224
|
+
create temp table _crc_fixture (label text primary key, src text not null) on commit drop;
|
|
225
|
+
insert into _crc_fixture(label, src) values
|
|
226
|
+
('latest', $f$select bm.id into v from agent_board.board_memberships bm where bm.board_id = p order by bm.created_at desc, bm.id desc limit 1$f$),
|
|
227
|
+
('keyed', $f$select bm.id into v from agent_board.board_memberships bm where bm.board_id = p order by case when bm.membership_state in ('joined', 'subscribed') then 0 else 1 end, bm.created_at desc, bm.id desc limit 1$f$),
|
|
228
|
+
('key-after', $f$select bm.id into v from agent_board.board_memberships bm where bm.board_id = p order by bm.created_at desc, case when bm.membership_state in ('joined', 'subscribed') then 0 else 1 end limit 1$f$),
|
|
229
|
+
('key-wrong-set', $f$select bm.id into v from agent_board.board_memberships bm where bm.board_id = p order by case when bm.membership_state in ('joined', 'removed') then 0 else 1 end, bm.created_at desc limit 1$f$),
|
|
230
|
+
('key-in-comment',$f$select bm.id into v from agent_board.board_memberships bm where bm.board_id = p order by /* case when bm.membership_state in ('joined', 'subscribed') then 0 else 1 end, */ bm.created_at desc limit 1$f$),
|
|
231
|
+
('paren-literal', $f$select bm.id into v from agent_board.board_memberships bm where bm.note <> ')' and bm.board_id = p order by bm.created_at desc limit 1$f$),
|
|
232
|
+
('distinct', $f$return query select * from (select distinct on (bm.agent_id) bm.* from agent_board.board_memberships bm where bm.board_id = p order by bm.agent_id, bm.created_at desc) x order by x.agent_id$f$),
|
|
233
|
+
('into-nolimit', $f$select bm.role into v from agent_board.board_memberships bm where bm.board_id = p order by bm.updated_at desc$f$),
|
|
234
|
+
('end-desc', $f$select bm.id into v from agent_board.board_memberships bm where bm.board_id = p order by case when bm.membership_state in ('joined', 'subscribed') then 0 else 1 end desc, bm.created_at desc limit 1$f$),
|
|
235
|
+
('end-times-zero',$f$select bm.id into v from agent_board.board_memberships bm where bm.board_id = p order by case when bm.membership_state in ('joined', 'subscribed') then 0 else 1 end * 0, bm.created_at desc limit 1$f$),
|
|
236
|
+
('then1-else0', $f$select bm.id into v from agent_board.board_memberships bm where bm.board_id = p order by case when bm.membership_state in ('joined', 'subscribed') then 1 else 0 end, bm.created_at desc limit 1$f$),
|
|
237
|
+
('end-cast', $f$select bm.id into v from agent_board.board_memberships bm where bm.board_id = p order by case when bm.membership_state in ('joined', 'subscribed') then 0 else 1 end::text, bm.created_at desc limit 1$f$),
|
|
238
|
+
('end-asc', $f$select bm.id into v from agent_board.board_memberships bm where bm.board_id = p order by case when bm.membership_state in ('joined', 'subscribed') then 0 else 1 end ASC nulls last, bm.created_at desc limit 1$f$),
|
|
239
|
+
('filtered', $f$select bm.id into v from agent_board.board_memberships bm where bm.board_id = p and bm.membership_state in ('joined','subscribed') order by bm.created_at desc limit 1$f$),
|
|
240
|
+
('outer-derived', $f$select x.id into v from (select bm.id, bm.created_at from agent_board.board_memberships bm where bm.board_id = p) x order by x.created_at desc limit 1$f$),
|
|
241
|
+
('outer-cte', $f$return query with h as (select bm.* from agent_board.board_memberships bm where bm.board_id = p) select h.id from h order by h.created_at desc limit 1$f$),
|
|
242
|
+
('view', $f$select m.board_membership_id into v from agent_board.v_board_memberships m where m.board_id = p order by m.created_at desc limit 1$f$),
|
|
243
|
+
('ordinal', $f$select bm.id, bm.created_at into v, t from agent_board.board_memberships bm where bm.board_id = p order by 2 desc limit 1$f$),
|
|
244
|
+
('nested-filter', $f$select bm.id into v from agent_board.board_memberships bm where bm.board_id = p and exists (select 1 from agent_board.agents o where o.id = bm.agent_id and o.membership_state in ('joined','subscribed')) order by bm.created_at desc limit 1$f$),
|
|
245
|
+
('not-a-site', $f$perform 1 from agent_board.board_memberships bm where bm.board_id = p order by bm.id for update; for r in select bm.id from agent_board.board_memberships bm where bm.membership_state in ('joined', 'subscribed') order by bm.created_at loop null; end loop$f$);
|
|
246
|
+
|
|
247
|
+
create temp view _crc_bodies as
|
|
248
|
+
select 'catalog'::text as kind,
|
|
249
|
+
p.proname::text as proname,
|
|
250
|
+
agent_board_test.stripped_body(p.oid, true) as body
|
|
251
|
+
from pg_proc p
|
|
252
|
+
join pg_namespace n on n.oid = p.pronamespace
|
|
253
|
+
where n.nspname = 'agent_board'
|
|
254
|
+
and p.prokind = 'f'
|
|
255
|
+
union all
|
|
256
|
+
select 'fixture', x.label, agent_board_test.strip_sql(x.src, true)
|
|
257
|
+
from _crc_fixture x;
|
|
258
|
+
|
|
259
|
+
create temp view _crc_sites as
|
|
260
|
+
with raw as (
|
|
261
|
+
select b.kind, b.proname, 'limit'::text as form, r.source, m[1] as where_span, m[2] as order_list, null::text as don
|
|
262
|
+
from _crc_bodies b cross join _crc_re as r cross join lateral regexp_matches(b.body, r.limit_re, 'g') as m
|
|
263
|
+
union all
|
|
264
|
+
select b.kind, b.proname, 'distinct', r.source, m[2], m[3], m[1]
|
|
265
|
+
from _crc_bodies b cross join _crc_re as r cross join lateral regexp_matches(b.body, r.distinct_re, 'g') as m
|
|
266
|
+
union all
|
|
267
|
+
select b.kind, b.proname, 'into', r.source, m[1], m[2], null
|
|
268
|
+
from _crc_bodies b cross join _crc_re as r cross join lateral regexp_matches(b.body, r.into_re, 'g') as m
|
|
269
|
+
),
|
|
270
|
+
cls as (
|
|
271
|
+
select raw.*,
|
|
272
|
+
nullif(regexp_instr(raw.order_list, k.key_re), 0) as key_at,
|
|
273
|
+
(select array_to_string(array_agg(l[1] order by ord), ',')
|
|
274
|
+
from regexp_matches(substring(raw.where_span from k.filter_re), k.lit_re, 'g')
|
|
275
|
+
with ordinality as fl(l, ord)) as filter_set
|
|
276
|
+
from raw join _crc_re as k on k.source = 'direct'
|
|
277
|
+
)
|
|
278
|
+
-- ACTIVE-FIRST means the key is the FIRST ordering item: nothing may precede it
|
|
279
|
+
-- except, for DISTINCT ON, exactly the DISTINCT ON expressions (which partition
|
|
280
|
+
-- and do not choose). The ordering operands are NOT inspected, so a recency order
|
|
281
|
+
-- spelled through an output alias or a column ordinal is judged the same way as
|
|
282
|
+
-- `created_at desc` -- and so is a non-recency order (a rank): any single-row
|
|
283
|
+
-- choice over memberships is in the class.
|
|
284
|
+
select kind, proname,
|
|
285
|
+
form || case when source = 'outer' then '(outer)' else '' end as form,
|
|
286
|
+
case
|
|
287
|
+
when key_at is not null
|
|
288
|
+
and (translate(substr(order_list, 1, key_at - 1), E' \t\r\n', '') = ''
|
|
289
|
+
or (form = 'distinct'
|
|
290
|
+
and translate(substr(order_list, 1, key_at - 1), E' \t\r\n', '')
|
|
291
|
+
= translate(substr(don, 2, length(don) - 2), E' \t\r\n', '') || ','))
|
|
292
|
+
then 'active-first'
|
|
293
|
+
when filter_set is not null then 'filtered:' || filter_set
|
|
294
|
+
else 'latest-row'
|
|
295
|
+
end as verdict
|
|
296
|
+
from cls;
|
|
297
|
+
|
|
298
|
+
-- ---------------------------------------------------------------------------
|
|
299
|
+
-- CRC-1: the exact population, derived from the catalog.
|
|
300
|
+
-- ---------------------------------------------------------------------------
|
|
301
|
+
select is(
|
|
302
|
+
(select array_agg(proname || '/' || form || '/' || verdict order by proname, form, verdict)
|
|
303
|
+
from _crc_sites where kind = 'catalog'),
|
|
304
|
+
array[
|
|
305
|
+
'_active_board_membership_id/limit/filtered:''joined'',''subscribed''',
|
|
306
|
+
'_grant_within_ceiling/limit/filtered:''joined'',''subscribed''',
|
|
307
|
+
'_upsert_board_membership_state/limit/active-first',
|
|
308
|
+
'join_board/limit/active-first',
|
|
309
|
+
'list_board_members/distinct/active-first',
|
|
310
|
+
'list_board_members/limit/active-first',
|
|
311
|
+
'list_board_pending_memberships/distinct/active-first',
|
|
312
|
+
'request_join_board/limit/filtered:''joined'',''subscribed''',
|
|
313
|
+
'resolve_board_request/limit/filtered:''joined'',''subscribed''',
|
|
314
|
+
'resolve_board_request/limit/filtered:''requested'',''invited''',
|
|
315
|
+
'update_board_membership/limit/active-first',
|
|
316
|
+
'update_board_membership/limit/active-first'
|
|
317
|
+
]::text[],
|
|
318
|
+
'CRC-1: every catalog statement choosing ONE board_memberships row is exactly this set');
|
|
319
|
+
|
|
320
|
+
-- ---------------------------------------------------------------------------
|
|
321
|
+
-- CRC-2: no site chooses the bare latest row.
|
|
322
|
+
-- ---------------------------------------------------------------------------
|
|
323
|
+
select is(
|
|
324
|
+
(select coalesce(array_agg(proname || '/' || form order by proname, form), '{}'::text[])
|
|
325
|
+
from _crc_sites where kind = 'catalog' and verdict = 'latest-row'),
|
|
326
|
+
'{}'::text[],
|
|
327
|
+
'CRC-2: no function chooses one board membership row without the MBROWID 3a key or an exact filter');
|
|
328
|
+
|
|
329
|
+
-- ---------------------------------------------------------------------------
|
|
330
|
+
-- CRC-3: each exemption's claim, positively -- the function and the EXACT set
|
|
331
|
+
-- its WHERE pins. Why each is legitimately not active-first:
|
|
332
|
+
-- _active_board_membership_id -- returns the ACTIVE row's id; the WHERE already
|
|
333
|
+
-- restricts to joined/subscribed, and the partial unique index allows at most
|
|
334
|
+
-- one such row, so there is nothing for the key to prefer (MBROWID 3c).
|
|
335
|
+
-- request_join_board -- the same: its idempotent return reads the
|
|
336
|
+
-- caller's existing ACTIVE row only.
|
|
337
|
+
-- resolve_board_request (active) -- the approval path's "already active" probe.
|
|
338
|
+
-- _grant_within_ceiling -- orders by ROLE RANK, not recency: it takes the
|
|
339
|
+
-- grantor's highest active role, among active rows only. With one active row
|
|
340
|
+
-- per (board, agent) the order is moot; it is in the class because the class
|
|
341
|
+
-- no longer requires a recency operand (an alias or ordinal would hide one).
|
|
342
|
+
-- resolve_board_request (pending) -- MBROWID 3c: the approval gate must see the
|
|
343
|
+
-- newest requested/invited marker, NOT prefer a coexisting active row; the
|
|
344
|
+
-- active case is handled by the probe above.
|
|
345
|
+
-- ---------------------------------------------------------------------------
|
|
346
|
+
select is(
|
|
347
|
+
(select array_agg(proname || '=' || substr(verdict, length('filtered:') + 1) order by proname, verdict)
|
|
348
|
+
from _crc_sites where kind = 'catalog' and verdict like 'filtered:%'),
|
|
349
|
+
array[
|
|
350
|
+
'_active_board_membership_id=''joined'',''subscribed''',
|
|
351
|
+
'_grant_within_ceiling=''joined'',''subscribed''',
|
|
352
|
+
'request_join_board=''joined'',''subscribed''',
|
|
353
|
+
'resolve_board_request=''joined'',''subscribed''',
|
|
354
|
+
'resolve_board_request=''requested'',''invited'''
|
|
355
|
+
]::text[],
|
|
356
|
+
'CRC-3: the five filtered exemptions pin exactly these membership_state sets');
|
|
357
|
+
|
|
358
|
+
-- ---------------------------------------------------------------------------
|
|
359
|
+
-- CRC-4: the active-first members, exactly.
|
|
360
|
+
-- ---------------------------------------------------------------------------
|
|
361
|
+
select is(
|
|
362
|
+
(select array_agg(proname || '/' || form order by proname, form)
|
|
363
|
+
from _crc_sites where kind = 'catalog' and verdict = 'active-first'),
|
|
364
|
+
array[
|
|
365
|
+
'_upsert_board_membership_state/limit',
|
|
366
|
+
'join_board/limit',
|
|
367
|
+
'list_board_members/distinct',
|
|
368
|
+
'list_board_members/limit',
|
|
369
|
+
'list_board_pending_memberships/distinct',
|
|
370
|
+
'update_board_membership/limit',
|
|
371
|
+
'update_board_membership/limit'
|
|
372
|
+
]::text[],
|
|
373
|
+
'CRC-4: these seven sites carry the MBROWID 3a active-first key');
|
|
374
|
+
|
|
375
|
+
-- ---------------------------------------------------------------------------
|
|
376
|
+
-- CRC-5..14: behaviour rows over fixture text, through the same objects.
|
|
377
|
+
-- ---------------------------------------------------------------------------
|
|
378
|
+
select is(
|
|
379
|
+
(select array_agg(form || '/' || verdict) from _crc_sites where kind = 'fixture' and proname = 'latest'),
|
|
380
|
+
array['limit/latest-row']::text[],
|
|
381
|
+
'CRC-5 REJECT: bare created_at desc, id desc + LIMIT is latest-row');
|
|
382
|
+
select is(
|
|
383
|
+
(select array_agg(form || '/' || verdict) from _crc_sites where kind = 'fixture' and proname = 'keyed'),
|
|
384
|
+
array['limit/active-first']::text[],
|
|
385
|
+
'CRC-6 ACCEPT: the MBROWID 3a key before the recency operands is active-first');
|
|
386
|
+
select is(
|
|
387
|
+
(select array_agg(form || '/' || verdict) from _crc_sites where kind = 'fixture' and proname = 'key-after'),
|
|
388
|
+
array['limit/latest-row']::text[],
|
|
389
|
+
'CRC-7 REJECT: the key AFTER a recency operand does not make the choice active-first');
|
|
390
|
+
select is(
|
|
391
|
+
(select array_agg(form || '/' || verdict) from _crc_sites where kind = 'fixture' and proname = 'key-wrong-set'),
|
|
392
|
+
array['limit/latest-row']::text[],
|
|
393
|
+
'CRC-8 REJECT: a key preferring (joined, removed) is not the active-first key');
|
|
394
|
+
select is(
|
|
395
|
+
(select array_agg(form || '/' || verdict) from _crc_sites where kind = 'fixture' and proname = 'key-in-comment'),
|
|
396
|
+
array['limit/latest-row']::text[],
|
|
397
|
+
'CRC-9 REJECT: a key written only in a comment is stripped and does not count');
|
|
398
|
+
select is(
|
|
399
|
+
(select array_agg(form || '/' || verdict) from _crc_sites where kind = 'fixture' and proname = 'paren-literal'),
|
|
400
|
+
array['limit/latest-row']::text[],
|
|
401
|
+
'CRC-10 REJECT: a `)` inside a string literal in the WHERE does not hide the site');
|
|
402
|
+
select is(
|
|
403
|
+
(select array_agg(form || '/' || verdict) from _crc_sites where kind = 'fixture' and proname = 'distinct'),
|
|
404
|
+
array['distinct/latest-row']::text[],
|
|
405
|
+
'CRC-11 REJECT: a DISTINCT ON per-agent choice by bare recency is latest-row (and the outer order is not a site)');
|
|
406
|
+
select is(
|
|
407
|
+
(select array_agg(form || '/' || verdict) from _crc_sites where kind = 'fixture' and proname = 'into-nolimit'),
|
|
408
|
+
array['into/latest-row']::text[],
|
|
409
|
+
'CRC-12 REJECT: SELECT ... INTO ordered by updated_at desc with no LIMIT keeps the first row, and is a site');
|
|
410
|
+
select is(
|
|
411
|
+
(select array_agg(form || '/' || verdict) from _crc_sites where kind = 'fixture' and proname = 'filtered'),
|
|
412
|
+
array['limit/filtered:''joined'',''subscribed''']::text[],
|
|
413
|
+
'CRC-13 ACCEPT-AS-EXEMPTION: a WHERE pinning membership_state reports its exact set');
|
|
414
|
+
select is(
|
|
415
|
+
(select count(*)::int from _crc_sites where kind = 'fixture' and proname = 'not-a-site'),
|
|
416
|
+
0,
|
|
417
|
+
'CRC-14: a lock-ordering PERFORM and an unbounded ascending iteration choose no row and are not sites');
|
|
418
|
+
select is(
|
|
419
|
+
(select array_agg(form || '/' || verdict) from _crc_sites where kind = 'fixture' and proname = 'outer-derived'),
|
|
420
|
+
array['limit(outer)/latest-row']::text[],
|
|
421
|
+
'CRC-15 REJECT: a derived table read by an OUTER order by recency + LIMIT is a site');
|
|
422
|
+
select is(
|
|
423
|
+
(select array_agg(form || '/' || verdict) from _crc_sites where kind = 'fixture' and proname = 'outer-cte'),
|
|
424
|
+
array['limit(outer)/latest-row']::text[],
|
|
425
|
+
'CRC-16 REJECT: a CTE over memberships chosen by the outer ORDER BY ... LIMIT is a site');
|
|
426
|
+
select is(
|
|
427
|
+
(select array_agg(form || '/' || verdict) from _crc_sites where kind = 'fixture' and proname = 'view'),
|
|
428
|
+
array['limit/latest-row']::text[],
|
|
429
|
+
'CRC-17 REJECT: a latest-row read through the unfiltered view v_board_memberships is a site');
|
|
430
|
+
select is(
|
|
431
|
+
(select array_agg(form || '/' || verdict) from _crc_sites where kind = 'fixture' and proname = 'ordinal'),
|
|
432
|
+
array['limit/latest-row']::text[],
|
|
433
|
+
'CRC-18 REJECT: ORDER BY a column ordinal (or an alias) is a site; the operand is not inspected');
|
|
434
|
+
select is(
|
|
435
|
+
(select array_agg(form || '/' || verdict) from _crc_sites where kind = 'fixture' and proname = 'nested-filter'),
|
|
436
|
+
array['limit/latest-row']::text[],
|
|
437
|
+
'CRC-19 REJECT: a membership_state filter inside a nested subquery is not this query''s filter');
|
|
438
|
+
select is(
|
|
439
|
+
(select array_agg(form || '/' || verdict) from _crc_sites where kind = 'fixture' and proname = 'end-desc'),
|
|
440
|
+
array['limit/latest-row']::text[],
|
|
441
|
+
'CRC-20 REJECT: `... END DESC` is inactive-first -- the exact reversal -- not the key');
|
|
442
|
+
select is(
|
|
443
|
+
(select array_agg(form || '/' || verdict) from _crc_sites where kind = 'fixture' and proname = 'end-times-zero'),
|
|
444
|
+
array['limit/latest-row']::text[],
|
|
445
|
+
'CRC-21 REJECT: `... END * 0` erases the preference and is not the key');
|
|
446
|
+
select is(
|
|
447
|
+
(select array_agg(form || '/' || verdict) from _crc_sites where kind = 'fixture' and proname = 'then1-else0'),
|
|
448
|
+
array['limit/latest-row']::text[],
|
|
449
|
+
'CRC-22 REJECT: `THEN 1 ELSE 0` is the reversal and is not the key');
|
|
450
|
+
select is(
|
|
451
|
+
(select array_agg(form || '/' || verdict) from _crc_sites where kind = 'fixture' and proname = 'end-cast'),
|
|
452
|
+
array['limit/latest-row']::text[],
|
|
453
|
+
'CRC-23 REJECT: `... END::text` (a cast continuation) is not the key');
|
|
454
|
+
select is(
|
|
455
|
+
(select array_agg(form || '/' || verdict) from _crc_sites where kind = 'fixture' and proname = 'end-asc'),
|
|
456
|
+
array['limit/active-first']::text[],
|
|
457
|
+
'CRC-24 ACCEPT: `... END ASC NULLS LAST` is the key (ASC is the default; NULLS is inert on a non-null CASE)');
|
|
458
|
+
|
|
459
|
+
select * from finish();
|
|
460
|
+
rollback;
|
|
@@ -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
|
-
--
|
|
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(
|
|
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
|
|
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', '
|
|
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
|
-
--
|
|
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-
|
|
234
|
-
('73020000-0000-4000-8000-000000000004', '73010000-0000-4000-8000-
|
|
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
|
-
--
|
|
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-
|
|
241
|
-
('73020000-0000-4000-8000-000000000011', '73010000-0000-4000-8000-
|
|
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
|
|
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
|
|
422
|
-
where agent_id =
|
|
423
|
-
|
|
424
|
-
|
|
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
|
|
428
|
-
where agent_id =
|
|
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-
|
|
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
|
-
|
|
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(
|
|
22
|
+
select plan(99);
|
|
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
|
|
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.
|
|
@@ -1300,6 +1300,19 @@ select is(
|
|
|
1300
1300
|
select set_config('request.jwt.claim.role', '', true);
|
|
1301
1301
|
select set_config('request.jwt.claim.sub', '', true);
|
|
1302
1302
|
|
|
1303
|
+
-- 99. list_board_members (ROOMCONTRACT, re-created active-first by #542) was in
|
|
1304
|
+
-- the assertion-2 inventory but had no count-equality row of its own, so a
|
|
1305
|
+
-- dropped `bm.id desc` at either site went unnoticed here; through the roster
|
|
1306
|
+
-- itself the tiebreak is unobservable (it only orders non-active rows). Pinned as
|
|
1307
|
+
-- the exact pair (created_at DESC sites, of them with id DESC) so a site that
|
|
1308
|
+
-- disappears is not read as agreement 0 = 0.
|
|
1309
|
+
select is(
|
|
1310
|
+
(select array[created_at_desc_count, total_order_count]
|
|
1311
|
+
from _rowid_function_order_counts
|
|
1312
|
+
where proname = 'list_board_members'),
|
|
1313
|
+
array[2, 2],
|
|
1314
|
+
'list_board_members: BOTH created_at DESC sites (authorization + projection) have the id DESC tiebreak');
|
|
1315
|
+
|
|
1303
1316
|
select finish();
|
|
1304
1317
|
|
|
1305
1318
|
rollback;
|