@consiliency/agent-board-schema 1.2.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.
Files changed (92) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +11 -0
  3. package/README.md +55 -0
  4. package/bin/agent-board-schema.js +97 -0
  5. package/fixtures/already-applied.json +181 -0
  6. package/fixtures/divergent-history.json +173 -0
  7. package/fixtures/production-history.json +173 -0
  8. package/migrations/20260501000001_agent_board_schema_version.sql +12 -0
  9. package/migrations/20260502000000_agent_board_schema.sql +180 -0
  10. package/migrations/20260503000000_agent_board_rpcs.sql +422 -0
  11. package/migrations/20260503000001_agent_board_coordination_rpcs.sql +229 -0
  12. package/migrations/20260504000000_agent_board_read_models.sql +206 -0
  13. package/migrations/20260505000000_agent_board_inbox_schema.sql +341 -0
  14. package/migrations/20260506000000_agent_board_inbox_delivery_rpcs.sql +1200 -0
  15. package/migrations/20260506000001_agent_board_inbox_callback_rpcs.sql +330 -0
  16. package/migrations/20260507000000_agent_board_inbox_reads.sql +334 -0
  17. package/migrations/20260508000000_agent_board_callback_runtime.sql +453 -0
  18. package/migrations/20260508000001_agent_board_inbox_service_role_reads.sql +10 -0
  19. package/migrations/20260510000000_agent_board_dblint_repairs.sql +413 -0
  20. package/migrations/20260511000000_agent_board_auth_schema.sql +146 -0
  21. package/migrations/20260511000001_agent_board_auth_credentials.sql +39 -0
  22. package/migrations/20260512000000_agent_board_auth_provisioning_rpcs.sql +622 -0
  23. package/migrations/20260512000001_agent_board_auth_v1_rpcs.sql +798 -0
  24. package/migrations/20260512000002_agent_board_auth_v2_rpcs.sql +1243 -0
  25. package/migrations/20260512000003_agent_board_auth_rls.sql +484 -0
  26. package/migrations/20260513000000_agent_board_backup_ops.sql +34 -0
  27. package/migrations/20260513000001_agent_board_retention.sql +160 -0
  28. package/migrations/20260513000002_agent_board_monitoring_views.sql +227 -0
  29. package/migrations/20260514000000_agent_board_auth_rls_id_qualification.sql +150 -0
  30. package/migrations/20260515000000_agent_board_boardopt.sql +493 -0
  31. package/migrations/20260516000000_agent_board_callbackcap.sql +113 -0
  32. package/migrations/20260517000000_agent_board_addressing.sql +542 -0
  33. package/migrations/20260628000000_agent_board_signing_store.sql +174 -0
  34. package/migrations/20260628000001_agent_board_signing_proof_rpcs.sql +215 -0
  35. package/migrations/20260628000002_agent_board_signature_rls_reads.sql +300 -0
  36. package/migrations/20260628000003_agent_board_signed_write_gateway.sql +288 -0
  37. package/migrations/20260628000004_agent_board_fleet_registry.sql +411 -0
  38. package/migrations/20260628000005_agent_board_fleet_policy.sql +509 -0
  39. package/migrations/20260628000006_agent_board_fleet_read_models.sql +170 -0
  40. package/migrations/20260629000100_agent_board_fleet_registry_repair.sql +411 -0
  41. package/migrations/20260629000101_agent_board_fleet_policy_refresh.sql +509 -0
  42. package/migrations/20260629000102_agent_board_fleet_read_models_refresh.sql +170 -0
  43. package/migrations/20260630000100_agent_board_harness_kind_open_vocab.sql +15 -0
  44. package/migrations/20260710000000_agent_board_authbound_boundary.sql +155 -0
  45. package/migrations/20260710000100_agent_board_schema_version_rpc.sql +14 -0
  46. package/migrations/20260710000400_agent_board_nonfleet_identity.sql +87 -0
  47. package/migrations/20260710000401_agent_board_workload_providers.sql +39 -0
  48. package/migrations/20260711000000_agent_board_provision_system_fail_closed.sql +242 -0
  49. package/migrations/20260711000100_canary_signed_fleet_enforcement.sql +234 -0
  50. package/migrations/20260712000000_agent_board_bundle_endpoint_compat.sql +97 -0
  51. package/migrations/20260713000000_agent_board_rls_reassert_board_scoping.sql +90 -0
  52. package/package.json +42 -0
  53. package/provenance.json +580 -0
  54. package/schema-manifest.json +458 -0
  55. package/src/check.js +23 -0
  56. package/src/index.js +50 -0
  57. package/src/materialize.js +73 -0
  58. package/src/plan.js +44 -0
  59. package/src/tooling.test.mjs +97 -0
  60. package/tests/agent_board_addressing.test.sql +166 -0
  61. package/tests/agent_board_auth_rls.test.sql +243 -0
  62. package/tests/agent_board_auth_rls_id_qualification.test.sql +225 -0
  63. package/tests/agent_board_auth_rpcs.test.sql +311 -0
  64. package/tests/agent_board_auth_schema.test.sql +411 -0
  65. package/tests/agent_board_authbound_boundary.test.sql +148 -0
  66. package/tests/agent_board_boardopt.test.sql +170 -0
  67. package/tests/agent_board_bundle_endpoint_compat.test.sql +49 -0
  68. package/tests/agent_board_callback_runtime.test.sql +425 -0
  69. package/tests/agent_board_callbackcap.test.sql +222 -0
  70. package/tests/agent_board_canary_signed_fleet_enforcement.test.sql +60 -0
  71. package/tests/agent_board_dblint_rpcs.test.sql +247 -0
  72. package/tests/agent_board_fleet_policy.test.sql +370 -0
  73. package/tests/agent_board_fleet_read_models.test.sql +158 -0
  74. package/tests/agent_board_fleet_registry.test.sql +149 -0
  75. package/tests/agent_board_inbox_reads.test.sql +471 -0
  76. package/tests/agent_board_inbox_rpcs.test.sql +452 -0
  77. package/tests/agent_board_inbox_schema.test.sql +355 -0
  78. package/tests/agent_board_nonfleet_identity.test.sql +26 -0
  79. package/tests/agent_board_rls_reassert_board_scoping.test.sql +39 -0
  80. package/tests/agent_board_schema_version.test.sql +24 -0
  81. package/tests/agent_board_schema_version_rpc.test.sql +40 -0
  82. package/tests/agent_board_signature_rls_reads.test.sql +133 -0
  83. package/tests/agent_board_signed_write_gateway.test.sql +219 -0
  84. package/tests/agent_board_signing_proof_rpcs.test.sql +143 -0
  85. package/tests/agent_board_signing_store.test.sql +183 -0
  86. package/tests/agent_board_smoke.test.sql +12 -0
  87. package/tests/agent_board_v1_invariants.test.sql +298 -0
  88. package/tests/agent_board_v2_invariants.test.sql +753 -0
  89. package/tests/agent_board_workload_providers.test.sql +15 -0
  90. package/tests/fixtures/adopter-base.sql +19 -0
  91. package/tests/fixtures/operator-hardening.sql +27 -0
  92. package/tests/fixtures/test-prereqs.sql +84 -0
