@consiliency/agent-board-schema 1.44.0 → 1.48.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.
@@ -0,0 +1,400 @@
1
+ -- #388 — the ownerless quota census. A read-only instrument, so the failure modes it can have are
2
+ -- (a) counting something acquire does not count, (b) reading healthy because it saw nothing, and
3
+ -- (c) answering per-caller while looking fleet-wide. Every assertion here is aimed at one of those.
4
+ --
5
+ -- OQ-6 IS THE ASSERTION THIS FILE EXISTS FOR. The census reproduces acquire_claim's quota count
6
+ -- predicate by hand, and a census whose arithmetic drifts from the enforcer's is silently about a
7
+ -- different question. So the UNDER agent is given three COUNTED claims and four NEAR-MISS rows,
8
+ -- each differing from a counted row in EXACTLY ONE dimension — a set released_at, a non-active
9
+ -- status, a lapsed lease, and an owned resource kind. Dropping any single conjunct from the view
10
+ -- makes exactly one of those rows count and OQ-6 reads 4 instead of 3. Asserting "the over agent
11
+ -- is over" would have proved nothing about the predicate at all: 51 rows are over the quota under
12
+ -- almost any predicate you could write.
13
+ --
14
+ -- OQ-25/26 ARE THE OTHER HALF, and they are the only assertions here that test the OPERATION
15
+ -- rather than a property of the reader. They put the AT agent at exactly the quota, flip the
16
+ -- guards on, and require that the census's `acquire_blocked` and acquire_claim's actual 54000
17
+ -- agree — then release one claim and require that BOTH flip together. A reader can agree with a
18
+ -- hand-copied predicate and still disagree with the function; only calling the function settles it.
19
+ --
20
+ -- OQ-14 IS THE ANTI-VACUOUS ASSERTION. The live answer is expected to be zero agents over quota,
21
+ -- so "0 offenders" must be provably distinguishable from "examined nothing" — the #367 shape,
22
+ -- where a reader returned zero rows about an entirely unmonitored fleet and read as perfect
23
+ -- health. It requires the summary's denominator to be STRICTLY GREATER than its finding, which is
24
+ -- true here because the fixture guarantees a holder that is not an offender, and which fails
25
+ -- immediately if the denominator is ever derived from the offender set.
26
+ --
27
+ -- SCOPING. Every per-agent assertion is keyed to this file's agent ids, and every summary
28
+ -- assertion is either an identity against the view (immune to pollution) or a recomputation
29
+ -- scoped by handle suffix. The summary counts every agent in the database; an absolute literal
30
+ -- against it would fail on other fixtures' leftovers and look like a real defect.
31
+ create extension if not exists pgtap with schema extensions;
32
+ set search_path = extensions, public, agent_board;
33
+
34
+ begin;
35
+
36
+ select plan(30);
37
+
38
+ -- ---------------------------------------------------------------------------
39
+ -- Fixture.
40
+ -- over — quota + 1 active ownerless claims: over by 1, and blocked
41
+ -- at — exactly quota: over by NOTHING, and still blocked (the asymmetry OQ-4/5 pin)
42
+ -- under — 3 counted claims (one per ownerless kind) + 4 near-miss rows + 1 advisory board
43
+ -- none — an active agent holding NO ownerless claims: a non-holder, absent from the view
44
+ -- susp — a SUSPENDED agent holding quota + 2: must still appear (OQ-12)
45
+ -- ---------------------------------------------------------------------------
46
+ create temp table _oq (
47
+ sfx text,
48
+ over_agent uuid, at_agent uuid, under_agent uuid, none_agent uuid, susp_agent uuid,
49
+ at_user uuid, adv_board uuid, adv_thread uuid
50
+ ) on commit drop;
51
+
52
+ do $$
53
+ declare
54
+ v_sfx text := replace(substr(gen_random_uuid()::text, 1, 8), '-', '');
55
+ v_over uuid := gen_random_uuid();
56
+ v_at uuid := gen_random_uuid();
57
+ v_under uuid := gen_random_uuid();
58
+ v_none uuid := gen_random_uuid();
59
+ v_susp uuid := gen_random_uuid();
60
+ v_at_user uuid;
61
+ v_board uuid := gen_random_uuid();
62
+ v_thread uuid := gen_random_uuid();
63
+ begin
64
+ -- Only the AT agent needs an auth identity: it is the one OQ-25/26 call acquire_claim as.
65
+ v_at_user := public.ensure_github_user(
66
+ 'oq-at-' || v_sfx, 'oq-at-' || v_sfx || '@example.test', 'oq-at-' || v_sfx, 'OQ At Quota');
67
+
68
+ insert into agent_board.agents(id, agent_type, handle, display_name, status, auth_user_id, owner_user_id)
69
+ values (v_over, 'agent', 'oq-over-' || v_sfx, 'OQ Over', 'active', null, null),
70
+ (v_at, 'agent', 'oq-at-' || v_sfx, 'OQ At Quota', 'active', v_at_user, v_at_user),
71
+ (v_under, 'agent', 'oq-under-' || v_sfx, 'OQ Under', 'active', null, null),
72
+ (v_none, 'agent', 'oq-none-' || v_sfx, 'OQ Non Holder','active', null, null),
73
+ -- Not active. A liveness reader excludes a decommissioned agent; a QUOTA reader must not
74
+ -- — a suspended agent still holding 52 live leases is the row an operator most needs.
75
+ (v_susp, 'agent', 'oq-susp-' || v_sfx, 'OQ Suspended', 'suspended', null, null);
76
+
77
+ -- An ADVISORY board, so one of the UNDER agent's counted claims is governed by the opt-out.
78
+ -- Inserted directly rather than through set_board_claim_policy: that RPC is owner-gated and
79
+ -- this file is about the reader, not about who may set the policy (that is BCP's subject).
80
+ insert into agent_board.boards(id, slug, name, created_by_agent_id, claim_policy)
81
+ values (v_board, 'oq-adv-' || v_sfx, 'OQ Advisory Board', v_under, 'advisory');
82
+
83
+ insert into agent_board.threads(id, thread_type, created_by_agent_id, subject, board_id)
84
+ values (v_thread, 'coordination', v_under, 'OQ advisory thread', v_board);
85
+
86
+ -- Mirrored into GUCs because OQ-25/27 run under `set local role authenticated`, and a temp
87
+ -- table belongs to the creating role — an authenticated read of _oq raises 42501 rather than
88
+ -- returning the id, which would abort the transaction and take the rest of the plan with it.
89
+ perform set_config('_oq.at_user', v_at_user::text, true);
90
+ perform set_config('_oq.at_agent', v_at::text, true);
91
+
92
+ insert into _oq values (v_sfx, v_over, v_at, v_under, v_none, v_susp, v_at_user, v_board, v_thread);
93
+ end $$;
94
+
95
+ -- OVER: quota + 1 active file claims.
96
+ insert into agent_board.claims(agent_id, resource_kind, resource_key, lease_expires_at)
97
+ select (select over_agent from _oq), 'file', 'oq-over-' || (select sfx from _oq) || '-' || g, now() + interval '1 hour'
98
+ from generate_series(1, agent_board._claimgate_ownerless_quota() + 1) g;
99
+
100
+ -- AT: exactly quota.
101
+ insert into agent_board.claims(agent_id, resource_kind, resource_key, lease_expires_at)
102
+ select (select at_agent from _oq), 'file', 'oq-at-' || (select sfx from _oq) || '-' || g, now() + interval '1 hour'
103
+ from generate_series(1, agent_board._claimgate_ownerless_quota()) g;
104
+
105
+ -- SUSP: quota + 2, held by an agent that is no longer active.
106
+ insert into agent_board.claims(agent_id, resource_kind, resource_key, lease_expires_at)
107
+ select (select susp_agent from _oq), 'tool', 'oq-susp-' || (select sfx from _oq) || '-' || g, now() + interval '1 hour'
108
+ from generate_series(1, agent_board._claimgate_ownerless_quota() + 2) g;
109
+
110
+ -- UNDER: three COUNTED claims, one per ownerless kind. The 'custom' one sits on the ADVISORY
111
+ -- board — counted all the same, because the opt-out gates whether the guard RUNS for an
112
+ -- acquisition, not whether the row COUNTS afterwards.
113
+ insert into agent_board.claims(agent_id, thread_id, resource_kind, resource_key, lease_expires_at)
114
+ values
115
+ ((select under_agent from _oq), null, 'file', 'oq-under-file-' || (select sfx from _oq), now() + interval '1 hour'),
116
+ ((select under_agent from _oq), null, 'tool', 'oq-under-tool-' || (select sfx from _oq), now() + interval '1 hour'),
117
+ ((select under_agent from _oq), (select adv_thread from _oq), 'custom', 'oq-under-custom-' || (select sfx from _oq), now() + interval '1 hour');
118
+
119
+ -- UNDER: four NEAR-MISS rows. Each differs from a counted row in EXACTLY ONE conjunct of the
120
+ -- predicate, so dropping that conjunct from the view makes this row count and OQ-6 fails naming
121
+ -- itself. A near-miss that differed in two dimensions would survive a mutation and prove nothing.
122
+ insert into agent_board.claims(agent_id, resource_kind, resource_key, status, released_at, lease_expires_at)
123
+ values
124
+ -- released_at is null
125
+ ((select under_agent from _oq), 'file', 'oq-nm-released-' || (select sfx from _oq), 'active', now(), now() + interval '1 hour'),
126
+ -- status = 'active'
127
+ ((select under_agent from _oq), 'file', 'oq-nm-status-' || (select sfx from _oq), 'expired', null, now() + interval '1 hour'),
128
+ -- lease_expires_at > now()
129
+ ((select under_agent from _oq), 'file', 'oq-nm-lease-' || (select sfx from _oq), 'active', null, now() - interval '1 hour'),
130
+ -- resource_kind in ('file','tool','custom') — an OWNED kind, governed by the fleet gate and not
131
+ -- by this quota at all.
132
+ ((select under_agent from _oq), 'machine', 'oq-nm-kind-' || (select sfx from _oq), 'active', null, now() + interval '1 hour');
133
+
134
+ -- NONE holds one owned-kind claim and nothing ownerless: it must be a non-holder, not a zero row.
135
+ insert into agent_board.claims(agent_id, resource_kind, resource_key, lease_expires_at)
136
+ values ((select none_agent from _oq), 'repository', 'oq-none-repo-' || (select sfx from _oq), now() + interval '1 hour');
137
+
138
+ -- The guards start OFF for OQ-20a. Deleted rather than assumed absent: another fixture in the
139
+ -- same database may have set it, and "no row" inferred from a fresh build is exactly the
140
+ -- premise-that-was-already-false shape. Deleting makes the starting regime a fact.
141
+ delete from agent_board.claimgate_enforcement_flags where scope_key = 'ownerless_guards';
142
+
143
+ -- ---------------------------------------------------------------------------
144
+ -- OQ-1..3: the over-quota agent. The headline case.
145
+ -- ---------------------------------------------------------------------------
146
+ select is(
147
+ (select active_ownerless_claims from agent_board.v_ownerless_claim_quota where agent_id = (select over_agent from _oq)),
148
+ agent_board._claimgate_ownerless_quota() + 1,
149
+ 'OQ-1: an agent holding quota+1 active ownerless claims is counted at quota+1');
150
+
151
+ select is(
152
+ (select overage from agent_board.v_ownerless_claim_quota where agent_id = (select over_agent from _oq)),
153
+ 1,
154
+ 'OQ-2: and its overage is 1 — the number of claims that exceed the ceiling, which is the convergence question');
155
+
156
+ select is(
157
+ (select acquire_blocked from agent_board.v_ownerless_claim_quota where agent_id = (select over_agent from _oq)),
158
+ true,
159
+ 'OQ-3: and it is acquire_blocked');
160
+
161
+ -- ---------------------------------------------------------------------------
162
+ -- OQ-4..5: THE ASYMMETRY. Acquire refuses at count >= quota, so an agent holding exactly the
163
+ -- quota is frozen while being over by nothing. Collapsing overage and acquire_blocked into one
164
+ -- boolean answers one of those two questions wrongly; these two assertions on the SAME agent are
165
+ -- what make the distinction load-bearing rather than decorative.
166
+ -- ---------------------------------------------------------------------------
167
+ select is(
168
+ (select overage from agent_board.v_ownerless_claim_quota where agent_id = (select at_agent from _oq)),
169
+ 0,
170
+ 'OQ-4: an agent at EXACTLY the quota has overage 0 — it exceeds the ceiling by nothing');
171
+
172
+ select is(
173
+ (select acquire_blocked from agent_board.v_ownerless_claim_quota where agent_id = (select at_agent from _oq)),
174
+ true,
175
+ 'OQ-5 THE ASYMMETRY: the same agent is nevertheless acquire_blocked, because acquire refuses at count >= quota — an overage of 0 does NOT mean it can take another lock');
176
+
177
+ -- ---------------------------------------------------------------------------
178
+ -- OQ-6..9: THE PREDICATE. Three counted claims beside four near-miss rows.
179
+ -- ---------------------------------------------------------------------------
180
+ select is(
181
+ (select active_ownerless_claims from agent_board.v_ownerless_claim_quota where agent_id = (select under_agent from _oq)),
182
+ 3,
183
+ 'OQ-6 THE PREDICATE: exactly 3 — the released, expired, lease-lapsed and owned-kind rows sitting beside them are each excluded by exactly one conjunct, so dropping any conjunct reads 4 here');
184
+
185
+ select is(
186
+ (select file_claims || '/' || tool_claims || '/' || custom_claims
187
+ from agent_board.v_ownerless_claim_quota where agent_id = (select under_agent from _oq)),
188
+ '1/1/1',
189
+ 'OQ-7: and all three ownerless kinds are counted — dropping one from the kind list reads 3 counted as 2');
190
+
191
+ select is(
192
+ (select claims_on_advisory_boards from agent_board.v_ownerless_claim_quota where agent_id = (select under_agent from _oq)),
193
+ 1,
194
+ 'OQ-8: the advisory-board share is reported SEPARATELY — one of the three sits on a claim_policy=advisory board and is still counted, because the opt-out gates whether the guard RUNS, not whether the row COUNTS');
195
+
196
+ select is(
197
+ (select overage::text || '/' || acquire_blocked::text
198
+ from agent_board.v_ownerless_claim_quota where agent_id = (select under_agent from _oq)),
199
+ '0/false',
200
+ 'OQ-9: a holder well under the quota is neither over nor blocked — it is still a holder, and OQ-14 depends on that');
201
+
202
+ -- ---------------------------------------------------------------------------
203
+ -- OQ-10..12: who appears at all.
204
+ -- ---------------------------------------------------------------------------
205
+ select is(
206
+ (select count(*)::int from agent_board.v_ownerless_claim_quota where agent_id = (select none_agent from _oq)),
207
+ 0,
208
+ 'OQ-10: an agent holding only OWNED-kind claims is not a holder and does not appear — a zero row would inflate the denominator with agents this quota does not govern');
209
+
210
+ select is(
211
+ (select agents_active from agent_board.ownerless_quota_census()),
212
+ (select count(*)::integer from agent_board.agents where status = 'active'),
213
+ 'OQ-11: agents_active is the FLEET, counted from agents — the widest denominator, non-zero even when nobody holds an ownerless claim at all');
214
+
215
+ select is(
216
+ (select overage from agent_board.v_ownerless_claim_quota where agent_id = (select susp_agent from _oq)),
217
+ 2,
218
+ 'OQ-12 NO STATUS FILTER: a SUSPENDED agent holding quota+2 live leases appears with its overage — an agent-status filter here would hide precisely the row an operator most needs');
219
+
220
+ -- ---------------------------------------------------------------------------
221
+ -- OQ-13..16: THE DENOMINATOR. Identities against the view, so they are immune to other fixtures.
222
+ -- ---------------------------------------------------------------------------
223
+ select is(
224
+ (select agents_holding_ownerless_claims from agent_board.ownerless_quota_census()),
225
+ (select count(*)::integer from agent_board.v_ownerless_claim_quota),
226
+ 'OQ-13: the summary''s population is counted from the VIEW, not derived from the offender set');
227
+
228
+ select ok(
229
+ (select agents_holding_ownerless_claims > agents_over_quota from agent_board.ownerless_quota_census()),
230
+ 'OQ-14 ANTI-VACUOUS: the denominator is STRICTLY greater than the finding — this fixture guarantees a holder that is not an offender, so a denominator derived from the offender set (which would make "0 over, of 0 examined" read as health) fails here');
231
+
232
+ select is(
233
+ (select agents_over_quota::text || ' over quota, ' ||
234
+ agents_acquire_blocked::text || ' acquire-blocked, of ' ||
235
+ agents_holding_ownerless_claims::text || ' holders'
236
+ from (select
237
+ count(*) filter (where overage > 0) as agents_over_quota,
238
+ count(*) filter (where acquire_blocked) as agents_acquire_blocked,
239
+ count(*) as agents_holding_ownerless_claims
240
+ from agent_board.v_ownerless_claim_quota v
241
+ join agent_board.agents a on a.id = v.agent_id
242
+ where a.handle like 'oq-%' || (select sfx from _oq)) s),
243
+ '2 over quota, 3 acquire-blocked, of 4 holders',
244
+ 'OQ-15: scoped to this fixture the reader states its finding WITH its population — over(2) < blocked(3) < holders(4), three numbers a single count could not carry');
245
+
246
+ select is(
247
+ (select agents_acquire_blocked from agent_board.ownerless_quota_census()),
248
+ (select count(*) filter (where acquire_blocked)::integer from agent_board.v_ownerless_claim_quota),
249
+ 'OQ-16: the summary''s acquire_blocked count is the view''s acquire_blocked count');
250
+
251
+ select is(
252
+ (select total_overage from agent_board.ownerless_quota_census()),
253
+ (select coalesce(sum(overage), 0)::integer from agent_board.v_ownerless_claim_quota),
254
+ 'OQ-17: total_overage is the sum of the per-agent overages — the size of the convergence problem, not the number of agents in it');
255
+
256
+ select is(
257
+ (select total_active_ownerless_claims from agent_board.ownerless_quota_census()),
258
+ (select coalesce(sum(active_ownerless_claims), 0)::integer from agent_board.v_ownerless_claim_quota),
259
+ 'OQ-18: total_active_ownerless_claims is the sum of the per-agent counts');
260
+
261
+ -- ---------------------------------------------------------------------------
262
+ -- OQ-19..20: THE REGIME. "0 over quota" means a different thing with the guards off, so the
263
+ -- summary states the flag and the quota it measured under.
264
+ -- ---------------------------------------------------------------------------
265
+ select is(
266
+ (select quota from agent_board.ownerless_quota_census()),
267
+ agent_board._claimgate_ownerless_quota(),
268
+ 'OQ-19: the summary reports the quota it measured against, from the single source acquire uses');
269
+
270
+ select is(
271
+ (select guards_enabled from agent_board.ownerless_quota_census()),
272
+ false,
273
+ 'OQ-20a: with the flag row deleted the summary says the guards are OFF — a count reported without its regime is a fact with no expiry (rule 5)');
274
+
275
+ insert into agent_board.claimgate_enforcement_flags(scope_key, enabled) values ('ownerless_guards', true)
276
+ on conflict (scope_key) do update set enabled = true;
277
+
278
+ select is(
279
+ (select guards_enabled from agent_board.ownerless_quota_census()),
280
+ true,
281
+ 'OQ-20b: and it tracks the flag — a hardcoded true or false here would misreport every census run under the other regime');
282
+
283
+ -- ---------------------------------------------------------------------------
284
+ -- OQ-21..24: SERVICE_ROLE ONLY. Under RLS an authenticated caller sees essentially its OWN
285
+ -- claims, so this view read by that caller returns one row — itself — and the census reports
286
+ -- "1 holder, 0 over quota": a confident, plausible, fleet-shaped answer that is wrong in the
287
+ -- reassuring direction, and the agent sitting at 60 claims is invisible to everyone but itself.
288
+ -- ---------------------------------------------------------------------------
289
+ select ok(
290
+ (select 'security_invoker=true' = any(c.reloptions)
291
+ from pg_class c join pg_namespace n on n.oid = c.relnamespace
292
+ where n.nspname = 'agent_board' and c.relname = 'v_ownerless_claim_quota'),
293
+ 'OQ-21: the view is security_invoker — if the grant is ever widened, rows are still RLS-filtered rather than leaked wholesale');
294
+
295
+ select ok(
296
+ (select not p.prosecdef
297
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
298
+ where n.nspname = 'agent_board' and p.proname = 'ownerless_quota_census'),
299
+ 'OQ-22: the summary is not SECURITY DEFINER either — OQ-21 inspects only the view, so a definer function over an invoker view would silently restore the fleet-wide read this design refuses');
300
+
301
+ select is(
302
+ (select coalesce(string_agg(r, ', ' order by r), '')
303
+ from unnest(array['anon','authenticated','service_role']) r
304
+ where has_table_privilege(r, 'agent_board.v_ownerless_claim_quota', 'select')),
305
+ 'service_role',
306
+ 'OQ-23 SECURITY: only service_role may read the view — an authenticated reader sees only its own claims and would report a per-caller answer that reads fleet-wide');
307
+
308
+ select is(
309
+ (select coalesce(string_agg(r, ', ' order by r), '')
310
+ from unnest(array['anon','authenticated','service_role']) r
311
+ where has_function_privilege(r, 'agent_board.ownerless_quota_census()', 'execute')),
312
+ 'service_role',
313
+ 'OQ-24: and the summary is service_role only — a new function is EXECUTE-to-PUBLIC by default, so granting service_role without the REVOKE would leave authenticated holding it through PUBLIC');
314
+
315
+ -- ---------------------------------------------------------------------------
316
+ -- OQ-25..28: THE OPERATION. Everything above tests the reader against a predicate copied by hand
317
+ -- from acquire_claim. These test it against acquire_claim.
318
+ -- ---------------------------------------------------------------------------
319
+ select set_config('request.jwt.claim.role', 'authenticated', true);
320
+ select set_config('request.jwt.claim.sub', current_setting('_oq.at_user'), true);
321
+ set local role authenticated;
322
+
323
+ select throws_ok($$
324
+ select agent_board.acquire_claim(
325
+ null, 'file', 'oq-at-probe-blocked', now() + interval '1 hour', null, null, '{}'::jsonb)
326
+ $$, '54000', null,
327
+ 'OQ-25 THE OPERATION: the agent OQ-5 called acquire_blocked really is refused by acquire_claim with 54000 — the census agreeing with a hand-copied predicate is not the same as agreeing with the function');
328
+
329
+ reset role;
330
+ -- Release exactly one, so the agent drops to quota-1 and BOTH answers must flip together.
331
+ update agent_board.claims
332
+ set released_at = now(), status = 'released'
333
+ where id = (select id from agent_board.claims
334
+ where agent_id = (select at_agent from _oq)
335
+ and resource_kind = 'file'
336
+ and status = 'active'
337
+ order by resource_key
338
+ limit 1);
339
+
340
+ select is(
341
+ (select active_ownerless_claims::text || '/' || acquire_blocked::text
342
+ from agent_board.v_ownerless_claim_quota where agent_id = (select at_agent from _oq)),
343
+ (agent_board._claimgate_ownerless_quota() - 1)::text || '/false',
344
+ 'OQ-26: releasing one drops the census to quota-1 and clears acquire_blocked');
345
+
346
+ select set_config('request.jwt.claim.role', 'authenticated', true);
347
+ select set_config('request.jwt.claim.sub', current_setting('_oq.at_user'), true);
348
+ set local role authenticated;
349
+
350
+ -- lives_ok + a SEPARATE state read (#113): a bare select from a raising RPC aborts the
351
+ -- transaction instead of failing one assertion, and pgTAP then emits no `not ok` at all.
352
+ select lives_ok($$
353
+ select agent_board.acquire_claim(
354
+ null, 'file', 'oq-at-probe-allowed', now() + interval '1 hour', null, null, '{}'::jsonb)
355
+ $$, 'OQ-27: and acquire_claim now ALLOWS the acquire the census said was no longer blocked — both answers flipped together');
356
+
357
+ -- Back to the owning role BEFORE the state read: OQ-28 reads _oq, and it must not be the
358
+ -- assertion that discovers the temp-table permission problem.
359
+ reset role;
360
+
361
+ select is(
362
+ (select active_ownerless_claims::text || '/' || acquire_blocked::text
363
+ from agent_board.v_ownerless_claim_quota where agent_id = (select at_agent from _oq)),
364
+ agent_board._claimgate_ownerless_quota()::text || '/true',
365
+ 'OQ-28: the state read for OQ-27 — the granted claim is back in the census, at the quota and blocked again');
366
+
367
+ -- ---------------------------------------------------------------------------
368
+ -- OQ-29: WHEN THE POLICY IS RESOLVED. `claims_on_advisory_boards` calls
369
+ -- _claimgate_board_claim_policy on the claim's thread at QUERY time, so it reports the board's
370
+ -- CURRENT policy — not the policy in force when the claim was acquired, which nothing records.
371
+ -- Reverting a board to 'inherit' therefore empties that column for claims already taken under it,
372
+ -- while the claims themselves keep counting toward the quota.
373
+ --
374
+ -- That is a real operational hole, not a nicety: specs/operations.md listed "acquired on a board
375
+ -- that was advisory and later set back to inherit" as one of three ways an agent ends up over
376
+ -- quota, and pointed the operator at this column to size it — the one population it structurally
377
+ -- cannot see. The column is right; the sentence was wrong, and this assertion is what stops it
378
+ -- growing back. The view CANNOT be fixed to report acquire-time policy, so only the prose can be
379
+ -- made true.
380
+ --
381
+ -- Deliberately the SAME claim OQ-8 read as 1. Nothing about the row changes here; only the board
382
+ -- does. A fresh fixture would prove the number, this proves the RESOLUTION TIME.
383
+ --
384
+ -- Flipped with a direct UPDATE, for the reason the fixture gives at the top of this file:
385
+ -- set_board_claim_policy is owner-gated, and giving UNDER an auth identity plus an owner
386
+ -- membership to exercise it would be testing who may set the policy (BCP's subject) rather than
387
+ -- what the reader does with it. updated_at is set as the RPC sets it, so the row is not a shape
388
+ -- the RPC could never produce.
389
+ update agent_board.boards
390
+ set claim_policy = 'inherit', updated_at = now()
391
+ where id = (select adv_board from _oq);
392
+
393
+ select is(
394
+ (select active_ownerless_claims::text || '/' || claims_on_advisory_boards::text
395
+ from agent_board.v_ownerless_claim_quota where agent_id = (select under_agent from _oq)),
396
+ '3/0',
397
+ 'OQ-29 QUERY-TIME RESOLUTION: the claim OQ-8 counted as advisory drops OUT of claims_on_advisory_boards once its board reverts to inherit — the column reports the board''s policy NOW, so claims acquired under an advisory board that was later reverted are invisible to it. Both halves are load-bearing: the claim still counts in active_ownerless_claims, so the column emptying is a re-resolution and not the claim disappearing');
398
+
399
+ select * from finish();
400
+ rollback;