incur 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/Cli.test.ts CHANGED
@@ -1,5 +1,13 @@
1
1
  import { Cli, Errors, z } from 'incur'
2
2
 
3
+ const originalIsTTY = process.stdout.isTTY
4
+ beforeAll(() => {
5
+ ;(process.stdout as any).isTTY = false
6
+ })
7
+ afterAll(() => {
8
+ ;(process.stdout as any).isTTY = originalIsTTY
9
+ })
10
+
3
11
  let __mockSkillsHash: string | undefined
4
12
 
5
13
  vi.mock('./SyncSkills.js', async (importOriginal) => {
@@ -46,8 +54,8 @@ describe('command', () => {
46
54
  const cli = Cli.create('test')
47
55
  const result = cli.command('greet', {
48
56
  args: z.object({ name: z.string() }),
49
- run({ args }) {
50
- return { message: `hello ${args.name}` }
57
+ run(c) {
58
+ return { message: `hello ${c.args.name}` }
51
59
  },
52
60
  })
53
61
  expect(result).toBe(cli)
@@ -59,8 +67,8 @@ describe('serve', () => {
59
67
  const cli = Cli.create('test')
60
68
  cli.command('greet', {
61
69
  args: z.object({ name: z.string() }),
62
- run({ args }) {
63
- return { message: `hello ${args.name}` }
70
+ run(c) {
71
+ return { message: `hello ${c.args.name}` }
64
72
  },
65
73
  })
66
74
 
@@ -75,8 +83,8 @@ describe('serve', () => {
75
83
  const cli = Cli.create('test')
76
84
  cli.command('greet', {
77
85
  args: z.object({ name: z.string() }),
78
- run({ args }) {
79
- return { message: `hello ${args.name}` }
86
+ run(c) {
87
+ return { message: `hello ${c.args.name}` }
80
88
  },
81
89
  })
82
90
 
@@ -97,8 +105,8 @@ describe('serve', () => {
97
105
  let receivedArgs: any
98
106
  cli.command('add', {
99
107
  args: z.object({ a: z.string(), b: z.string() }),
100
- run({ args }) {
101
- receivedArgs = args
108
+ run(c) {
109
+ receivedArgs = c.args
102
110
  return {}
103
111
  },
104
112
  })
@@ -128,6 +136,20 @@ describe('serve', () => {
128
136
 
129
137
  const { output, exitCode } = await serve(cli, ['nonexistent'])
130
138
  expect(exitCode).toBe(1)
139
+ expect(output).toMatchInlineSnapshot(`
140
+ "code: COMMAND_NOT_FOUND
141
+ message: 'nonexistent' is not a command. See 'test --help' for a list of available commands.
142
+ "
143
+ `)
144
+ })
145
+
146
+ test('outputs human error for unknown command in TTY', async () => {
147
+ ;(process.stdout as any).isTTY = true
148
+ const cli = Cli.create('test')
149
+
150
+ const { output, exitCode } = await serve(cli, ['nonexistent'])
151
+ ;(process.stdout as any).isTTY = false
152
+ expect(exitCode).toBe(1)
131
153
  expect(output).toMatchInlineSnapshot(`
132
154
  "Error: 'nonexistent' is not a command. See 'test --help' for a list of available commands.
133
155
  "
@@ -161,6 +183,25 @@ describe('serve', () => {
161
183
 
162
184
  const { output, exitCode } = await serve(cli, ['fail'])
163
185
  expect(exitCode).toBe(1)
186
+ expect(output).toMatchInlineSnapshot(`
187
+ "code: UNKNOWN
188
+ message: boom
189
+ "
190
+ `)
191
+ })
192
+
193
+ test('wraps handler errors in human format in TTY', async () => {
194
+ ;(process.stdout as any).isTTY = true
195
+ const cli = Cli.create('test')
196
+ cli.command('fail', {
197
+ run() {
198
+ throw new Error('boom')
199
+ },
200
+ })
201
+
202
+ const { output, exitCode } = await serve(cli, ['fail'])
203
+ ;(process.stdout as any).isTTY = false
204
+ expect(exitCode).toBe(1)
164
205
  expect(output).toMatchInlineSnapshot(`
165
206
  "Error: boom
166
207
  "
@@ -181,6 +222,30 @@ describe('serve', () => {
181
222
 
182
223
  const { output, exitCode } = await serve(cli, ['fail'])
183
224
  expect(exitCode).toBe(1)
225
+ expect(output).toMatchInlineSnapshot(`
226
+ "code: NOT_AUTHENTICATED
227
+ message: Token not found
228
+ retryable: false
229
+ "
230
+ `)
231
+ })
232
+
233
+ test('IncurError shows human format in TTY', async () => {
234
+ ;(process.stdout as any).isTTY = true
235
+ const cli = Cli.create('test')
236
+ cli.command('fail', {
237
+ run() {
238
+ throw new Errors.IncurError({
239
+ code: 'NOT_AUTHENTICATED',
240
+ message: 'Token not found',
241
+ retryable: false,
242
+ })
243
+ },
244
+ })
245
+
246
+ const { output, exitCode } = await serve(cli, ['fail'])
247
+ ;(process.stdout as any).isTTY = false
248
+ expect(exitCode).toBe(1)
184
249
  expect(output).toMatchInlineSnapshot(`
185
250
  "Error (NOT_AUTHENTICATED): Token not found
186
251
  "
@@ -191,16 +256,62 @@ describe('serve', () => {
191
256
  const cli = Cli.create('test')
192
257
  cli.command('greet', {
193
258
  args: z.object({ name: z.string() }),
194
- run({ args }) {
195
- return { message: `hello ${args.name}` }
259
+ run(c) {
260
+ return { message: `hello ${c.args.name}` }
196
261
  },
197
262
  })
198
263
 
199
264
  const { output, exitCode } = await serve(cli, ['greet'])
200
265
  expect(exitCode).toBe(1)
266
+ expect(output).toContain('VALIDATION_ERROR')
267
+ })
268
+
269
+ test('ValidationError shows human format in TTY', async () => {
270
+ ;(process.stdout as any).isTTY = true
271
+ const cli = Cli.create('test')
272
+ cli.command('greet', {
273
+ args: z.object({ name: z.string() }),
274
+ run(c) {
275
+ return { message: `hello ${c.args.name}` }
276
+ },
277
+ })
278
+
279
+ const { output, exitCode } = await serve(cli, ['greet'])
280
+ ;(process.stdout as any).isTTY = false
281
+ expect(exitCode).toBe(1)
201
282
  expect(output).toContain('Error: missing required argument <name>')
202
283
  })
203
284
 
285
+ test('agent is true when not TTY', async () => {
286
+ let agent: boolean | undefined
287
+ const cli = Cli.create('test')
288
+ cli.command('ping', {
289
+ run(c) {
290
+ agent = c.agent
291
+ return {}
292
+ },
293
+ })
294
+
295
+ await serve(cli, ['ping'])
296
+ expect(agent).toBe(true)
297
+ })
298
+
299
+ test('agent is false when TTY', async () => {
300
+ ;(process.stdout as any).isTTY = true
301
+ let agent: boolean | undefined
302
+ const cli = Cli.create('test')
303
+ cli.command('ping', {
304
+ run(c) {
305
+ agent = c.agent
306
+ return {}
307
+ },
308
+ })
309
+
310
+ await serve(cli, ['ping'])
311
+ ;(process.stdout as any).isTTY = false
312
+ expect(agent).toBe(false)
313
+ })
314
+
204
315
  test('supports async handlers', async () => {
205
316
  const cli = Cli.create('test')
206
317
  cli.command('async', {
@@ -567,6 +678,24 @@ describe('subcommands', () => {
567
678
 
568
679
  const { output, exitCode } = await serve(cli, ['pr', 'unknown'])
569
680
  expect(exitCode).toBe(1)
681
+ expect(output).toMatchInlineSnapshot(`
682
+ "code: COMMAND_NOT_FOUND
683
+ message: 'unknown' is not a command. See 'test pr --help' for a list of available commands.
684
+ "
685
+ `)
686
+ })
687
+
688
+ test('unknown subcommand shows human error in TTY', async () => {
689
+ ;(process.stdout as any).isTTY = true
690
+ const cli = Cli.create('test')
691
+ const pr = Cli.create('pr', { description: 'PR management' })
692
+ .command('list', { run: () => ({}) })
693
+ .command('create', { run: () => ({}) })
694
+ cli.command(pr)
695
+
696
+ const { output, exitCode } = await serve(cli, ['pr', 'unknown'])
697
+ ;(process.stdout as any).isTTY = false
698
+ expect(exitCode).toBe(1)
570
699
  expect(output).toMatchInlineSnapshot(`
571
700
  "Error: 'unknown' is not a command. See 'test pr --help' for a list of available commands.
572
701
  "
@@ -628,6 +757,26 @@ describe('subcommands', () => {
628
757
 
629
758
  const { output, exitCode } = await serve(cli, ['pr', 'fail'])
630
759
  expect(exitCode).toBe(1)
760
+ expect(output).toMatchInlineSnapshot(`
761
+ "code: UNKNOWN
762
+ message: sub-boom
763
+ "
764
+ `)
765
+ })
766
+
767
+ test('error in sub-command shows human format in TTY', async () => {
768
+ ;(process.stdout as any).isTTY = true
769
+ const cli = Cli.create('test')
770
+ const pr = Cli.create('pr', { description: 'PR management' }).command('fail', {
771
+ run() {
772
+ throw new Error('sub-boom')
773
+ },
774
+ })
775
+ cli.command(pr)
776
+
777
+ const { output, exitCode } = await serve(cli, ['pr', 'fail'])
778
+ ;(process.stdout as any).isTTY = false
779
+ expect(exitCode).toBe(1)
631
780
  expect(output).toMatchInlineSnapshot(`
632
781
  "Error: sub-boom
633
782
  "
@@ -653,8 +802,8 @@ describe('cta', () => {
653
802
  test('string shorthand for cta commands', async () => {
654
803
  const cli = Cli.create('test')
655
804
  cli.command('list', {
656
- run({ ok }) {
657
- return ok({ items: [] }, { cta: { commands: ['get 1', 'get 2'] } })
805
+ run(c) {
806
+ return c.ok({ items: [] }, { cta: { commands: ['get 1', 'get 2'] } })
658
807
  },
659
808
  })
660
809
 
@@ -669,8 +818,8 @@ describe('cta', () => {
669
818
  test('tuple shorthand with description', async () => {
670
819
  const cli = Cli.create('test')
671
820
  cli.command('list', {
672
- run({ ok }) {
673
- return ok(
821
+ run(c) {
822
+ return c.ok(
674
823
  { items: [] },
675
824
  {
676
825
  cta: { commands: [{ command: 'get 1', description: 'View item 1' }] },
@@ -689,8 +838,8 @@ describe('cta', () => {
689
838
  test('tuple form with args/options', async () => {
690
839
  const cli = Cli.create('test')
691
840
  cli.command('create', {
692
- run({ ok }) {
693
- return ok(
841
+ run(c) {
842
+ return c.ok(
694
843
  { id: 1 },
695
844
  {
696
845
  cta: {
@@ -718,8 +867,8 @@ describe('cta', () => {
718
867
  test('tuple form boolean args format as placeholders', async () => {
719
868
  const cli = Cli.create('test')
720
869
  cli.command('list', {
721
- run({ ok }) {
722
- return ok(
870
+ run(c) {
871
+ return c.ok(
723
872
  { items: [] },
724
873
  {
725
874
  cta: { commands: [{ command: 'get', args: { id: true }, options: { format: true } }] },
@@ -736,8 +885,8 @@ describe('cta', () => {
736
885
  test('custom cta description', async () => {
737
886
  const cli = Cli.create('test')
738
887
  cli.command('create', {
739
- run({ ok }) {
740
- return ok(
888
+ run(c) {
889
+ return c.ok(
741
890
  { id: 1 },
742
891
  {
743
892
  cta: { description: 'View the created item:', commands: ['get 1'] },
@@ -910,6 +1059,23 @@ describe('leaf cli', () => {
910
1059
  })
911
1060
  const { output, exitCode } = await serve(cli, [])
912
1061
  expect(exitCode).toBe(1)
1062
+ expect(output).toMatchInlineSnapshot(`
1063
+ "code: UNKNOWN
1064
+ message: boom
1065
+ "
1066
+ `)
1067
+ })
1068
+
1069
+ test('errors show human format in TTY', async () => {
1070
+ ;(process.stdout as any).isTTY = true
1071
+ const cli = Cli.create('fail', {
1072
+ run() {
1073
+ throw new Error('boom')
1074
+ },
1075
+ })
1076
+ const { output, exitCode } = await serve(cli, [])
1077
+ ;(process.stdout as any).isTTY = false
1078
+ expect(exitCode).toBe(1)
913
1079
  expect(output).toMatchInlineSnapshot(`
914
1080
  "Error: boom
915
1081
  "
@@ -1172,8 +1338,8 @@ describe('env', () => {
1172
1338
  env: z.object({
1173
1339
  API_TOKEN: z.string().describe('Auth token'),
1174
1340
  }),
1175
- run({ env }) {
1176
- receivedEnv = env
1341
+ run(c) {
1342
+ receivedEnv = c.env
1177
1343
  return { ok: true }
1178
1344
  },
1179
1345
  })
@@ -1205,8 +1371,8 @@ describe('env', () => {
1205
1371
  env: z.object({
1206
1372
  API_URL: z.string().default('https://api.example.com').describe('API URL'),
1207
1373
  }),
1208
- run({ env }) {
1209
- receivedEnv = env
1374
+ run(c) {
1375
+ receivedEnv = c.env
1210
1376
  return { ok: true }
1211
1377
  },
1212
1378
  })
@@ -1300,8 +1466,8 @@ describe('env', () => {
1300
1466
  DEBUG: z.boolean().default(false).describe('Debug mode'),
1301
1467
  PORT: z.number().default(3000).describe('Port'),
1302
1468
  }),
1303
- run({ env }) {
1304
- receivedEnv = env
1469
+ run(c) {
1470
+ receivedEnv = c.env
1305
1471
  return { ok: true }
1306
1472
  },
1307
1473
  })
@@ -1329,9 +1495,7 @@ describe('skills staleness', () => {
1329
1495
  cli.command('ping', { description: 'Health check', run: () => ({ pong: true }) })
1330
1496
 
1331
1497
  await serve(cli, ['ping'])
1332
- expect(stderrSpy).toHaveBeenCalledWith(
1333
- expect.stringContaining("Skills are out of date. Run 'pnpx test skills add' to update."),
1334
- )
1498
+ expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining("Skills are out of date. Run '"))
1335
1499
  })
1336
1500
 
1337
1501
  test('does not warn when hash matches', async () => {
@@ -1371,3 +1535,256 @@ describe('skills staleness', () => {
1371
1535
  expect(stderrSpy).not.toHaveBeenCalled()
1372
1536
  })
1373
1537
  })
1538
+
1539
+ describe('outputPolicy', () => {
1540
+ beforeEach(() => {
1541
+ ;(process.stdout as any).isTTY = true
1542
+ })
1543
+ afterEach(() => {
1544
+ ;(process.stdout as any).isTTY = false
1545
+ })
1546
+
1547
+ test('default (all): displays data in human mode', async () => {
1548
+ const cli = Cli.create('test')
1549
+ cli.command('ping', { run: () => ({ pong: true }) })
1550
+
1551
+ const { output } = await serve(cli, ['ping'])
1552
+ expect(output).toContain('pong: true')
1553
+ })
1554
+
1555
+ test('agent-only on command: suppresses data in human mode', async () => {
1556
+ const cli = Cli.create('test')
1557
+ cli.command('ping', { outputPolicy: 'agent-only', run: () => ({ pong: true }) })
1558
+
1559
+ const { output } = await serve(cli, ['ping'])
1560
+ expect(output).toBe('')
1561
+ })
1562
+
1563
+ test('agent-only on command: still outputs in agent mode (--json)', async () => {
1564
+ const cli = Cli.create('test')
1565
+ cli.command('ping', { outputPolicy: 'agent-only', run: () => ({ pong: true }) })
1566
+
1567
+ const { output } = await serve(cli, ['ping', '--json'])
1568
+ expect(output).toContain('"pong"')
1569
+ })
1570
+
1571
+ test('agent-only on root CLI: inherited by commands', async () => {
1572
+ const cli = Cli.create('test', { outputPolicy: 'agent-only' })
1573
+ cli.command('ping', { run: () => ({ pong: true }) })
1574
+
1575
+ const { output } = await serve(cli, ['ping'])
1576
+ expect(output).toBe('')
1577
+ })
1578
+
1579
+ test('agent-only on group: inherited by child commands', async () => {
1580
+ const cli = Cli.create('test')
1581
+ const sub = Cli.create('sub', { outputPolicy: 'agent-only' })
1582
+ sub.command('ping', { run: () => ({ pong: true }) })
1583
+ cli.command(sub)
1584
+
1585
+ const { output } = await serve(cli, ['sub', 'ping'])
1586
+ expect(output).toBe('')
1587
+ })
1588
+
1589
+ test('command overrides group outputPolicy', async () => {
1590
+ const cli = Cli.create('test')
1591
+ const sub = Cli.create('sub', { outputPolicy: 'agent-only' })
1592
+ sub.command('ping', { outputPolicy: 'all', run: () => ({ pong: true }) })
1593
+ cli.command(sub)
1594
+
1595
+ const { output } = await serve(cli, ['sub', 'ping'])
1596
+ expect(output).toContain('pong: true')
1597
+ })
1598
+
1599
+ test('agent-only suppresses streaming chunks in human mode', async () => {
1600
+ const cli = Cli.create('test')
1601
+ cli.command('stream', {
1602
+ outputPolicy: 'agent-only',
1603
+ async *run() {
1604
+ yield { step: 1 }
1605
+ yield { step: 2 }
1606
+ },
1607
+ })
1608
+
1609
+ const { output } = await serve(cli, ['stream'])
1610
+ expect(output).toBe('')
1611
+ })
1612
+
1613
+ test('agent-only still shows errors in human mode', async () => {
1614
+ const cli = Cli.create('test')
1615
+ cli.command('fail', {
1616
+ outputPolicy: 'agent-only',
1617
+ run(c) {
1618
+ return c.error({ code: 'FAILED', message: 'something broke' })
1619
+ },
1620
+ })
1621
+
1622
+ const { output } = await serve(cli, ['fail'])
1623
+ expect(output).toContain('Error (FAILED): something broke')
1624
+ })
1625
+
1626
+ test('agent-only still shows CTAs in human mode', async () => {
1627
+ const cli = Cli.create('test')
1628
+ cli.command('ping', {
1629
+ outputPolicy: 'agent-only',
1630
+ run(c) {
1631
+ return c.ok({ pong: true }, { cta: { commands: ['ping'] } })
1632
+ },
1633
+ })
1634
+
1635
+ const { output } = await serve(cli, ['ping'])
1636
+ expect(output).not.toContain('pong')
1637
+ expect(output).toContain('ping')
1638
+ })
1639
+
1640
+ test('agent-only suppresses data when TTY', async () => {
1641
+ ;(process.stdout as any).isTTY = true
1642
+ const cli = Cli.create('test')
1643
+ cli.command('ping', { outputPolicy: 'agent-only', run: () => ({ pong: true }) })
1644
+
1645
+ const { output } = await serve(cli, ['ping'])
1646
+ expect(output).toBe('')
1647
+ })
1648
+
1649
+ test('agent-only still displays data when not TTY (piped)', async () => {
1650
+ ;(process.stdout as any).isTTY = false
1651
+ const cli = Cli.create('test')
1652
+ cli.command('ping', { outputPolicy: 'agent-only', run: () => ({ pong: true }) })
1653
+
1654
+ const { output } = await serve(cli, ['ping'])
1655
+ expect(output).toContain('pong')
1656
+ })
1657
+
1658
+ test('all displays data regardless of TTY', async () => {
1659
+ const cli = Cli.create('test')
1660
+ cli.command('ping', { outputPolicy: 'all', run: () => ({ pong: true }) })
1661
+
1662
+ ;(process.stdout as any).isTTY = true
1663
+ const tty = await serve(cli, ['ping'])
1664
+ expect(tty.output).toContain('pong: true')
1665
+
1666
+ ;(process.stdout as any).isTTY = false
1667
+ const piped = await serve(cli, ['ping'])
1668
+ expect(piped.output).toContain('pong')
1669
+ })
1670
+
1671
+ test('agent-only streaming suppresses when TTY, outputs when piped', async () => {
1672
+ const cli = Cli.create('test')
1673
+ cli.command('stream', {
1674
+ outputPolicy: 'agent-only',
1675
+ async *run() {
1676
+ yield { step: 1 }
1677
+ },
1678
+ })
1679
+
1680
+ ;(process.stdout as any).isTTY = true
1681
+ const tty = await serve(cli, ['stream'])
1682
+ expect(tty.output).toBe('')
1683
+
1684
+ ;(process.stdout as any).isTTY = false
1685
+ const piped = await serve(cli, ['stream'])
1686
+ expect(piped.output).toContain('step: 1')
1687
+ })
1688
+
1689
+ test('e2e: realistic multi-level CLI with mixed policies', async () => {
1690
+ const cli = Cli.create('tool', { description: 'A deployment tool' })
1691
+
1692
+ // Top-level command with agent-only
1693
+ cli.command('deploy', {
1694
+ outputPolicy: 'agent-only',
1695
+ args: z.object({ env: z.enum(['staging', 'production']) }),
1696
+ run(c) {
1697
+ return c.ok(
1698
+ { id: 'deploy-123', url: `https://${c.args.env}.example.com` },
1699
+ { cta: { commands: [{ command: 'status', description: 'Check status' }] } },
1700
+ )
1701
+ },
1702
+ })
1703
+
1704
+ // Group with inherited agent-only
1705
+ const internal = Cli.create('internal', {
1706
+ description: 'Internal commands',
1707
+ outputPolicy: 'agent-only',
1708
+ })
1709
+ internal.command('sync', { run: () => ({ synced: 42, duration: '1.2s' }) })
1710
+ internal.command('healthcheck', {
1711
+ outputPolicy: 'all',
1712
+ run: () => ({ healthy: true }),
1713
+ })
1714
+
1715
+ // Group without policy — children default to 'all'
1716
+ const db = Cli.create('db', { description: 'Database commands' })
1717
+ db.command('migrate', { run: () => ({ migrated: 3 }) })
1718
+
1719
+ cli.command(internal)
1720
+ cli.command(db)
1721
+
1722
+ // deploy: agent-only suppresses data, shows CTA
1723
+ const deploy = await serve(cli, ['deploy', 'staging'])
1724
+ expect(deploy.output).not.toContain('deploy-123')
1725
+ expect(deploy.output).toContain('Check status')
1726
+
1727
+ // deploy --verbose: agent mode shows everything
1728
+ const deployVerbose = await serve(cli, ['deploy', 'staging', '--verbose'])
1729
+ expect(deployVerbose.output).toContain('deploy-123')
1730
+ expect(deployVerbose.output).toContain('staging.example.com')
1731
+
1732
+ // deploy --json: agent mode shows data
1733
+ const deployJson = await serve(cli, ['deploy', 'staging', '--json'])
1734
+ expect(deployJson.output).toContain('deploy-123')
1735
+
1736
+ // internal sync: inherits agent-only from group
1737
+ const sync = await serve(cli, ['internal', 'sync'])
1738
+ expect(sync.output).toBe('')
1739
+
1740
+ // internal sync --json: agent mode works
1741
+ const syncJson = await serve(cli, ['internal', 'sync', '--json'])
1742
+ expect(syncJson.output).toContain('42')
1743
+
1744
+ // internal healthcheck: overrides to 'all'
1745
+ const health = await serve(cli, ['internal', 'healthcheck'])
1746
+ expect(health.output).toContain('healthy: true')
1747
+
1748
+ // db migrate: no policy, defaults to 'all'
1749
+ const migrate = await serve(cli, ['db', 'migrate'])
1750
+ expect(migrate.output).toContain('migrated: 3')
1751
+ })
1752
+
1753
+ test('e2e: agent-only with streaming and error in nested group', async () => {
1754
+ const cli = Cli.create('tool')
1755
+ const ops = Cli.create('ops', {
1756
+ description: 'Operations',
1757
+ outputPolicy: 'agent-only',
1758
+ })
1759
+
1760
+ ops.command('logs', {
1761
+ async *run() {
1762
+ yield { line: 'Starting...' }
1763
+ yield { line: 'Processing...' }
1764
+ yield { line: 'Done.' }
1765
+ },
1766
+ })
1767
+
1768
+ ops.command('restart', {
1769
+ run(c) {
1770
+ return c.error({ code: 'PERMISSION_DENIED', message: 'Requires admin role' })
1771
+ },
1772
+ })
1773
+
1774
+ cli.command(ops)
1775
+
1776
+ // Streaming: agent-only suppresses chunks in human mode
1777
+ const logs = await serve(cli, ['ops', 'logs'])
1778
+ expect(logs.output).toBe('')
1779
+
1780
+ // Streaming: --format jsonl still works
1781
+ const logsJsonl = await serve(cli, ['ops', 'logs', '--format', 'jsonl'])
1782
+ expect(logsJsonl.output).toContain('"type":"chunk"')
1783
+ expect(logsJsonl.output).toContain('Starting...')
1784
+
1785
+ // Errors still display in human mode despite agent-only
1786
+ const restart = await serve(cli, ['ops', 'restart'])
1787
+ expect(restart.output).toContain('Error (PERMISSION_DENIED): Requires admin role')
1788
+ expect(restart.exitCode).toBe(1)
1789
+ })
1790
+ })