@consiliency/agent-board-schema 1.22.0 → 1.24.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/README.md +75 -1
- package/fixtures/already-applied.json +40 -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/migrations/20260905000100_agent_board_consent_gating_reconcile.sql +151 -0
- package/package.json +1 -1
- package/schema-manifest.json +93 -3
- package/src/check.js +23 -0
- package/src/index.js +20 -0
- package/src/plan.js +79 -5
- package/src/tooling.test.mjs +237 -4
- 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_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 +46 -2
- package/tests/agent_board_fedsession_provider_absence_gate.test.sql +170 -0
- package/tests/agent_board_inbox_reads.test.sql +16 -19
- package/tests/agent_board_outbox_wake_ordering.test.sql +452 -0
- package/tests/agent_board_portal_offline_invariant.test.sql +1 -1
- package/tests/agent_board_roomlink.test.sql +2 -2
- package/tests/agent_board_wake_recovery.test.sql +756 -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/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/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/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/wake_recovery_late_commit.spec +207 -0
- package/tests/seeds/20260901000100_agent_board_atomic_fleet_onboarding.sql.pre.sql +110 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
-- Consiliency/message-board#193 pgTAP: enroll_federated_human must reject an
|
|
2
|
+
-- ABSENT (issuer, audience) BEFORE it provisions or locks the human owner.
|
|
3
|
+
--
|
|
4
|
+
-- WHY THE GATE IS WHERE IT IS. PostgreSQL cannot row-lock a row that does not
|
|
5
|
+
-- exist, so with the provider absent there was nothing for the board/provider
|
|
6
|
+
-- ordering to attach to: the function skipped it, locked auth.users, and delegated
|
|
7
|
+
-- -- and the delegate's fresh READ COMMITTED snapshot could then see a provider
|
|
8
|
+
-- committed in the gap and auto-join its default board while this transaction held
|
|
9
|
+
-- the owner. Owner -> board, inverted against fleet onboarding.
|
|
10
|
+
--
|
|
11
|
+
-- WHAT THIS FILE CAN AND CANNOT PROVE. A single pgTAP transaction cannot exhibit
|
|
12
|
+
-- the interleaving; the load-bearing proof is
|
|
13
|
+
-- tests/isolation/specs/fedsession_provider_absent_owner_lock.spec, where a guard
|
|
14
|
+
-- session holds the owner row and the pre-fix function BLOCKS while the fixed one
|
|
15
|
+
-- raises without waiting. What is pinned HERE is (a) that the rejection comes from
|
|
16
|
+
-- the pre-lock gate and not from the delegate's identical-verdict step (4) -- which
|
|
17
|
+
-- is why the two messages differ -- (b) that it is positionally ahead of the owner
|
|
18
|
+
-- provisioning in the shipped definition, and (c) that the deeper provider gates and
|
|
19
|
+
-- the auto-join allow path are unchanged.
|
|
20
|
+
create extension if not exists pgtap with schema extensions;
|
|
21
|
+
set search_path = extensions, public, agent_board;
|
|
22
|
+
|
|
23
|
+
begin;
|
|
24
|
+
|
|
25
|
+
select plan(8);
|
|
26
|
+
|
|
27
|
+
-- G-1: the ABSENT-provider rejection, identified by the pre-lock gate's own message.
|
|
28
|
+
-- The delegate raises the same 42501 with a DIFFERENT message, so an errcode-only
|
|
29
|
+
-- assertion here would pass with the gate removed.
|
|
30
|
+
select throws_ok(
|
|
31
|
+
$$ select agent_board.enroll_federated_human(
|
|
32
|
+
'https://lo193-absent.test', 'message-board', 'lo193-orphan',
|
|
33
|
+
jsonb_build_object('sub','lo193-orphan'), null) $$,
|
|
34
|
+
'42501',
|
|
35
|
+
'issuer/audience not in the identity-provider allowlist (rejected before any owner or board lock)',
|
|
36
|
+
'G-1: an absent (issuer,audience) is rejected by the PRE-LOCK gate');
|
|
37
|
+
|
|
38
|
+
-- G-2: and it is positionally ahead of owner provisioning in the shipped body.
|
|
39
|
+
--
|
|
40
|
+
-- WHAT THIS ASSERTION HAD TO SURVIVE, because each version read as evidence while
|
|
41
|
+
-- being defeatable. The marker is located on a body with comments and dollar-quoted
|
|
42
|
+
-- blocks stripped and single-quoted literals KEPT (the raise message is a literal,
|
|
43
|
+
-- so stripping literals would remove the very thing being located), and it is
|
|
44
|
+
-- anchored to the RAISE SHAPE rather than to the bare message:
|
|
45
|
+
-- * bare message on raw text -> a one-line comment quoting the message passes it
|
|
46
|
+
-- with the gate moved below _ensure_agent_auth_user;
|
|
47
|
+
-- * bare message on the stripped body -> `raise debug 'ordering note: %', '<msg>'`
|
|
48
|
+
-- or `declare v_note text := '<msg>'` passes it the same way. Both verified.
|
|
49
|
+
-- * raise-exception-shaped on the stripped body -> all four masks fail, including
|
|
50
|
+
-- a composite that also makes the provider freeze null-tolerant and goes 8/8
|
|
51
|
+
-- GREEN against every other assertion here with the owner provisioned before the
|
|
52
|
+
-- verdict.
|
|
53
|
+
-- RESIDUAL, stated rather than implied: this narrows the vector, it does not close
|
|
54
|
+
-- the shape. A dead-code `raise exception '<msg>'` earlier in the body still passes.
|
|
55
|
+
-- What that residual cannot reach is the LOCK order: a gate moved below the
|
|
56
|
+
-- auth.users FOR UPDATE is caught by
|
|
57
|
+
-- tests/isolation/specs/fedsession_provider_absent_owner_lock.spec (the call goes
|
|
58
|
+
-- `<waiting ...>` instead of raising), verified by mutation. A gate moved below
|
|
59
|
+
-- _ensure_agent_auth_user but ABOVE that lock is caught by NEITHER the spec nor the
|
|
60
|
+
-- pre-remedy G-2 -- which is precisely the hole this anchoring closes.
|
|
61
|
+
select is(
|
|
62
|
+
(select case
|
|
63
|
+
when p_gate = 0 then 'gate-missing'
|
|
64
|
+
when p_ensure = 0 or p_lock is null then 'owner-provisioning-not-found'
|
|
65
|
+
when p_gate < p_ensure and p_gate < p_lock then 'gate-before-owner'
|
|
66
|
+
else 'gate-after-owner'
|
|
67
|
+
end
|
|
68
|
+
from (
|
|
69
|
+
select coalesce(strpos(d, substring(d from
|
|
70
|
+
$g$raise\s+exception\s+'issuer/audience not in the identity-provider allowlist \(rejected before any owner or board lock\)'$g$)), 0) as p_gate,
|
|
71
|
+
strpos(d, '_ensure_agent_auth_user') as p_ensure,
|
|
72
|
+
strpos(d, substring(d from 'from\s+auth\.users[^;]{0,250}?for\s+update')) as p_lock
|
|
73
|
+
from (select regexp_replace(p.prosrc,
|
|
74
|
+
$re$(\$[A-Za-z_0-9]*\$).*?\1|('([^']|'')*')|(--[^\n]*)|(/\*([^*]|\*+[^*/])*\*+/)$re$,
|
|
75
|
+
'\2', 'g') as d
|
|
76
|
+
from pg_proc p
|
|
77
|
+
join pg_namespace n on n.oid = p.pronamespace
|
|
78
|
+
where n.nspname = 'agent_board'
|
|
79
|
+
and p.proname = 'enroll_federated_human') s
|
|
80
|
+
) t),
|
|
81
|
+
'gate-before-owner',
|
|
82
|
+
'G-2: the absence gate precedes both _ensure_agent_auth_user and the auth.users lock');
|
|
83
|
+
|
|
84
|
+
-- Fixtures for the allow side and for the deeper gates the new one must not shadow.
|
|
85
|
+
insert into agent_board.agents(id, agent_type, kind, handle, display_name, status)
|
|
86
|
+
values ('193a0000-0000-0000-0000-000000000001', 'agent', 'agent',
|
|
87
|
+
'lo193-owner', 'LO193 Owner', 'active');
|
|
88
|
+
|
|
89
|
+
select count(*) from agent_board.create_board(
|
|
90
|
+
'193a0000-0000-0000-0000-000000000001', 'lo193-board', 'LO193 Board',
|
|
91
|
+
'Consiliency/message-board#193 provider-absence gate', jsonb_build_object(), 'private');
|
|
92
|
+
|
|
93
|
+
insert into agent_board.identity_providers(
|
|
94
|
+
issuer, audience, supported, policy_approved,
|
|
95
|
+
allowed_scopes, allowed_board_scopes, allowed_agent_kinds, default_board_id)
|
|
96
|
+
values
|
|
97
|
+
('https://lo193-autojoin.test', 'message-board', true, true,
|
|
98
|
+
ARRAY[]::text[], ARRAY[]::text[], ARRAY['agent','human'],
|
|
99
|
+
(select id from agent_board.boards where slug = 'lo193-board')),
|
|
100
|
+
('https://lo193-noboard.test', 'message-board', true, true,
|
|
101
|
+
ARRAY[]::text[], ARRAY[]::text[], ARRAY['agent','human'], null),
|
|
102
|
+
('https://lo193-unapproved.test', 'message-board', true, false,
|
|
103
|
+
ARRAY[]::text[], ARRAY[]::text[], ARRAY['agent','human'], null);
|
|
104
|
+
|
|
105
|
+
-- G-3/G-4: provider PRESENT with a designated default board -- enrollment succeeds
|
|
106
|
+
-- and the auto-join still lands. The gate narrowed the absent case only.
|
|
107
|
+
select lives_ok(
|
|
108
|
+
$$ select agent_board.enroll_federated_human(
|
|
109
|
+
'https://lo193-autojoin.test', 'message-board', 'lo193-joiner',
|
|
110
|
+
jsonb_build_object('sub','lo193-joiner'), 'LO193 Joiner') $$,
|
|
111
|
+
'G-3: an allowlisted provider with a default board still enrolls');
|
|
112
|
+
select is(
|
|
113
|
+
(select count(*)::int
|
|
114
|
+
from agent_board.board_memberships m
|
|
115
|
+
join agent_board.agents a on a.id = m.agent_id
|
|
116
|
+
where m.board_id = (select id from agent_board.boards where slug = 'lo193-board')
|
|
117
|
+
and a.metadata #>> ARRAY['federation','subject'] = 'lo193-joiner'
|
|
118
|
+
and m.membership_state = 'joined'),
|
|
119
|
+
1, 'G-4: the fedsession auto-join still lands a joined membership');
|
|
120
|
+
|
|
121
|
+
-- G-5: provider PRESENT with a NULL default board -- enrolls, no auto-join. This is
|
|
122
|
+
-- the neighbouring absence 20260901000140 already handled (NULL designation), kept
|
|
123
|
+
-- distinct from the provider-absent case this migration closes.
|
|
124
|
+
select lives_ok(
|
|
125
|
+
$$ select agent_board.enroll_federated_human(
|
|
126
|
+
'https://lo193-noboard.test', 'message-board', 'lo193-plain',
|
|
127
|
+
jsonb_build_object('sub','lo193-plain'), 'LO193 Plain') $$,
|
|
128
|
+
'G-5: an allowlisted provider with no default board still enrolls');
|
|
129
|
+
|
|
130
|
+
-- G-6: the deeper provider gates are NOT shadowed -- an allowlisted but
|
|
131
|
+
-- unapproved provider still fails inside the delegate, with the delegate's own
|
|
132
|
+
-- message. Asserted positively so "42501 came from somewhere" is never enough.
|
|
133
|
+
select throws_ok(
|
|
134
|
+
$$ select agent_board.enroll_federated_human(
|
|
135
|
+
'https://lo193-unapproved.test', 'message-board', 'lo193-unapproved',
|
|
136
|
+
jsonb_build_object('sub','lo193-unapproved'), null) $$,
|
|
137
|
+
'42501', 'identity provider is not policy-approved',
|
|
138
|
+
'G-6: a present-but-unapproved provider is still rejected by the delegate');
|
|
139
|
+
|
|
140
|
+
-- G-7: the pre-lock gate is the ONLY new rejection -- a present provider never
|
|
141
|
+
-- reaches it. (Mutating the gate to fire unconditionally fails G-3/G-5/G-7.)
|
|
142
|
+
select is(
|
|
143
|
+
(select count(*)::int
|
|
144
|
+
from agent_board.agents a
|
|
145
|
+
where a.kind = 'human'
|
|
146
|
+
and a.metadata #>> ARRAY['federation','subject'] in ('lo193-joiner', 'lo193-plain')),
|
|
147
|
+
2, 'G-7: both allowlisted enrollments provisioned their human agents');
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
-- G-8: the CLASS of routes that reject an absent provider, enumerated from the
|
|
151
|
+
-- catalog rather than described in prose. All four reject with 42501; none
|
|
152
|
+
-- serializes the absence, and that is the recorded decision (#195) -- the
|
|
153
|
+
-- serialization alternative was built, measured and reverted because no shipped
|
|
154
|
+
-- caller can distinguish 42501 from a retryable code. A new route joining this
|
|
155
|
+
-- class fails here and names itself.
|
|
156
|
+
select is(
|
|
157
|
+
(select string_agg(p.proname, ', ' order by p.proname)
|
|
158
|
+
from pg_proc p
|
|
159
|
+
join pg_namespace n on n.oid = p.pronamespace
|
|
160
|
+
where n.nspname = 'agent_board'
|
|
161
|
+
and p.prokind = 'f'
|
|
162
|
+
and regexp_replace(p.prosrc,
|
|
163
|
+
$re$(\$[A-Za-z_0-9]*\$).*?\1|('([^']|'')*')|(--[^\n]*)|(/\*([^*]|\*+[^*/])*\*+/)$re$,
|
|
164
|
+
'\2', 'g') ~ 'not in the identity-provider allowlist'),
|
|
165
|
+
'_enroll_federated_human_after_revocation_gate, _enroll_federated_identity_once, '
|
|
166
|
+
|| 'enroll_federated_human, enroll_federated_identity',
|
|
167
|
+
'G-8: exactly four routes reject an absent identity provider');
|
|
168
|
+
|
|
169
|
+
select * from finish();
|
|
170
|
+
rollback;
|
|
@@ -20,25 +20,22 @@ select has_view('agent_board', 'v_callback_delivery_health', 'v_callback_deliver
|
|
|
20
20
|
select has_column('agent_board', 'v_agent_dashboard', 'inbox_unread_count', 'v_agent_dashboard exposes inbox_unread_count');
|
|
21
21
|
select has_column('agent_board', 'v_agent_dashboard', 'inbox_pending_action_count', 'v_agent_dashboard exposes inbox_pending_action_count');
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
and c.reloptions @> array['security_invoker=true']
|
|
40
|
-
),
|
|
41
|
-
'all inbox read views use security_invoker=true'
|
|
23
|
+
-- Derived from the catalog, not from a list written here (message-board#191 board
|
|
24
|
+
-- review). This is the flag that ACTUALLY carries cross-agent isolation for
|
|
25
|
+
-- everything built on these views: an inner `security_invoker` view re-anchors
|
|
26
|
+
-- base-table RLS to the real querying user through any outer definer view, so the
|
|
27
|
+
-- outer view's own flag is inert while this one is load-bearing. A view added to
|
|
28
|
+
-- the schema and omitted from the old nine-name list was an unpinned read surface.
|
|
29
|
+
-- There are no exemptions -- all 26 agent_board views are invoker -- so a new one
|
|
30
|
+
-- that is not fails here and NAMES ITSELF.
|
|
31
|
+
select is(
|
|
32
|
+
(select coalesce(string_agg(c.relname, ', ' order by c.relname), '')
|
|
33
|
+
from pg_class c
|
|
34
|
+
where c.relnamespace = 'agent_board'::regnamespace
|
|
35
|
+
and c.relkind in ('v', 'm')
|
|
36
|
+
and not coalesce(c.reloptions, '{}'::text[]) @> array['security_invoker=true']),
|
|
37
|
+
'',
|
|
38
|
+
'every agent_board view is security_invoker -- catalog-derived, no exemption list; a definer read surface names itself here'
|
|
42
39
|
);
|
|
43
40
|
|
|
44
41
|
select ok(
|
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
-- OUTBOXWAKE / #187 — monotonic wake ordering for the notification outbox.
|
|
2
|
+
--
|
|
3
|
+
-- WHAT THIS FILE PINS, and why each assertion is shaped the way it is:
|
|
4
|
+
--
|
|
5
|
+
-- * WO-1 SHAPE — `callback_outbox_events.seq` exists, is bigint, is NOT NULL, and is
|
|
6
|
+
-- GENERATED **ALWAYS** (`pg_attribute.attidentity = 'a'`). `by default` would be
|
|
7
|
+
-- 'd' and would make the ordering evidence caller-suppliable; that is the whole
|
|
8
|
+
-- property, so it is read from the catalog and not inferred from the DDL text.
|
|
9
|
+
-- * WO-2 EVIDENCE — an INSERT that names `seq` is REJECTED by the server (428C9).
|
|
10
|
+
-- A shape assertion alone would not prove the server enforces it.
|
|
11
|
+
-- * WO-3 CLASS — catalog-derived, per repo rule #1. The writer set comes from
|
|
12
|
+
-- `pg_get_functiondef`, not from a list written here, so a NEW writer fails and
|
|
13
|
+
-- NAMES ITSELF. The companion assertion closes the only escape hatch that
|
|
14
|
+
-- `generated always` leaves open: `overriding system value`. Neither has an
|
|
15
|
+
-- exemption list; each named writer is ALSO positively checked (WO-6/WO-7) to
|
|
16
|
+
-- actually produce monotonic seq, so this is not an argument from absence.
|
|
17
|
+
-- * WO-4 ORDERING — the uuid tiebreak is GONE from claim_callback_outbox_events and
|
|
18
|
+
-- `seq` replaced it, checked against the catalog body.
|
|
19
|
+
-- * WO-5 RESTATEMENT — the claim function was DROPPED and recreated (its return type
|
|
20
|
+
-- changed), which discards the ACL and the COMMENT and re-grants EXECUTE to PUBLIC
|
|
21
|
+
-- by default. These assertions fail if the migration forgot to restate them.
|
|
22
|
+
-- * WO-6/WO-7 BEHAVIOUR — the acceptance criteria: a real fan-out through the real
|
|
23
|
+
-- writer, in ONE transaction, yields N distinct ordered seq while `created_at` is
|
|
24
|
+
-- byte-identical across all N; a bounded claim takes the LOWEST seq; and the queue is
|
|
25
|
+
-- partitioned by STATUS so nothing is handed out twice or dropped.
|
|
26
|
+
--
|
|
27
|
+
-- WHAT THIS FILE DELIBERATELY DOES NOT ASSERT: that `seq > my cursor` recovers everything
|
|
28
|
+
-- unhandled. `seq` is monotonic in ASSIGNMENT order, not COMMIT order, so a late-committing
|
|
29
|
+
-- transaction lands below an already-persisted cursor. See the WO-7 block for the two
|
|
30
|
+
-- assertions that were removed for encoding that false property.
|
|
31
|
+
--
|
|
32
|
+
-- Allow-side calls are wrapped in lives_ok(...) with a SEPARATE state check
|
|
33
|
+
-- (message-board#113): a bare `is((select ... from rpc(...)), ...)` aborts the
|
|
34
|
+
-- transaction on a raise, pgTAP emits NO `not ok`, and every later assertion in the
|
|
35
|
+
-- file silently does not run.
|
|
36
|
+
|
|
37
|
+
begin;
|
|
38
|
+
|
|
39
|
+
select plan(37);
|
|
40
|
+
|
|
41
|
+
-- ---------------------------------------------------------------------------
|
|
42
|
+
-- WO-1 SHAPE
|
|
43
|
+
-- ---------------------------------------------------------------------------
|
|
44
|
+
select has_column('agent_board', 'callback_outbox_events', 'seq',
|
|
45
|
+
'WO-1 callback_outbox_events has a seq column');
|
|
46
|
+
select col_type_is('agent_board', 'callback_outbox_events', 'seq', 'bigint',
|
|
47
|
+
'WO-1 seq is bigint');
|
|
48
|
+
select col_not_null('agent_board', 'callback_outbox_events', 'seq',
|
|
49
|
+
'WO-1 seq is not null');
|
|
50
|
+
|
|
51
|
+
select is(
|
|
52
|
+
(select a.attidentity
|
|
53
|
+
from pg_attribute a
|
|
54
|
+
where a.attrelid = 'agent_board.callback_outbox_events'::regclass
|
|
55
|
+
and a.attname = 'seq'),
|
|
56
|
+
'a',
|
|
57
|
+
'WO-1 seq is GENERATED ALWAYS as identity (attidentity=a), not BY DEFAULT (d) -- the value is evidence of write order and must not be caller-suppliable');
|
|
58
|
+
|
|
59
|
+
select ok(
|
|
60
|
+
(select bool_or(i.indisunique)
|
|
61
|
+
from pg_index i
|
|
62
|
+
join pg_class c on c.oid = i.indexrelid
|
|
63
|
+
where i.indrelid = 'agent_board.callback_outbox_events'::regclass
|
|
64
|
+
and pg_get_indexdef(i.indexrelid) ~ '\(seq\)$'),
|
|
65
|
+
'WO-1 a UNIQUE index enforces seq uniqueness');
|
|
66
|
+
|
|
67
|
+
select ok(
|
|
68
|
+
(select count(*) = 1
|
|
69
|
+
from pg_index i
|
|
70
|
+
where i.indrelid = 'agent_board.callback_outbox_events'::regclass
|
|
71
|
+
and pg_get_indexdef(i.indexrelid) ~ '\(recipient_agent_id, seq\)$'),
|
|
72
|
+
'WO-1 (recipient_agent_id, seq) index exists -- the consumer access path is "rows for me newer than my cursor"');
|
|
73
|
+
|
|
74
|
+
select isnt(
|
|
75
|
+
(select col_description('agent_board.callback_outbox_events'::regclass, a.attnum)
|
|
76
|
+
from pg_attribute a
|
|
77
|
+
where a.attrelid = 'agent_board.callback_outbox_events'::regclass
|
|
78
|
+
and a.attname = 'seq'),
|
|
79
|
+
null,
|
|
80
|
+
'WO-1 seq carries a COMMENT stating what it is evidence of');
|
|
81
|
+
|
|
82
|
+
-- ---------------------------------------------------------------------------
|
|
83
|
+
-- WO-3 CLASS (catalog-derived; stated before the fixture so it cannot depend on it)
|
|
84
|
+
-- ---------------------------------------------------------------------------
|
|
85
|
+
-- The writer set is READ FROM THE CATALOG. A new function that inserts into the
|
|
86
|
+
-- outbox changes this string, fails the assertion, and prints its own name in the
|
|
87
|
+
-- diff -- which is the only way a completeness claim survives contact with a future
|
|
88
|
+
-- change (repo rule #1).
|
|
89
|
+
select is(
|
|
90
|
+
(select coalesce(string_agg(p.proname, ', ' order by p.proname), '')
|
|
91
|
+
from pg_proc p
|
|
92
|
+
join pg_namespace n on n.oid = p.pronamespace
|
|
93
|
+
where n.nspname = 'agent_board'
|
|
94
|
+
and p.prokind = 'f'
|
|
95
|
+
and p.prolang <> (select oid from pg_language where lanname = 'internal')
|
|
96
|
+
and regexp_replace(pg_get_functiondef(p.oid), '--[^\n]*', '', 'g')
|
|
97
|
+
~ 'insert into agent_board\.callback_outbox_events'),
|
|
98
|
+
'_queue_board_post_callbacks, _queue_inbox_callbacks',
|
|
99
|
+
'WO-3 CLASS: the catalog names exactly the two known outbox writers -- a new writer fails here and NAMES ITSELF');
|
|
100
|
+
|
|
101
|
+
-- `generated always` is enforced by the server, and OVERRIDING SYSTEM VALUE is its
|
|
102
|
+
-- ONLY bypass. Asserting the empty set over the whole schema (not over a hand list of
|
|
103
|
+
-- writers) means a future writer cannot quietly assert an ordering it did not earn.
|
|
104
|
+
select is(
|
|
105
|
+
(select coalesce(string_agg(p.proname, ', ' order by p.proname), '')
|
|
106
|
+
from pg_proc p
|
|
107
|
+
join pg_namespace n on n.oid = p.pronamespace
|
|
108
|
+
where n.nspname = 'agent_board'
|
|
109
|
+
and p.prokind = 'f'
|
|
110
|
+
and p.prolang <> (select oid from pg_language where lanname = 'internal')
|
|
111
|
+
and regexp_replace(pg_get_functiondef(p.oid), '--[^\n]*', '', 'g')
|
|
112
|
+
~* 'insert into agent_board\.callback_outbox_events'
|
|
113
|
+
and regexp_replace(pg_get_functiondef(p.oid), '--[^\n]*', '', 'g')
|
|
114
|
+
~* 'overriding\s+(system|user)\s+value'),
|
|
115
|
+
'',
|
|
116
|
+
'WO-3 CLASS: no outbox writer uses OVERRIDING SYSTEM VALUE -- the identity bypass stays closed');
|
|
117
|
+
|
|
118
|
+
-- ---------------------------------------------------------------------------
|
|
119
|
+
-- WO-4 ORDERING
|
|
120
|
+
-- ---------------------------------------------------------------------------
|
|
121
|
+
select ok(
|
|
122
|
+
(select regexp_replace(pg_get_functiondef(p.oid), '--[^\n]*', '', 'g')
|
|
123
|
+
~ 'order by coe\.available_at asc, coe\.seq asc'
|
|
124
|
+
from pg_proc p
|
|
125
|
+
join pg_namespace n on n.oid = p.pronamespace
|
|
126
|
+
where n.nspname = 'agent_board' and p.proname = 'claim_callback_outbox_events'),
|
|
127
|
+
'WO-4 claim_callback_outbox_events orders candidates by (available_at, seq)');
|
|
128
|
+
|
|
129
|
+
select ok(
|
|
130
|
+
(select regexp_replace(pg_get_functiondef(p.oid), '--[^\n]*', '', 'g')
|
|
131
|
+
!~ 'coe\.id asc'
|
|
132
|
+
from pg_proc p
|
|
133
|
+
join pg_namespace n on n.oid = p.pronamespace
|
|
134
|
+
where n.nspname = 'agent_board' and p.proname = 'claim_callback_outbox_events'),
|
|
135
|
+
'WO-4 the random-uuid tiebreak (coe.id asc) is GONE from the claim ordering');
|
|
136
|
+
|
|
137
|
+
-- This is the PIN for "the batch is returned in seq order", and it is a source check
|
|
138
|
+
-- on purpose. The behavioural assertion below (WO-7, "RETURNED in seq order") does NOT
|
|
139
|
+
-- pin it: deleting `order by u.seq asc` from the function leaves the plan feeding the
|
|
140
|
+
-- ordered candidate CTE straight through, so the observed order stays ascending and the
|
|
141
|
+
-- mutation produces NO failure. A property no mutation can break is not covered, so the
|
|
142
|
+
-- pin has to be stated where the mutation actually lands.
|
|
143
|
+
select ok(
|
|
144
|
+
(select regexp_replace(pg_get_functiondef(p.oid), '--[^\n]*', '', 'g')
|
|
145
|
+
~ 'order by u\.seq asc'
|
|
146
|
+
from pg_proc p
|
|
147
|
+
join pg_namespace n on n.oid = p.pronamespace
|
|
148
|
+
where n.nspname = 'agent_board' and p.proname = 'claim_callback_outbox_events'),
|
|
149
|
+
'WO-4 the claimed batch is RETURNED ordered by seq, not merely selected in seq order');
|
|
150
|
+
|
|
151
|
+
select ok(
|
|
152
|
+
(select 'outbox_seq' = any(p.proargnames)
|
|
153
|
+
from pg_proc p
|
|
154
|
+
join pg_namespace n on n.oid = p.pronamespace
|
|
155
|
+
where n.nspname = 'agent_board' and p.proname = 'claim_callback_outbox_events'),
|
|
156
|
+
'WO-4 the claim RPC returns outbox_seq, so a worker can persist a cursor');
|
|
157
|
+
|
|
158
|
+
-- ---------------------------------------------------------------------------
|
|
159
|
+
-- WO-5 ACL + COMMENT restated after DROP FUNCTION
|
|
160
|
+
-- ---------------------------------------------------------------------------
|
|
161
|
+
select ok(
|
|
162
|
+
has_function_privilege('service_role', 'agent_board.claim_callback_outbox_events(integer)', 'EXECUTE'),
|
|
163
|
+
'WO-5 service_role EXECUTE was re-granted after the drop/recreate');
|
|
164
|
+
|
|
165
|
+
select ok(
|
|
166
|
+
not has_function_privilege('authenticated', 'agent_board.claim_callback_outbox_events(integer)', 'EXECUTE'),
|
|
167
|
+
'WO-5 authenticated has NO EXECUTE -- recreating a definer function re-grants PUBLIC by default');
|
|
168
|
+
|
|
169
|
+
select ok(
|
|
170
|
+
not has_function_privilege('anon', 'agent_board.claim_callback_outbox_events(integer)', 'EXECUTE'),
|
|
171
|
+
'WO-5 anon has NO EXECUTE on the claim RPC');
|
|
172
|
+
|
|
173
|
+
select is(
|
|
174
|
+
(select count(*)::integer
|
|
175
|
+
from pg_proc p
|
|
176
|
+
join pg_namespace n on n.oid = p.pronamespace
|
|
177
|
+
where n.nspname = 'agent_board'
|
|
178
|
+
and p.proname = 'claim_callback_outbox_events'
|
|
179
|
+
and array_to_string(coalesce(p.proacl, '{}'::aclitem[]), ',') ~ '(^|,)=X/'),
|
|
180
|
+
0,
|
|
181
|
+
'WO-5 PUBLIC holds no EXECUTE entry in the claim RPC ACL');
|
|
182
|
+
|
|
183
|
+
select isnt(
|
|
184
|
+
(select obj_description(p.oid, 'pg_proc')
|
|
185
|
+
from pg_proc p
|
|
186
|
+
join pg_namespace n on n.oid = p.pronamespace
|
|
187
|
+
where n.nspname = 'agent_board' and p.proname = 'claim_callback_outbox_events'),
|
|
188
|
+
null,
|
|
189
|
+
'WO-5 the COMMENT discarded by DROP FUNCTION was restated');
|
|
190
|
+
|
|
191
|
+
-- The five assertions above name ONE function, which is exactly the shape that has failed
|
|
192
|
+
-- here before: they verify the function this migration happens to touch, and say nothing
|
|
193
|
+
-- about the next one. These two are the class, derived from pg_catalog with NO exemption
|
|
194
|
+
-- list: every SECURITY DEFINER function whose body reaches callback_outbox_events must be
|
|
195
|
+
-- closed to PUBLIC/anon/authenticated and open to service_role. A function added to this
|
|
196
|
+
-- migration (or any later one) that forgets its revoke or its grant fails here and PRINTS
|
|
197
|
+
-- ITS OWN NAME. Verified exemption-free at the time of writing: all eight members of the
|
|
198
|
+
-- class satisfy both directions, so neither assertion passes by finding nothing.
|
|
199
|
+
select is(
|
|
200
|
+
(select coalesce(string_agg(p.proname, ', ' order by p.proname), '')
|
|
201
|
+
from pg_proc p
|
|
202
|
+
where p.pronamespace = 'agent_board'::regnamespace
|
|
203
|
+
and p.prosecdef
|
|
204
|
+
and pg_get_functiondef(p.oid) ~ 'callback_outbox_events'
|
|
205
|
+
and (has_function_privilege('public', p.oid, 'EXECUTE')
|
|
206
|
+
or has_function_privilege('anon', p.oid, 'EXECUTE')
|
|
207
|
+
or has_function_privilege('authenticated', p.oid, 'EXECUTE'))),
|
|
208
|
+
'',
|
|
209
|
+
'WO-5 CLASS: no SECURITY DEFINER function reaching callback_outbox_events is executable by PUBLIC, anon, or authenticated -- DROP FUNCTION discards the ACL and CREATE re-grants PUBLIC');
|
|
210
|
+
|
|
211
|
+
select is(
|
|
212
|
+
(select coalesce(string_agg(p.proname, ', ' order by p.proname), '')
|
|
213
|
+
from pg_proc p
|
|
214
|
+
where p.pronamespace = 'agent_board'::regnamespace
|
|
215
|
+
and p.prosecdef
|
|
216
|
+
and pg_get_functiondef(p.oid) ~ 'callback_outbox_events'
|
|
217
|
+
and not has_function_privilege('service_role', p.oid, 'EXECUTE')),
|
|
218
|
+
'',
|
|
219
|
+
'WO-5 CLASS: every SECURITY DEFINER function reaching callback_outbox_events is executable by service_role -- a dropped-and-recreated function that lost its grant names itself here');
|
|
220
|
+
|
|
221
|
+
-- ---------------------------------------------------------------------------
|
|
222
|
+
-- Fixture: a real board fan-out. Five subscribers, one verified endpoint each.
|
|
223
|
+
-- ---------------------------------------------------------------------------
|
|
224
|
+
-- claim_callback_outbox_events polls the GLOBAL ready queue. Neutralize any rows a
|
|
225
|
+
-- previous fixture left ready so this file controls the claim; the surrounding
|
|
226
|
+
-- transaction rolls it back.
|
|
227
|
+
update agent_board.callback_outbox_events
|
|
228
|
+
set outbox_status = 'delivered',
|
|
229
|
+
delivered_at = coalesce(delivered_at, now()),
|
|
230
|
+
updated_at = now()
|
|
231
|
+
where outbox_status in ('pending', 'retryable')
|
|
232
|
+
and available_at <= now();
|
|
233
|
+
|
|
234
|
+
insert into agent_board.agents(id, agent_type, handle, display_name, status)
|
|
235
|
+
values ('18700000-0000-4000-8000-000000000000', 'agent', 'wake187-sender', 'Wake187 Sender', 'active');
|
|
236
|
+
insert into agent_board.agents(id, agent_type, handle, display_name, status)
|
|
237
|
+
select ('18700000-0000-4000-8000-' || lpad(n::text, 12, '0'))::uuid,
|
|
238
|
+
'agent', 'wake187-sub-' || n, 'Wake187 Subscriber ' || n, 'active'
|
|
239
|
+
from generate_series(1, 5) n;
|
|
240
|
+
|
|
241
|
+
insert into agent_board.boards(id, slug, name, created_by_agent_id, visibility, status)
|
|
242
|
+
values ('18701000-0000-4000-8000-000000000001', 'wake187-board', 'Wake187 Board',
|
|
243
|
+
'18700000-0000-4000-8000-000000000000', 'open', 'active');
|
|
244
|
+
|
|
245
|
+
insert into agent_board.threads(id, thread_type, created_by_agent_id, subject, board_id)
|
|
246
|
+
values ('18702000-0000-4000-8000-000000000001', 'coordination',
|
|
247
|
+
'18700000-0000-4000-8000-000000000000', 'Wake187 thread',
|
|
248
|
+
'18701000-0000-4000-8000-000000000001');
|
|
249
|
+
|
|
250
|
+
insert into agent_board.messages(id, thread_id, author_agent_id, message_sequence, message_type, body)
|
|
251
|
+
values ('18703000-0000-4000-8000-000000000001', '18702000-0000-4000-8000-000000000001',
|
|
252
|
+
'18700000-0000-4000-8000-000000000000', 1, 'comment', 'wake187 board post');
|
|
253
|
+
|
|
254
|
+
insert into agent_board.board_memberships(id, board_id, agent_id, role, membership_state, created_by_agent_id)
|
|
255
|
+
select ('18704000-0000-4000-8000-' || lpad(n::text, 12, '0'))::uuid,
|
|
256
|
+
'18701000-0000-4000-8000-000000000001',
|
|
257
|
+
('18700000-0000-4000-8000-' || lpad(n::text, 12, '0'))::uuid,
|
|
258
|
+
'member', 'joined', '18700000-0000-4000-8000-000000000000'
|
|
259
|
+
from generate_series(1, 5) n;
|
|
260
|
+
|
|
261
|
+
insert into agent_board.callback_endpoints(id, owner_agent_id, endpoint_url, endpoint_status, verification_requested_at, verified_at)
|
|
262
|
+
select ('18705000-0000-4000-8000-' || lpad(n::text, 12, '0'))::uuid,
|
|
263
|
+
('18700000-0000-4000-8000-' || lpad(n::text, 12, '0'))::uuid,
|
|
264
|
+
'https://example.test/wake187/' || n,
|
|
265
|
+
'verified', now(), now()
|
|
266
|
+
from generate_series(1, 5) n;
|
|
267
|
+
|
|
268
|
+
insert into agent_board.callback_subscriptions(
|
|
269
|
+
id, endpoint_id, agent_id, subscription_scope, event_category,
|
|
270
|
+
board_id, board_membership_id, subscription_status)
|
|
271
|
+
select ('18706000-0000-4000-8000-' || lpad(n::text, 12, '0'))::uuid,
|
|
272
|
+
('18705000-0000-4000-8000-' || lpad(n::text, 12, '0'))::uuid,
|
|
273
|
+
('18700000-0000-4000-8000-' || lpad(n::text, 12, '0'))::uuid,
|
|
274
|
+
'board', 'board.post.delivered',
|
|
275
|
+
'18701000-0000-4000-8000-000000000001',
|
|
276
|
+
('18704000-0000-4000-8000-' || lpad(n::text, 12, '0'))::uuid,
|
|
277
|
+
'active'
|
|
278
|
+
from generate_series(1, 5) n;
|
|
279
|
+
|
|
280
|
+
-- ---------------------------------------------------------------------------
|
|
281
|
+
-- WO-6 FAN-OUT: one transaction, one statement, five rows.
|
|
282
|
+
-- ---------------------------------------------------------------------------
|
|
283
|
+
-- The capture tables are created EMPTY first and the call INSERTs into them. If a
|
|
284
|
+
-- call raises, lives_ok reports a named `not ok` AND every later assertion still runs
|
|
285
|
+
-- against an empty table -- whereas `create temp table ... as select` would leave the
|
|
286
|
+
-- table missing, abort the transaction, and silently truncate the plan (#113).
|
|
287
|
+
create temp table wake187_fanout(inserted_count integer);
|
|
288
|
+
select lives_ok($$
|
|
289
|
+
insert into wake187_fanout
|
|
290
|
+
select agent_board._queue_board_post_callbacks(
|
|
291
|
+
'18701000-0000-4000-8000-000000000001'::uuid,
|
|
292
|
+
'18703000-0000-4000-8000-000000000001'::uuid,
|
|
293
|
+
'Wake187 subject', 'Wake187 preview', 'high', '{}'::jsonb) as inserted_count
|
|
294
|
+
$$, 'WO-6 the board fan-out writer runs [call succeeds]');
|
|
295
|
+
|
|
296
|
+
select is(
|
|
297
|
+
(select inserted_count from wake187_fanout),
|
|
298
|
+
5,
|
|
299
|
+
'WO-6 _queue_board_post_callbacks wrote one outbox row per subscriber');
|
|
300
|
+
|
|
301
|
+
create temp table wake187_rows as
|
|
302
|
+
select id, seq, created_at, recipient_agent_id
|
|
303
|
+
from agent_board.callback_outbox_events
|
|
304
|
+
where board_id = '18701000-0000-4000-8000-000000000001';
|
|
305
|
+
|
|
306
|
+
select is(
|
|
307
|
+
(select count(distinct created_at)::integer from wake187_rows),
|
|
308
|
+
1,
|
|
309
|
+
'WO-6 all five rows share ONE created_at -- now() is transaction_timestamp(), so the fan-out CANNOT be ordered by time');
|
|
310
|
+
|
|
311
|
+
select is(
|
|
312
|
+
(select count(distinct seq)::integer from wake187_rows),
|
|
313
|
+
5,
|
|
314
|
+
'WO-6 the same five rows have five DISTINCT seq values');
|
|
315
|
+
|
|
316
|
+
select is(
|
|
317
|
+
(select (max(seq) - min(seq) + 1)::integer from wake187_rows),
|
|
318
|
+
5,
|
|
319
|
+
'WO-6 the five seq values are contiguous -- a total order with no gap or reuse');
|
|
320
|
+
|
|
321
|
+
-- ---------------------------------------------------------------------------
|
|
322
|
+
-- WO-7 CLAIM DETERMINISM + CURSOR RECOVERY (acceptance criteria 2 and 3)
|
|
323
|
+
-- ---------------------------------------------------------------------------
|
|
324
|
+
create temp table wake187_batch1(outbox_event_id uuid, outbox_seq bigint);
|
|
325
|
+
select lives_ok($$
|
|
326
|
+
insert into wake187_batch1
|
|
327
|
+
select outbox_event_id, outbox_seq from agent_board.claim_callback_outbox_events(2)
|
|
328
|
+
$$, 'WO-7 a bounded claim runs [call succeeds]');
|
|
329
|
+
|
|
330
|
+
select is(
|
|
331
|
+
(select count(*)::integer from wake187_batch1),
|
|
332
|
+
2,
|
|
333
|
+
'WO-7 the bounded claim returned exactly the requested batch size');
|
|
334
|
+
|
|
335
|
+
-- The defect: with a random-uuid tiebreak over identical timestamps, WHICH two rows a
|
|
336
|
+
-- batch of two takes was arbitrary. It must now be the two lowest seq.
|
|
337
|
+
select is(
|
|
338
|
+
(select array_agg(outbox_seq order by outbox_seq) from wake187_batch1),
|
|
339
|
+
(select array_agg(seq order by seq) from (select seq from wake187_rows order by seq limit 2) lo),
|
|
340
|
+
'WO-7 the claim took the two LOWEST seq rows -- deterministic across identical created_at');
|
|
341
|
+
|
|
342
|
+
-- Corroboration only, NOT the pin: removing the ORDER BY from the returned batch does
|
|
343
|
+
-- not make this fail (see the WO-4 source assertion above for why).
|
|
344
|
+
select is(
|
|
345
|
+
(select array_agg(outbox_seq) from wake187_batch1),
|
|
346
|
+
(select array_agg(outbox_seq order by outbox_seq) from wake187_batch1),
|
|
347
|
+
'WO-7 the batch is observed in seq order [corroborates WO-4; the ORDER BY itself is pinned there]');
|
|
348
|
+
|
|
349
|
+
-- WHAT WAS HERE BEFORE, AND WHY IT IS GONE. Two assertions used to partition this queue
|
|
350
|
+
-- with `seq > cursor` / `seq <= cursor` and call the result lossless recovery. Inside one
|
|
351
|
+
-- transaction that is true, and it generalises to a property that IS FALSE: `seq` is
|
|
352
|
+
-- monotonic in ASSIGNMENT order, not in COMMIT order, so a transaction that inserts early
|
|
353
|
+
-- and commits late lands BELOW a cursor a consumer has already persisted and is skipped.
|
|
354
|
+
-- The isolation spec proves exactly that (`late_committer_holds_a_lower_seq_than_the_
|
|
355
|
+
-- persisted_cursor`), and `agent-board-runtime/src/notifications.ts` really does gate on
|
|
356
|
+
-- it: `shouldReplayMissedEvent` returns `Number(stored) < hint.sequence`. An assertion
|
|
357
|
+
-- that encodes a false property is worse than no assertion, so both are replaced by the
|
|
358
|
+
-- property that actually holds and that the claim path actually relies on: the queue is
|
|
359
|
+
-- partitioned by STATUS, not by seq.
|
|
360
|
+
select is(
|
|
361
|
+
(select array_agg(coe.seq order by coe.seq)
|
|
362
|
+
from agent_board.callback_outbox_events coe
|
|
363
|
+
where coe.board_id = '18701000-0000-4000-8000-000000000001'
|
|
364
|
+
and coe.outbox_status = 'pending'),
|
|
365
|
+
(select array_agg(r.seq order by r.seq)
|
|
366
|
+
from wake187_rows r
|
|
367
|
+
where r.seq not in (select outbox_seq from wake187_batch1)),
|
|
368
|
+
'WO-7 the rows still pending are EXACTLY the ones the claim did not take -- the claim path is lossless because it partitions on STATUS, never on seq');
|
|
369
|
+
|
|
370
|
+
select is(
|
|
371
|
+
(select array_agg(coe.seq order by coe.seq)
|
|
372
|
+
from agent_board.callback_outbox_events coe
|
|
373
|
+
where coe.board_id = '18701000-0000-4000-8000-000000000001'
|
|
374
|
+
and coe.outbox_status = 'delivering'),
|
|
375
|
+
(select array_agg(outbox_seq order by outbox_seq) from wake187_batch1),
|
|
376
|
+
'WO-7 the rows now delivering are EXACTLY the claimed batch -- no row is handed out twice and none is silently dropped');
|
|
377
|
+
|
|
378
|
+
create temp table wake187_batch2(outbox_event_id uuid, outbox_seq bigint);
|
|
379
|
+
select lives_ok($$
|
|
380
|
+
insert into wake187_batch2
|
|
381
|
+
select outbox_event_id, outbox_seq from agent_board.claim_callback_outbox_events(10)
|
|
382
|
+
$$, 'WO-7 the follow-up claim runs [call succeeds]');
|
|
383
|
+
|
|
384
|
+
select is(
|
|
385
|
+
(select array_agg(outbox_seq order by outbox_seq) from wake187_batch2),
|
|
386
|
+
(select array_agg(seq order by seq) from (select seq from wake187_rows order by seq offset 2) hi),
|
|
387
|
+
'WO-7 the follow-up claim takes the remaining rows in seq order, resuming exactly where the cursor stopped');
|
|
388
|
+
|
|
389
|
+
-- ---------------------------------------------------------------------------
|
|
390
|
+
-- WO-2 EVIDENCE: the identity is not caller-suppliable (checked against the SERVER,
|
|
391
|
+
-- not the DDL text). Stated last so its failed INSERT cannot disturb the fixture.
|
|
392
|
+
-- ---------------------------------------------------------------------------
|
|
393
|
+
select throws_ok($$
|
|
394
|
+
insert into agent_board.callback_outbox_events(
|
|
395
|
+
seq, endpoint_id, subscription_id, recipient_agent_id, board_id,
|
|
396
|
+
event_category, delivery_mode, source_kind, source_message_id,
|
|
397
|
+
idempotency_key, priority)
|
|
398
|
+
values (
|
|
399
|
+
999999, '18705000-0000-4000-8000-000000000001'::uuid,
|
|
400
|
+
'18706000-0000-4000-8000-000000000001'::uuid,
|
|
401
|
+
'18700000-0000-4000-8000-000000000001'::uuid,
|
|
402
|
+
'18701000-0000-4000-8000-000000000001'::uuid,
|
|
403
|
+
'board.post.delivered', 'board', 'message',
|
|
404
|
+
'18703000-0000-4000-8000-000000000001'::uuid,
|
|
405
|
+
'wake187:forged-seq', 'high')
|
|
406
|
+
$$, '428C9',
|
|
407
|
+
null,
|
|
408
|
+
'WO-2 an INSERT that supplies seq is REJECTED by the server -- GENERATED ALWAYS, so write order cannot be forged');
|
|
409
|
+
|
|
410
|
+
-- The second writer, positively: it is subject to the same identity and its row lands
|
|
411
|
+
-- ABOVE the fan-out. An enumeration (WO-3) that named a writer doing neither would be
|
|
412
|
+
-- an exemption asserted by absence, which is how a hole hid here before.
|
|
413
|
+
insert into agent_board.delivery_recipients(id, delivery_mode, recipient_agent_id, source_kind, source_message_id)
|
|
414
|
+
values ('18707000-0000-4000-8000-000000000001', 'direct',
|
|
415
|
+
'18700000-0000-4000-8000-000000000001', 'message',
|
|
416
|
+
'18703000-0000-4000-8000-000000000001');
|
|
417
|
+
|
|
418
|
+
insert into agent_board.inbox_items(
|
|
419
|
+
id, delivery_recipient_id, recipient_agent_id, delivery_mode, source_kind,
|
|
420
|
+
source_message_id, subject, preview, priority, action_state,
|
|
421
|
+
visible_in_inbox, visible_in_board_feed, fetch_descriptor)
|
|
422
|
+
values ('18708000-0000-4000-8000-000000000001', '18707000-0000-4000-8000-000000000001',
|
|
423
|
+
'18700000-0000-4000-8000-000000000001', 'direct', 'message',
|
|
424
|
+
'18703000-0000-4000-8000-000000000001', 'Wake187 inbox', 'Wake187 inbox preview',
|
|
425
|
+
'high', 'new', true, false, '{}'::jsonb);
|
|
426
|
+
|
|
427
|
+
insert into agent_board.callback_subscriptions(
|
|
428
|
+
id, endpoint_id, agent_id, subscription_scope, event_category, subscription_status)
|
|
429
|
+
values ('18709000-0000-4000-8000-000000000001', '18705000-0000-4000-8000-000000000001',
|
|
430
|
+
'18700000-0000-4000-8000-000000000001', 'own_inbox', 'inbox.item.delivered', 'active');
|
|
431
|
+
|
|
432
|
+
create temp table wake187_inbox(inserted_count integer);
|
|
433
|
+
select lives_ok($$
|
|
434
|
+
insert into wake187_inbox
|
|
435
|
+
select agent_board._queue_inbox_callbacks(
|
|
436
|
+
'18708000-0000-4000-8000-000000000001'::uuid, 'inbox.item.delivered') as inserted_count
|
|
437
|
+
$$, 'WO-6 the inbox fan-out writer runs [call succeeds]');
|
|
438
|
+
|
|
439
|
+
select is(
|
|
440
|
+
(select inserted_count from wake187_inbox),
|
|
441
|
+
1,
|
|
442
|
+
'WO-6 _queue_inbox_callbacks wrote its outbox row');
|
|
443
|
+
|
|
444
|
+
select ok(
|
|
445
|
+
(select coe.seq > (select max(seq) from wake187_rows)
|
|
446
|
+
from agent_board.callback_outbox_events coe
|
|
447
|
+
where coe.idempotency_key = 'inbox.item.delivered:18708000-0000-4000-8000-000000000001:18709000-0000-4000-8000-000000000001'),
|
|
448
|
+
'WO-6 the second writer shares the SAME sequence -- its row sorts strictly after the earlier fan-out, so the order is schema-wide, not per-writer');
|
|
449
|
+
|
|
450
|
+
select * from finish();
|
|
451
|
+
|
|
452
|
+
rollback;
|