@consiliency/agent-board-schema 1.55.0 → 1.57.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,353 @@
1
+ -- ONBOARD (v11) SL-2 pgTAP: list_my_board_requests() (IF-0-ONBOARD-4) and
2
+ -- invite_board_for_display(text) (IF-0-ONBOARD-11), migration 20261007000100.
3
+ --
4
+ -- Driven by the real operations only: request_join_board, resolve_board_request (approve and
5
+ -- reject), leave_board then request_join_board again, create_invite_link / redeem_invite_link /
6
+ -- revoke_invite_link, enroll_federated_human. Two fixtures are AGE only, because now() is
7
+ -- constant inside the test transaction and neither the TTL nor an invite expiry can be waited
8
+ -- out: one request event's created_at is moved 31 days back (OB-L13), and one invite's
9
+ -- expires_at is moved into the past (OB-I05). Both rows were first written by the real RPC.
10
+ --
11
+ -- Identity is set by the whole-JSON claim GUC only (the consent suite's method): SET LOCAL
12
+ -- ROLE is never used, so EXECUTE posture is asserted separately through
13
+ -- has_function_privilege, not by calling as a role.
14
+ --
15
+ -- Every call to a function under test is a lives_ok capture into a temp table, then separate
16
+ -- state checks (AGENTS.md rule 3): a missing or re-signatured function FAILS the capture and
17
+ -- leaves the checks to fail on an empty table, instead of aborting the file.
18
+ --
19
+ -- Mutation receipts (plans/evidence/onboard/mutation-receipts.md, "## SL-2"):
20
+ -- identity parameter added -> OB-L02
21
+ -- agent_id = caller dropped -> OB-L10, OB-L16
22
+ -- created_by_agent_id as the source -> OB-L10
23
+ -- service_role granted -> OB-L05
24
+ -- IF-11 granted to authenticated -> OB-I13
25
+ -- revoked / expired / exhausted predicate dropped -> OB-I04 / OB-I05 / OB-I07
26
+ --
27
+ -- HAND-COUNTED plan: OB-L01..L23 (23) + OB-I01..I16 with I03b (17) + OB-R01..R03 and the two
28
+ -- named redeem pins (5) = 45.
29
+ create extension if not exists pgtap with schema extensions;
30
+ set search_path = extensions, public, agent_board;
31
+
32
+ begin;
33
+
34
+ select plan(45);
35
+
36
+ create function pg_temp.claims(p_sub text) returns void language plpgsql as $$
37
+ begin
38
+ perform set_config('request.jwt.claims', json_build_object('role','authenticated','sub',p_sub)::text, true);
39
+ end $$;
40
+ create function pg_temp.service() returns void language plpgsql as $$
41
+ begin perform set_config('request.jwt.claims', '', true); end $$;
42
+ create function pg_temp.state(b uuid, a uuid) returns text language sql as $$
43
+ select membership_state from agent_board.board_memberships
44
+ where board_id = b and agent_id = a
45
+ order by case when membership_state in ('joined','subscribed') then 0 else 1 end,
46
+ created_at desc, id desc
47
+ limit 1;
48
+ $$;
49
+ create function pg_temp.sha(p_code text) returns text language sql as $$
50
+ select encode(extensions.digest(p_code, 'sha256'), 'hex');
51
+ $$;
52
+
53
+ -- ---------------------------------------------------------------------------
54
+ -- Fixture: principals and boards (service context; create_board seeds the owner).
55
+ -- ---------------------------------------------------------------------------
56
+ insert into auth.users(id, instance_id, aud, role, email)
57
+ select ('0b000000-0000-0000-0000-' || lpad(n::text,12,'0'))::uuid,
58
+ '00000000-0000-0000-0000-000000000000','authenticated','authenticated','onb'||n||'@t.test'
59
+ from generate_series(1,4) n;
60
+
61
+ -- 1 = owner, 2 = requester R, 3 = another principal X, 4 = invite redeemer / exhauster Y.
62
+ insert into agent_board.agents(id, agent_type, kind, handle, display_name, status, auth_user_id)
63
+ select ('0b100000-0000-4000-8000-' || lpad(n::text,12,'0'))::uuid,
64
+ 'agent','agent','onb-agent-'||n,'ONB Agent '||n,'active',
65
+ ('0b000000-0000-0000-0000-' || lpad(n::text,12,'0'))::uuid
66
+ from generate_series(1,4) n;
67
+
68
+ create temp table _ob (
69
+ owner uuid, r uuid, x uuid, y uuid, h uuid, h_user uuid,
70
+ b_approve uuid, b_reject uuid, b_rerequest uuid, b_expire uuid,
71
+ b_invite uuid, b_other uuid, b_human uuid, b_links uuid,
72
+ code_join text, code_live text, code_revoked text, code_expired text,
73
+ code_exhaust text, code_replay text, code_other text
74
+ ) on commit drop;
75
+
76
+ do $$
77
+ declare
78
+ v_owner uuid := '0b100000-0000-4000-8000-000000000001';
79
+ v_iss text := 'https://onb-fed-' || replace(substr(gen_random_uuid()::text, 1, 8), '-', '') || '.test';
80
+ v_h uuid; v_h_user uuid;
81
+ v_ba uuid; v_bb uuid; v_bc uuid; v_bd uuid; v_be uuid; v_bg uuid; v_bf uuid; v_bk uuid;
82
+ begin
83
+ select board_id into v_ba from agent_board.create_board(v_owner, 'onb-approve', 'ONB Approve', null, '{}'::jsonb, 'open');
84
+ select board_id into v_bb from agent_board.create_board(v_owner, 'onb-reject', 'ONB Reject', null, '{}'::jsonb, 'open');
85
+ select board_id into v_bc from agent_board.create_board(v_owner, 'onb-rerequest', 'ONB Rerequest', null, '{}'::jsonb, 'open');
86
+ select board_id into v_bd from agent_board.create_board(v_owner, 'onb-expire', 'ONB Expire', null, '{}'::jsonb, 'invite_only');
87
+ select board_id into v_be from agent_board.create_board(v_owner, 'onb-invite', 'ONB Invite', null, '{}'::jsonb, 'open');
88
+ select board_id into v_bg from agent_board.create_board(v_owner, 'onb-other', 'ONB Other', null, '{}'::jsonb, 'open');
89
+ select board_id into v_bf from agent_board.create_board(v_owner, 'onb-human', 'ONB Human', null, '{}'::jsonb, 'open');
90
+ select board_id into v_bk from agent_board.create_board(v_owner, 'onb-links', 'ONB Links', null, '{}'::jsonb, 'private');
91
+
92
+ -- A Portal human: the federated provider, then the fed-human agent it provisions.
93
+ insert into agent_board.identity_providers
94
+ (issuer, audience, supported, policy_approved, allowed_agent_kinds, default_board_id)
95
+ values (v_iss, 'message-board', true, true, '{human}', null);
96
+ select e.agent_id, e.auth_user_id into v_h, v_h_user
97
+ from agent_board.enroll_federated_human(
98
+ v_iss, 'message-board', 'onb-human-sub',
99
+ jsonb_build_object('sub', 'onb-human-sub', 'email_verified', false), 'ONB Human') e;
100
+
101
+ insert into _ob(owner, r, x, y, h, h_user,
102
+ b_approve, b_reject, b_rerequest, b_expire, b_invite, b_other, b_human, b_links)
103
+ values (v_owner,
104
+ '0b100000-0000-4000-8000-000000000002', '0b100000-0000-4000-8000-000000000003',
105
+ '0b100000-0000-4000-8000-000000000004', v_h, v_h_user,
106
+ v_ba, v_bb, v_bc, v_bd, v_be, v_bg, v_bf, v_bk);
107
+
108
+ update _ob set
109
+ code_join = (select code from agent_board.create_invite_link(v_be, 'member', 'joined', null, null, v_owner, '{}'::jsonb)),
110
+ code_live = (select code from agent_board.create_invite_link(v_bk, 'member', 'joined', null, null, v_owner, '{}'::jsonb)),
111
+ code_revoked = (select code from agent_board.create_invite_link(v_bk, 'member', 'joined', null, null, v_owner, '{}'::jsonb)),
112
+ code_expired = (select code from agent_board.create_invite_link(v_bk, 'member', 'joined', null, null, v_owner, '{}'::jsonb)),
113
+ code_exhaust = (select code from agent_board.create_invite_link(v_bk, 'member', 'joined', 1, null, v_owner, '{}'::jsonb)),
114
+ code_replay = (select code from agent_board.create_invite_link(v_bk, 'member', 'joined', null, null, v_owner, '{}'::jsonb)),
115
+ code_other = (select code from agent_board.create_invite_link(v_bk, 'member', 'joined', null, null, v_owner, '{}'::jsonb));
116
+ end
117
+ $$;
118
+
119
+ create temp table _req (
120
+ who text, board_id uuid, board_name text, board_slug text, requested_at timestamptz,
121
+ membership_state text, expires_at timestamptz, expired boolean
122
+ ) on commit drop;
123
+ create temp table _disp (
124
+ label text, board_id uuid, board_name text, board_slug text, preset_role text
125
+ ) on commit drop;
126
+
127
+ -- ===========================================================================
128
+ -- list_my_board_requests: catalog posture
129
+ -- ===========================================================================
130
+ select ok(to_regprocedure('agent_board.list_my_board_requests()') is not null,
131
+ 'OB-L01: list_my_board_requests() exists with no arguments');
132
+ select is(
133
+ (select string_agg(coalesce(pg_get_function_arguments(p.oid), ''), '|')
134
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
135
+ where n.nspname = 'agent_board' and p.proname = 'list_my_board_requests'),
136
+ '',
137
+ 'OB-L02: exactly one list_my_board_requests overload, and it takes NO parameter (identity is never an argument)');
138
+ select is(
139
+ (select p.prosecdef::text || '/' || p.provolatile::text
140
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
141
+ where n.nspname = 'agent_board' and p.proname = 'list_my_board_requests'),
142
+ 'true/s',
143
+ 'OB-L03: list_my_board_requests is SECURITY DEFINER and STABLE');
144
+ select is(has_function_privilege('authenticated', to_regprocedure('agent_board.list_my_board_requests()'), 'EXECUTE'),
145
+ true, 'OB-L04: authenticated holds EXECUTE on list_my_board_requests');
146
+ select is(has_function_privilege('service_role', to_regprocedure('agent_board.list_my_board_requests()'), 'EXECUTE'),
147
+ false, 'OB-L05: service_role holds NO EXECUTE on list_my_board_requests (no caller identity)');
148
+ select is(has_function_privilege('anon', to_regprocedure('agent_board.list_my_board_requests()'), 'EXECUTE'),
149
+ false, 'OB-L06: anon holds NO EXECUTE on list_my_board_requests');
150
+ select is(has_function_privilege('public', to_regprocedure('agent_board.list_my_board_requests()'), 'EXECUTE'),
151
+ false, 'OB-L07: PUBLIC holds NO EXECUTE on list_my_board_requests');
152
+
153
+ -- ===========================================================================
154
+ -- Real operations
155
+ -- ===========================================================================
156
+ -- R requests four boards.
157
+ select pg_temp.claims('0b000000-0000-0000-0000-000000000002');
158
+ do $$ declare o _ob; begin select * into o from _ob;
159
+ perform agent_board.request_join_board(o.b_approve, o.r);
160
+ perform agent_board.request_join_board(o.b_reject, o.r);
161
+ perform agent_board.request_join_board(o.b_rerequest, o.r);
162
+ perform agent_board.request_join_board(o.b_expire, o.r);
163
+ end $$;
164
+
165
+ -- X requests the approve board and a board R never asked for.
166
+ select pg_temp.claims('0b000000-0000-0000-0000-000000000003');
167
+ do $$ declare o _ob; begin select * into o from _ob;
168
+ perform agent_board.request_join_board(o.b_approve, o.x);
169
+ perform agent_board.request_join_board(o.b_other, o.x);
170
+ end $$;
171
+
172
+ -- The owner approves A and C, rejects B. Passing the owner as p_created_by_agent_id makes the
173
+ -- sink overwrite created_by_agent_id on R's rows -- the reason that column is not a source.
174
+ select pg_temp.claims('0b000000-0000-0000-0000-000000000001');
175
+ do $$ declare o _ob; begin select * into o from _ob;
176
+ perform agent_board.resolve_board_request(o.b_approve, o.r, 'joined', o.owner, '{}'::jsonb);
177
+ perform agent_board.resolve_board_request(o.b_reject, o.r, 'removed', o.owner, '{}'::jsonb);
178
+ perform agent_board.resolve_board_request(o.b_rerequest, o.r, 'joined', o.owner, '{}'::jsonb);
179
+ end $$;
180
+
181
+ -- R leaves C and requests it again; R joins E through an invite with no request.
182
+ select pg_temp.claims('0b000000-0000-0000-0000-000000000002');
183
+ do $$ declare o _ob; begin select * into o from _ob;
184
+ perform agent_board.leave_board(o.b_rerequest, o.r);
185
+ perform agent_board.request_join_board(o.b_rerequest, o.r);
186
+ perform agent_board.redeem_invite_link(o.code_join);
187
+ end $$;
188
+
189
+ -- The Portal human requests F from its own session.
190
+ select pg_temp.claims((select h_user::text from _ob));
191
+ do $$ declare o _ob; begin select * into o from _ob;
192
+ perform agent_board.request_join_board(o.b_human, o.h);
193
+ end $$;
194
+
195
+ -- AGE fixture: R's request on D is moved past the board's 30-day TTL.
196
+ select pg_temp.service();
197
+ update agent_board.board_membership_events e
198
+ set created_at = now() - interval '31 days'
199
+ from _ob
200
+ where e.board_id = _ob.b_expire and e.agent_id = _ob.r
201
+ and e.provenance = 'target-authored' and e.result_state = 'requested';
202
+
203
+ -- ===========================================================================
204
+ -- list_my_board_requests: behaviour
205
+ -- ===========================================================================
206
+ select pg_temp.claims('0b000000-0000-0000-0000-000000000002');
207
+ select lives_ok($$ insert into _req select 'R', * from agent_board.list_my_board_requests() $$,
208
+ 'OB-L08: R lists its requests [call succeeds]');
209
+ select pg_temp.claims('0b000000-0000-0000-0000-000000000003');
210
+ select lives_ok($$ insert into _req select 'X', * from agent_board.list_my_board_requests() $$,
211
+ 'OB-L09: X lists its requests [call succeeds]');
212
+
213
+ select is(
214
+ (select string_agg(board_slug, ',' order by board_slug) from _req where who = 'R'),
215
+ 'onb-approve,onb-expire,onb-reject,onb-rerequest',
216
+ 'OB-L10: R sees exactly its four requested boards -- approved, rejected, re-requested and expired');
217
+ select is((select membership_state from _req r, _ob where r.who = 'R' and r.board_id = _ob.b_approve),
218
+ 'joined', 'OB-L11: an approved request reads its current state, joined');
219
+ select is((select membership_state from _req r, _ob where r.who = 'R' and r.board_id = _ob.b_reject),
220
+ 'left', 'OB-L12: a rejected request reads its current state, left (REJECTSEM)');
221
+ select is((select membership_state || '/' || expired::text from _req r, _ob where r.who = 'R' and r.board_id = _ob.b_expire),
222
+ 'requested/true', 'OB-L13: a request older than join_request_ttl_days is returned with expired = true');
223
+ select is((select count(*)::int from _req where who = 'R' and not expired),
224
+ 3, 'OB-L14: the three requests inside the TTL are expired = false');
225
+ select is(
226
+ (select membership_state || '/' || (select count(*) from _req q where q.who = 'R' and q.board_id = r.board_id)::text
227
+ from _req r, _ob where r.who = 'R' and r.board_id = _ob.b_rerequest),
228
+ 'requested/1',
229
+ 'OB-L15: leave then re-request reads requested, in ONE row for the board');
230
+ select is((select count(*)::int from _req r, _ob where r.who = 'R' and r.board_id in (_ob.b_other, _ob.b_human)),
231
+ 0, 'OB-L16: another principal''s requests are invisible to R');
232
+ select is(
233
+ (select pg_temp.state(_ob.b_invite, _ob.r) || '/' ||
234
+ (select count(*) from _req r where r.who = 'R' and r.board_id = _ob.b_invite)::text from _ob),
235
+ 'joined/0',
236
+ 'OB-L17: an invite-only join (R is joined) is NOT returned as a request');
237
+ select is(
238
+ (select (r.board_name = 'ONB Approve' and r.expires_at = r.requested_at + interval '30 days')::text
239
+ from _req r, _ob where r.who = 'R' and r.board_id = _ob.b_approve),
240
+ 'true', 'OB-L18: a row carries the board name and expires_at = requested_at + join_request_ttl_days');
241
+ select is(
242
+ (select string_agg(board_slug || '=' || membership_state, ',' order by board_slug) from _req where who = 'X'),
243
+ 'onb-approve=requested,onb-other=requested',
244
+ 'OB-L19: X sees only its own requests, with its own state on the shared board');
245
+
246
+ select pg_temp.claims((select h_user::text from _ob));
247
+ select lives_ok($$ insert into _req select 'H', * from agent_board.list_my_board_requests() $$,
248
+ 'OB-L20: the Portal human session lists [call succeeds]');
249
+ select is((select string_agg(board_slug || '=' || membership_state, ',') from _req where who = 'H'),
250
+ 'onb-human=requested',
251
+ 'OB-L21: a Portal human session sees its fed-human agent''s request');
252
+
253
+ select pg_temp.service();
254
+ select lives_ok($$ insert into _req select 'S', * from agent_board.list_my_board_requests() $$,
255
+ 'OB-L22: a session with no caller identity calls the function [call succeeds]');
256
+ select is((select count(*)::int from _req where who = 'S'),
257
+ 0, 'OB-L23: a session with no caller identity lists nothing');
258
+
259
+ -- ===========================================================================
260
+ -- invite_board_for_display: catalog posture
261
+ -- ===========================================================================
262
+ select ok(to_regprocedure('agent_board.invite_board_for_display(text)') is not null,
263
+ 'OB-I01: invite_board_for_display(text) exists');
264
+ select is(
265
+ (select count(*)::int from pg_indexes
266
+ where schemaname = 'agent_board' and tablename = 'board_invite_tokens'
267
+ and indexname = 'idx_agent_board_board_invite_tokens_code_sha256'
268
+ and indexdef like '%digest(code, ''sha256''%'),
269
+ 1, 'OB-I02: board_invite_tokens has the functional digest index');
270
+
271
+ -- ===========================================================================
272
+ -- Real operations on the links board
273
+ -- ===========================================================================
274
+ select pg_temp.claims('0b000000-0000-0000-0000-000000000001');
275
+ do $$ begin perform agent_board.revoke_invite_link((select code_revoked from _ob)); end $$;
276
+ select pg_temp.service();
277
+ -- AGE fixture: a code minted by create_invite_link is moved past its expiry.
278
+ update agent_board.board_invite_tokens t set expires_at = now() - interval '1 second'
279
+ from _ob where t.code = _ob.code_expired;
280
+
281
+ select lives_ok($$ insert into _disp select 'exhaust-before', * from agent_board.invite_board_for_display(pg_temp.sha((select code_exhaust from _ob))) $$,
282
+ 'OB-I03: the capped code is looked up before its last use [call succeeds]');
283
+
284
+ -- Y spends the capped code's only use.
285
+ select pg_temp.claims('0b000000-0000-0000-0000-000000000004');
286
+ do $$ begin perform agent_board.redeem_invite_link((select code_exhaust from _ob)); end $$;
287
+ select pg_temp.service();
288
+
289
+ select lives_ok($$
290
+ insert into _disp select 'live', * from agent_board.invite_board_for_display(pg_temp.sha((select code_live from _ob)));
291
+ insert into _disp select 'live-upper', * from agent_board.invite_board_for_display(upper(pg_temp.sha((select code_live from _ob))));
292
+ insert into _disp select 'revoked', * from agent_board.invite_board_for_display(pg_temp.sha((select code_revoked from _ob)));
293
+ insert into _disp select 'expired', * from agent_board.invite_board_for_display(pg_temp.sha((select code_expired from _ob)));
294
+ insert into _disp select 'exhausted', * from agent_board.invite_board_for_display(pg_temp.sha((select code_exhaust from _ob)));
295
+ insert into _disp select 'unknown', * from agent_board.invite_board_for_display(pg_temp.sha('onb-no-such-code'));
296
+ $$, 'OB-I03b: every code is looked up [call succeeds]');
297
+
298
+ select is((select count(*)::int from _disp where label = 'revoked'),
299
+ 0, 'OB-I04: a revoked code returns no row');
300
+ select is((select count(*)::int from _disp where label = 'expired'),
301
+ 0, 'OB-I05: an expired code returns no row');
302
+ select is(
303
+ (select uses_count::text || '/' || max_uses::text from agent_board.board_invite_tokens t, _ob where t.code = _ob.code_exhaust),
304
+ '1/1', 'OB-I06: the capped code was exhausted by a real redeem (uses_count = max_uses)');
305
+ select is((select count(*)::int from _disp where label = 'exhausted'),
306
+ 0, 'OB-I07: an exhausted code returns no row');
307
+ select is((select count(*)::int from _disp where label = 'exhaust-before'),
308
+ 1, 'OB-I08: the same code resolved while a use remained (the exhausted check is discriminating)');
309
+ select is((select count(*)::int from _disp where label = 'unknown'),
310
+ 0, 'OB-I09: an unknown digest returns no row');
311
+ select is(
312
+ (select (d.board_id = _ob.b_links)::text || '/' || d.board_name || '/' || d.board_slug || '/' || d.preset_role
313
+ from _disp d, _ob where d.label = 'live'),
314
+ 'true/ONB Links/onb-links/member',
315
+ 'OB-I10: a usable code resolves to its board id, name, slug and preset role');
316
+ select is((select count(*)::int from _disp where label = 'live-upper'),
317
+ 1, 'OB-I11: the digest is compared case-insensitively');
318
+ select is(has_function_privilege('service_role', to_regprocedure('agent_board.invite_board_for_display(text)'), 'EXECUTE'),
319
+ true, 'OB-I12: service_role holds EXECUTE on invite_board_for_display');
320
+ select is(has_function_privilege('authenticated', to_regprocedure('agent_board.invite_board_for_display(text)'), 'EXECUTE'),
321
+ false, 'OB-I13: authenticated holds NO EXECUTE on invite_board_for_display');
322
+ select is(has_function_privilege('anon', to_regprocedure('agent_board.invite_board_for_display(text)'), 'EXECUTE'),
323
+ false, 'OB-I14: anon holds NO EXECUTE on invite_board_for_display');
324
+ select is(has_function_privilege('public', to_regprocedure('agent_board.invite_board_for_display(text)'), 'EXECUTE'),
325
+ false, 'OB-I15: PUBLIC holds NO EXECUTE on invite_board_for_display');
326
+ select is(
327
+ (select p.prosecdef::text || '/' || p.provolatile::text
328
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
329
+ where n.nspname = 'agent_board' and p.proname = 'invite_board_for_display'),
330
+ 'true/s',
331
+ 'OB-I16: invite_board_for_display is SECURITY DEFINER and STABLE');
332
+
333
+ -- ===========================================================================
334
+ -- Redeem behaviour on the real RPC (EC-ONBOARD-1's pin)
335
+ -- ===========================================================================
336
+ select pg_temp.claims('0b000000-0000-0000-0000-000000000002');
337
+ select lives_ok($$ select agent_board.redeem_invite_link((select code_replay from _ob)) $$,
338
+ 'OB-R01: R redeems an unlimited invite [call succeeds]');
339
+ select lives_ok($$ select agent_board.redeem_invite_link((select code_replay from _ob)) $$,
340
+ 'OB-R02: R replays the same invite [call succeeds]');
341
+ select is(
342
+ (select uses_count from agent_board.board_invite_tokens t, _ob where t.code = _ob.code_replay),
343
+ 1, 'redeem_replay_no_use: replaying the same invite consumes no use');
344
+ select lives_ok($$ select agent_board.redeem_invite_link((select code_other from _ob)) $$,
345
+ 'OB-R03: R, already a member, redeems a different invite [call succeeds]');
346
+ select pg_temp.service();
347
+ select is(
348
+ (select pg_temp.state(_ob.b_links, _ob.r) || '/' || t.uses_count::text
349
+ from agent_board.board_invite_tokens t, _ob where t.code = _ob.code_other),
350
+ 'joined/1', 'redeem_other_invite_consumes: redeeming a different invite while already a member consumes one');
351
+
352
+ select * from finish();
353
+ rollback;