@consiliency/agent-board-schema 1.58.0 → 1.60.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.
@@ -38,9 +38,9 @@ describe('manifest integrity', () => {
38
38
  expect(manifest.schemaVersion).toBe(8);
39
39
  expect(manifest.minClientVersion).toMatch(/^\d+\.\d+\.\d+$/);
40
40
  });
41
- it('registers the four MBAUTHZ + four MBSENDAUTHZ + four MBSESSION + four MBDELEG + three MBFED + three MBHARDEN + two MBPORTALGATE + two MBPORTAL-PR32 + two ROOMOWN + two ROOMLINK + one ROOMDELEG + one LOCALAUTH + one ROOMUIAPI + one FEDSESSION + one FEDSESSION-AUTOJOIN + one GATENULL + one MEMBSTATE + one AGENTSDIR + one LEAVEIDEM + one ROLERESET + one MBROWID + one MBHYGIENE + one MBCONSENT-A + one MBCONSENT-B(a) + one MBCONSENT-B(i) + one MBCONSENT-B(ii) + one MBCONSENT-B(iii) + one MBHARDEN-#97 + one MBHARDEN-#118 + one INVITEPOLICY + one OBSGATE + one OBSGATE-2 + one OBSGATE-3 + one OBSGATE-4 + one FLEETMACHINERLS + one RESOLVENOOP + one ROOMDELEG-LANEB + one CLAIMKEY + two MINTHARDEN + one FLEETMACHINEHIJACK + one CLAIMGATE-SHADOW + one CLAIMGATE-ENFORCE + one FLEETMACHINEREACTIVATION + one DELEGCAPCEILING + one FLEETMACHINERECLAIM + one DELEGSOLEWRITER + one FLEETDOMAINRECLAIM + five ATOMICFLEETONBOARDING (onboarding + ceiling-lock + revocation-evidence + fedsession-gate + fedsession-lock-order) + one OUTBOXWAKE + one ENROLLLOCKORDER + one FEDPROVIDERABSENCE + one WAKERECOVERY + one CONSENTRECONCILE + one ENROLLRESUMPTION + one SIGNEDAUTHORITY + one SIGNERLIFECYCLE + one LEDGERAUTHORITY + one CEILINGLOCKCOMMENT + one LEDGERBOARDCREATE + one RECEIPTSUBJECTGATE + one TASKTHREADLOCKORDER + one CLAIMSUBJECTGATE + one FEDSESSIONIDENTITYGATE + one FEDIDENTITYSLUG + one INVITENODOWNGRADE + one SIGNEDWRITEFLAG + one SIGNEDWRITEMODEL + one RTCREDCLAIM + one ORIGINPOSTURE + one BOARDCLAIMPOLICY + one HEARTBEATPRESERVE + one LIVENESSDENOM + one OWNERLESSRENEWTTL + one OWNERLESSQUOTACENSUS + one FLEETDOMAINNODOWNGRADE + one OWNERLESSQUOTACONVERGE + one ADVISORYREVERTRESET + one CLAIMPOLICYRACE + one CLAIMPOLICYLOCKGRANT + one SESSIONCHALLENGESUPERSEDE + one ROOMCONTRACT + one CALLBACKCONSUMER + one ENROLLTOKENSINGLEUSE + one ROOMCONTRACT-ACTIVEFIRST + one ONBOARD + one HOSTED forward migrations (migrationCount = prior 113 + 5 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 = 156)', () => {
41
+ it('registers the four MBAUTHZ + four MBSENDAUTHZ + four MBSESSION + four MBDELEG + three MBFED + three MBHARDEN + two MBPORTALGATE + two MBPORTAL-PR32 + two ROOMOWN + two ROOMLINK + one ROOMDELEG + one LOCALAUTH + one ROOMUIAPI + one FEDSESSION + one FEDSESSION-AUTOJOIN + one GATENULL + one MEMBSTATE + one AGENTSDIR + one LEAVEIDEM + one ROLERESET + one MBROWID + one MBHYGIENE + one MBCONSENT-A + one MBCONSENT-B(a) + one MBCONSENT-B(i) + one MBCONSENT-B(ii) + one MBCONSENT-B(iii) + one MBHARDEN-#97 + one MBHARDEN-#118 + one INVITEPOLICY + one OBSGATE + one OBSGATE-2 + one OBSGATE-3 + one OBSGATE-4 + one FLEETMACHINERLS + one RESOLVENOOP + one ROOMDELEG-LANEB + one CLAIMKEY + two MINTHARDEN + one FLEETMACHINEHIJACK + one CLAIMGATE-SHADOW + one CLAIMGATE-ENFORCE + one FLEETMACHINEREACTIVATION + one DELEGCAPCEILING + one FLEETMACHINERECLAIM + one DELEGSOLEWRITER + one FLEETDOMAINRECLAIM + five ATOMICFLEETONBOARDING (onboarding + ceiling-lock + revocation-evidence + fedsession-gate + fedsession-lock-order) + one OUTBOXWAKE + one ENROLLLOCKORDER + one FEDPROVIDERABSENCE + one WAKERECOVERY + one CONSENTRECONCILE + one ENROLLRESUMPTION + one SIGNEDAUTHORITY + one SIGNERLIFECYCLE + one LEDGERAUTHORITY + one CEILINGLOCKCOMMENT + one LEDGERBOARDCREATE + one RECEIPTSUBJECTGATE + one TASKTHREADLOCKORDER + one CLAIMSUBJECTGATE + one FEDSESSIONIDENTITYGATE + one FEDIDENTITYSLUG + one INVITENODOWNGRADE + one SIGNEDWRITEFLAG + one SIGNEDWRITEMODEL + one RTCREDCLAIM + one ORIGINPOSTURE + one BOARDCLAIMPOLICY + one HEARTBEATPRESERVE + one LIVENESSDENOM + one OWNERLESSRENEWTTL + one OWNERLESSQUOTACENSUS + one FLEETDOMAINNODOWNGRADE + one OWNERLESSQUOTACONVERGE + one ADVISORYREVERTRESET + one CLAIMPOLICYRACE + one CLAIMPOLICYLOCKGRANT + one SESSIONCHALLENGESUPERSEDE + one ROOMCONTRACT + one CALLBACKCONSUMER + one ENROLLTOKENSINGLEUSE + one ROOMCONTRACT-ACTIVEFIRST + one ONBOARD + one HOSTED + one HOSTED-FOLLOWUPS + one OWNPOST forward migrations (migrationCount = prior 113 + 5 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 = 158)', () => {
42
42
  expect(manifest.migrationCount).toBe(manifest.migrations.length);
43
- expect(manifest.migrationCount).toBe(156);
43
+ expect(manifest.migrationCount).toBe(158);
44
44
  // MBSESSION switched enforcement on (scoped-send re-grant): the client-visible
45
45
  // schema version bumped to 8. MBDELEG tightens delegation and MBFED adds only
46
46
  // enrollment-only additive surface (identity_providers registry, enrollment audit,
@@ -349,6 +349,13 @@ describe('manifest integrity', () => {
349
349
  // HOSTED (v11): the agent_board_delegate role, delegate instances, the exchange and
350
350
  // tombstone RPCs, add_delegate_to_board, the D14 read predicates and the D16 refusals.
351
351
  '20261008000100_agent_board_hosted_delegates.sql',
352
+ // HOSTED follow-ups (#591): the owner's verified (iss, sub) on each delegate instance and the
353
+ // revocation marker revoke_human_subtree derives from it; the tombstone's and the exchange's
354
+ // lock order against the subtree revokers (AB15).
355
+ '20261009000100_agent_board_hosted_followups.sql',
356
+ // OWNPOST (#608): an agent's own board post is a feed-only row for it, not an inbox item;
357
+ // a board-wide task fan-out skips its creator; own_inbox callbacks skip feed-only rows.
358
+ '20261010000100_agent_board_own_post_not_delivered.sql',
352
359
  ]) {
353
360
  expect(names.has(f)).toBe(true);
354
361
  }
@@ -29,7 +29,9 @@ begin
29
29
  values
30
30
  (v_actor, 'agent', 'agent', 'addressing-actor-' || v_suffix, 'ADDRESSING Actor', 'active', '{"role":"phase_worker","groupAliases":["ops"]}'::jsonb),
31
31
  (v_worker, 'agent', 'agent', 'addressing-worker-' || v_suffix, 'ADDRESSING Worker', 'active', '{"role":"phase_worker","groupAliases":["ops"]}'::jsonb),
32
- (v_reviewer, 'agent', 'agent', 'addressing-reviewer-' || v_suffix, 'ADDRESSING Reviewer', 'active', '{"roles":["reviewer"],"groupAliases":["review"]}'::jsonb),
32
+ -- `role: phase_worker` on the reviewer too: since #608 a set descriptor never reaches its own
33
+ -- sender, so the direct-send ambiguity case below needs TWO matching agents besides the actor.
34
+ (v_reviewer, 'agent', 'agent', 'addressing-reviewer-' || v_suffix, 'ADDRESSING Reviewer', 'active', '{"role":"phase_worker","roles":["reviewer"],"groupAliases":["review"]}'::jsonb),
33
35
  (v_human, 'human', 'human', 'addressing-human-' || v_suffix, 'ADDRESSING Human', 'active', '{}'::jsonb);
34
36
 
35
37
  insert into agent_board.threads(id, thread_type, created_by_agent_id, subject)
@@ -21,7 +21,7 @@ set search_path = extensions, public, agent_board;
21
21
 
22
22
  begin;
23
23
 
24
- select plan(129);
24
+ select plan(143);
25
25
 
26
26
  -- The test role switches to agent_board_delegate. On PostgreSQL 16+ the role's creator holds
27
27
  -- ADMIN OPTION but not SET, so grant SET to the current role for this transaction only (rolled
@@ -1105,9 +1105,11 @@ select is(
1105
1105
  'N2: a suspended owner cannot bring up a new delegate instance, and nothing is written');
1106
1106
 
1107
1107
  -- Owner E: never enrolled as a human on this board (no human row), then durably revoked. The
1108
- -- marker is SEEDED: revoke_human_subtree writes it only from a federated enrollment's audit row
1109
- -- of an agent the human owns (a keyed agent via enroll_federated_identity), which this file does
1110
- -- not build. The row is the shape revoke_human_subtree writes, keyed by the verified (iss, sub).
1108
+ -- marker is SEEDED here, by hand, so that E's delegate row stays active and 128 isolates the
1109
+ -- marker arm alone (revoke_human_subtree would also revoke the delegate). The row is the shape
1110
+ -- revoke_human_subtree writes, keyed by the verified (iss, sub). The real revocation of a
1111
+ -- delegate-only owner, which now writes the marker from the instance's recorded (iss, sub)
1112
+ -- (#591 item 4, AB15), is section 24.
1111
1113
  -- 127 -- control: an owner with no human row is allowed, as before
1112
1114
  select is(
1113
1115
  pg_temp.exch(pg_temp.claim('inst_eeeeeeeeeeeeeeee_01', 'grant_eeeeeeeeeeeeee_01_1', 1, 'hosted-owner-e', 'client-e'), 'E1'),
@@ -1130,5 +1132,147 @@ select is(
1130
1132
  '42501:owner_inactive 0 0 0',
1131
1133
  'N2: the revocation marker alone refuses a new instance, and nothing is written');
1132
1134
 
1135
+ -- ===========================================================================
1136
+ -- 24. #591 item 4 -- the owner's (iss, sub) on each instance, and the marker it gives
1137
+ -- revoke_human_subtree (AB15); #591 item 9 -- owner_inactive before instance_revoked
1138
+ -- ===========================================================================
1139
+ -- An owner whose only principals are hosted delegates has no enrollment audit row, so before
1140
+ -- 1.59.0 revoke_human_subtree wrote it no marker, and a fresh first exchange passed. Both RPCs now
1141
+ -- record the verified pair, and the revocation derives a marker from it.
1142
+ -- plpgsql, not sql: each read runs as its own query, so it sees what an action earlier in the
1143
+ -- same statement wrote (the reason pg_temp.q exists).
1144
+ create function pg_temp.pair(p_iid text) returns text language plpgsql as $$
1145
+ begin
1146
+ return coalesce((select coalesce(owner_issuer, '(null)') || ' ' || coalesce(owner_subject, '(null)')
1147
+ from agent_board.delegate_instances where instance_id = p_iid), '(no row)');
1148
+ end $$;
1149
+ create function pg_temp.owner_of(p_iid text) returns uuid language plpgsql as $$
1150
+ begin
1151
+ return (select owner_user_id from agent_board.delegate_instances where instance_id = p_iid);
1152
+ end $$;
1153
+ create function pg_temp.markers(p_sub text) returns text language plpgsql as $$
1154
+ begin
1155
+ return (select count(*)::text || coalesce(' ' || string_agg(r.owner_user_id::text, ','), '')
1156
+ from agent_board.fleet_human_revocations r
1157
+ where r.issuer = 'https://portal.hosted.test' and r.subject = p_sub);
1158
+ end $$;
1159
+ -- 130 -- the exchange records the pair (D1 was exchanged in 123)
1160
+ select is(pg_temp.pair('inst_dddddddddddddddd_01'), 'https://portal.hosted.test hosted-owner-d',
1161
+ 'AB15: the exchange records the owner''s verified (iss, sub) on the instance');
1162
+ -- 131 -- a revocation before the first exchange records it too (owner H has nothing else)
1163
+ select is(
1164
+ pg_temp.tomb('inst_hhhhhhhhhhhhhhhh_01', 'hosted-owner-h') || ' ' || pg_temp.pair('inst_hhhhhhhhhhhhhhhh_01'),
1165
+ 'first=true https://portal.hosted.test hosted-owner-h',
1166
+ 'AB15: a pre-exchange tombstone records the owner''s verified (iss, sub) on the row it creates');
1167
+ -- 132 -- once recorded, the pair never changes
1168
+ select is(
1169
+ pg_temp.try($q$update agent_board.delegate_instances set owner_subject = 'someone-else' where instance_id = 'inst_dddddddddddddddd_01'$q$)
1170
+ || ' ' || pg_temp.pair('inst_dddddddddddddddd_01'),
1171
+ '23514:delegate_instances owner identity never changes once recorded https://portal.hosted.test hosted-owner-d',
1172
+ 'AB15: the recorded owner pair is immutable');
1173
+
1174
+ -- Owner G: never enrolled, live; the control owner, and the legacy-row fills.
1175
+ select pg_temp.svc();
1176
+ -- 133 -- a LEGACY row (pre-1.59.0 shape: no pair) is filled by its first exchange
1177
+ select is(
1178
+ pg_temp.exch(pg_temp.claim('inst_gggggggggggggggg_01', 'grant_gggggggggggggg_01_1', 1, 'hosted-owner-g', 'client-g'), 'G1')
1179
+ || ' ' || pg_temp.try(format($q$insert into agent_board.delegate_instances(instance_id, owner_user_id) values ('inst_gggggggggggggggg_l1', %L), ('inst_gggggggggggggggg_l2', %L)$q$,
1180
+ pg_temp.owner_of('inst_gggggggggggggggg_01'), pg_temp.owner_of('inst_gggggggggggggggg_01')))
1181
+ || ' ' || pg_temp.pair('inst_gggggggggggggggg_l1')
1182
+ || ' ' || pg_temp.exch(pg_temp.claim('inst_gggggggggggggggg_l1', 'grant_gggggggggggggg_l1_1', 1, 'hosted-owner-g', 'client-g'), 'GL1')
1183
+ || ' ' || pg_temp.pair('inst_gggggggggggggggg_l1'),
1184
+ 'ok ok (null) (null) ok https://portal.hosted.test hosted-owner-g',
1185
+ 'AB15: a legacy instance with no recorded pair is filled by its next exchange');
1186
+ -- 134 -- ...and by a tombstone
1187
+ select is(
1188
+ pg_temp.tomb('inst_gggggggggggggggg_l2', 'hosted-owner-g') || ' ' || pg_temp.pair('inst_gggggggggggggggg_l2'),
1189
+ 'first=true https://portal.hosted.test hosted-owner-g',
1190
+ 'AB15: a legacy instance with no recorded pair is filled by its tombstone');
1191
+
1192
+ -- Owner F: never enrolled as a human; its ONLY principal is a hosted delegate.
1193
+ -- 135 -- revoke_human_subtree for real, as service_role
1194
+ select is(
1195
+ pg_temp.exch(pg_temp.claim('inst_ffffffffffffffff_01', 'grant_ffffffffffffff_01_1', 1, 'hosted-owner-f', 'client-f'), 'F1')
1196
+ || ' ' || pg_temp.try(format($q$select agent_board.revoke_human_subtree(%L, %L, 'hosted 591 test')$q$,
1197
+ pg_temp.owner_of('inst_ffffffffffffffff_01'), pg_temp.v('K')))
1198
+ || ' ' || pg_temp.q(format($q$select status from agent_board.agents where id = %L$q$, pg_temp.v('F1.agent'))),
1199
+ 'ok ok revoked',
1200
+ '#591 item 4: an owner with only a hosted delegate is revoked, and its delegate with it');
1201
+ -- 136 -- the marker exists, keyed by the verified pair, naming the owner
1202
+ select is(pg_temp.markers('hosted-owner-f'), '1 ' || pg_temp.owner_of('inst_ffffffffffffffff_01')::text,
1203
+ '#591 item 4: revoking a delegate-only owner writes the durable (iss, sub) marker from the instance''s recorded pair');
1204
+ -- 137 -- so a fresh first exchange is refused, and writes nothing
1205
+ select is(
1206
+ pg_temp.exch(pg_temp.claim('inst_ffffffffffffffff_02', 'grant_ffffffffffffff_02_1', 1, 'hosted-owner-f', 'client-f'), 'x')
1207
+ || ' ' || pg_temp.n2_rows('inst_ffffffffffffffff_02'),
1208
+ '42501:owner_inactive 0 0 0',
1209
+ '#591 item 4: after the revocation a new first exchange for that owner is refused owner_inactive, and nothing is written');
1210
+ -- 138 -- control: another live owner, never enrolled, still brings up a new instance
1211
+ select is(
1212
+ pg_temp.exch(pg_temp.claim('inst_gggggggggggggggg_02', 'grant_gggggggggggggg_02_1', 1, 'hosted-owner-g', 'client-g'), 'G2')
1213
+ || ' ' || pg_temp.markers('hosted-owner-g'),
1214
+ 'ok 0',
1215
+ '#591 item 4 control: a different, unrevoked owner''s first exchange succeeds and has no marker');
1216
+ -- 139 -- #591 item 9: a revoked owner AND a tombstoned instance -> owner_inactive, not instance_revoked
1217
+ select is(
1218
+ pg_temp.tomb('inst_ffffffffffffffff_01', 'hosted-owner-f')
1219
+ || ' ' || pg_temp.exch(pg_temp.claim('inst_ffffffffffffffff_01', 'grant_ffffffffffffff_01_2', 2, 'hosted-owner-f', 'client-f'), 'x'),
1220
+ 'first=true 42501:owner_inactive',
1221
+ 'AB14 precedence: the owner check runs before the instance''s own refusals -- a tombstoned instance of a revoked owner is refused owner_inactive, not instance_revoked');
1222
+ -- 140 -- owner H, whose only row is a pre-exchange tombstone (131): its revocation marks it too
1223
+ select is(
1224
+ pg_temp.try(format($q$select agent_board.revoke_human_subtree(%L, %L, 'hosted 591 test')$q$,
1225
+ pg_temp.owner_of('inst_hhhhhhhhhhhhhhhh_01'), pg_temp.v('K')))
1226
+ || ' ' || pg_temp.markers('hosted-owner-h')
1227
+ || ' ' || pg_temp.exch(pg_temp.claim('inst_hhhhhhhhhhhhhhhh_02', 'grant_hhhhhhhhhhhhhh_02_1', 1, 'hosted-owner-h', 'client-h'), 'x')
1228
+ || ' ' || pg_temp.n2_rows('inst_hhhhhhhhhhhhhhhh_02'),
1229
+ 'ok 1 ' || pg_temp.owner_of('inst_hhhhhhhhhhhhhhhh_01')::text || ' 42501:owner_inactive 0 0 0',
1230
+ '#591 item 4: an owner whose only row is a pre-exchange tombstone is marked by its revocation, and a new instance is refused');
1231
+ -- 141 -- the class: every function that WRITES delegate_instances (any INSERT, UPDATE, MERGE or
1232
+ -- DELETE, from the catalog, through the shared matcher family) is one of the two RPCs that record
1233
+ -- the pair, and every row this file created carries one except the two hand-inserted legacy rows,
1234
+ -- which their exchange and tombstone filled (133, 134). A new writer fails this and names itself.
1235
+ -- Every non-system schema, not only agent_board: a writer in public or extensions bypasses the pair
1236
+ -- just as well (mutation-checked with one in public). agent_board_test is excluded because it holds
1237
+ -- this suite's matcher functions, which carry relation names as data and write nothing.
1238
+ select is(
1239
+ (select string_agg(p.proname, ',' order by p.proname)
1240
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
1241
+ where n.nspname not in ('pg_catalog', 'information_schema', 'agent_board_test')
1242
+ and n.nspname !~ '^pg_'
1243
+ and p.prokind in ('f', 'p')
1244
+ and agent_board_test.stripped_body(p.oid) ~ agent_board_test.write_re('delegate_instances'))
1245
+ || ' ' || (select count(*) from agent_board.delegate_instances where owner_issuer is null)::text,
1246
+ 'exchange_delegate_credential,tombstone_delegate_instance 0',
1247
+ 'AB15: exactly the exchange and the tombstone write delegate_instances (catalog-derived), and no row is left without the owner pair');
1248
+
1249
+ -- 142 -- the pair is both-or-neither and never empty (delegate_instances_owner_identity_shape)
1250
+ select is(
1251
+ pg_temp.try(format($q$insert into agent_board.delegate_instances(instance_id, owner_user_id, owner_issuer) values ('inst_gggggggggggggggg_s1', %L, 'https://portal.hosted.test')$q$, pg_temp.owner_of('inst_gggggggggggggggg_01')))
1252
+ || ' ' || pg_temp.try(format($q$insert into agent_board.delegate_instances(instance_id, owner_user_id, owner_issuer, owner_subject) values ('inst_gggggggggggggggg_s2', %L, 'https://portal.hosted.test', '')$q$, pg_temp.owner_of('inst_gggggggggggggggg_01'))),
1253
+ '23514:new row for relation "delegate_instances" violates check constraint "delegate_instances_owner_identity_shape" 23514:new row for relation "delegate_instances" violates check constraint "delegate_instances_owner_identity_shape"',
1254
+ 'AB15: an owner pair is recorded whole or not at all, and never empty');
1255
+
1256
+ -- 143 -- a LEGACY TOMBSTONED row (pre-1.59.0: tombstoned, no pair) is filled by a repeated
1257
+ -- revocation. Nothing else ever writes such a row again: the exchange refuses it
1258
+ -- (instance_revoked) before its UPDATE, and a tombstone used to return early. Owner J has only this
1259
+ -- row (never enrolled, never exchanged), so the pair is the only evidence revoke_human_subtree can
1260
+ -- mark it from. Then a new first exchange of J is refused owner_inactive and writes nothing.
1261
+ select pg_temp.put('J.owner', agent_board._ensure_agent_auth_user(
1262
+ agent_board._federated_identity_slug('https://portal.hosted.test', 'hosted-owner-j'))::text);
1263
+ select is(
1264
+ pg_temp.try(format($q$insert into agent_board.delegate_instances(instance_id, owner_user_id, tombstoned_at, tombstone_reason) values ('inst_jjjjjjjjjjjjjjjj_l1', %L, now(), 'owner_revoked')$q$, pg_temp.v('J.owner')))
1265
+ || ' ' || pg_temp.pair('inst_jjjjjjjjjjjjjjjj_l1')
1266
+ || ' ' || pg_temp.tomb('inst_jjjjjjjjjjjjjjjj_l1', 'hosted-owner-j')
1267
+ || ' ' || pg_temp.pair('inst_jjjjjjjjjjjjjjjj_l1')
1268
+ || ' ' || pg_temp.try(format($q$select agent_board.revoke_human_subtree(%L, %L, 'hosted 591 test')$q$,
1269
+ pg_temp.v('J.owner'), pg_temp.v('K')))
1270
+ || ' ' || pg_temp.markers('hosted-owner-j')
1271
+ || ' ' || pg_temp.exch(pg_temp.claim('inst_jjjjjjjjjjjjjjjj_02', 'grant_jjjjjjjjjjjjjj_02_1', 1, 'hosted-owner-j', 'client-j'), 'x')
1272
+ || ' ' || pg_temp.n2_rows('inst_jjjjjjjjjjjjjjjj_02'),
1273
+ 'ok (null) (null) first=false https://portal.hosted.test hosted-owner-j ok 1 ' || pg_temp.v('J.owner')
1274
+ || ' 42501:owner_inactive 0 0 0',
1275
+ 'AB15: a legacy tombstoned row is filled by a repeated revocation, so revoking its delegate-only owner marks it and a new instance is refused');
1276
+
1133
1277
  select * from finish();
1134
1278
  rollback;
@@ -378,7 +378,9 @@ begin
378
378
  '{}'::jsonb
379
379
  );
380
380
  v_board_message := v_result.message_id;
381
- insert into _tap(line) select is(v_result.delivered_recipient_count, 3, 'board delivery reaches joined and subscribed members only (incl. the seeded owner-creator)');
381
+ -- #608: the sender is the board's seeded owner-creator, and the author of a post is not one of
382
+ -- its recipients (its row is feed-only), so two of the three active members are counted.
383
+ insert into _tap(line) select is(v_result.delivered_recipient_count, 2, 'board delivery reaches joined and subscribed members only, not the author (#608)');
382
384
  insert into _tap(line) select is(
383
385
  (select count(*)::integer from agent_board.delivery_recipients where source_message_id = v_board_message and recipient_agent_id = v_left_member),
384
386
  0,
@@ -409,7 +411,8 @@ begin
409
411
  '{}'::jsonb
410
412
  );
411
413
  v_task := v_result.task_id;
412
- insert into _tap(line) select is(v_result.delivered_recipient_count, 4, 'tasking counts explicit and board fan-out attempts (incl. the seeded owner-creator)');
414
+ -- #608: the creator (the seeded owner-creator) keeps a feed-only row that is not counted: 1 explicit + 2 board.
415
+ insert into _tap(line) select is(v_result.delivered_recipient_count, 3, 'tasking counts explicit and board fan-out attempts, not the creator (#608)');
413
416
  insert into _tap(line) select is(
414
417
  (select count(*)::integer from agent_board.delivery_recipients where source_task_id = v_task),
415
418
  3,