@consiliency/agent-board-schema 1.12.0 → 1.23.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 +160 -0
- package/migrations/20260731000100_agent_board_agentsdir_column_scope.sql +88 -0
- package/migrations/20260801000100_agent_board_leaveidem_terminal_noop.sql +317 -0
- package/migrations/20260802000100_agent_board_rolereset_reentry_clamp.sql +237 -0
- package/migrations/20260803000100_agent_board_rowid_total_order.sql +1201 -0
- package/migrations/20260804000100_agent_board_hygiene_actor_binding_rls.sql +229 -0
- package/migrations/20260806000100_agent_board_consent_substrate.sql +339 -0
- package/migrations/20260807000100_agent_board_consent_emission.sql +619 -0
- package/migrations/20260808000100_agent_board_rejectsem.sql +145 -0
- package/migrations/20260809000100_agent_board_consent_gating.sql +492 -0
- package/migrations/20260810000100_agent_board_consent_locking.sql +485 -0
- package/migrations/20260811000100_agent_board_retention_metadata_revoke.sql +23 -0
- package/migrations/20260812000100_agent_board_pubexec_class_revoke.sql +54 -0
- package/migrations/20260813000100_agent_board_invitepolicy_block.sql +420 -0
- package/migrations/20260814000100_agent_board_obsgate_thread_post_policy.sql +239 -0
- package/migrations/20260815000100_agent_board_obsgate2_authoring_gates.sql +287 -0
- package/migrations/20260816000100_agent_board_obsgate3_task_lifecycle.sql +118 -0
- package/migrations/20260817000100_agent_board_obsgate4_claims_lifecycle.sql +250 -0
- package/migrations/20260818000100_agent_board_fleet_machines_rls_correlation.sql +43 -0
- package/migrations/20260819000100_agent_board_resolve_noop_retire.sql +166 -0
- package/migrations/20260820000100_agent_board_roomdeleg_lane_b.sql +206 -0
- package/migrations/20260821000100_agent_board_claimgate_claimkey.sql +372 -0
- package/migrations/20260822000100_agent_board_mint_harden.sql +710 -0
- package/migrations/20260822000200_agent_board_mint_harden_fingerprint_backfill.sql +207 -0
- package/migrations/20260823000100_agent_board_fleet_machine_hijack.sql +106 -0
- package/migrations/20260824000100_agent_board_claimgate_shadow.sql +443 -0
- package/migrations/20260825000100_agent_board_claimgate_enforce.sql +660 -0
- package/migrations/20260826000100_agent_board_fleet_machine_reactivation.sql +514 -0
- package/migrations/20260828000100_agent_board_delegation_cap_ceiling.sql +474 -0
- package/migrations/20260829000100_agent_board_fleet_machine_reclaim.sql +432 -0
- package/migrations/20260830000100_agent_board_delegation_sole_writer.sql +41 -0
- package/migrations/20260831000100_agent_board_fleet_domain_reclaim.sql +389 -0
- package/migrations/20260901000100_agent_board_atomic_fleet_onboarding.sql +1496 -0
- package/migrations/20260901000110_agent_board_fleet_onboarding_ceiling_lock.sql +604 -0
- package/migrations/20260901000120_agent_board_human_revocation_evidence_completeness.sql +78 -0
- package/migrations/20260901000130_agent_board_fedsession_human_revocation_gate.sql +74 -0
- package/migrations/20260901000140_agent_board_fedsession_board_owner_lock_order.sql +102 -0
- package/migrations/20260902000100_agent_board_outbox_wake_ordering.sql +184 -0
- package/migrations/20260903000100_agent_board_enroll_identity_board_owner_lock_order.sql +604 -0
- package/migrations/20260903000200_agent_board_fedsession_provider_absence_gate.sql +177 -0
- package/migrations/20260904000100_agent_board_wake_recovery_surface.sql +155 -0
- package/package.json +1 -1
- package/schema-manifest.json +363 -3
- package/src/tooling.test.mjs +113 -2
- package/tests/agent_board_actorspoof.test.sql +349 -0
- package/tests/agent_board_agentsdir.test.sql +154 -0
- package/tests/agent_board_atomic_fleet_onboarding.test.sql +623 -0
- package/tests/agent_board_atomic_fleet_onboarding_backfill.test.sql +142 -0
- package/tests/agent_board_atomic_fleet_onboarding_runtime.test.sql +1250 -0
- package/tests/agent_board_authbound_boundary.test.sql +9 -1
- package/tests/agent_board_claimgate_enforce.test.sql +464 -0
- package/tests/agent_board_claimgate_predicate.test.sql +360 -0
- package/tests/agent_board_claimkey.test.sql +229 -0
- package/tests/agent_board_consent_backfill.test.sql +48 -0
- package/tests/agent_board_consent_emission.test.sql +601 -0
- package/tests/agent_board_consent_gating.test.sql +489 -0
- package/tests/agent_board_consent_substrate.test.sql +720 -0
- package/tests/agent_board_delegation_cap_ceiling.test.sql +303 -0
- package/tests/agent_board_delegation_enforcement.test.sql +55 -6
- package/tests/agent_board_delegation_policies.test.sql +107 -1
- package/tests/agent_board_enroll_federated_identity.test.sql +3 -3
- package/tests/agent_board_enroll_pop_binding.test.sql +13 -3
- package/tests/agent_board_enrollment_audit_content.test.sql +11 -7
- package/tests/agent_board_federated_lock_order_class.test.sql +390 -0
- package/tests/agent_board_fedsession.test.sql +51 -5
- package/tests/agent_board_fedsession_provider_absence_gate.test.sql +170 -0
- package/tests/agent_board_fleet_domain_reclaim.test.sql +419 -0
- package/tests/agent_board_fleet_machine_hijack.test.sql +190 -0
- package/tests/agent_board_fleet_machine_reactivation.test.sql +426 -0
- package/tests/agent_board_fleet_machine_reclaim.test.sql +509 -0
- package/tests/agent_board_fleet_machines_rls.test.sql +79 -0
- package/tests/agent_board_fleet_policy.test.sql +2 -2
- package/tests/agent_board_grants_hygiene.test.sql +408 -0
- package/tests/agent_board_identity_providers.test.sql +9 -0
- package/tests/agent_board_inbox_reads.test.sql +45 -31
- package/tests/agent_board_inbox_rpcs.test.sql +44 -34
- package/tests/agent_board_invitepolicy.test.sql +205 -0
- package/tests/agent_board_leaveidem.test.sql +194 -0
- package/tests/agent_board_mint_fleet_agent_session.test.sql +10 -10
- package/tests/agent_board_mint_harden.test.sql +303 -0
- package/tests/agent_board_mint_harden_backfill.test.sql +95 -0
- package/tests/agent_board_obsgate.test.sql +178 -0
- package/tests/agent_board_obsgate2.test.sql +153 -0
- package/tests/agent_board_obsgate3.test.sql +170 -0
- package/tests/agent_board_obsgate4.test.sql +196 -0
- package/tests/agent_board_outbox_wake_ordering.test.sql +452 -0
- package/tests/agent_board_portal_offline_invariant.test.sql +14 -3
- package/tests/agent_board_pubexec_class.test.sql +53 -0
- package/tests/agent_board_rejectsem.test.sql +271 -0
- package/tests/agent_board_resigning_refresh.test.sql +1 -1
- package/tests/agent_board_resolve_noop_retire.test.sql +71 -0
- package/tests/agent_board_retention_metadata_acl.test.sql +53 -0
- package/tests/agent_board_revoke_agent_subtree.test.sql +3 -3
- package/tests/agent_board_rolereset.test.sql +351 -0
- package/tests/agent_board_roomdeleg.test.sql +5 -3
- package/tests/agent_board_roomdeleg_lane_b.test.sql +144 -0
- package/tests/agent_board_roomlink.test.sql +27 -11
- package/tests/agent_board_rowid.test.sql +1274 -0
- package/tests/agent_board_signature_rls_reads.test.sql +3 -3
- package/tests/agent_board_signed_write_gateway.test.sql +3 -3
- package/tests/agent_board_signing_challenge_pending_cap.test.sql +2 -2
- package/tests/agent_board_signing_challenge_reaper.test.sql +2 -2
- package/tests/agent_board_signing_proof_rpcs.test.sql +4 -4
- package/tests/agent_board_signing_store.test.sql +6 -6
- package/tests/agent_board_wake_recovery.test.sql +756 -0
- package/tests/fixtures/isolation-substrate.sql +93 -0
- package/tests/fixtures/test-prereqs.sql +33 -0
- package/tests/isolation/expected/atomic_fedsession_fleet_lock_order.out +79 -0
- package/tests/isolation/expected/atomic_fedsession_human_revoke.out +87 -0
- package/tests/isolation/expected/atomic_fleet_authority_races.out +469 -0
- package/tests/isolation/expected/atomic_fleet_binding_revoke.out +103 -0
- package/tests/isolation/expected/atomic_fleet_board_archive.out +117 -0
- package/tests/isolation/expected/atomic_fleet_human_fresh_revoke.out +136 -0
- package/tests/isolation/expected/atomic_fleet_identity_disposition.out +61 -0
- package/tests/isolation/expected/atomic_fleet_lifecycle_races.out +1479 -0
- package/tests/isolation/expected/atomic_fleet_onboarding.out +75 -0
- package/tests/isolation/expected/atomic_fleet_subtree_lock_order.out +60 -0
- package/tests/isolation/expected/ceiling_setter_lock_before_upsert.out +95 -0
- package/tests/isolation/expected/claimgate_ownerless_quota.out +36 -0
- package/tests/isolation/expected/claimkey_renew_race.out +38 -0
- package/tests/isolation/expected/consent_approval_lock.out +43 -0
- package/tests/isolation/expected/consent_emit_actual_state.out +37 -0
- package/tests/isolation/expected/domain_reclaim_revoke_race.out +40 -0
- package/tests/isolation/expected/enroll_identity_board_owner_lock_order.out +55 -0
- package/tests/isolation/expected/fedsession_provider_absent_owner_lock.out +37 -0
- package/tests/isolation/expected/onboarding_ceiling_lock_order.out +123 -0
- package/tests/isolation/expected/outbox_wake_ordering.out +86 -0
- package/tests/isolation/expected/reactivate_revoke_race.out +80 -0
- package/tests/isolation/expected/reclaim_revoke_race.out +40 -0
- package/tests/isolation/expected/wake_recovery_late_commit.out +89 -0
- package/tests/isolation/specs/atomic_fedsession_fleet_lock_order.spec +117 -0
- package/tests/isolation/specs/atomic_fedsession_human_revoke.spec +66 -0
- package/tests/isolation/specs/atomic_fleet_authority_races.spec +235 -0
- package/tests/isolation/specs/atomic_fleet_binding_revoke.spec +127 -0
- package/tests/isolation/specs/atomic_fleet_board_archive.spec +134 -0
- package/tests/isolation/specs/atomic_fleet_human_fresh_revoke.spec +142 -0
- package/tests/isolation/specs/atomic_fleet_identity_disposition.spec +118 -0
- package/tests/isolation/specs/atomic_fleet_lifecycle_races.spec +244 -0
- package/tests/isolation/specs/atomic_fleet_onboarding.spec +106 -0
- package/tests/isolation/specs/atomic_fleet_subtree_lock_order.spec +147 -0
- package/tests/isolation/specs/ceiling_setter_lock_before_upsert.spec +117 -0
- package/tests/isolation/specs/claimgate_ownerless_quota.spec +77 -0
- package/tests/isolation/specs/claimkey_renew_race.spec +67 -0
- package/tests/isolation/specs/consent_approval_lock.spec +98 -0
- package/tests/isolation/specs/consent_emit_actual_state.spec +79 -0
- package/tests/isolation/specs/domain_reclaim_revoke_race.spec +88 -0
- package/tests/isolation/specs/enroll_identity_board_owner_lock_order.spec +117 -0
- package/tests/isolation/specs/fedsession_provider_absent_owner_lock.spec +63 -0
- package/tests/isolation/specs/onboarding_ceiling_lock_order.spec +183 -0
- package/tests/isolation/specs/outbox_wake_ordering.spec +202 -0
- package/tests/isolation/specs/reactivate_revoke_race.spec +115 -0
- package/tests/isolation/specs/reclaim_revoke_race.spec +94 -0
- package/tests/isolation/specs/wake_recovery_late_commit.spec +207 -0
- package/tests/seeds/20260806000100_agent_board_consent_substrate.sql.pre.sql +19 -0
- package/tests/seeds/20260822000200_agent_board_mint_harden_fingerprint_backfill.sql.pre.sql +39 -0
- package/tests/seeds/20260901000100_agent_board_atomic_fleet_onboarding.sql.pre.sql +110 -0
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
-- MBCONSENT-B(a) pgTAP: EMISSION only. No gating.
|
|
2
|
+
--
|
|
3
|
+
-- The claim under test: every membership transition writes exactly one ledger event whose
|
|
4
|
+
-- provenance is DERIVED FROM THE SESSION and cannot be influenced by any caller argument.
|
|
5
|
+
--
|
|
6
|
+
-- Methodology:
|
|
7
|
+
-- * RPC probes are driven by the JWT claim triplet only. SET LOCAL ROLE is never used here:
|
|
8
|
+
-- it would revoke _is_service_role() and mask the operator branch behind a permission
|
|
9
|
+
-- error on the function itself.
|
|
10
|
+
-- * p_created_by_agent_id is passed DELIBERATELY WRONG in the anti-forge cases. If provenance
|
|
11
|
+
-- were keyed off it rather than the session, those assertions flip.
|
|
12
|
+
create extension if not exists pgtap with schema extensions;
|
|
13
|
+
set search_path = extensions, public, agent_board;
|
|
14
|
+
|
|
15
|
+
begin;
|
|
16
|
+
|
|
17
|
+
select plan(68);
|
|
18
|
+
|
|
19
|
+
create function pg_temp.claims(p_sub text) returns void language plpgsql as $$
|
|
20
|
+
begin
|
|
21
|
+
perform set_config('request.jwt.claims', json_build_object('role','authenticated','sub',p_sub)::text, true);
|
|
22
|
+
end $$;
|
|
23
|
+
create function pg_temp.service() returns void language plpgsql as $$
|
|
24
|
+
begin
|
|
25
|
+
perform set_config('request.jwt.claims', '', true);
|
|
26
|
+
end $$;
|
|
27
|
+
create function pg_temp.latest(p_agent uuid)
|
|
28
|
+
returns table(kind text, prov text, actor uuid, state text) language sql as $$
|
|
29
|
+
select event_kind, provenance, actor_agent_id, result_state
|
|
30
|
+
from agent_board.board_membership_events
|
|
31
|
+
where agent_id = p_agent order by seq desc limit 1;
|
|
32
|
+
$$;
|
|
33
|
+
|
|
34
|
+
insert into auth.users(id, instance_id, aud, role, email)
|
|
35
|
+
select ('f0000000-0000-0000-0000-' || lpad(n::text,12,'0'))::uuid,
|
|
36
|
+
'00000000-0000-0000-0000-000000000000', 'authenticated', 'authenticated',
|
|
37
|
+
'emit'||n||'@t.test'
|
|
38
|
+
from generate_series(1,6) n;
|
|
39
|
+
|
|
40
|
+
insert into agent_board.agents(id, agent_type, kind, handle, display_name, status, auth_user_id)
|
|
41
|
+
select ('f1000000-0000-4000-8000-' || lpad(n::text,12,'0'))::uuid,
|
|
42
|
+
'agent','agent','emit-agent-'||n,'Emit Agent '||n,'active',
|
|
43
|
+
('f0000000-0000-0000-0000-' || lpad(n::text,12,'0'))::uuid
|
|
44
|
+
from generate_series(1,6) n;
|
|
45
|
+
|
|
46
|
+
insert into agent_board.boards(id, slug, name, created_by_agent_id, visibility, status)
|
|
47
|
+
values ('f2000000-0000-4000-8000-000000000001','emit-b1','Emit B1',
|
|
48
|
+
'f1000000-0000-4000-8000-000000000001','private','active');
|
|
49
|
+
|
|
50
|
+
-- ===========================================================================
|
|
51
|
+
-- 1. Every transition emits exactly ONE event
|
|
52
|
+
-- ===========================================================================
|
|
53
|
+
select pg_temp.claims('f0000000-0000-0000-0000-000000000002');
|
|
54
|
+
select lives_ok($$
|
|
55
|
+
select agent_board._upsert_board_membership_state(
|
|
56
|
+
'f2000000-0000-4000-8000-000000000001','f1000000-0000-4000-8000-000000000002','joined')
|
|
57
|
+
$$, 'emission: a self-join succeeds');
|
|
58
|
+
|
|
59
|
+
select is(
|
|
60
|
+
(select count(*)::int from agent_board.board_membership_events
|
|
61
|
+
where agent_id = 'f1000000-0000-4000-8000-000000000002'),
|
|
62
|
+
1,
|
|
63
|
+
'emission: exactly ONE event per transition, not zero and not two');
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
-- ===========================================================================
|
|
67
|
+
-- 2. Provenance is derived from the SESSION, not from any argument
|
|
68
|
+
-- ===========================================================================
|
|
69
|
+
select is((select prov from pg_temp.latest('f1000000-0000-4000-8000-000000000002')),
|
|
70
|
+
'target-authored', 'derivation: the subject acting on itself is target-authored');
|
|
71
|
+
select is((select actor from pg_temp.latest('f1000000-0000-4000-8000-000000000002')),
|
|
72
|
+
'f1000000-0000-4000-8000-000000000002'::uuid, 'derivation: ...with the subject as actor');
|
|
73
|
+
select is((select kind from pg_temp.latest('f1000000-0000-4000-8000-000000000002')),
|
|
74
|
+
'consent_self_join', 'derivation: ...and the self-join kind');
|
|
75
|
+
|
|
76
|
+
-- ANTI-FORGE. p_created_by_agent_id is a caller ASSERTION; the session is EVIDENCE. Here the
|
|
77
|
+
-- argument NAMES THE SUBJECT while the session is a third party. If provenance were keyed off
|
|
78
|
+
-- the argument this would read target-authored -- a forged consent event.
|
|
79
|
+
select pg_temp.claims('f0000000-0000-0000-0000-000000000001');
|
|
80
|
+
select lives_ok($$
|
|
81
|
+
select agent_board._upsert_board_membership_state(
|
|
82
|
+
'f2000000-0000-4000-8000-000000000001','f1000000-0000-4000-8000-000000000003','invited',
|
|
83
|
+
'f1000000-0000-4000-8000-000000000003')
|
|
84
|
+
$$, 'anti-forge setup: a third party acts, naming the SUBJECT in p_created_by_agent_id');
|
|
85
|
+
|
|
86
|
+
select is((select prov from pg_temp.latest('f1000000-0000-4000-8000-000000000003')),
|
|
87
|
+
'board-authored',
|
|
88
|
+
'ANTI-FORGE: provenance follows the SESSION, not p_created_by_agent_id');
|
|
89
|
+
select is((select actor from pg_temp.latest('f1000000-0000-4000-8000-000000000003')),
|
|
90
|
+
'f1000000-0000-4000-8000-000000000001'::uuid,
|
|
91
|
+
'ANTI-FORGE: ...and the actor is the real session, not the named argument');
|
|
92
|
+
select isnt((select actor from pg_temp.latest('f1000000-0000-4000-8000-000000000003')),
|
|
93
|
+
'f1000000-0000-4000-8000-000000000003'::uuid,
|
|
94
|
+
'ANTI-FORGE: the subject is NOT recorded as having acted');
|
|
95
|
+
|
|
96
|
+
-- ===========================================================================
|
|
97
|
+
-- 3. service_role is ALWAYS operator-designated -- even with a resolvable subject
|
|
98
|
+
-- ===========================================================================
|
|
99
|
+
-- This is the laundering path a review caught on the abandoned larger change: an operator
|
|
100
|
+
-- acting while some agent id happens to resolve from its token was recorded as the subject's
|
|
101
|
+
-- own act. An operator placement is an operator placement.
|
|
102
|
+
select pg_temp.service();
|
|
103
|
+
select lives_ok($$
|
|
104
|
+
select agent_board._upsert_board_membership_state(
|
|
105
|
+
'f2000000-0000-4000-8000-000000000001','f1000000-0000-4000-8000-000000000004','joined',
|
|
106
|
+
'f1000000-0000-4000-8000-000000000004')
|
|
107
|
+
$$, 'operator setup: service_role places a subject');
|
|
108
|
+
|
|
109
|
+
select is((select prov from pg_temp.latest('f1000000-0000-4000-8000-000000000004')),
|
|
110
|
+
'operator-designated', 'OPERATOR: service_role records operator-designated');
|
|
111
|
+
select is((select actor from pg_temp.latest('f1000000-0000-4000-8000-000000000004')),
|
|
112
|
+
null::uuid, 'OPERATOR: ...with a NULL actor');
|
|
113
|
+
select is((select kind from pg_temp.latest('f1000000-0000-4000-8000-000000000004')),
|
|
114
|
+
'operator_designation', 'OPERATOR: ...and the designation kind');
|
|
115
|
+
|
|
116
|
+
-- ...and with a subject RESOLVABLE from the service session, it is STILL operator-designated.
|
|
117
|
+
select set_config('request.jwt.claims',
|
|
118
|
+
'{"role":"service_role","sub":"f0000000-0000-0000-0000-000000000004"}', true);
|
|
119
|
+
select lives_ok($$
|
|
120
|
+
select agent_board._upsert_board_membership_state(
|
|
121
|
+
'f2000000-0000-4000-8000-000000000001','f1000000-0000-4000-8000-000000000004','subscribed')
|
|
122
|
+
$$, 'operator setup: service_role acts with a RESOLVABLE subject in its token');
|
|
123
|
+
select is((select prov from pg_temp.latest('f1000000-0000-4000-8000-000000000004')),
|
|
124
|
+
'operator-designated',
|
|
125
|
+
'OPERATOR: a resolvable subject does NOT make an operator action target-authored');
|
|
126
|
+
select is((select actor from pg_temp.latest('f1000000-0000-4000-8000-000000000004')),
|
|
127
|
+
null::uuid, 'OPERATOR: ...actor stays NULL');
|
|
128
|
+
|
|
129
|
+
-- ===========================================================================
|
|
130
|
+
-- 4. An unattributable write FAILS CLOSED
|
|
131
|
+
-- ===========================================================================
|
|
132
|
+
-- Neither service_role nor a resolvable actor. An earlier draft of this logic labelled that
|
|
133
|
+
-- case operator-designated, which manufactures evidence of an operator placement that never
|
|
134
|
+
-- happened. It must raise instead.
|
|
135
|
+
select pg_temp.claims('00000000-0000-0000-0000-0000000000ff');
|
|
136
|
+
select throws_ok($$
|
|
137
|
+
select agent_board._upsert_board_membership_state(
|
|
138
|
+
'f2000000-0000-4000-8000-000000000001','f1000000-0000-4000-8000-000000000002','left')
|
|
139
|
+
$$, '42501', 'membership change has no attributable actor',
|
|
140
|
+
'FAIL CLOSED: an unattributable write raises rather than inventing a provenance');
|
|
141
|
+
|
|
142
|
+
-- STATEMENT-LEVEL ATOMICITY. The refused write above did not just skip the LEDGER (section 6
|
|
143
|
+
-- pins the empty ledger); the membership WRITE that preceded the failing emit rolled back with
|
|
144
|
+
-- it. _upsert writes the row and PERFORMs _emit in ONE function body, so an emit exception
|
|
145
|
+
-- unwinds the row too -- no partial `left` can commit. Agent 2 was `joined` before the refused
|
|
146
|
+
-- `left`; had the write leaked it would read `left` here.
|
|
147
|
+
-- This is NOT redundant with the ledger-empty check: an autonomous/independent membership write
|
|
148
|
+
-- would still raise, still leave the ledger empty, and still commit the `left` -- only reading
|
|
149
|
+
-- the membership row catches that leak.
|
|
150
|
+
select is(
|
|
151
|
+
(select membership_state from agent_board.board_memberships
|
|
152
|
+
where board_id = 'f2000000-0000-4000-8000-000000000001'
|
|
153
|
+
and agent_id = 'f1000000-0000-4000-8000-000000000002'),
|
|
154
|
+
'joined',
|
|
155
|
+
'ATOMICITY: the refused write rolled its membership row back too -- state is still joined, no partial left committed');
|
|
156
|
+
|
|
157
|
+
-- ===========================================================================
|
|
158
|
+
-- 5. Event-kind mapping across the state space
|
|
159
|
+
-- ===========================================================================
|
|
160
|
+
select pg_temp.claims('f0000000-0000-0000-0000-000000000002');
|
|
161
|
+
select lives_ok($$ select agent_board._upsert_board_membership_state(
|
|
162
|
+
'f2000000-0000-4000-8000-000000000001','f1000000-0000-4000-8000-000000000002','left') $$,
|
|
163
|
+
'kinds setup: the subject leaves');
|
|
164
|
+
select is((select kind from pg_temp.latest('f1000000-0000-4000-8000-000000000002')),
|
|
165
|
+
'self_withdraw', 'kinds: target-authored + left is a self-withdraw, not an eviction');
|
|
166
|
+
|
|
167
|
+
select pg_temp.claims('f0000000-0000-0000-0000-000000000001');
|
|
168
|
+
select lives_ok($$ select agent_board._upsert_board_membership_state(
|
|
169
|
+
'f2000000-0000-4000-8000-000000000001','f1000000-0000-4000-8000-000000000002','removed') $$,
|
|
170
|
+
'kinds setup: a third party removes');
|
|
171
|
+
select is((select kind from pg_temp.latest('f1000000-0000-4000-8000-000000000002')),
|
|
172
|
+
'eviction', 'kinds: board-authored + removed is an eviction, not a self-withdraw');
|
|
173
|
+
|
|
174
|
+
select pg_temp.claims('f0000000-0000-0000-0000-000000000002');
|
|
175
|
+
select lives_ok($$ select agent_board._upsert_board_membership_state(
|
|
176
|
+
'f2000000-0000-4000-8000-000000000001','f1000000-0000-4000-8000-000000000002','requested') $$,
|
|
177
|
+
'kinds setup: the subject requests');
|
|
178
|
+
select is((select kind from pg_temp.latest('f1000000-0000-4000-8000-000000000002')),
|
|
179
|
+
'consent_join_request', 'kinds: target-authored + requested is a join request');
|
|
180
|
+
|
|
181
|
+
-- Exactly-once ACROSS THE CALL GRAPH, driven through a FACADE rather than the sink. Counting
|
|
182
|
+
-- call sites per function cannot see double-emission: adding a second emit to a wrapper that
|
|
183
|
+
-- already calls the emitting sink leaves BOTH functions with one textual call while a single
|
|
184
|
+
-- transition writes two events. Placed here, after the derivation assertions, because it moves
|
|
185
|
+
-- agent 2's state and would otherwise change what a later assertion reads.
|
|
186
|
+
-- Counted RELATIVE to this call, via the seq high-water mark. An absolute count over the
|
|
187
|
+
-- subject's history is polluted by earlier sections -- it read 2 because a previous test had
|
|
188
|
+
-- already written a `left` for this agent, and that failure looks exactly like a real
|
|
189
|
+
-- double-emission defect.
|
|
190
|
+
create temporary table _seq_mark on commit drop as
|
|
191
|
+
select coalesce(max(seq), 0) as seq from agent_board.board_membership_events;
|
|
192
|
+
|
|
193
|
+
select pg_temp.claims('f0000000-0000-0000-0000-000000000002');
|
|
194
|
+
select lives_ok($$
|
|
195
|
+
select agent_board.update_board_membership(
|
|
196
|
+
'f2000000-0000-4000-8000-000000000001','f1000000-0000-4000-8000-000000000002','left')
|
|
197
|
+
$$, 'call-graph setup: a transition driven through the update_board_membership facade');
|
|
198
|
+
|
|
199
|
+
select is(
|
|
200
|
+
(select count(*)::int from agent_board.board_membership_events e
|
|
201
|
+
where e.seq > (select seq from _seq_mark)),
|
|
202
|
+
1,
|
|
203
|
+
'emission: a FACADE transition emits once across the whole call graph, not twice');
|
|
204
|
+
|
|
205
|
+
-- ===========================================================================
|
|
206
|
+
-- 5b. COMPLETENESS -- every writer emits, not just the shared sink
|
|
207
|
+
-- ===========================================================================
|
|
208
|
+
-- The first version of this migration instrumented ONLY _upsert_board_membership_state. Four
|
|
209
|
+
-- functions write board_memberships DIRECTLY and emitted nothing, so a membership created
|
|
210
|
+
-- through them carried a SILENT ledger and would read as non-consenting once gating lands.
|
|
211
|
+
-- This assertion pins the completeness claim itself, derived from the catalog rather than from
|
|
212
|
+
-- a hand-maintained list -- a new writer added later will fail it.
|
|
213
|
+
-- Covers INSERT **and** UPDATE writers, and fails CLOSED: a function that writes
|
|
214
|
+
-- board_memberships must either emit, or appear on the exemption list below with a stated
|
|
215
|
+
-- reason. Keying only on INSERT would miss a future path that flips membership_state via
|
|
216
|
+
-- UPDATE -- and catching FUTURE writers is the entire purpose of this assertion.
|
|
217
|
+
--
|
|
218
|
+
-- The single exemption is grant_board_role: it sets `role` only, with membership_state
|
|
219
|
+
-- appearing exclusively in its WHERE clauses, so it cannot change membership state and a role
|
|
220
|
+
-- change is not a consent event. If that ever stops being true, this assertion fails.
|
|
221
|
+
-- COMMENTS ARE STRIPPED and CALL SITES ARE COUNTED. Checking that the emitter name appears
|
|
222
|
+
-- anywhere in the body is fail-open twice over: the name in a comment or a dead branch would
|
|
223
|
+
-- satisfy it, and calling the emitter TWICE would too -- while this suite claims exactly one
|
|
224
|
+
-- event per transition.
|
|
225
|
+
-- Strips BOTH comment forms. The first version stripped only `--`, so a /* */ commented-out
|
|
226
|
+
-- call still satisfied the completeness assertion.
|
|
227
|
+
create function pg_temp.code(p_oid oid) returns text language sql stable as $$
|
|
228
|
+
select regexp_replace(
|
|
229
|
+
regexp_replace(pg_get_functiondef(p_oid), '/\*.*?\*/', '', 'gs'),
|
|
230
|
+
'--[^\n]*', '', 'g');
|
|
231
|
+
$$;
|
|
232
|
+
|
|
233
|
+
select is(
|
|
234
|
+
(select coalesce(string_agg(p.proname, ', ' order by p.proname), '')
|
|
235
|
+
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
236
|
+
where n.nspname = 'agent_board' and p.prokind = 'f'
|
|
237
|
+
and p.prolang <> (select oid from pg_language where lanname = 'internal')
|
|
238
|
+
and pg_temp.code(p.oid) ~* '(insert into|update)[[:space:]]+agent_board\.board_memberships'
|
|
239
|
+
and pg_temp.code(p.oid) !~ 'perform agent_board\._emit_membership_event'
|
|
240
|
+
and p.proname <> 'grant_board_role'),
|
|
241
|
+
'',
|
|
242
|
+
'COMPLETENESS: every function writing board_memberships emits, or is a stated exemption');
|
|
243
|
+
|
|
244
|
+
-- ...and none of them emits MORE than once, which would break one-event-per-transition.
|
|
245
|
+
select is(
|
|
246
|
+
(select coalesce(string_agg(p.proname || '=' ||
|
|
247
|
+
(length(pg_temp.code(p.oid)) - length(replace(pg_temp.code(p.oid),
|
|
248
|
+
'perform agent_board._emit_membership_event', '')))
|
|
249
|
+
/ length('perform agent_board._emit_membership_event'), ', ' order by p.proname), '')
|
|
250
|
+
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
251
|
+
where n.nspname = 'agent_board' and p.prokind = 'f'
|
|
252
|
+
and p.prolang <> (select oid from pg_language where lanname = 'internal')
|
|
253
|
+
and pg_temp.code(p.oid) ~ 'perform agent_board\._emit_membership_event'
|
|
254
|
+
and (length(pg_temp.code(p.oid)) - length(replace(pg_temp.code(p.oid),
|
|
255
|
+
'perform agent_board._emit_membership_event', '')))
|
|
256
|
+
/ length('perform agent_board._emit_membership_event') <> 1),
|
|
257
|
+
'',
|
|
258
|
+
'COMPLETENESS: no writer emits more than once (exactly one event per transition)');
|
|
259
|
+
|
|
260
|
+
-- ...and the one exemption is genuinely state-blind: membership_state never appears in a SET.
|
|
261
|
+
select ok(
|
|
262
|
+
(select pg_get_functiondef(p.oid) !~* 'set[^;]*membership_state[[:space:]]*='
|
|
263
|
+
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
264
|
+
where n.nspname = 'agent_board' and p.proname = 'grant_board_role'),
|
|
265
|
+
'COMPLETENESS: the exemption holds -- grant_board_role never SETs membership_state');
|
|
266
|
+
|
|
267
|
+
-- ...and the emitter itself is the single implementation, not five copies.
|
|
268
|
+
select is(
|
|
269
|
+
(select count(*)::int
|
|
270
|
+
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
271
|
+
where n.nspname = 'agent_board' and p.prokind = 'f'
|
|
272
|
+
and p.proname <> '_emit_membership_event'
|
|
273
|
+
and pg_get_functiondef(p.oid) ~* 'insert into[[:space:]]+agent_board\.board_membership_events'),
|
|
274
|
+
0,
|
|
275
|
+
'COMPLETENESS: only _emit_membership_event writes the ledger -- the derivation cannot drift');
|
|
276
|
+
|
|
277
|
+
-- ===========================================================================
|
|
278
|
+
-- 5c. The RAW WRITERS, exercised behaviourally with their FROZEN event kinds
|
|
279
|
+
-- ===========================================================================
|
|
280
|
+
-- Instrumenting them is not enough: the frozen mapping names SPECIFIC kinds that a generic
|
|
281
|
+
-- (provenance, state) derivation cannot produce -- owner_seed, consent_link_redeem,
|
|
282
|
+
-- operator_repair. A structural check that "the emitter is called" would miss a wrong kind.
|
|
283
|
+
|
|
284
|
+
select pg_temp.claims('f0000000-0000-0000-0000-000000000003');
|
|
285
|
+
select lives_ok($$
|
|
286
|
+
select agent_board.create_board('f1000000-0000-4000-8000-000000000003',
|
|
287
|
+
'emit-owned', 'Emit Owned', null, '{}'::jsonb, 'private')
|
|
288
|
+
$$, 'raw writer setup: an agent creates a board');
|
|
289
|
+
|
|
290
|
+
select is((select kind from pg_temp.latest('f1000000-0000-4000-8000-000000000003')),
|
|
291
|
+
'owner_seed',
|
|
292
|
+
'RAW WRITER: create_board emits owner_seed, not the generic consent_self_join');
|
|
293
|
+
select is((select prov from pg_temp.latest('f1000000-0000-4000-8000-000000000003')),
|
|
294
|
+
'target-authored',
|
|
295
|
+
'RAW WRITER: ...target-authored, because the creator seeds their OWN seat');
|
|
296
|
+
|
|
297
|
+
-- A board whose owner has NO membership row -- exactly what bootstrap exists to repair.
|
|
298
|
+
insert into agent_board.boards(id, slug, name, created_by_agent_id, visibility, status)
|
|
299
|
+
values ('f2000000-0000-4000-8000-000000000009','emit-orphan','Emit Orphan',
|
|
300
|
+
'f1000000-0000-4000-8000-000000000004','private','active');
|
|
301
|
+
|
|
302
|
+
select pg_temp.service();
|
|
303
|
+
select lives_ok($$ select agent_board.bootstrap_board_owner_memberships() $$,
|
|
304
|
+
'raw writer setup: operator bootstrap runs');
|
|
305
|
+
|
|
306
|
+
-- Asserting the EVENT, not merely that the call lives. A structural check cannot prove the
|
|
307
|
+
-- emit is REACHABLE -- moving it into `if false then` would satisfy one. Only this can.
|
|
308
|
+
select is(
|
|
309
|
+
(select event_kind from agent_board.board_membership_events
|
|
310
|
+
where board_id = 'f2000000-0000-4000-8000-000000000009'
|
|
311
|
+
and agent_id = 'f1000000-0000-4000-8000-000000000004'
|
|
312
|
+
order by seq desc limit 1),
|
|
313
|
+
'operator_repair',
|
|
314
|
+
'RAW WRITER: bootstrap emits operator_repair, not the generic operator_designation');
|
|
315
|
+
|
|
316
|
+
select is(
|
|
317
|
+
(select provenance from agent_board.board_membership_events
|
|
318
|
+
where board_id = 'f2000000-0000-4000-8000-000000000009'
|
|
319
|
+
and agent_id = 'f1000000-0000-4000-8000-000000000004'
|
|
320
|
+
order by seq desc limit 1),
|
|
321
|
+
'operator-designated',
|
|
322
|
+
'RAW WRITER: ...operator-designated, never the owner''s own act');
|
|
323
|
+
|
|
324
|
+
select is(
|
|
325
|
+
(select result_state from agent_board.board_membership_events
|
|
326
|
+
where board_id = 'f2000000-0000-4000-8000-000000000009'
|
|
327
|
+
and agent_id = 'f1000000-0000-4000-8000-000000000004'
|
|
328
|
+
order by seq desc limit 1),
|
|
329
|
+
(select membership_state from agent_board.board_memberships
|
|
330
|
+
where board_id = 'f2000000-0000-4000-8000-000000000009'
|
|
331
|
+
and agent_id = 'f1000000-0000-4000-8000-000000000004'),
|
|
332
|
+
'RAW WRITER: bootstrap records the ACTUAL row state, not a literal');
|
|
333
|
+
|
|
334
|
+
-- REDEEM MUST RECORD THE ACTUAL OUTCOME, NOT THE TOKEN'S PRESET.
|
|
335
|
+
-- The redeem upsert's ON CONFLICT lifts only the ROLE and leaves an existing membership_state
|
|
336
|
+
-- untouched. So an already-SUBSCRIBED member redeeming a JOINED-preset link stays subscribed --
|
|
337
|
+
-- and emitting the preset would write a ledger row that contradicts the table it describes.
|
|
338
|
+
insert into agent_board.board_memberships(board_id, agent_id, role, membership_state, created_by_agent_id)
|
|
339
|
+
values ('f2000000-0000-4000-8000-000000000001','f1000000-0000-4000-8000-000000000005',
|
|
340
|
+
'member','subscribed','f1000000-0000-4000-8000-000000000001');
|
|
341
|
+
|
|
342
|
+
insert into agent_board.board_invite_tokens(code, board_id, preset_role, preset_membership_state,
|
|
343
|
+
max_uses, expires_at, created_by_agent_id)
|
|
344
|
+
values ('emit-token-1','f2000000-0000-4000-8000-000000000001','member','joined',
|
|
345
|
+
5, now() + interval '7 days','f1000000-0000-4000-8000-000000000001');
|
|
346
|
+
|
|
347
|
+
select pg_temp.claims('f0000000-0000-0000-0000-000000000005');
|
|
348
|
+
select lives_ok($$
|
|
349
|
+
select agent_board._redeem_invite_for_agent('emit-token-1','f1000000-0000-4000-8000-000000000005')
|
|
350
|
+
$$, 'redeem setup: an already-SUBSCRIBED member redeems a JOINED-preset link');
|
|
351
|
+
|
|
352
|
+
select is(
|
|
353
|
+
(select membership_state from agent_board.board_memberships
|
|
354
|
+
where board_id = 'f2000000-0000-4000-8000-000000000001'
|
|
355
|
+
and agent_id = 'f1000000-0000-4000-8000-000000000005'),
|
|
356
|
+
'subscribed',
|
|
357
|
+
'redeem: the membership row STAYS subscribed -- ON CONFLICT lifts role only');
|
|
358
|
+
|
|
359
|
+
select is((select state from pg_temp.latest('f1000000-0000-4000-8000-000000000005')),
|
|
360
|
+
'subscribed',
|
|
361
|
+
'REDEEM: the event records the ACTUAL resulting state, not the token preset');
|
|
362
|
+
select is((select kind from pg_temp.latest('f1000000-0000-4000-8000-000000000005')),
|
|
363
|
+
'consent_link_redeem',
|
|
364
|
+
'REDEEM: ...with the frozen consent_link_redeem kind');
|
|
365
|
+
|
|
366
|
+
-- ===========================================================================
|
|
367
|
+
-- 5d. Self-TOGGLE vs self-JOIN, and the two gaps a structural check cannot close
|
|
368
|
+
-- ===========================================================================
|
|
369
|
+
-- The frozen mapping distinguishes a toggle between two ACTIVE states from a fresh join.
|
|
370
|
+
-- Deriving from (provenance, state) alone cannot tell them apart -- both are target-authored
|
|
371
|
+
-- and both land active -- so the sink names the kind from the PRIOR state.
|
|
372
|
+
select pg_temp.claims('f0000000-0000-0000-0000-000000000002');
|
|
373
|
+
select lives_ok($$ select agent_board._upsert_board_membership_state(
|
|
374
|
+
'f2000000-0000-4000-8000-000000000001','f1000000-0000-4000-8000-000000000002','joined') $$,
|
|
375
|
+
'toggle setup: the subject joins');
|
|
376
|
+
select is((select kind from pg_temp.latest('f1000000-0000-4000-8000-000000000002')),
|
|
377
|
+
'consent_self_join', 'TOGGLE: a fresh join is consent_self_join');
|
|
378
|
+
|
|
379
|
+
select lives_ok($$ select agent_board._upsert_board_membership_state(
|
|
380
|
+
'f2000000-0000-4000-8000-000000000001','f1000000-0000-4000-8000-000000000002','subscribed') $$,
|
|
381
|
+
'toggle setup: the subject switches to subscribed');
|
|
382
|
+
select is((select kind from pg_temp.latest('f1000000-0000-4000-8000-000000000002')),
|
|
383
|
+
'consent_self_toggle',
|
|
384
|
+
'TOGGLE: joined -> subscribed is a TOGGLE, not a second join');
|
|
385
|
+
|
|
386
|
+
-- The two boundaries of the toggle rule, both verified live before being pinned:
|
|
387
|
+
-- * NO prior row -> v_prior_state is NULL, `null in (...)` is NULL, so the CASE falls through
|
|
388
|
+
-- to the generic derivation. A first-ever join straight to `subscribed` is a JOIN.
|
|
389
|
+
-- * a prior row that is NOT active (left/removed) -> re-entry is a fresh act of consent, not
|
|
390
|
+
-- a toggle. Someone returning after leaving is consenting again, and the ledger must say so.
|
|
391
|
+
select pg_temp.claims('f0000000-0000-0000-0000-000000000006');
|
|
392
|
+
select lives_ok($$ select agent_board._upsert_board_membership_state(
|
|
393
|
+
'f2000000-0000-4000-8000-000000000001','f1000000-0000-4000-8000-000000000006','subscribed') $$,
|
|
394
|
+
'toggle boundary setup: a first-ever membership, straight to subscribed');
|
|
395
|
+
select is((select kind from pg_temp.latest('f1000000-0000-4000-8000-000000000006')),
|
|
396
|
+
'consent_self_join',
|
|
397
|
+
'TOGGLE boundary: with NO prior row it is a JOIN, not a toggle');
|
|
398
|
+
|
|
399
|
+
select lives_ok($$ select agent_board._upsert_board_membership_state(
|
|
400
|
+
'f2000000-0000-4000-8000-000000000001','f1000000-0000-4000-8000-000000000006','left') $$,
|
|
401
|
+
'toggle boundary setup: they leave');
|
|
402
|
+
select lives_ok($$ select agent_board._upsert_board_membership_state(
|
|
403
|
+
'f2000000-0000-4000-8000-000000000001','f1000000-0000-4000-8000-000000000006','joined') $$,
|
|
404
|
+
'toggle boundary setup: they come back');
|
|
405
|
+
select is((select kind from pg_temp.latest('f1000000-0000-4000-8000-000000000006')),
|
|
406
|
+
'consent_self_join',
|
|
407
|
+
'TOGGLE boundary: re-entry from LEFT is a fresh JOIN -- returning is consenting again');
|
|
408
|
+
|
|
409
|
+
-- A third party moving an ACTIVE member between active states is NOT a self-toggle.
|
|
410
|
+
select pg_temp.claims('f0000000-0000-0000-0000-000000000001');
|
|
411
|
+
select lives_ok($$ select agent_board._upsert_board_membership_state(
|
|
412
|
+
'f2000000-0000-4000-8000-000000000001','f1000000-0000-4000-8000-000000000002','joined') $$,
|
|
413
|
+
'toggle setup: a third party moves them back');
|
|
414
|
+
select is((select kind from pg_temp.latest('f1000000-0000-4000-8000-000000000002')),
|
|
415
|
+
'resolution',
|
|
416
|
+
'TOGGLE: a third party is board-authored resolution, never a self-toggle');
|
|
417
|
+
|
|
418
|
+
-- FEDSESSION, behaviourally. A textual call-site count cannot tell whether the emit sits
|
|
419
|
+
-- inside `if found` -- moving it out would emit on idempotent no-insert calls, and hiding it
|
|
420
|
+
-- behind `if false` would emit none. Both keep exactly one textual call.
|
|
421
|
+
-- A DEDICATED board so the counts below are isolated. Agent 4 already carries operator events
|
|
422
|
+
-- on board 1 from the operator section, and counting across both made the baseline wrong --
|
|
423
|
+
-- the assertion failed on my own fixture, not on the code.
|
|
424
|
+
insert into agent_board.boards(id, slug, name, created_by_agent_id, visibility, status)
|
|
425
|
+
values ('f2000000-0000-4000-8000-00000000000f','emit-fed','Emit Fed',
|
|
426
|
+
'f1000000-0000-4000-8000-000000000001','private','active');
|
|
427
|
+
|
|
428
|
+
insert into agent_board.identity_providers(id, issuer, audience, default_board_id)
|
|
429
|
+
values ('emit-prov','https://p.test','aud','f2000000-0000-4000-8000-00000000000f');
|
|
430
|
+
|
|
431
|
+
select pg_temp.service();
|
|
432
|
+
select lives_ok($$
|
|
433
|
+
select agent_board._fedsession_autojoin_default_board(
|
|
434
|
+
(select p from agent_board.identity_providers p where p.id = 'emit-prov'),
|
|
435
|
+
'f1000000-0000-4000-8000-000000000004')
|
|
436
|
+
$$, 'fedsession setup: first auto-join');
|
|
437
|
+
|
|
438
|
+
select is(
|
|
439
|
+
(select count(*)::int from agent_board.board_membership_events
|
|
440
|
+
where agent_id = 'f1000000-0000-4000-8000-000000000004'
|
|
441
|
+
and board_id = 'f2000000-0000-4000-8000-00000000000f'),
|
|
442
|
+
1,
|
|
443
|
+
'FEDSESSION: the first auto-join emits exactly one event');
|
|
444
|
+
|
|
445
|
+
-- ...and that one event records the ACTUAL inserted state and its frozen kind, not merely SOME
|
|
446
|
+
-- event. Counting alone is mutation-blind: changing the emitted `joined` literal, or the
|
|
447
|
+
-- explicit `operator_designation` kind the writer passes, keeps the count at one. The fedsession
|
|
448
|
+
-- insert has NO on-conflict-do-update path -- it inserts exactly this state or nothing -- so the
|
|
449
|
+
-- emitted literal must equal the row, and a state literal here (unlike the upserting writers) is
|
|
450
|
+
-- correct precisely because there is no divergent existing state to preserve.
|
|
451
|
+
select is(
|
|
452
|
+
(select result_state from agent_board.board_membership_events
|
|
453
|
+
where agent_id = 'f1000000-0000-4000-8000-000000000004'
|
|
454
|
+
and board_id = 'f2000000-0000-4000-8000-00000000000f'
|
|
455
|
+
order by seq desc limit 1),
|
|
456
|
+
'joined',
|
|
457
|
+
'FEDSESSION: the auto-join event records the frozen joined literal, not merely some state');
|
|
458
|
+
|
|
459
|
+
select is(
|
|
460
|
+
(select result_state from agent_board.board_membership_events
|
|
461
|
+
where agent_id = 'f1000000-0000-4000-8000-000000000004'
|
|
462
|
+
and board_id = 'f2000000-0000-4000-8000-00000000000f'
|
|
463
|
+
order by seq desc limit 1),
|
|
464
|
+
(select membership_state from agent_board.board_memberships
|
|
465
|
+
where agent_id = 'f1000000-0000-4000-8000-000000000004'
|
|
466
|
+
and board_id = 'f2000000-0000-4000-8000-00000000000f'),
|
|
467
|
+
'FEDSESSION: literal == actual -- the emitted state matches the row the no-conflict insert placed');
|
|
468
|
+
|
|
469
|
+
select is(
|
|
470
|
+
(select event_kind from agent_board.board_membership_events
|
|
471
|
+
where agent_id = 'f1000000-0000-4000-8000-000000000004'
|
|
472
|
+
and board_id = 'f2000000-0000-4000-8000-00000000000f'
|
|
473
|
+
order by seq desc limit 1),
|
|
474
|
+
'operator_designation',
|
|
475
|
+
'FEDSESSION: ...with the frozen operator_designation kind the writer passes explicitly');
|
|
476
|
+
|
|
477
|
+
-- ...and a SECOND call is a no-op (the NOT EXISTS suppresses the insert) so it must emit NOTHING.
|
|
478
|
+
select lives_ok($$
|
|
479
|
+
select agent_board._fedsession_autojoin_default_board(
|
|
480
|
+
(select p from agent_board.identity_providers p where p.id = 'emit-prov'),
|
|
481
|
+
'f1000000-0000-4000-8000-000000000004')
|
|
482
|
+
$$, 'fedsession setup: idempotent second call');
|
|
483
|
+
|
|
484
|
+
select is(
|
|
485
|
+
(select count(*)::int from agent_board.board_membership_events
|
|
486
|
+
where agent_id = 'f1000000-0000-4000-8000-000000000004'
|
|
487
|
+
and board_id = 'f2000000-0000-4000-8000-00000000000f'),
|
|
488
|
+
1,
|
|
489
|
+
'FEDSESSION: a no-op second call emits NOTHING -- still exactly one event');
|
|
490
|
+
|
|
491
|
+
-- ===========================================================================
|
|
492
|
+
-- 5e. The FULL reachable (provenance, state) -> kind matrix
|
|
493
|
+
-- ===========================================================================
|
|
494
|
+
-- Sampling three combinations left the mapping mutation-blind: changing any UNASSERTED cell
|
|
495
|
+
-- passed the suite. Asserted here as a single set comparison, so an added or altered cell
|
|
496
|
+
-- fails and NAMES itself rather than silently diverging from the frozen record.
|
|
497
|
+
--
|
|
498
|
+
-- Driven through the emitter directly. Reaching every cell through the RPCs would need a
|
|
499
|
+
-- fixture per cell and still miss the operator+pending rows, which no route produces.
|
|
500
|
+
select pg_temp.service();
|
|
501
|
+
|
|
502
|
+
create function pg_temp.kind_of(p_state text) returns text language plpgsql as $$
|
|
503
|
+
declare v text;
|
|
504
|
+
begin
|
|
505
|
+
perform agent_board._emit_membership_event(
|
|
506
|
+
'f2000000-0000-4000-8000-000000000001'::uuid,
|
|
507
|
+
'f1000000-0000-4000-8000-000000000006'::uuid, p_state);
|
|
508
|
+
select event_kind into v from agent_board.board_membership_events
|
|
509
|
+
where agent_id = 'f1000000-0000-4000-8000-000000000006' order by seq desc limit 1;
|
|
510
|
+
return v;
|
|
511
|
+
end $$;
|
|
512
|
+
|
|
513
|
+
select is(
|
|
514
|
+
(select string_agg(s || '=' || pg_temp.kind_of(s), ' ' order by s)
|
|
515
|
+
from unnest(array['joined','subscribed','invited','requested','left','removed']) s),
|
|
516
|
+
'invited=invite_offer joined=operator_designation left=operator_removal '
|
|
517
|
+
|| 'removed=operator_removal requested=request_manufactured subscribed=operator_designation',
|
|
518
|
+
'MATRIX: the operator row maps exactly -- pending states are NEUTRAL, not operator_repair');
|
|
519
|
+
|
|
520
|
+
-- The TARGET-AUTHORED row. Asserting only the operator row left every self-authored cell
|
|
521
|
+
-- mutation-blind -- and those are the cells that decide whether someone consented.
|
|
522
|
+
select pg_temp.claims('f0000000-0000-0000-0000-000000000006');
|
|
523
|
+
select is(
|
|
524
|
+
(select string_agg(s || '=' || pg_temp.kind_of(s), ' ' order by s)
|
|
525
|
+
from unnest(array['joined','subscribed','invited','requested','left','removed']) s),
|
|
526
|
+
'invited=consent_self_invite joined=consent_self_join left=self_withdraw '
|
|
527
|
+
|| 'removed=self_withdraw requested=consent_join_request subscribed=consent_self_join',
|
|
528
|
+
'MATRIX: the target-authored row maps exactly');
|
|
529
|
+
|
|
530
|
+
-- The BOARD-AUTHORED row. Reachable through update_board_membership's other-agent path, and
|
|
531
|
+
-- previously unasserted -- changing board-authored `requested` to invite_offer would have passed.
|
|
532
|
+
select pg_temp.claims('f0000000-0000-0000-0000-000000000001');
|
|
533
|
+
select is(
|
|
534
|
+
(select string_agg(s || '=' || pg_temp.kind_of(s), ' ' order by s)
|
|
535
|
+
from unnest(array['joined','subscribed','invited','requested','left','removed']) s),
|
|
536
|
+
'invited=invite_offer joined=resolution left=eviction '
|
|
537
|
+
|| 'removed=eviction requested=request_manufactured subscribed=resolution',
|
|
538
|
+
'MATRIX: the board-authored row maps exactly');
|
|
539
|
+
select pg_temp.service();
|
|
540
|
+
|
|
541
|
+
-- An unmapped combination must RAISE rather than land unlabelled in an append-only ledger.
|
|
542
|
+
select throws_ok($$
|
|
543
|
+
select agent_board._emit_membership_event(
|
|
544
|
+
'f2000000-0000-4000-8000-000000000001'::uuid,
|
|
545
|
+
'f1000000-0000-4000-8000-000000000006'::uuid, 'not-a-state')
|
|
546
|
+
$$, '22023', null,
|
|
547
|
+
'MATRIX: an unmapped (provenance, state) raises rather than silently mislabelling');
|
|
548
|
+
|
|
549
|
+
-- A no-change active write is NOT a toggle. join_board and update_board_membership both
|
|
550
|
+
-- permit joined->joined, so this is reachable, and it was recorded as a toggle.
|
|
551
|
+
select pg_temp.claims('f0000000-0000-0000-0000-000000000006');
|
|
552
|
+
select lives_ok($$ select agent_board._upsert_board_membership_state(
|
|
553
|
+
'f2000000-0000-4000-8000-000000000001','f1000000-0000-4000-8000-000000000006','joined') $$,
|
|
554
|
+
'no-change setup: the subject writes joined while already joined');
|
|
555
|
+
select is((select kind from pg_temp.latest('f1000000-0000-4000-8000-000000000006')),
|
|
556
|
+
'consent_self_join',
|
|
557
|
+
'MATRIX: a no-change active write is a JOIN, not a toggle of nothing');
|
|
558
|
+
|
|
559
|
+
-- ===========================================================================
|
|
560
|
+
-- 6. SCOPE: this migration emits but does not GATE
|
|
561
|
+
-- ===========================================================================
|
|
562
|
+
-- SCOPE BOUNDARY, updated when gating landed. This assertion previously read "no route
|
|
563
|
+
-- consults the predicate yet" and FAILED the moment MBCONSENT-B(ii) shipped -- which is what a
|
|
564
|
+
-- scope guard is for. It now names the expected consumer SET, so a route that starts consulting
|
|
565
|
+
-- the predicate without being added here fails and names itself.
|
|
566
|
+
--
|
|
567
|
+
-- resolve_board_request is the sole predicate READER. join_board and update_board_membership
|
|
568
|
+
-- gate STRUCTURALLY (the actor must be the subject; no other-agent promotion), which needs no
|
|
569
|
+
-- ledger read -- a structural refusal is stronger than a predicate check, because it cannot be
|
|
570
|
+
-- satisfied by manufacturing evidence.
|
|
571
|
+
select is(
|
|
572
|
+
(select coalesce(string_agg(p.proname, ', ' order by p.proname), '')
|
|
573
|
+
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
574
|
+
where n.nspname = 'agent_board' and p.prokind = 'f'
|
|
575
|
+
and p.prolang <> (select oid from pg_language where lanname = 'internal')
|
|
576
|
+
and has_function_privilege('authenticated', p.oid, 'EXECUTE')
|
|
577
|
+
and pg_get_functiondef(p.oid) ~ '_membership_consent_present'),
|
|
578
|
+
'resolve_board_request, update_board_membership',
|
|
579
|
+
'scope: exactly TWO authenticated routes read the predicate');
|
|
580
|
+
|
|
581
|
+
-- ...and the ledger it fills is the one MBCONSENT-A froze.
|
|
582
|
+
-- The refused write must leave NO trace. Asserted against the SUBJECT of the refused attempt
|
|
583
|
+
-- rather than a global row count: a hardcoded total breaks every time a test is added, which
|
|
584
|
+
-- makes it noise rather than a signal, and it was already wrong once here.
|
|
585
|
+
select is(
|
|
586
|
+
(select count(*)::int from agent_board.board_membership_events
|
|
587
|
+
where agent_id = 'f1000000-0000-4000-8000-000000000002'
|
|
588
|
+
and result_state = 'left'
|
|
589
|
+
and provenance = 'operator-designated'),
|
|
590
|
+
0,
|
|
591
|
+
'scope: the refused unattributable write left NO ledger row, invented or otherwise');
|
|
592
|
+
|
|
593
|
+
-- ...and every event that DID land carries a real kind and provenance.
|
|
594
|
+
select is(
|
|
595
|
+
(select count(*)::int from agent_board.board_membership_events
|
|
596
|
+
where event_kind is null or provenance is null),
|
|
597
|
+
0,
|
|
598
|
+
'scope: no event landed with a null kind or provenance');
|
|
599
|
+
|
|
600
|
+
select finish();
|
|
601
|
+
rollback;
|