@crewhaus/ir-passes 0.1.4 → 0.1.6

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.
package/src/index.test.ts DELETED
@@ -1,380 +0,0 @@
1
- /**
2
- * Section 28 — `ir-passes` tests:
3
- * - T1 per built-in pass
4
- * - T9 idempotence (apply(apply(x)) === apply(x))
5
- * - T4 fixture replay
6
- */
7
- import { describe, expect, test } from "bun:test";
8
- import type { IrNode, IrV0 } from "@crewhaus/ir";
9
- import {
10
- DEFAULT_PIPELINE,
11
- IrPassError,
12
- applyPasses,
13
- deadToolElimination,
14
- permissionRuleCanonicalize,
15
- promptCachePrefixSort,
16
- redundantMcpServerCollapse,
17
- transactionPolicyEnforcement,
18
- } from "./index";
19
-
20
- function makeCli(overrides: Partial<IrV0> = {}): IrV0 {
21
- return {
22
- version: 0,
23
- name: "test",
24
- target: "cli",
25
- agent: { model: "claude-opus-4-7", instructions: "be helpful" },
26
- tools: [],
27
- toolConfigs: {},
28
- mcp_servers: {},
29
- permissions: { rules: [] },
30
- subAgents: [],
31
- compaction: {},
32
- ...overrides,
33
- };
34
- }
35
-
36
- describe("ir-passes — deadToolElimination (T1)", () => {
37
- test("no rules + no sub-agents → returns input unchanged (no inference)", () => {
38
- const ir = makeCli({ tools: ["Read", "Write", "Bash"] });
39
- const out = deadToolElimination(ir) as IrV0;
40
- expect(out.tools).toEqual(ir.tools);
41
- });
42
-
43
- test("with rules referring only to Read+Bash → drops Write", () => {
44
- const ir = makeCli({
45
- tools: ["Read", "Write", "Bash"],
46
- permissions: {
47
- rules: [
48
- { type: "alwaysAllow", pattern: "Read" },
49
- { type: "alwaysAllow", pattern: "Bash(*)" },
50
- ],
51
- },
52
- });
53
- const out = deadToolElimination(ir) as IrV0;
54
- expect([...out.tools].sort()).toEqual(["Bash", "Read"]);
55
- });
56
-
57
- test("sub-agent that uses Write keeps Write in the parent's tool list", () => {
58
- const ir = makeCli({
59
- tools: ["Read", "Write"],
60
- permissions: { rules: [{ type: "alwaysAllow", pattern: "Read" }] },
61
- subAgents: [
62
- {
63
- name: "writer",
64
- description: "writes files",
65
- instructions: "x",
66
- tools: ["Write"],
67
- permissions: "inherit",
68
- inheritBypass: false,
69
- },
70
- ],
71
- });
72
- const out = deadToolElimination(ir) as IrV0;
73
- expect([...out.tools].sort()).toEqual(["Read", "Write"]);
74
- });
75
-
76
- test("non-cli targets pass through unchanged", () => {
77
- // Use a fixture-shaped non-cli IR
78
- const ir: IrNode = {
79
- version: 0,
80
- name: "wf",
81
- target: "workflow",
82
- steps: [],
83
- } as unknown as IrNode;
84
- expect(deadToolElimination(ir)).toBe(ir);
85
- });
86
- });
87
-
88
- describe("ir-passes — redundantMcpServerCollapse (T1)", () => {
89
- test("dedup stdio servers by (command, args)", () => {
90
- const ir = makeCli({
91
- mcp_servers: {
92
- a: { transport: "stdio", command: "npx", args: ["@x"] },
93
- b: { transport: "stdio", command: "npx", args: ["@x"] },
94
- c: { transport: "stdio", command: "npx", args: ["@y"] },
95
- },
96
- });
97
- const out = redundantMcpServerCollapse(ir) as IrV0;
98
- expect(Object.keys(out.mcp_servers).sort()).toEqual(["a", "c"]);
99
- });
100
-
101
- test("dedup sse servers by url", () => {
102
- const ir = makeCli({
103
- mcp_servers: {
104
- a: { transport: "sse", url: "http://x" },
105
- b: { transport: "sse", url: "http://x" },
106
- c: { transport: "sse", url: "http://y" },
107
- },
108
- });
109
- const out = redundantMcpServerCollapse(ir) as IrV0;
110
- expect(Object.keys(out.mcp_servers).sort()).toEqual(["a", "c"]);
111
- });
112
-
113
- test("returns input unchanged when no duplicates", () => {
114
- const ir = makeCli({
115
- mcp_servers: {
116
- a: { transport: "stdio", command: "x", args: [] },
117
- },
118
- });
119
- expect(redundantMcpServerCollapse(ir)).toBe(ir);
120
- });
121
- });
122
-
123
- describe("ir-passes — permissionRuleCanonicalize (T1)", () => {
124
- test("sorts by tier (deny > ask > allow) then alpha within tier", () => {
125
- const ir = makeCli({
126
- permissions: {
127
- rules: [
128
- { type: "alwaysAllow", pattern: "Read" },
129
- { type: "alwaysDeny", pattern: "Bash(rm *)" },
130
- { type: "alwaysAsk", pattern: "Edit" },
131
- { type: "alwaysAllow", pattern: "Bash" },
132
- { type: "alwaysDeny", pattern: "Write" },
133
- ],
134
- },
135
- });
136
- const out = permissionRuleCanonicalize(ir) as IrV0;
137
- expect(out.permissions.rules.map((r) => `${r.type}:${r.pattern}`)).toEqual([
138
- "alwaysDeny:Bash(rm *)",
139
- "alwaysDeny:Write",
140
- "alwaysAsk:Edit",
141
- "alwaysAllow:Bash",
142
- "alwaysAllow:Read",
143
- ]);
144
- });
145
-
146
- test("dedups exact duplicates", () => {
147
- const ir = makeCli({
148
- permissions: {
149
- rules: [
150
- { type: "alwaysAllow", pattern: "Read" },
151
- { type: "alwaysAllow", pattern: "Read" },
152
- { type: "alwaysAllow", pattern: "Bash" },
153
- ],
154
- },
155
- });
156
- const out = permissionRuleCanonicalize(ir) as IrV0;
157
- expect(out.permissions.rules.length).toBe(2);
158
- });
159
-
160
- test("returns input unchanged when already canonical", () => {
161
- const ir = makeCli({
162
- permissions: {
163
- rules: [
164
- { type: "alwaysDeny", pattern: "Bash(rm *)" },
165
- { type: "alwaysAllow", pattern: "Read" },
166
- ],
167
- },
168
- });
169
- const out = permissionRuleCanonicalize(ir);
170
- expect(out).toBe(ir);
171
- });
172
- });
173
-
174
- describe("ir-passes — promptCachePrefixSort (T1 stub)", () => {
175
- test("v0 stub returns input unchanged", () => {
176
- const ir = makeCli();
177
- expect(promptCachePrefixSort(ir)).toBe(ir);
178
- });
179
- });
180
-
181
- describe("ir-passes — applyPasses + idempotence (T9)", () => {
182
- test("applyPasses runs the default pipeline", () => {
183
- const ir = makeCli({
184
- tools: ["Read", "Write", "Bash"],
185
- permissions: {
186
- rules: [
187
- { type: "alwaysAllow", pattern: "Read" },
188
- { type: "alwaysAllow", pattern: "Read" },
189
- ],
190
- },
191
- mcp_servers: {
192
- a: { transport: "stdio", command: "x", args: [] },
193
- b: { transport: "stdio", command: "x", args: [] },
194
- },
195
- });
196
- const once = applyPasses(ir) as IrV0;
197
- expect([...once.tools].sort()).toEqual(["Read"]);
198
- expect(once.permissions.rules.length).toBe(1);
199
- expect(Object.keys(once.mcp_servers).length).toBe(1);
200
- });
201
-
202
- test("idempotence: applyPasses(applyPasses(x)) === applyPasses(x)", () => {
203
- const ir = makeCli({
204
- tools: ["Read", "Write", "Bash"],
205
- permissions: {
206
- rules: [
207
- { type: "alwaysAllow", pattern: "Read" },
208
- { type: "alwaysDeny", pattern: "Bash" },
209
- ],
210
- },
211
- mcp_servers: {
212
- a: { transport: "stdio", command: "x", args: ["y"] },
213
- b: { transport: "stdio", command: "x", args: ["y"] },
214
- },
215
- });
216
- const a = applyPasses(ir);
217
- const b = applyPasses(a);
218
- expect(JSON.stringify(b)).toBe(JSON.stringify(a));
219
- });
220
-
221
- test("custom pipeline applied in order", () => {
222
- const calls: string[] = [];
223
- const passes = [
224
- (n: IrNode) => {
225
- calls.push("a");
226
- return n;
227
- },
228
- (n: IrNode) => {
229
- calls.push("b");
230
- return n;
231
- },
232
- ];
233
- applyPasses(makeCli(), { passes });
234
- expect(calls).toEqual(["a", "b"]);
235
- });
236
-
237
- test("DEFAULT_PIPELINE has 6 passes (+ wellFormednessCheck from Track F §57)", () => {
238
- expect(DEFAULT_PIPELINE.length).toBe(6);
239
- });
240
- });
241
-
242
- describe("ir-passes — transactionPolicyEnforcement (T1, T8)", () => {
243
- const baseChain = {
244
- id: "base-mainnet",
245
- kind: "evm" as const,
246
- rpcUrls: [{ kind: "literal" as const, value: "https://rpc.test" }],
247
- rpcPolicy: "single" as const,
248
- finality: { kind: "finalized" as const },
249
- reorgTolerant: true,
250
- };
251
- const treasuryWallet = {
252
- id: "treasury",
253
- chainId: "base-mainnet",
254
- custody: "user-controlled" as const,
255
- signingPolicy: "explicit-user-approval" as const,
256
- };
257
- const usdcContract = {
258
- id: "usdc",
259
- chainId: "base-mainnet",
260
- address: "0xusdc",
261
- abiRef: "abi://erc20",
262
- };
263
-
264
- test("empty subsystem is a no-op (existing specs untouched)", () => {
265
- const ir = makeCli();
266
- expect(transactionPolicyEnforcement(ir)).toBe(ir);
267
- });
268
-
269
- test("valid subsystem passes through unchanged", () => {
270
- const ir = makeCli({
271
- chains: [baseChain],
272
- wallets: [treasuryWallet],
273
- contracts: [usdcContract],
274
- transactionPolicy: {
275
- defaultWriteApproval: "required",
276
- allowedContracts: ["usdc"],
277
- simulationRequired: true,
278
- },
279
- });
280
- expect(transactionPolicyEnforcement(ir)).toBe(ir);
281
- });
282
-
283
- test("rejects wallets[].chainId not declared in chains[]", () => {
284
- const ir = makeCli({
285
- chains: [baseChain],
286
- wallets: [{ ...treasuryWallet, chainId: "polygon-mainnet" }],
287
- });
288
- expect(() => transactionPolicyEnforcement(ir)).toThrow(IrPassError);
289
- });
290
-
291
- test("rejects contracts[].chainId not declared in chains[]", () => {
292
- const ir = makeCli({
293
- chains: [baseChain],
294
- contracts: [{ ...usdcContract, chainId: "polygon-mainnet" }],
295
- });
296
- expect(() => transactionPolicyEnforcement(ir)).toThrow(/not declared in chains\[\]/);
297
- });
298
-
299
- test("rejects allowedContracts entry not in contracts[].id", () => {
300
- const ir = makeCli({
301
- chains: [baseChain],
302
- contracts: [usdcContract],
303
- transactionPolicy: {
304
- defaultWriteApproval: "required",
305
- allowedContracts: ["unknown-token"],
306
- simulationRequired: true,
307
- },
308
- });
309
- expect(() => transactionPolicyEnforcement(ir)).toThrow(/unknown-token/);
310
- });
311
-
312
- test("rejects approval=none with non-automated wallet", () => {
313
- const ir = makeCli({
314
- chains: [baseChain],
315
- wallets: [treasuryWallet],
316
- contracts: [usdcContract],
317
- transactionPolicy: {
318
- defaultWriteApproval: "none",
319
- allowedContracts: ["usdc"],
320
- simulationRequired: false,
321
- },
322
- });
323
- expect(() => transactionPolicyEnforcement(ir)).toThrow(/automated/);
324
- });
325
-
326
- test("accepts approval=none when every wallet is automated", () => {
327
- const ir = makeCli({
328
- chains: [baseChain],
329
- wallets: [
330
- {
331
- ...treasuryWallet,
332
- custody: "kms",
333
- signingPolicy: "automated",
334
- keyRef: { kind: "env", name: "KMS_KEY" },
335
- },
336
- ],
337
- contracts: [usdcContract],
338
- transactionPolicy: {
339
- defaultWriteApproval: "none",
340
- allowedContracts: ["usdc"],
341
- simulationRequired: false,
342
- },
343
- });
344
- expect(transactionPolicyEnforcement(ir)).toBe(ir);
345
- });
346
-
347
- test("rejects kms wallet without keyRef", () => {
348
- const ir = makeCli({
349
- chains: [baseChain],
350
- wallets: [{ ...treasuryWallet, custody: "kms" }],
351
- });
352
- expect(() => transactionPolicyEnforcement(ir)).toThrow(/keyRef/);
353
- });
354
-
355
- test("rejects duplicate chains[].id", () => {
356
- const ir = makeCli({ chains: [baseChain, baseChain] });
357
- expect(() => transactionPolicyEnforcement(ir)).toThrow(/duplicate chains/);
358
- });
359
-
360
- test("rejects duplicate wallets[].id", () => {
361
- const ir = makeCli({
362
- chains: [baseChain],
363
- wallets: [treasuryWallet, treasuryWallet],
364
- });
365
- expect(() => transactionPolicyEnforcement(ir)).toThrow(/duplicate wallets/);
366
- });
367
-
368
- test("non-blockchain shapes (managed, voice, browser, eval) pass through unchanged", () => {
369
- const ir: IrNode = {
370
- version: 0,
371
- name: "test-mgd",
372
- target: "managed",
373
- agent: { model: "claude-opus-4-7", instructions: "hi" },
374
- tenants: [{ id: "t1", budget: { maxInputTokens: 1, maxOutputTokens: 1 } }],
375
- permissions: { rules: [] },
376
- compaction: {},
377
- };
378
- expect(transactionPolicyEnforcement(ir)).toBe(ir);
379
- });
380
- });