@@ -0,0 +1,222 @@
1
+ create extension if not exists pgtap with schema extensions;
2
+ set search_path = extensions, public, agent_board;
3
+
4
+ begin;
5
+
6
+ select plan(8);
7
+
8
+ create temp table _callbackcap_fixture (
9
+ agent_id uuid,
10
+ thread_id uuid,
11
+ board_id uuid,
12
+ endpoint_id uuid
13
+ ) on commit drop;
14
+
15
+ grant all on table _callbackcap_fixture to authenticated, service_role;
16
+
17
+ do $$
18
+ declare
19
+ v_agent uuid := gen_random_uuid();
20
+ v_thread uuid := gen_random_uuid();
21
+ v_board uuid;
22
+ v_endpoint record;
23
+ begin
24
+ insert into agent_board.agents(id, agent_type, handle, display_name)
25
+ values (v_agent, 'agent', 'callbackcap-agent-' || substr(v_agent::text, 1, 8), 'CALLBACKCAP Agent');
26
+
27
+ insert into agent_board.threads(id, thread_type, created_by_agent_id, subject)
28
+ values (v_thread, 'coordination', v_agent, 'CALLBACKCAP thread');
29
+
30
+ select board_id into v_board
31
+ from agent_board.create_board(
32
+ v_agent,
33
+ 'callbackcap-board-' || substr(v_agent::text, 1, 8),
34
+ 'CALLBACKCAP Board',
35
+ 'callback capability regression',
36
+ '{}'::jsonb,
37
+ 'open'
38
+ );
39
+ perform agent_board.join_board(v_board, v_agent, 'subscribed', v_agent, '{}'::jsonb);
40
+
41
+ select * into v_endpoint
42
+ from agent_board.register_callback_endpoint(
43
+ v_agent,
44
+ 'https://callbackcap.example.test/hook',
45
+ '{"purpose":"callbackcap"}'::jsonb
46
+ );
47
+
48
+ insert into _callbackcap_fixture(agent_id, thread_id, board_id, endpoint_id)
49
+ values (v_agent, v_thread, v_board, v_endpoint.endpoint_id);
50
+ end
51
+ $$;
52
+
53
+ do $$
54
+ declare
55
+ v_fixture record;
56
+ begin
57
+ select * into v_fixture from _callbackcap_fixture;
58
+ begin
59
+ perform *
60
+ from agent_board.verify_callback_endpoint(v_fixture.endpoint_id, v_fixture.agent_id, '{}'::jsonb);
61
+ raise exception 'verify_callback_endpoint unexpectedly accepted missing controlProofRef';
62
+ exception
63
+ when sqlstate '22023' then null;
64
+ end;
65
+ end
66
+ $$;
67
+ select pass('verify_callback_endpoint requires bounded controlProofRef metadata');
68
+
69
+ do $$
70
+ declare
71
+ v_fixture record;
72
+ v_verified record;
73
+ v_metadata jsonb;
74
+ begin
75
+ select * into v_fixture from _callbackcap_fixture;
76
+ select * into v_verified
77
+ from agent_board.verify_callback_endpoint(
78
+ v_fixture.endpoint_id,
79
+ v_fixture.agent_id,
80
+ '{"controlProofRef":"callbackcap/operator-note/1"}'::jsonb
81
+ );
82
+
83
+ if v_verified.endpoint_status <> 'verified' then
84
+ raise exception 'endpoint did not verify';
85
+ end if;
86
+
87
+ select metadata into v_metadata
88
+ from agent_board.callback_endpoints
89
+ where id = v_fixture.endpoint_id;
90
+
91
+ if v_metadata ->> 'verificationMethod' <> 'administrative_enablement'
92
+ or v_metadata ->> 'controlProofRef' <> 'callbackcap/operator-note/1'
93
+ or v_metadata ? 'signingSecret' then
94
+ raise exception 'verification metadata was not bounded and proof-referenced: %', v_metadata;
95
+ end if;
96
+ end
97
+ $$;
98
+ select pass('verify_callback_endpoint records administrative enablement plus control proof metadata');
99
+
100
+ do $$
101
+ declare
102
+ v_fixture record;
103
+ begin
104
+ select * into v_fixture from _callbackcap_fixture;
105
+ perform *
106
+ from agent_board.subscribe_callback_endpoint(
107
+ v_fixture.endpoint_id,
108
+ v_fixture.agent_id,
109
+ 'board',
110
+ 'board.post.delivered',
111
+ v_fixture.board_id,
112
+ '{}'::jsonb
113
+ );
114
+ perform *
115
+ from agent_board.subscribe_callback_endpoint(
116
+ v_fixture.endpoint_id,
117
+ v_fixture.agent_id,
118
+ 'board',
119
+ 'board.post.updated',
120
+ v_fixture.board_id,
121
+ '{}'::jsonb
122
+ );
123
+ end
124
+ $$;
125
+ select pass('board callback subscriptions accept delivered and updated categories with boardId');
126
+
127
+ do $$
128
+ declare
129
+ v_fixture record;
130
+ begin
131
+ select * into v_fixture from _callbackcap_fixture;
132
+ begin
133
+ perform *
134
+ from agent_board.subscribe_callback_endpoint(
135
+ v_fixture.endpoint_id,
136
+ v_fixture.agent_id,
137
+ 'board',
138
+ 'board.post.delivered',
139
+ null,
140
+ '{}'::jsonb
141
+ );
142
+ raise exception 'board callback subscription unexpectedly accepted missing boardId';
143
+ exception
144
+ when sqlstate '22023' then null;
145
+ end;
146
+ end
147
+ $$;
148
+ select pass('board callback subscriptions require boardId');
149
+
150
+ do $$
151
+ declare
152
+ v_fixture record;
153
+ begin
154
+ select * into v_fixture from _callbackcap_fixture;
155
+ begin
156
+ perform *
157
+ from agent_board.subscribe_callback_endpoint(
158
+ v_fixture.endpoint_id,
159
+ v_fixture.agent_id,
160
+ 'own_inbox',
161
+ 'board.post.delivered',
162
+ null,
163
+ '{}'::jsonb
164
+ );
165
+ raise exception 'own_inbox callback subscription unexpectedly accepted board category';
166
+ exception
167
+ when sqlstate '22023' then null;
168
+ end;
169
+ end
170
+ $$;
171
+ select pass('own_inbox callback subscriptions reject board categories');
172
+
173
+ do $$
174
+ declare
175
+ v_fixture record;
176
+ v_post record;
177
+ begin
178
+ select * into v_fixture from _callbackcap_fixture;
179
+ select * into v_post
180
+ from agent_board.post_board_message(
181
+ v_fixture.thread_id,
182
+ v_fixture.agent_id,
183
+ v_fixture.board_id,
184
+ 'CALLBACKCAP board post',
185
+ 'board callback outbox proof',
186
+ 'comment',
187
+ 'medium',
188
+ 'callbackcap-board-post',
189
+ '{}'::jsonb
190
+ );
191
+
192
+ if coalesce(v_post.callback_outbox_count, 0) < 1 then
193
+ raise exception 'board post did not enqueue board callback outbox work';
194
+ end if;
195
+ end
196
+ $$;
197
+ select pass('board post delivery enqueues callback outbox work for board subscriptions');
198
+
199
+ select ok(
200
+ exists (
201
+ select 1
202
+ from agent_board.callback_outbox_events
203
+ where event_category = 'board.post.delivered'
204
+ and idempotency_key like 'board.post.delivered:%'
205
+ and board_id = (select board_id from _callbackcap_fixture)
206
+ ),
207
+ 'board callback outbox rows carry board category and idempotency key'
208
+ );
209
+
210
+ select ok(
211
+ not exists (
212
+ select 1
213
+ from agent_board.callback_endpoints
214
+ where id = (select endpoint_id from _callbackcap_fixture)
215
+ and metadata ? 'signingSecret'
216
+ ),
217
+ 'callback endpoint metadata does not expose signing secrets'
218
+ );
219
+
220
+ select finish();
221
+
222
+ rollback;
@@ -0,0 +1,60 @@
1
+ create extension if not exists pgtap with schema extensions;
2
+ set search_path = extensions, public, agent_board;
3
+
4
+ begin;
5
+
6
+ select plan(8);
7
+
8
+ select has_function('agent_board', 'evaluate_signed_write_fleet_policy', array['text', 'jsonb', 'jsonb'], 'signed fleet policy evaluator exists');
9
+ select has_function('agent_board', 'signed_send_multicast_message', array['jsonb', 'jsonb'], 'signed multicast wrapper exists');
10
+ select has_function('agent_board', 'signed_create_tasking_delivery', array['jsonb', 'jsonb'], 'signed tasking wrapper exists');
11
+
12
+ select function_privs_are(
13
+ 'agent_board',
14
+ 'send_direct_message',
15
+ array['uuid','uuid','uuid','text','text','text','text','text','jsonb'],
16
+ 'authenticated',
17
+ array[]::text[],
18
+ 'authenticated cannot bypass direct signed gateway'
19
+ );
20
+
21
+ select function_privs_are(
22
+ 'agent_board',
23
+ 'send_multicast_message',
24
+ array['uuid','uuid','uuid[]','text','text','text','text','text','jsonb'],
25
+ 'authenticated',
26
+ array[]::text[],
27
+ 'authenticated cannot bypass multicast signed gateway'
28
+ );
29
+
30
+ select function_privs_are(
31
+ 'agent_board',
32
+ 'post_board_message',
33
+ array['uuid','uuid','uuid','text','text','text','text','text','jsonb'],
34
+ 'authenticated',
35
+ array[]::text[],
36
+ 'authenticated cannot bypass board signed gateway'
37
+ );
38
+
39
+ select function_privs_are(
40
+ 'agent_board',
41
+ 'create_tasking_delivery',
42
+ array['uuid','uuid','text','text','uuid[]','uuid','text','text','text','jsonb'],
43
+ 'authenticated',
44
+ array[]::text[],
45
+ 'authenticated cannot bypass tasking signed gateway'
46
+ );
47
+
48
+ select ok(
49
+ not exists (
50
+ select 1 from information_schema.routine_privileges
51
+ where routine_schema = 'agent_board'
52
+ and routine_name in ('evaluate_signed_write_fleet_policy', 'signed_send_multicast_message', 'signed_create_tasking_delivery')
53
+ and grantee in ('anon', 'authenticated')
54
+ ),
55
+ 'new enforcement functions remain service-role only'
56
+ );
57
+
58
+ select finish();
59
+
60
+ rollback;
@@ -0,0 +1,247 @@
1
+ create extension if not exists pgtap with schema extensions;
2
+ set search_path = extensions, public, agent_board;
3
+
4
+ begin;
5
+
6
+ select plan(10);
7
+
8
+ insert into agent_board.agents(id, agent_type, handle, display_name)
9
+ values
10
+ ('00000000-0000-0000-0000-000000000101', 'agent', 'test_dblint_creator', 'DBLINT Creator'),
11
+ ('00000000-0000-0000-0000-000000000102', 'agent', 'test_dblint_resolver', 'DBLINT Resolver'),
12
+ ('00000000-0000-0000-0000-000000000103', 'agent', 'test_dblint_recipient_one', 'DBLINT Recipient One'),
13
+ ('00000000-0000-0000-0000-000000000104', 'agent', 'test_dblint_recipient_two', 'DBLINT Recipient Two');
14
+
15
+ insert into agent_board.threads(id, thread_type, created_by_agent_id, subject)
16
+ values (
17
+ '00000000-0000-0000-0000-000000000201',
18
+ 'coordination',
19
+ '00000000-0000-0000-0000-000000000101',
20
+ 'DBLINT RPC coverage'
21
+ );
22
+
23
+ create temporary table dblint_primary_task on commit drop as
24
+ select *
25
+ from agent_board.create_task(
26
+ '00000000-0000-0000-0000-000000000201',
27
+ '00000000-0000-0000-0000-000000000101',
28
+ 'Primary task',
29
+ 'seed task for dblint coverage',
30
+ null,
31
+ null,
32
+ 'medium',
33
+ 'distributed',
34
+ '{}'::jsonb
35
+ );
36
+
37
+ create temporary table dblint_appended on commit drop as
38
+ select *
39
+ from agent_board.append_task_event(
40
+ (select task_id from dblint_primary_task),
41
+ '00000000-0000-0000-0000-000000000101',
42
+ 'commented',
43
+ jsonb_build_object('note', 'second event')
44
+ );
45
+
46
+ select is(
47
+ (select event_sequence from dblint_appended),
48
+ 2::bigint,
49
+ '_next_task_event_sequence advances through append_task_event'
50
+ );
51
+
52
+ create temporary table dblint_tasking on commit drop as
53
+ select *
54
+ from agent_board.create_tasking_delivery(
55
+ '00000000-0000-0000-0000-000000000201',
56
+ '00000000-0000-0000-0000-000000000101',
57
+ 'Tasking delivery title',
58
+ 'Tasking delivery details',
59
+ array[
60
+ '00000000-0000-0000-0000-000000000103'::uuid,
61
+ '00000000-0000-0000-0000-000000000103'::uuid,
62
+ '00000000-0000-0000-0000-000000000104'::uuid
63
+ ],
64
+ null,
65
+ 'high',
66
+ 'distributed',
67
+ null,
68
+ '{}'::jsonb
69
+ );
70
+
71
+ select ok(
72
+ (
73
+ select td.created_event_id = te.id
74
+ from dblint_tasking td
75
+ join agent_board.task_events te
76
+ on te.task_id = td.task_id
77
+ and te.event_sequence = 1
78
+ ),
79
+ 'create_tasking_delivery reuses the first task event lookup'
80
+ );
81
+
82
+ select is(
83
+ (select delivered_recipient_count from dblint_tasking),
84
+ 2,
85
+ 'create_tasking_delivery suppresses duplicate explicit recipients'
86
+ );
87
+
88
+ create temporary table dblint_conflict on commit drop as
89
+ select *
90
+ from agent_board.raise_conflict(
91
+ '00000000-0000-0000-0000-000000000201',
92
+ '00000000-0000-0000-0000-000000000101',
93
+ 'Need a conflict record',
94
+ 'plan',
95
+ 'high',
96
+ (select task_id from dblint_primary_task),
97
+ jsonb_build_object('seed', 'conflict')
98
+ );
99
+
100
+ create temporary table dblint_resolved_conflict on commit drop as
101
+ select *
102
+ from agent_board.resolve_conflict(
103
+ (select conflict_id from dblint_conflict),
104
+ '00000000-0000-0000-0000-000000000102',
105
+ jsonb_build_object('resolution', 'done')
106
+ );
107
+
108
+ select is(
109
+ (select status from dblint_resolved_conflict),
110
+ 'resolved',
111
+ 'resolve_conflict marks the conflict resolved'
112
+ );
113
+
114
+ select ok(
115
+ (
116
+ select c.details @> jsonb_build_object('seed', 'conflict', 'resolution', 'done')
117
+ from agent_board.conflicts c
118
+ where c.id = (select conflict_id from dblint_conflict)
119
+ ),
120
+ 'resolve_conflict merges detail payloads without ambiguity regressions'
121
+ );
122
+
123
+ create temporary table dblint_escalation on commit drop as
124
+ select *
125
+ from agent_board.raise_escalation(
126
+ '00000000-0000-0000-0000-000000000201',
127
+ '00000000-0000-0000-0000-000000000101',
128
+ 'Need an escalation record',
129
+ 'approval',
130
+ 'high',
131
+ (select task_id from dblint_primary_task),
132
+ '00000000-0000-0000-0000-000000000102',
133
+ jsonb_build_object('seed', 'escalation')
134
+ );
135
+
136
+ create temporary table dblint_resolved_escalation on commit drop as
137
+ select *
138
+ from agent_board.resolve_escalation(
139
+ (select escalation_id from dblint_escalation),
140
+ '00000000-0000-0000-0000-000000000102',
141
+ jsonb_build_object('resolution', 'approved')
142
+ );
143
+
144
+ select is(
145
+ (select status from dblint_resolved_escalation),
146
+ 'resolved',
147
+ 'resolve_escalation marks the escalation resolved'
148
+ );
149
+
150
+ select ok(
151
+ (
152
+ select e.details @> jsonb_build_object('seed', 'escalation', 'resolution', 'approved')
153
+ from agent_board.escalations e
154
+ where e.id = (select escalation_id from dblint_escalation)
155
+ ),
156
+ 'resolve_escalation merges detail payloads without ambiguity regressions'
157
+ );
158
+
159
+ create temporary table dblint_heartbeat_first on commit drop as
160
+ select *
161
+ from agent_board.heartbeat(
162
+ '00000000-0000-0000-0000-000000000101',
163
+ '00000000-0000-0000-0000-000000000201',
164
+ 'ready',
165
+ 'initial heartbeat',
166
+ interval '5 minutes',
167
+ '{}'::jsonb
168
+ );
169
+
170
+ create temporary table dblint_heartbeat_second on commit drop as
171
+ select *
172
+ from agent_board.heartbeat(
173
+ '00000000-0000-0000-0000-000000000101',
174
+ '00000000-0000-0000-0000-000000000201',
175
+ 'busy',
176
+ 'updated heartbeat',
177
+ interval '10 minutes',
178
+ jsonb_build_object('pass', 2)
179
+ );
180
+
181
+ select ok(
182
+ (
183
+ select count(*) = 1
184
+ from agent_board.presence p
185
+ where p.agent_id = '00000000-0000-0000-0000-000000000101'
186
+ and p.availability = 'busy'
187
+ ),
188
+ 'heartbeat upserts a single presence row through the repaired conflict target'
189
+ );
190
+
191
+ create temporary table dblint_message_one on commit drop as
192
+ select *
193
+ from agent_board.post_message(
194
+ '00000000-0000-0000-0000-000000000201',
195
+ '00000000-0000-0000-0000-000000000101',
196
+ 'mark-read seed message',
197
+ 'comment',
198
+ null,
199
+ '{}'::jsonb
200
+ );
201
+
202
+ create temporary table dblint_message_two on commit drop as
203
+ select *
204
+ from agent_board.post_message(
205
+ '00000000-0000-0000-0000-000000000201',
206
+ '00000000-0000-0000-0000-000000000101',
207
+ 'mark-read second message',
208
+ 'comment',
209
+ null,
210
+ '{}'::jsonb
211
+ );
212
+
213
+ create temporary table dblint_mark_read_first on commit drop as
214
+ select *
215
+ from agent_board.mark_read(
216
+ '00000000-0000-0000-0000-000000000201',
217
+ '00000000-0000-0000-0000-000000000101',
218
+ 1
219
+ );
220
+
221
+ create temporary table dblint_mark_read_second on commit drop as
222
+ select *
223
+ from agent_board.mark_read(
224
+ '00000000-0000-0000-0000-000000000201',
225
+ '00000000-0000-0000-0000-000000000101',
226
+ 99
227
+ );
228
+
229
+ select ok(
230
+ (
231
+ select count(*) = 1
232
+ from agent_board.read_receipts rr
233
+ where rr.thread_id = '00000000-0000-0000-0000-000000000201'
234
+ and rr.agent_id = '00000000-0000-0000-0000-000000000101'
235
+ ),
236
+ 'mark_read upserts a single read receipt row through the repaired conflict target'
237
+ );
238
+
239
+ select is(
240
+ (select last_read_message_sequence from dblint_mark_read_second),
241
+ 2::bigint,
242
+ 'mark_read caps the stored sequence at the thread maximum'
243
+ );
244
+
245
+ select finish();
246
+
247
+ rollback;