incur 0.2.0 → 0.2.2
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/README.md +158 -9
- package/SKILL.md +149 -0
- package/dist/Cli.d.ts +16 -4
- package/dist/Cli.d.ts.map +1 -1
- package/dist/Cli.js +384 -31
- package/dist/Cli.js.map +1 -1
- package/dist/Errors.d.ts +4 -0
- package/dist/Errors.d.ts.map +1 -1
- package/dist/Errors.js +3 -0
- package/dist/Errors.js.map +1 -1
- package/dist/Filter.d.ts +14 -0
- package/dist/Filter.d.ts.map +1 -0
- package/dist/Filter.js +134 -0
- package/dist/Filter.js.map +1 -0
- package/dist/Help.js +5 -0
- package/dist/Help.js.map +1 -1
- package/dist/Mcp.d.ts +26 -0
- package/dist/Mcp.d.ts.map +1 -1
- package/dist/Mcp.js +2 -2
- package/dist/Mcp.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/middleware.d.ts +8 -2
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js.map +1 -1
- package/package.json +2 -1
- package/src/Cli.test-d.ts +25 -0
- package/src/Cli.test.ts +829 -0
- package/src/Cli.ts +492 -37
- package/src/Errors.ts +5 -0
- package/src/Filter.test.ts +237 -0
- package/src/Filter.ts +139 -0
- package/src/Help.test.ts +35 -0
- package/src/Help.ts +5 -0
- package/src/Mcp.ts +3 -3
- package/src/e2e.test.ts +715 -0
- package/src/index.ts +1 -0
- package/src/middleware.ts +9 -2
package/src/Cli.test.ts
CHANGED
|
@@ -580,6 +580,121 @@ describe('--llms', () => {
|
|
|
580
580
|
})
|
|
581
581
|
})
|
|
582
582
|
|
|
583
|
+
describe('--schema', () => {
|
|
584
|
+
test('returns command schema in toon format', async () => {
|
|
585
|
+
const cli = Cli.create('test')
|
|
586
|
+
cli.command('greet', {
|
|
587
|
+
args: z.object({ name: z.string().describe('Name to greet') }),
|
|
588
|
+
options: z.object({ loud: z.boolean().default(false).describe('Shout') }),
|
|
589
|
+
output: z.object({ message: z.string() }),
|
|
590
|
+
run(c) {
|
|
591
|
+
return { message: `hello ${c.args.name}` }
|
|
592
|
+
},
|
|
593
|
+
})
|
|
594
|
+
const { output } = await serve(cli, ['greet', '--schema'])
|
|
595
|
+
expect(output).toContain('args')
|
|
596
|
+
expect(output).toContain('options')
|
|
597
|
+
expect(output).toContain('output')
|
|
598
|
+
})
|
|
599
|
+
|
|
600
|
+
test('returns command schema as JSON', async () => {
|
|
601
|
+
const cli = Cli.create('test')
|
|
602
|
+
cli.command('greet', {
|
|
603
|
+
args: z.object({ name: z.string().describe('Name to greet') }),
|
|
604
|
+
options: z.object({ loud: z.boolean().default(false).describe('Shout') }),
|
|
605
|
+
output: z.object({ message: z.string() }),
|
|
606
|
+
run(c) {
|
|
607
|
+
return { message: `hello ${c.args.name}` }
|
|
608
|
+
},
|
|
609
|
+
})
|
|
610
|
+
const { output } = await serve(cli, ['greet', '--schema', '--format', 'json'])
|
|
611
|
+
expect(JSON.parse(output)).toMatchInlineSnapshot(`
|
|
612
|
+
{
|
|
613
|
+
"args": {
|
|
614
|
+
"additionalProperties": false,
|
|
615
|
+
"properties": {
|
|
616
|
+
"name": {
|
|
617
|
+
"description": "Name to greet",
|
|
618
|
+
"type": "string",
|
|
619
|
+
},
|
|
620
|
+
},
|
|
621
|
+
"required": [
|
|
622
|
+
"name",
|
|
623
|
+
],
|
|
624
|
+
"type": "object",
|
|
625
|
+
},
|
|
626
|
+
"options": {
|
|
627
|
+
"additionalProperties": false,
|
|
628
|
+
"properties": {
|
|
629
|
+
"loud": {
|
|
630
|
+
"default": false,
|
|
631
|
+
"description": "Shout",
|
|
632
|
+
"type": "boolean",
|
|
633
|
+
},
|
|
634
|
+
},
|
|
635
|
+
"required": [
|
|
636
|
+
"loud",
|
|
637
|
+
],
|
|
638
|
+
"type": "object",
|
|
639
|
+
},
|
|
640
|
+
"output": {
|
|
641
|
+
"additionalProperties": false,
|
|
642
|
+
"properties": {
|
|
643
|
+
"message": {
|
|
644
|
+
"type": "string",
|
|
645
|
+
},
|
|
646
|
+
},
|
|
647
|
+
"required": [
|
|
648
|
+
"message",
|
|
649
|
+
],
|
|
650
|
+
"type": "object",
|
|
651
|
+
},
|
|
652
|
+
}
|
|
653
|
+
`)
|
|
654
|
+
})
|
|
655
|
+
|
|
656
|
+
test('on root command', async () => {
|
|
657
|
+
const cli = Cli.create('test', {
|
|
658
|
+
args: z.object({ name: z.string() }),
|
|
659
|
+
output: z.object({ greeting: z.string() }),
|
|
660
|
+
run(c) {
|
|
661
|
+
return { greeting: `hi ${c.args.name}` }
|
|
662
|
+
},
|
|
663
|
+
})
|
|
664
|
+
const { output } = await serve(cli, ['--schema', '--format', 'json'])
|
|
665
|
+
const parsed = JSON.parse(output)
|
|
666
|
+
expect(parsed.args).toBeDefined()
|
|
667
|
+
expect(parsed.output).toBeDefined()
|
|
668
|
+
})
|
|
669
|
+
|
|
670
|
+
test('on unknown command shows error', async () => {
|
|
671
|
+
const cli = Cli.create('test')
|
|
672
|
+
cli.command('greet', { run: () => ({}) })
|
|
673
|
+
const { output, exitCode } = await serve(cli, ['nope', '--schema'])
|
|
674
|
+
expect(output).toContain("'nope' is not a command")
|
|
675
|
+
expect(exitCode).toBe(1)
|
|
676
|
+
})
|
|
677
|
+
|
|
678
|
+
test('on group shows available commands', async () => {
|
|
679
|
+
const cli = Cli.create('test')
|
|
680
|
+
const pr = Cli.create('pr', { description: 'PR management' }).command('list', {
|
|
681
|
+
description: 'List PRs',
|
|
682
|
+
run: () => ({ items: [] }),
|
|
683
|
+
})
|
|
684
|
+
cli.command(pr)
|
|
685
|
+
const { output } = await serve(cli, ['pr', '--schema'])
|
|
686
|
+
expect(output).toContain('pr')
|
|
687
|
+
expect(output).toContain('list')
|
|
688
|
+
})
|
|
689
|
+
|
|
690
|
+
test('omits empty schema sections', async () => {
|
|
691
|
+
const cli = Cli.create('test')
|
|
692
|
+
cli.command('ping', { run: () => ({ pong: true }) })
|
|
693
|
+
const { output } = await serve(cli, ['ping', '--schema', '--format', 'json'])
|
|
694
|
+
expect(JSON.parse(output)).toMatchInlineSnapshot('{}')
|
|
695
|
+
})
|
|
696
|
+
})
|
|
697
|
+
|
|
583
698
|
describe('subcommands', () => {
|
|
584
699
|
test('creates a command group with name and description', () => {
|
|
585
700
|
const pr = Cli.create('pr', { description: 'PR management' })
|
|
@@ -739,9 +854,14 @@ describe('subcommands', () => {
|
|
|
739
854
|
list
|
|
740
855
|
|
|
741
856
|
Global Options:
|
|
857
|
+
--filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
|
|
742
858
|
--format <toon|json|yaml|md|jsonl> Output format
|
|
743
859
|
--help Show help
|
|
744
860
|
--llms Print LLM-readable manifest
|
|
861
|
+
--schema Show JSON Schema for a command
|
|
862
|
+
--token-count Print token count of output (instead of output)
|
|
863
|
+
--token-limit <n> Limit output to n tokens
|
|
864
|
+
--token-offset <n> Skip first n tokens of output
|
|
745
865
|
--verbose Show full output envelope
|
|
746
866
|
"
|
|
747
867
|
`)
|
|
@@ -1173,10 +1293,15 @@ describe('help', () => {
|
|
|
1173
1293
|
skills add Sync skill files to your agent
|
|
1174
1294
|
|
|
1175
1295
|
Global Options:
|
|
1296
|
+
--filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
|
|
1176
1297
|
--format <toon|json|yaml|md|jsonl> Output format
|
|
1177
1298
|
--help Show help
|
|
1178
1299
|
--llms Print LLM-readable manifest
|
|
1179
1300
|
--mcp Start as MCP stdio server
|
|
1301
|
+
--schema Show JSON Schema for a command
|
|
1302
|
+
--token-count Print token count of output (instead of output)
|
|
1303
|
+
--token-limit <n> Limit output to n tokens
|
|
1304
|
+
--token-offset <n> Skip first n tokens of output
|
|
1180
1305
|
--verbose Show full output envelope
|
|
1181
1306
|
--version Show version
|
|
1182
1307
|
"
|
|
@@ -1206,10 +1331,15 @@ describe('help', () => {
|
|
|
1206
1331
|
skills add Sync skill files to your agent
|
|
1207
1332
|
|
|
1208
1333
|
Global Options:
|
|
1334
|
+
--filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
|
|
1209
1335
|
--format <toon|json|yaml|md|jsonl> Output format
|
|
1210
1336
|
--help Show help
|
|
1211
1337
|
--llms Print LLM-readable manifest
|
|
1212
1338
|
--mcp Start as MCP stdio server
|
|
1339
|
+
--schema Show JSON Schema for a command
|
|
1340
|
+
--token-count Print token count of output (instead of output)
|
|
1341
|
+
--token-limit <n> Limit output to n tokens
|
|
1342
|
+
--token-offset <n> Skip first n tokens of output
|
|
1213
1343
|
--verbose Show full output envelope
|
|
1214
1344
|
--version Show version
|
|
1215
1345
|
"
|
|
@@ -1235,9 +1365,14 @@ describe('help', () => {
|
|
|
1235
1365
|
name Name
|
|
1236
1366
|
|
|
1237
1367
|
Global Options:
|
|
1368
|
+
--filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
|
|
1238
1369
|
--format <toon|json|yaml|md|jsonl> Output format
|
|
1239
1370
|
--help Show help
|
|
1240
1371
|
--llms Print LLM-readable manifest
|
|
1372
|
+
--schema Show JSON Schema for a command
|
|
1373
|
+
--token-count Print token count of output (instead of output)
|
|
1374
|
+
--token-limit <n> Limit output to n tokens
|
|
1375
|
+
--token-offset <n> Skip first n tokens of output
|
|
1241
1376
|
--verbose Show full output envelope
|
|
1242
1377
|
"
|
|
1243
1378
|
`)
|
|
@@ -1264,9 +1399,14 @@ describe('help', () => {
|
|
|
1264
1399
|
list List PRs
|
|
1265
1400
|
|
|
1266
1401
|
Global Options:
|
|
1402
|
+
--filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
|
|
1267
1403
|
--format <toon|json|yaml|md|jsonl> Output format
|
|
1268
1404
|
--help Show help
|
|
1269
1405
|
--llms Print LLM-readable manifest
|
|
1406
|
+
--schema Show JSON Schema for a command
|
|
1407
|
+
--token-count Print token count of output (instead of output)
|
|
1408
|
+
--token-limit <n> Limit output to n tokens
|
|
1409
|
+
--token-offset <n> Skip first n tokens of output
|
|
1270
1410
|
--verbose Show full output envelope
|
|
1271
1411
|
"
|
|
1272
1412
|
`)
|
|
@@ -1354,10 +1494,15 @@ describe('help', () => {
|
|
|
1354
1494
|
skills add Sync skill files to your agent
|
|
1355
1495
|
|
|
1356
1496
|
Global Options:
|
|
1497
|
+
--filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
|
|
1357
1498
|
--format <toon|json|yaml|md|jsonl> Output format
|
|
1358
1499
|
--help Show help
|
|
1359
1500
|
--llms Print LLM-readable manifest
|
|
1360
1501
|
--mcp Start as MCP stdio server
|
|
1502
|
+
--schema Show JSON Schema for a command
|
|
1503
|
+
--token-count Print token count of output (instead of output)
|
|
1504
|
+
--token-limit <n> Limit output to n tokens
|
|
1505
|
+
--token-offset <n> Skip first n tokens of output
|
|
1361
1506
|
--verbose Show full output envelope
|
|
1362
1507
|
--version Show version
|
|
1363
1508
|
"
|
|
@@ -1381,9 +1526,14 @@ describe('help', () => {
|
|
|
1381
1526
|
Run "tool status" to check deployment progress.
|
|
1382
1527
|
|
|
1383
1528
|
Global Options:
|
|
1529
|
+
--filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
|
|
1384
1530
|
--format <toon|json|yaml|md|jsonl> Output format
|
|
1385
1531
|
--help Show help
|
|
1386
1532
|
--llms Print LLM-readable manifest
|
|
1533
|
+
--schema Show JSON Schema for a command
|
|
1534
|
+
--token-count Print token count of output (instead of output)
|
|
1535
|
+
--token-limit <n> Limit output to n tokens
|
|
1536
|
+
--token-offset <n> Skip first n tokens of output
|
|
1387
1537
|
--verbose Show full output envelope
|
|
1388
1538
|
"
|
|
1389
1539
|
`)
|
|
@@ -1471,9 +1621,14 @@ describe('env', () => {
|
|
|
1471
1621
|
Usage: test deploy
|
|
1472
1622
|
|
|
1473
1623
|
Global Options:
|
|
1624
|
+
--filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
|
|
1474
1625
|
--format <toon|json|yaml|md|jsonl> Output format
|
|
1475
1626
|
--help Show help
|
|
1476
1627
|
--llms Print LLM-readable manifest
|
|
1628
|
+
--schema Show JSON Schema for a command
|
|
1629
|
+
--token-count Print token count of output (instead of output)
|
|
1630
|
+
--token-limit <n> Limit output to n tokens
|
|
1631
|
+
--token-offset <n> Skip first n tokens of output
|
|
1477
1632
|
--verbose Show full output envelope
|
|
1478
1633
|
|
|
1479
1634
|
Environment Variables:
|
|
@@ -1504,9 +1659,14 @@ describe('env', () => {
|
|
|
1504
1659
|
Usage: test deploy
|
|
1505
1660
|
|
|
1506
1661
|
Global Options:
|
|
1662
|
+
--filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
|
|
1507
1663
|
--format <toon|json|yaml|md|jsonl> Output format
|
|
1508
1664
|
--help Show help
|
|
1509
1665
|
--llms Print LLM-readable manifest
|
|
1666
|
+
--schema Show JSON Schema for a command
|
|
1667
|
+
--token-count Print token count of output (instead of output)
|
|
1668
|
+
--token-limit <n> Limit output to n tokens
|
|
1669
|
+
--token-offset <n> Skip first n tokens of output
|
|
1510
1670
|
--verbose Show full output envelope
|
|
1511
1671
|
|
|
1512
1672
|
Environment Variables:
|
|
@@ -2104,6 +2264,43 @@ describe('outputPolicy', () => {
|
|
|
2104
2264
|
expect(captured).toEqual({ agent: false, command: 'deploy' })
|
|
2105
2265
|
})
|
|
2106
2266
|
|
|
2267
|
+
test('e2e: middleware and run context expose format metadata', async () => {
|
|
2268
|
+
let mwCaptured:
|
|
2269
|
+
| {
|
|
2270
|
+
format: string
|
|
2271
|
+
formatExplicit: boolean
|
|
2272
|
+
}
|
|
2273
|
+
| undefined
|
|
2274
|
+
let runCaptured:
|
|
2275
|
+
| {
|
|
2276
|
+
format: string
|
|
2277
|
+
formatExplicit: boolean
|
|
2278
|
+
}
|
|
2279
|
+
| undefined
|
|
2280
|
+
|
|
2281
|
+
const cli = Cli.create('test')
|
|
2282
|
+
.use(async (c, next) => {
|
|
2283
|
+
mwCaptured = {
|
|
2284
|
+
format: c.format,
|
|
2285
|
+
formatExplicit: c.formatExplicit,
|
|
2286
|
+
}
|
|
2287
|
+
await next()
|
|
2288
|
+
})
|
|
2289
|
+
.command('deploy', {
|
|
2290
|
+
run(c) {
|
|
2291
|
+
runCaptured = {
|
|
2292
|
+
format: c.format,
|
|
2293
|
+
formatExplicit: c.formatExplicit,
|
|
2294
|
+
}
|
|
2295
|
+
return { ok: true }
|
|
2296
|
+
},
|
|
2297
|
+
})
|
|
2298
|
+
|
|
2299
|
+
await serve(cli, ['deploy', '--format', 'json'])
|
|
2300
|
+
expect(mwCaptured).toEqual({ format: 'json', formatExplicit: true })
|
|
2301
|
+
expect(runCaptured).toEqual({ format: 'json', formatExplicit: true })
|
|
2302
|
+
})
|
|
2303
|
+
|
|
2107
2304
|
test('e2e: middleware works with streaming handlers', async () => {
|
|
2108
2305
|
const order: string[] = []
|
|
2109
2306
|
const cli = Cli.create('test')
|
|
@@ -2404,6 +2601,88 @@ test('streaming: generator returns error in buffered mode', async () => {
|
|
|
2404
2601
|
expect(output).toContain('RET_ERR')
|
|
2405
2602
|
})
|
|
2406
2603
|
|
|
2604
|
+
test('c.error({ exitCode }) uses custom exit code', async () => {
|
|
2605
|
+
const cli = Cli.create('test')
|
|
2606
|
+
cli.command('fail', {
|
|
2607
|
+
run(c) {
|
|
2608
|
+
return c.error({ code: 'AUTH', message: 'not authed', exitCode: 10 })
|
|
2609
|
+
},
|
|
2610
|
+
})
|
|
2611
|
+
|
|
2612
|
+
const { output, exitCode } = await serve(cli, ['fail'])
|
|
2613
|
+
expect(exitCode).toBe(10)
|
|
2614
|
+
expect(output).toMatchInlineSnapshot(`
|
|
2615
|
+
"code: AUTH
|
|
2616
|
+
message: not authed
|
|
2617
|
+
"
|
|
2618
|
+
`)
|
|
2619
|
+
})
|
|
2620
|
+
|
|
2621
|
+
test('c.error() without exitCode defaults to 1', async () => {
|
|
2622
|
+
const cli = Cli.create('test')
|
|
2623
|
+
cli.command('fail', {
|
|
2624
|
+
run(c) {
|
|
2625
|
+
return c.error({ code: 'BAD', message: 'fail' })
|
|
2626
|
+
},
|
|
2627
|
+
})
|
|
2628
|
+
|
|
2629
|
+
const { output, exitCode } = await serve(cli, ['fail'])
|
|
2630
|
+
expect(exitCode).toBe(1)
|
|
2631
|
+
expect(output).toMatchInlineSnapshot(`
|
|
2632
|
+
"code: BAD
|
|
2633
|
+
message: fail
|
|
2634
|
+
"
|
|
2635
|
+
`)
|
|
2636
|
+
})
|
|
2637
|
+
|
|
2638
|
+
test('middleware c.error({ exitCode }) uses custom exit code', async () => {
|
|
2639
|
+
const cli = Cli.create('test')
|
|
2640
|
+
cli.use((c) => {
|
|
2641
|
+
return c.error({ code: 'MW_ERR', message: 'blocked', exitCode: 42 })
|
|
2642
|
+
})
|
|
2643
|
+
cli.command('anything', { run: () => ({}) })
|
|
2644
|
+
|
|
2645
|
+
const { output, exitCode } = await serve(cli, ['anything'])
|
|
2646
|
+
expect(exitCode).toBe(42)
|
|
2647
|
+
expect(output).toMatchInlineSnapshot(`
|
|
2648
|
+
"code: MW_ERR
|
|
2649
|
+
message: blocked
|
|
2650
|
+
"
|
|
2651
|
+
`)
|
|
2652
|
+
})
|
|
2653
|
+
|
|
2654
|
+
test('thrown IncurError with exitCode uses custom exit code', async () => {
|
|
2655
|
+
const cli = Cli.create('test')
|
|
2656
|
+
cli.command('fail', {
|
|
2657
|
+
run() {
|
|
2658
|
+
throw new Errors.IncurError({ code: 'RATE_LIMITED', message: 'too fast', exitCode: 99 })
|
|
2659
|
+
},
|
|
2660
|
+
})
|
|
2661
|
+
|
|
2662
|
+
const { output, exitCode } = await serve(cli, ['fail'])
|
|
2663
|
+
expect(exitCode).toBe(99)
|
|
2664
|
+
expect(output).toMatchInlineSnapshot(`
|
|
2665
|
+
"code: RATE_LIMITED
|
|
2666
|
+
message: too fast
|
|
2667
|
+
retryable: false
|
|
2668
|
+
"
|
|
2669
|
+
`)
|
|
2670
|
+
})
|
|
2671
|
+
|
|
2672
|
+
test('streaming: c.error({ exitCode }) in yield uses custom exit code', async () => {
|
|
2673
|
+
const cli = Cli.create('test')
|
|
2674
|
+
cli.command('fail', {
|
|
2675
|
+
async *run(c) {
|
|
2676
|
+
yield { step: 1 }
|
|
2677
|
+
yield c.error({ code: 'STREAM_ERR', message: 'mid-stream', exitCode: 77 })
|
|
2678
|
+
},
|
|
2679
|
+
})
|
|
2680
|
+
|
|
2681
|
+
const { output, exitCode } = await serve(cli, ['fail', '--format', 'jsonl'])
|
|
2682
|
+
expect(exitCode).toBe(77)
|
|
2683
|
+
expect(output).toContain('STREAM_ERR')
|
|
2684
|
+
})
|
|
2685
|
+
|
|
2407
2686
|
test('deprecated short flag emits warning', async () => {
|
|
2408
2687
|
const cli = Cli.create('app').command('deploy', {
|
|
2409
2688
|
options: z.object({
|
|
@@ -2640,3 +2919,553 @@ describe('fetch', async () => {
|
|
|
2640
2919
|
expect(output).toContain('Proxy to API')
|
|
2641
2920
|
})
|
|
2642
2921
|
})
|
|
2922
|
+
|
|
2923
|
+
describe('--filter-output', () => {
|
|
2924
|
+
test('selects specific keys', async () => {
|
|
2925
|
+
const cli = Cli.create('test')
|
|
2926
|
+
cli.command('user', {
|
|
2927
|
+
run() {
|
|
2928
|
+
return { name: 'alice', age: 30, email: 'alice@example.com' }
|
|
2929
|
+
},
|
|
2930
|
+
})
|
|
2931
|
+
const { output } = await serve(cli, ['user', '--filter-output', 'name,age'])
|
|
2932
|
+
expect(output).toMatchInlineSnapshot(`
|
|
2933
|
+
"name: alice
|
|
2934
|
+
age: 30
|
|
2935
|
+
"
|
|
2936
|
+
`)
|
|
2937
|
+
})
|
|
2938
|
+
|
|
2939
|
+
test('returns scalar for single key', async () => {
|
|
2940
|
+
const cli = Cli.create('test')
|
|
2941
|
+
cli.command('greet', {
|
|
2942
|
+
args: z.object({ name: z.string() }),
|
|
2943
|
+
run(c) {
|
|
2944
|
+
return { message: `hello ${c.args.name}` }
|
|
2945
|
+
},
|
|
2946
|
+
})
|
|
2947
|
+
const { output } = await serve(cli, ['greet', 'world', '--filter-output', 'message'])
|
|
2948
|
+
expect(output).toMatchInlineSnapshot(`
|
|
2949
|
+
"hello world
|
|
2950
|
+
"
|
|
2951
|
+
`)
|
|
2952
|
+
})
|
|
2953
|
+
|
|
2954
|
+
test('dot notation filters nested keys', async () => {
|
|
2955
|
+
const cli = Cli.create('test')
|
|
2956
|
+
cli.command('profile', {
|
|
2957
|
+
run() {
|
|
2958
|
+
return { user: { name: 'alice', email: 'a@b.com' }, status: 'active' }
|
|
2959
|
+
},
|
|
2960
|
+
})
|
|
2961
|
+
const { output } = await serve(cli, ['profile', '--filter-output', 'user.name'])
|
|
2962
|
+
expect(output).toMatchInlineSnapshot(`
|
|
2963
|
+
"user:
|
|
2964
|
+
name: alice
|
|
2965
|
+
"
|
|
2966
|
+
`)
|
|
2967
|
+
})
|
|
2968
|
+
|
|
2969
|
+
test('array slice', async () => {
|
|
2970
|
+
const cli = Cli.create('test')
|
|
2971
|
+
cli.command('list', {
|
|
2972
|
+
run() {
|
|
2973
|
+
return { items: [1, 2, 3, 4, 5] }
|
|
2974
|
+
},
|
|
2975
|
+
})
|
|
2976
|
+
const { output } = await serve(cli, ['list', '--filter-output', 'items[0,3]'])
|
|
2977
|
+
expect(output).toMatchInlineSnapshot(`
|
|
2978
|
+
"items[3]: 1,2,3
|
|
2979
|
+
"
|
|
2980
|
+
`)
|
|
2981
|
+
})
|
|
2982
|
+
|
|
2983
|
+
test('works with --format json', async () => {
|
|
2984
|
+
const cli = Cli.create('test')
|
|
2985
|
+
cli.command('user', {
|
|
2986
|
+
run() {
|
|
2987
|
+
return { name: 'alice', age: 30, email: 'alice@example.com' }
|
|
2988
|
+
},
|
|
2989
|
+
})
|
|
2990
|
+
const { output } = await serve(cli, ['user', '--filter-output', 'name,age', '--format', 'json'])
|
|
2991
|
+
const parsed = JSON.parse(output)
|
|
2992
|
+
expect(parsed).toEqual({ name: 'alice', age: 30 })
|
|
2993
|
+
})
|
|
2994
|
+
})
|
|
2995
|
+
|
|
2996
|
+
async function fetchJson(cli: Cli.Cli<any, any, any>, req: Request) {
|
|
2997
|
+
const res = await cli.fetch(req)
|
|
2998
|
+
const body = await res.json()
|
|
2999
|
+
body.meta.duration = '<stripped>'
|
|
3000
|
+
return { status: res.status, body }
|
|
3001
|
+
}
|
|
3002
|
+
|
|
3003
|
+
describe('fetch', () => {
|
|
3004
|
+
test('GET /health → 200', async () => {
|
|
3005
|
+
const cli = Cli.create('test')
|
|
3006
|
+
cli.command('health', { run: () => ({ ok: true }) })
|
|
3007
|
+
expect(await fetchJson(cli, new Request('http://localhost/health'))).toMatchInlineSnapshot(`
|
|
3008
|
+
{
|
|
3009
|
+
"body": {
|
|
3010
|
+
"data": {
|
|
3011
|
+
"ok": true,
|
|
3012
|
+
},
|
|
3013
|
+
"meta": {
|
|
3014
|
+
"command": "health",
|
|
3015
|
+
"duration": "<stripped>",
|
|
3016
|
+
},
|
|
3017
|
+
"ok": true,
|
|
3018
|
+
},
|
|
3019
|
+
"status": 200,
|
|
3020
|
+
}
|
|
3021
|
+
`)
|
|
3022
|
+
})
|
|
3023
|
+
|
|
3024
|
+
test('GET /unknown → 404', async () => {
|
|
3025
|
+
const cli = Cli.create('test')
|
|
3026
|
+
cli.command('health', { run: () => ({}) })
|
|
3027
|
+
expect(await fetchJson(cli, new Request('http://localhost/unknown'))).toMatchInlineSnapshot(`
|
|
3028
|
+
{
|
|
3029
|
+
"body": {
|
|
3030
|
+
"error": {
|
|
3031
|
+
"code": "COMMAND_NOT_FOUND",
|
|
3032
|
+
"message": "'unknown' is not a command for 'test'.",
|
|
3033
|
+
},
|
|
3034
|
+
"meta": {
|
|
3035
|
+
"command": "unknown",
|
|
3036
|
+
"duration": "<stripped>",
|
|
3037
|
+
},
|
|
3038
|
+
"ok": false,
|
|
3039
|
+
},
|
|
3040
|
+
"status": 404,
|
|
3041
|
+
}
|
|
3042
|
+
`)
|
|
3043
|
+
})
|
|
3044
|
+
|
|
3045
|
+
test('GET / with root command → 200', async () => {
|
|
3046
|
+
const cli = Cli.create('test', { run: () => ({ root: true }) })
|
|
3047
|
+
expect(await fetchJson(cli, new Request('http://localhost/'))).toMatchInlineSnapshot(`
|
|
3048
|
+
{
|
|
3049
|
+
"body": {
|
|
3050
|
+
"data": {
|
|
3051
|
+
"root": true,
|
|
3052
|
+
},
|
|
3053
|
+
"meta": {
|
|
3054
|
+
"command": "test",
|
|
3055
|
+
"duration": "<stripped>",
|
|
3056
|
+
},
|
|
3057
|
+
"ok": true,
|
|
3058
|
+
},
|
|
3059
|
+
"status": 200,
|
|
3060
|
+
}
|
|
3061
|
+
`)
|
|
3062
|
+
})
|
|
3063
|
+
|
|
3064
|
+
test('GET / without root command → 404', async () => {
|
|
3065
|
+
const cli = Cli.create('test')
|
|
3066
|
+
cli.command('health', { run: () => ({}) })
|
|
3067
|
+
expect(await fetchJson(cli, new Request('http://localhost/'))).toMatchInlineSnapshot(`
|
|
3068
|
+
{
|
|
3069
|
+
"body": {
|
|
3070
|
+
"error": {
|
|
3071
|
+
"code": "COMMAND_NOT_FOUND",
|
|
3072
|
+
"message": "No root command defined.",
|
|
3073
|
+
},
|
|
3074
|
+
"meta": {
|
|
3075
|
+
"command": "/",
|
|
3076
|
+
"duration": "<stripped>",
|
|
3077
|
+
},
|
|
3078
|
+
"ok": false,
|
|
3079
|
+
},
|
|
3080
|
+
"status": 404,
|
|
3081
|
+
}
|
|
3082
|
+
`)
|
|
3083
|
+
})
|
|
3084
|
+
|
|
3085
|
+
test('GET search params → options', async () => {
|
|
3086
|
+
const cli = Cli.create('test')
|
|
3087
|
+
cli.command('users', {
|
|
3088
|
+
options: z.object({ limit: z.coerce.number().default(10) }),
|
|
3089
|
+
run: (c) => ({ limit: c.options.limit }),
|
|
3090
|
+
})
|
|
3091
|
+
expect(await fetchJson(cli, new Request('http://localhost/users?limit=5'))).toMatchInlineSnapshot(`
|
|
3092
|
+
{
|
|
3093
|
+
"body": {
|
|
3094
|
+
"data": {
|
|
3095
|
+
"limit": 5,
|
|
3096
|
+
},
|
|
3097
|
+
"meta": {
|
|
3098
|
+
"command": "users",
|
|
3099
|
+
"duration": "<stripped>",
|
|
3100
|
+
},
|
|
3101
|
+
"ok": true,
|
|
3102
|
+
},
|
|
3103
|
+
"status": 200,
|
|
3104
|
+
}
|
|
3105
|
+
`)
|
|
3106
|
+
})
|
|
3107
|
+
|
|
3108
|
+
test('POST body → options', async () => {
|
|
3109
|
+
const cli = Cli.create('test')
|
|
3110
|
+
cli.command('users', {
|
|
3111
|
+
options: z.object({ name: z.string() }),
|
|
3112
|
+
run: (c) => ({ created: true, name: c.options.name }),
|
|
3113
|
+
})
|
|
3114
|
+
const req = new Request('http://localhost/users', {
|
|
3115
|
+
method: 'POST',
|
|
3116
|
+
headers: { 'content-type': 'application/json' },
|
|
3117
|
+
body: JSON.stringify({ name: 'Bob' }),
|
|
3118
|
+
})
|
|
3119
|
+
expect(await fetchJson(cli, req)).toMatchInlineSnapshot(`
|
|
3120
|
+
{
|
|
3121
|
+
"body": {
|
|
3122
|
+
"data": {
|
|
3123
|
+
"created": true,
|
|
3124
|
+
"name": "Bob",
|
|
3125
|
+
},
|
|
3126
|
+
"meta": {
|
|
3127
|
+
"command": "users",
|
|
3128
|
+
"duration": "<stripped>",
|
|
3129
|
+
},
|
|
3130
|
+
"ok": true,
|
|
3131
|
+
},
|
|
3132
|
+
"status": 200,
|
|
3133
|
+
}
|
|
3134
|
+
`)
|
|
3135
|
+
})
|
|
3136
|
+
|
|
3137
|
+
test('trailing path segments → positional args', async () => {
|
|
3138
|
+
const cli = Cli.create('test')
|
|
3139
|
+
cli.command('users', {
|
|
3140
|
+
args: z.object({ id: z.coerce.number() }),
|
|
3141
|
+
run: (c) => ({ id: c.args.id }),
|
|
3142
|
+
})
|
|
3143
|
+
expect(await fetchJson(cli, new Request('http://localhost/users/42'))).toMatchInlineSnapshot(`
|
|
3144
|
+
{
|
|
3145
|
+
"body": {
|
|
3146
|
+
"data": {
|
|
3147
|
+
"id": 42,
|
|
3148
|
+
},
|
|
3149
|
+
"meta": {
|
|
3150
|
+
"command": "users",
|
|
3151
|
+
"duration": "<stripped>",
|
|
3152
|
+
},
|
|
3153
|
+
"ok": true,
|
|
3154
|
+
},
|
|
3155
|
+
"status": 200,
|
|
3156
|
+
}
|
|
3157
|
+
`)
|
|
3158
|
+
})
|
|
3159
|
+
|
|
3160
|
+
test('nested command resolution', async () => {
|
|
3161
|
+
const sub = Cli.create('users')
|
|
3162
|
+
sub.command('list', { run: () => ({ users: [] }) })
|
|
3163
|
+
const cli = Cli.create('test')
|
|
3164
|
+
cli.command(sub)
|
|
3165
|
+
expect(await fetchJson(cli, new Request('http://localhost/users/list'))).toMatchInlineSnapshot(`
|
|
3166
|
+
{
|
|
3167
|
+
"body": {
|
|
3168
|
+
"data": {
|
|
3169
|
+
"users": [],
|
|
3170
|
+
},
|
|
3171
|
+
"meta": {
|
|
3172
|
+
"command": "users list",
|
|
3173
|
+
"duration": "<stripped>",
|
|
3174
|
+
},
|
|
3175
|
+
"ok": true,
|
|
3176
|
+
},
|
|
3177
|
+
"status": 200,
|
|
3178
|
+
}
|
|
3179
|
+
`)
|
|
3180
|
+
})
|
|
3181
|
+
|
|
3182
|
+
test('validation error → 400', async () => {
|
|
3183
|
+
const cli = Cli.create('test')
|
|
3184
|
+
cli.command('users', {
|
|
3185
|
+
args: z.object({ id: z.coerce.number() }),
|
|
3186
|
+
run: (c) => ({ id: c.args.id }),
|
|
3187
|
+
})
|
|
3188
|
+
const { status, body } = await fetchJson(cli, new Request('http://localhost/users'))
|
|
3189
|
+
expect(status).toBe(400)
|
|
3190
|
+
expect(body.ok).toBe(false)
|
|
3191
|
+
expect(body.error.code).toBe('VALIDATION_ERROR')
|
|
3192
|
+
})
|
|
3193
|
+
|
|
3194
|
+
test('thrown error → 500', async () => {
|
|
3195
|
+
const cli = Cli.create('test')
|
|
3196
|
+
cli.command('fail', {
|
|
3197
|
+
run() {
|
|
3198
|
+
throw new Error('boom')
|
|
3199
|
+
},
|
|
3200
|
+
})
|
|
3201
|
+
expect(await fetchJson(cli, new Request('http://localhost/fail'))).toMatchInlineSnapshot(`
|
|
3202
|
+
{
|
|
3203
|
+
"body": {
|
|
3204
|
+
"error": {
|
|
3205
|
+
"code": "UNKNOWN",
|
|
3206
|
+
"message": "boom",
|
|
3207
|
+
},
|
|
3208
|
+
"meta": {
|
|
3209
|
+
"command": "fail",
|
|
3210
|
+
"duration": "<stripped>",
|
|
3211
|
+
},
|
|
3212
|
+
"ok": false,
|
|
3213
|
+
},
|
|
3214
|
+
"status": 500,
|
|
3215
|
+
}
|
|
3216
|
+
`)
|
|
3217
|
+
})
|
|
3218
|
+
|
|
3219
|
+
test('async generator → NDJSON streaming response', async () => {
|
|
3220
|
+
const cli = Cli.create('test')
|
|
3221
|
+
cli.command('stream', {
|
|
3222
|
+
async *run() {
|
|
3223
|
+
yield { progress: 1 }
|
|
3224
|
+
yield { progress: 2 }
|
|
3225
|
+
return { done: true }
|
|
3226
|
+
},
|
|
3227
|
+
})
|
|
3228
|
+
const res = await cli.fetch(new Request('http://localhost/stream'))
|
|
3229
|
+
expect(res.status).toBe(200)
|
|
3230
|
+
expect(res.headers.get('content-type')).toBe('application/x-ndjson')
|
|
3231
|
+
const text = await res.text()
|
|
3232
|
+
const lines = text.trim().split('\n').map((l) => JSON.parse(l))
|
|
3233
|
+
expect(lines).toMatchInlineSnapshot(`
|
|
3234
|
+
[
|
|
3235
|
+
{
|
|
3236
|
+
"data": {
|
|
3237
|
+
"progress": 1,
|
|
3238
|
+
},
|
|
3239
|
+
"type": "chunk",
|
|
3240
|
+
},
|
|
3241
|
+
{
|
|
3242
|
+
"data": {
|
|
3243
|
+
"progress": 2,
|
|
3244
|
+
},
|
|
3245
|
+
"type": "chunk",
|
|
3246
|
+
},
|
|
3247
|
+
{
|
|
3248
|
+
"meta": {
|
|
3249
|
+
"command": "stream",
|
|
3250
|
+
},
|
|
3251
|
+
"ok": true,
|
|
3252
|
+
"type": "done",
|
|
3253
|
+
},
|
|
3254
|
+
]
|
|
3255
|
+
`)
|
|
3256
|
+
})
|
|
3257
|
+
|
|
3258
|
+
test('middleware sets var → command sees it', async () => {
|
|
3259
|
+
const cli = Cli.create('test', {
|
|
3260
|
+
vars: z.object({ user: z.string().default('anonymous') }),
|
|
3261
|
+
})
|
|
3262
|
+
cli.use(async (c, next) => {
|
|
3263
|
+
c.set('user', 'alice')
|
|
3264
|
+
await next()
|
|
3265
|
+
})
|
|
3266
|
+
cli.command('whoami', {
|
|
3267
|
+
run: (c) => ({ user: c.var.user }),
|
|
3268
|
+
})
|
|
3269
|
+
expect(await fetchJson(cli, new Request('http://localhost/whoami'))).toMatchInlineSnapshot(`
|
|
3270
|
+
{
|
|
3271
|
+
"body": {
|
|
3272
|
+
"data": {
|
|
3273
|
+
"user": "alice",
|
|
3274
|
+
},
|
|
3275
|
+
"meta": {
|
|
3276
|
+
"command": "whoami",
|
|
3277
|
+
"duration": "<stripped>",
|
|
3278
|
+
},
|
|
3279
|
+
"ok": true,
|
|
3280
|
+
},
|
|
3281
|
+
"status": 200,
|
|
3282
|
+
}
|
|
3283
|
+
`)
|
|
3284
|
+
})
|
|
3285
|
+
|
|
3286
|
+
test('middleware error → error response', async () => {
|
|
3287
|
+
const cli = Cli.create('test')
|
|
3288
|
+
cli.use((c) => {
|
|
3289
|
+
c.error({ code: 'UNAUTHORIZED', message: 'not allowed' })
|
|
3290
|
+
})
|
|
3291
|
+
cli.command('secret', { run: () => ({ secret: true }) })
|
|
3292
|
+
expect(await fetchJson(cli, new Request('http://localhost/secret'))).toMatchInlineSnapshot(`
|
|
3293
|
+
{
|
|
3294
|
+
"body": {
|
|
3295
|
+
"error": {
|
|
3296
|
+
"code": "UNAUTHORIZED",
|
|
3297
|
+
"message": "not allowed",
|
|
3298
|
+
},
|
|
3299
|
+
"meta": {
|
|
3300
|
+
"command": "secret",
|
|
3301
|
+
"duration": "<stripped>",
|
|
3302
|
+
},
|
|
3303
|
+
"ok": false,
|
|
3304
|
+
},
|
|
3305
|
+
"status": 500,
|
|
3306
|
+
}
|
|
3307
|
+
`)
|
|
3308
|
+
})
|
|
3309
|
+
|
|
3310
|
+
test('fetch gateway → forwards request', async () => {
|
|
3311
|
+
const handler = (req: Request) => {
|
|
3312
|
+
const url = new URL(req.url)
|
|
3313
|
+
return new Response(JSON.stringify({ path: url.pathname }), {
|
|
3314
|
+
headers: { 'content-type': 'application/json' },
|
|
3315
|
+
})
|
|
3316
|
+
}
|
|
3317
|
+
const cli = Cli.create('test')
|
|
3318
|
+
cli.command('api', { fetch: handler })
|
|
3319
|
+
const res = await cli.fetch(new Request('http://localhost/api/users/list'))
|
|
3320
|
+
expect(res.status).toBe(200)
|
|
3321
|
+
const body = await res.json()
|
|
3322
|
+
expect(body).toMatchInlineSnapshot(`
|
|
3323
|
+
{
|
|
3324
|
+
"path": "/api/users/list",
|
|
3325
|
+
}
|
|
3326
|
+
`)
|
|
3327
|
+
})
|
|
3328
|
+
|
|
3329
|
+
describe('mcp over http', () => {
|
|
3330
|
+
function mcpCli() {
|
|
3331
|
+
const cli = Cli.create('test', { version: '1.0.0' })
|
|
3332
|
+
cli.command('greet', {
|
|
3333
|
+
description: 'Greet someone',
|
|
3334
|
+
args: z.object({ name: z.string() }),
|
|
3335
|
+
run: (c) => ({ message: `hello ${c.args.name}` }),
|
|
3336
|
+
})
|
|
3337
|
+
cli.command('ping', {
|
|
3338
|
+
description: 'Ping',
|
|
3339
|
+
run: () => ({ pong: true }),
|
|
3340
|
+
})
|
|
3341
|
+
return cli
|
|
3342
|
+
}
|
|
3343
|
+
|
|
3344
|
+
async function mcpRequest(cli: Cli.Cli<any, any, any>, body: unknown, sessionId?: string) {
|
|
3345
|
+
const headers: Record<string, string> = { 'content-type': 'application/json', accept: 'application/json, text/event-stream' }
|
|
3346
|
+
if (sessionId) headers['mcp-session-id'] = sessionId
|
|
3347
|
+
return cli.fetch(
|
|
3348
|
+
new Request('http://localhost/mcp', {
|
|
3349
|
+
method: 'POST',
|
|
3350
|
+
headers,
|
|
3351
|
+
body: JSON.stringify(body),
|
|
3352
|
+
}),
|
|
3353
|
+
)
|
|
3354
|
+
}
|
|
3355
|
+
|
|
3356
|
+
async function initSession(cli: Cli.Cli<any, any, any>) {
|
|
3357
|
+
const res = await mcpRequest(cli, {
|
|
3358
|
+
jsonrpc: '2.0',
|
|
3359
|
+
id: 1,
|
|
3360
|
+
method: 'initialize',
|
|
3361
|
+
params: {
|
|
3362
|
+
protocolVersion: '2025-03-26',
|
|
3363
|
+
capabilities: {},
|
|
3364
|
+
clientInfo: { name: 'test-client', version: '1.0.0' },
|
|
3365
|
+
},
|
|
3366
|
+
})
|
|
3367
|
+
const sessionId = res.headers.get('mcp-session-id')
|
|
3368
|
+
const body = await res.json()
|
|
3369
|
+
// Send initialized notification
|
|
3370
|
+
await mcpRequest(cli, { jsonrpc: '2.0', method: 'notifications/initialized' }, sessionId!)
|
|
3371
|
+
return { sessionId: sessionId!, body }
|
|
3372
|
+
}
|
|
3373
|
+
|
|
3374
|
+
test('POST /mcp with initialize → valid MCP response', async () => {
|
|
3375
|
+
const cli = mcpCli()
|
|
3376
|
+
const res = await mcpRequest(cli, {
|
|
3377
|
+
jsonrpc: '2.0',
|
|
3378
|
+
id: 1,
|
|
3379
|
+
method: 'initialize',
|
|
3380
|
+
params: {
|
|
3381
|
+
protocolVersion: '2025-03-26',
|
|
3382
|
+
capabilities: {},
|
|
3383
|
+
clientInfo: { name: 'test-client', version: '1.0.0' },
|
|
3384
|
+
},
|
|
3385
|
+
})
|
|
3386
|
+
expect(res.status).toBe(200)
|
|
3387
|
+
const body = await res.json()
|
|
3388
|
+
expect({
|
|
3389
|
+
serverInfo: body.result.serverInfo,
|
|
3390
|
+
hasTools: 'tools' in (body.result.capabilities ?? {}),
|
|
3391
|
+
}).toMatchInlineSnapshot(`
|
|
3392
|
+
{
|
|
3393
|
+
"hasTools": true,
|
|
3394
|
+
"serverInfo": {
|
|
3395
|
+
"name": "test",
|
|
3396
|
+
"version": "1.0.0",
|
|
3397
|
+
},
|
|
3398
|
+
}
|
|
3399
|
+
`)
|
|
3400
|
+
})
|
|
3401
|
+
|
|
3402
|
+
test('POST /mcp with tools/list → returns registered tools', async () => {
|
|
3403
|
+
const cli = mcpCli()
|
|
3404
|
+
const { sessionId } = await initSession(cli)
|
|
3405
|
+
const res = await mcpRequest(
|
|
3406
|
+
cli,
|
|
3407
|
+
{ jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} },
|
|
3408
|
+
sessionId,
|
|
3409
|
+
)
|
|
3410
|
+
expect(res.status).toBe(200)
|
|
3411
|
+
const body = await res.json()
|
|
3412
|
+
const tools = body.result.tools.map((t: any) => ({
|
|
3413
|
+
name: t.name,
|
|
3414
|
+
description: t.description,
|
|
3415
|
+
hasInputSchema: Object.keys(t.inputSchema?.properties ?? {}).length > 0,
|
|
3416
|
+
}))
|
|
3417
|
+
expect(tools).toMatchInlineSnapshot(`
|
|
3418
|
+
[
|
|
3419
|
+
{
|
|
3420
|
+
"description": "Greet someone",
|
|
3421
|
+
"hasInputSchema": true,
|
|
3422
|
+
"name": "greet",
|
|
3423
|
+
},
|
|
3424
|
+
{
|
|
3425
|
+
"description": "Ping",
|
|
3426
|
+
"hasInputSchema": false,
|
|
3427
|
+
"name": "ping",
|
|
3428
|
+
},
|
|
3429
|
+
]
|
|
3430
|
+
`)
|
|
3431
|
+
})
|
|
3432
|
+
|
|
3433
|
+
test('POST /mcp with tools/call → executes command', async () => {
|
|
3434
|
+
const cli = mcpCli()
|
|
3435
|
+
const { sessionId } = await initSession(cli)
|
|
3436
|
+
const res = await mcpRequest(
|
|
3437
|
+
cli,
|
|
3438
|
+
{
|
|
3439
|
+
jsonrpc: '2.0',
|
|
3440
|
+
id: 3,
|
|
3441
|
+
method: 'tools/call',
|
|
3442
|
+
params: { name: 'greet', arguments: { name: 'world' } },
|
|
3443
|
+
},
|
|
3444
|
+
sessionId,
|
|
3445
|
+
)
|
|
3446
|
+
expect(res.status).toBe(200)
|
|
3447
|
+
const body = await res.json()
|
|
3448
|
+
expect({
|
|
3449
|
+
isError: body.result.isError,
|
|
3450
|
+
content: JSON.parse(body.result.content[0].text),
|
|
3451
|
+
}).toMatchInlineSnapshot(`
|
|
3452
|
+
{
|
|
3453
|
+
"content": {
|
|
3454
|
+
"message": "hello world",
|
|
3455
|
+
},
|
|
3456
|
+
"isError": undefined,
|
|
3457
|
+
}
|
|
3458
|
+
`)
|
|
3459
|
+
})
|
|
3460
|
+
|
|
3461
|
+
test('non-/mcp paths still route to command API', async () => {
|
|
3462
|
+
const cli = mcpCli()
|
|
3463
|
+
const { body } = await fetchJson(cli, new Request('http://localhost/ping'))
|
|
3464
|
+
expect(body.data).toMatchInlineSnapshot(`
|
|
3465
|
+
{
|
|
3466
|
+
"pong": true,
|
|
3467
|
+
}
|
|
3468
|
+
`)
|
|
3469
|
+
})
|
|
3470
|
+
})
|
|
3471
|
+
})
|