Skip to content

Commit af5df84

Browse files
os-warrenclaude
andcommitted
test(approvals): measurement harness for the #10153 manager org screen (no fix)
Pins the CURRENT behaviour so the premise and the tiering question are reproducible: the manager branch resolves across the organization boundary, the sibling position expansion is screened (and is not reject-everything), team is not screened either, and a sole cross-org manager approver under onEmptyApprovers: 'fail' opens today while a screened type in the identical shape throws NO_APPROVERS. No fix is implemented. The card tripped its re-tiering wire (Clause-2) and is handed back for dispatch at the required tier. Part of #10153 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PnJHU45vPJj5UQrxe946Bx
1 parent 04096f1 commit af5df84

1 file changed

Lines changed: 145 additions & 0 deletions

File tree

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
/**
3+
* MEASUREMENT HARNESS for #10153 — it pins the CURRENT (defective) behaviour on
4+
* purpose, so the premise and the tiering question are reproducible rather than
5+
* argued. It is NOT the fix and it is NOT on a pull request.
6+
*
7+
* ⛔ Whoever implements the org screen must INVERT `PREMISE A` and `CLAUSE-2 (a)`
8+
* — they assert today's cross-org resolution, which is exactly what the fix
9+
* removes. `PREMISE B` / `B2` / `CLAUSE-2 (b)` describe the sibling treatment
10+
* and stay as they are.
11+
*
12+
* What it measures, all on one tree:
13+
* A — `manager` resolves a manager whose only `sys_member` row is in
14+
* another organization, into this organization's approver slate.
15+
* B/B2 — the sibling `position` expansion IS screened, and the screen is not
16+
* reject-everything (a same-org holder still resolves).
17+
* W — `team` is NOT screened either, which is why #10153's warrant
18+
* ("every sibling expansion is org-scoped") does not hold as stated.
19+
* C-a/b — the tiering question: today a sole cross-org `manager` approver
20+
* under `onEmptyApprovers: 'fail'` OPENS the request; a screened type
21+
* in the identical shape THROWS `NO_APPROVERS`. Applying the screen
22+
* therefore moves that input from accepted to refused.
23+
*/
24+
import { describe, it, expect, beforeEach } from 'vitest';
25+
import { ApprovalService } from './approval-service.js';
26+
27+
function makeFakeEngine() {
28+
const tables: Record<string, any[]> = {};
29+
const ensure = (n: string) => (tables[n] ??= []);
30+
function matches(row: any, filter: any): boolean {
31+
if (!filter || typeof filter !== 'object') return true;
32+
for (const [k, v] of Object.entries(filter)) {
33+
if (k === '$or') { if (!(v as any[]).some(s => matches(row, s))) return false; continue; }
34+
if (k === '$and') { if (!(v as any[]).every(s => matches(row, s))) return false; continue; }
35+
const rv = row[k];
36+
if (v != null && typeof v === 'object' && '$in' in (v as any)) {
37+
if (!(v as any).$in.includes(rv)) return false; continue;
38+
}
39+
if (v != null && typeof v === 'object' && '$ne' in (v as any)) {
40+
if (rv === (v as any).$ne) return false; continue;
41+
}
42+
if (rv !== v) return false;
43+
}
44+
return true;
45+
}
46+
return {
47+
_tables: tables,
48+
async find(object: string, options?: any) {
49+
const rows = ensure(object).filter(r => matches(r, options?.filter ?? options?.where));
50+
return rows.slice(0, options?.limit ?? 1000);
51+
},
52+
async insert(object: string, data: any) { ensure(object).push({ ...data }); return { ...data }; },
53+
async update(object: string, idOrData: any, _opts?: any) {
54+
const data = typeof idOrData === 'object' ? idOrData : _opts;
55+
const id = typeof idOrData === 'object' ? idOrData.id : idOrData;
56+
const t = ensure(object); const i = t.findIndex(r => r.id === id);
57+
if (i >= 0) t[i] = { ...t[i], ...data };
58+
return t[i];
59+
},
60+
async delete() { return {}; },
61+
registerHook() {}, unregisterHooksByPackage() { return 0; }, async fire() {},
62+
};
63+
}
64+
65+
const ORG_A = 'org_a';
66+
const CTX_A = { userId: 'u_sub', organizationId: ORG_A, positions: [], permissions: [] } as any;
67+
68+
function input(approvers: any[], configExtra: Record<string, any> = {}) {
69+
return {
70+
object: 'opportunity', recordId: 'opp1', runId: 'run_1', nodeId: 'approve_step',
71+
flowName: 'deal_approval',
72+
config: { approvers, behavior: 'first_response' as const, lockRecord: false, ...configExtra },
73+
record: { id: 'opp1', owner_id: 'u_sub', amount: 100 },
74+
};
75+
}
76+
77+
describe('#10153 probe', () => {
78+
let engine: ReturnType<typeof makeFakeEngine>;
79+
let svc: ApprovalService;
80+
let n = 0;
81+
82+
beforeEach(() => {
83+
engine = makeFakeEngine();
84+
n = 0;
85+
svc = new ApprovalService({
86+
engine: engine as any,
87+
clock: { now: () => new Date(new Date('2026-01-15T10:00:00Z').getTime() + (n++) * 1000) },
88+
});
89+
// Directory: submitter in org_a; his manager is a member of org_b ONLY.
90+
engine._tables['sys_user'] = [
91+
{ id: 'u_sub', manager_id: 'u_mgr_b' },
92+
{ id: 'u_mgr_b', manager_id: null },
93+
];
94+
engine._tables['sys_member'] = [
95+
{ id: 'm1', user_id: 'u_sub', organization_id: ORG_A, role: 'member' },
96+
{ id: 'm2', user_id: 'u_mgr_b', organization_id: 'org_b', role: 'member' },
97+
];
98+
// Sibling directory: the only `cfo` holder is in org_b.
99+
engine._tables['sys_user_position'] = [
100+
{ id: 'p1', user_id: 'u_pos_b', position: 'cfo', organization_id: 'org_b' },
101+
];
102+
});
103+
104+
it('PREMISE A — `manager` resolves ACROSS the org boundary (unscreened)', async () => {
105+
const req = await svc.openNodeRequest(input([{ type: 'manager' }]), CTX_A);
106+
console.log('[PROBE A] request org =', req.organization_id, 'pending_approvers =', JSON.stringify(req.pending_approvers));
107+
expect(req.pending_approvers).toEqual(['u_mgr_b']);
108+
});
109+
110+
it('PREMISE B — sibling `position` IS screened to the request org (same tree)', async () => {
111+
const req = await svc.openNodeRequest(input([{ type: 'position', value: 'cfo' }]), CTX_A);
112+
console.log('[PROBE B] pending_approvers =', JSON.stringify(req.pending_approvers));
113+
expect(req.pending_approvers).toEqual(['position:cfo']);
114+
});
115+
116+
it('PREMISE B2 — same-org `position` holder DOES resolve (screen is not reject-everything)', async () => {
117+
engine._tables['sys_user_position'].push({ id: 'p2', user_id: 'u_pos_a', position: 'cfo', organization_id: ORG_A });
118+
const req = await svc.openNodeRequest(input([{ type: 'position', value: 'cfo' }]), CTX_A);
119+
console.log('[PROBE B2] pending_approvers =', JSON.stringify(req.pending_approvers));
120+
expect(req.pending_approvers).toEqual(['u_pos_a']);
121+
});
122+
123+
it('CLAUSE-2 (a) — TODAY: sole cross-org `manager` + onEmptyApprovers:fail SUCCEEDS', async () => {
124+
const req = await svc.openNodeRequest(input([{ type: 'manager' }], { onEmptyApprovers: 'fail' }), CTX_A);
125+
console.log('[PROBE C-a] opened OK, status =', req.status, 'approvers =', JSON.stringify(req.pending_approvers));
126+
expect(req.status).toBe('pending');
127+
});
128+
129+
it('CLAUSE-2 (b) — a SCREENED sibling in the same shape THROWS NO_APPROVERS', async () => {
130+
let err: any = null;
131+
try {
132+
await svc.openNodeRequest(input([{ type: 'position', value: 'cfo' }], { onEmptyApprovers: 'fail' }), CTX_A);
133+
} catch (e) { err = e; }
134+
console.log('[PROBE C-b] threw =', err ? String(err.message).slice(0, 90) : 'NOTHING');
135+
expect(err).toBeTruthy();
136+
});
137+
138+
it('WARRANT — sibling `team` is NOT org-screened either (cross-org team resolves)', async () => {
139+
engine._tables['sys_team'] = [{ id: 'team_b', name: 'B team', organization_id: 'org_b' }];
140+
engine._tables['sys_team_member'] = [{ id: 'tm1', team_id: 'team_b', user_id: 'u_team_b' }];
141+
const req = await svc.openNodeRequest(input([{ type: 'team', value: 'team_b' }]), CTX_A);
142+
console.log('[PROBE W] org_a request, org_b team -> pending_approvers =', JSON.stringify(req.pending_approvers));
143+
expect(req.pending_approvers).toEqual(['u_team_b']);
144+
});
145+
});

0 commit comments

Comments
 (0)