argsbarg 1.3.1 → 1.4.1

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 CHANGED
@@ -9,12 +9,24 @@ shell output regressions.
9
9
 
10
10
  import { completionBashScript, completionZshScript } from "./completion.ts";
11
11
  import { cliHelpRender } from "./help.ts";
12
- import { CliCommand, CliFallbackMode, CliOptionKind } from "./index.ts";
12
+ import { CliCommand, CliFallbackMode, CliOptionKind, cliInvoke } from "./index.ts";
13
+ import {
14
+ allMcpResources,
15
+ collectMcpTools,
16
+ mcpToolCallToArgv,
17
+ mcpToolDescription,
18
+ sanitizeToolSegment,
19
+ } from "./mcp/tools.ts";
20
+ import { applyShellEnv, loadEnvFile } from "./mcp/env.ts";
21
+ import { buildToolCallSuccess } from "./mcp/result.ts";
13
22
  import { ParseKind, parse, postParseValidate } from "./parse.ts";
14
23
  import { cliSchemaJson } from "./schema.ts";
15
24
  import { cliValidateRoot } from "./validate.ts";
16
25
  import { expect, test } from "bun:test";
17
26
  import { $ } from "bun";
27
+ import { mkdtempSync, writeFileSync } from "node:fs";
28
+ import { tmpdir } from "node:os";
29
+ import { join } from "node:path";
18
30
 
19
31
  test("bundled short presence flags", () => {
20
32
  const root: CliCommand = {
@@ -639,4 +651,683 @@ test("completion scripts offer --schema at the program root only", () => {
639
651
 
640
652
  const zsh = completionZshScript(root);
641
653
  expect(zsh).toContain("'--schema:Print the full command tree as JSON.'");
654
+ });
655
+
656
+ const nestedMcpFixture: CliCommand = {
657
+ key: "nested.ts",
658
+ description: "Nested groups demo.",
659
+ mcpServer: { name: "nested-demo", version: "1.0.0" },
660
+ commands: [
661
+ {
662
+ key: "stat",
663
+ description: "File metadata.",
664
+ options: [
665
+ {
666
+ name: "json",
667
+ description: "Emit handler output as JSON.",
668
+ kind: CliOptionKind.Presence,
669
+ },
670
+ ],
671
+ commands: [
672
+ {
673
+ key: "owner",
674
+ description: "Ownership helpers.",
675
+ commands: [
676
+ {
677
+ key: "lookup",
678
+ description: "Resolve owner info.",
679
+ options: [
680
+ {
681
+ name: "user-name",
682
+ description: "User to look up.",
683
+ kind: CliOptionKind.String,
684
+ shortName: "u",
685
+ },
686
+ ],
687
+ positionals: [
688
+ {
689
+ name: "path",
690
+ description: "File or directory.",
691
+ kind: CliOptionKind.String,
692
+ },
693
+ ],
694
+ handler: () => {},
695
+ },
696
+ ],
697
+ },
698
+ ],
699
+ },
700
+ {
701
+ key: "read",
702
+ description: "Print the first line of each file.",
703
+ positionals: [
704
+ {
705
+ name: "files",
706
+ description: "Paths to read.",
707
+ kind: CliOptionKind.String,
708
+ argMax: 0,
709
+ },
710
+ ],
711
+ handler: () => {},
712
+ },
713
+ {
714
+ key: "hidden",
715
+ description: "Internal debug.",
716
+ mcpTool: { enabled: false },
717
+ handler: () => {},
718
+ },
719
+ ],
720
+ fallbackCommand: "read",
721
+ fallbackMode: CliFallbackMode.MissingOrUnknown,
722
+ };
723
+
724
+ /** Sends NDJSON MCP requests to a subprocess and collects responses by id. */
725
+ async function mcpRequest(
726
+ requests: object[],
727
+ opts?: { script?: string; env?: Record<string, string> },
728
+ ): Promise<Map<string | number, object>> {
729
+ const script = opts?.script ?? "examples/nested.ts";
730
+ const proc = Bun.spawn(["bun", "run", script, "mcp"], {
731
+ stdin: "pipe",
732
+ stdout: "pipe",
733
+ stderr: "pipe",
734
+ env: opts?.env ? { ...process.env, ...opts.env } : process.env,
735
+ });
736
+
737
+ const input = requests.map((r) => JSON.stringify(r) + "\n").join("");
738
+ proc.stdin.write(input);
739
+ proc.stdin.end();
740
+
741
+ const timeout = setTimeout(() => proc.kill(), 10_000);
742
+ const stdout = await new Response(proc.stdout).text();
743
+ await proc.exited;
744
+ clearTimeout(timeout);
745
+
746
+ const byId = new Map<string | number, object>();
747
+ for (const line of stdout.split("\n")) {
748
+ const trimmed = line.trim();
749
+ if (!trimmed) {
750
+ continue;
751
+ }
752
+ const msg = JSON.parse(trimmed) as { id?: string | number };
753
+ if (msg.id !== undefined) {
754
+ byId.set(msg.id, msg);
755
+ }
756
+ }
757
+ return byId;
758
+ }
759
+
760
+ test("sanitizeToolSegment normalizes dotted app keys", () => {
761
+ expect(sanitizeToolSegment("minimal.ts")).toBe("minimal_ts");
762
+ });
763
+
764
+ test("mcpToolDescription formats CLI path and root-leaf prefix", () => {
765
+ expect(mcpToolDescription(["stat", "owner", "lookup"], "nested.ts", "Resolve owner info.")).toBe(
766
+ "stat owner lookup — Resolve owner info.",
767
+ );
768
+ expect(mcpToolDescription(["read"], "nested.ts", "Print files.")).toBe("read — Print files.");
769
+ expect(mcpToolDescription([], "helloapp", "Tiny demo.")).toBe("helloapp — Tiny demo.");
770
+ });
771
+
772
+ test("collectMcpTools lists user leaf commands only", () => {
773
+ const tools = collectMcpTools(nestedMcpFixture);
774
+ const names = tools.map((t) => t.name);
775
+ expect(names).toContain("stat_owner_lookup");
776
+ expect(names).toContain("read");
777
+ expect(names).not.toContain("hidden");
778
+ expect(names).not.toContain("mcp");
779
+ expect(names).not.toContain("completion");
780
+ const lookup = tools.find((t) => t.name === "stat_owner_lookup")!;
781
+ expect(lookup.description).toBe("stat owner lookup — Resolve owner info.");
782
+ });
783
+
784
+ test("collectMcpTools merges parent options into inputSchema", () => {
785
+ const tools = collectMcpTools(nestedMcpFixture);
786
+ const lookup = tools.find((t) => t.name === "stat_owner_lookup")!;
787
+ const schema = lookup.inputSchema as { properties: Record<string, unknown>; required?: string[] };
788
+ expect(schema.properties.json).toBeDefined();
789
+ expect(schema.required).toContain("path");
790
+ });
791
+
792
+ test("mcpToolCallToArgv builds nested lookup argv", () => {
793
+ const tools = collectMcpTools(nestedMcpFixture);
794
+ const lookup = tools.find((t) => t.name === "stat_owner_lookup")!;
795
+ const argv = mcpToolCallToArgv(nestedMcpFixture, lookup, {
796
+ "user-name": "alice",
797
+ path: "./x",
798
+ json: true,
799
+ });
800
+ expect(argv).toEqual(["stat", "owner", "lookup", "--json", "--user-name", "alice", "./x"]);
801
+ });
802
+
803
+ test("mcpToolCallToArgv expands varargs positionals", () => {
804
+ const tools = collectMcpTools(nestedMcpFixture);
805
+ const read = tools.find((t) => t.name === "read")!;
806
+ const argv = mcpToolCallToArgv(nestedMcpFixture, read, { files: ["a", "b"] });
807
+ expect(argv).toEqual(["read", "a", "b"]);
808
+ });
809
+
810
+ test("reserved command name mcp is rejected", () => {
811
+ const root: CliCommand = {
812
+ key: "app",
813
+ description: "",
814
+ commands: [
815
+ {
816
+ key: "mcp",
817
+ description: "bad",
818
+ handler: () => {},
819
+ },
820
+ ],
821
+ };
822
+ expect(() => cliValidateRoot(root)).toThrow(/Reserved command name: mcp/);
823
+ });
824
+
825
+ test("mcpServer on non-root node is rejected", () => {
826
+ const root: CliCommand = {
827
+ key: "app",
828
+ description: "",
829
+ commands: [
830
+ {
831
+ key: "x",
832
+ description: "cmd",
833
+ mcpServer: {},
834
+ handler: () => {},
835
+ },
836
+ ],
837
+ };
838
+ expect(() => cliValidateRoot(root)).toThrow(/mcpServer is only supported on the program root/);
839
+ });
840
+
841
+ test("mcpTool on root is rejected", () => {
842
+ const root: CliCommand = {
843
+ key: "app",
844
+ description: "",
845
+ mcpTool: { enabled: false },
846
+ handler: () => {},
847
+ };
848
+ expect(() => cliValidateRoot(root)).toThrow(/mcpTool is only supported on leaf commands/);
849
+ });
850
+
851
+ test("mcpTool on routing node is rejected", () => {
852
+ const root: CliCommand = {
853
+ key: "app",
854
+ description: "",
855
+ commands: [
856
+ {
857
+ key: "group",
858
+ description: "group",
859
+ mcpTool: { enabled: false },
860
+ commands: [
861
+ {
862
+ key: "leaf",
863
+ description: "leaf",
864
+ handler: () => {},
865
+ },
866
+ ],
867
+ },
868
+ ],
869
+ };
870
+ expect(() => cliValidateRoot(root)).toThrow(/mcpTool is only supported on leaf commands/);
871
+ });
872
+
873
+ test("buildToolCallSuccess returns stdout only", () => {
874
+ const result = buildToolCallSuccess("hello\n", "");
875
+ expect(result.isError).toBe(false);
876
+ expect(result.content).toEqual([{ type: "text", text: "hello\n" }]);
877
+ expect(result.structuredContent).toBeUndefined();
878
+ });
879
+
880
+ test("buildToolCallSuccess adds stderr as second content block", () => {
881
+ const result = buildToolCallSuccess("out\n", "warn\n");
882
+ expect(result.content).toEqual([
883
+ { type: "text", text: "out\n" },
884
+ { type: "text", text: "warn" },
885
+ ]);
886
+ expect(result.structuredContent).toBeUndefined();
887
+ });
888
+
889
+ test("buildToolCallSuccess stderr-only still includes stdout slot", () => {
890
+ const result = buildToolCallSuccess("", "warn\n");
891
+ expect(result.content).toEqual([
892
+ { type: "text", text: "" },
893
+ { type: "text", text: "warn" },
894
+ ]);
895
+ });
896
+
897
+ test("buildToolCallSuccess parses JSON structuredContent", () => {
898
+ const result = buildToolCallSuccess('{"a":1}\n', "");
899
+ expect(result.structuredContent).toEqual({ a: 1 });
900
+ expect(result.content[0]!.text).toBe('{"a":1}\n');
901
+ });
902
+
903
+ test("buildToolCallSuccess skips structuredContent for plain text", () => {
904
+ const result = buildToolCallSuccess("lookup user=x\n", "");
905
+ expect(result.structuredContent).toBeUndefined();
906
+ });
907
+
908
+ test("buildToolCallSuccess parses JSON primitives", () => {
909
+ const result = buildToolCallSuccess("true\n", "");
910
+ expect(result.structuredContent).toBe(true);
911
+ });
912
+
913
+ test("MCP initialize returns tools and resources capabilities", async () => {
914
+ const responses = await mcpRequest([{ jsonrpc: "2.0", id: 1, method: "initialize", params: {} }]);
915
+ const res = responses.get(1) as { result: { capabilities: Record<string, unknown> } };
916
+ expect(res.result.capabilities.tools).toBeDefined();
917
+ expect(res.result.capabilities.resources).toBeDefined();
918
+ });
919
+
920
+ test("MCP tools/list includes stat_owner_lookup", async () => {
921
+ const responses = await mcpRequest([{ jsonrpc: "2.0", id: 2, method: "tools/list", params: {} }]);
922
+ const res = responses.get(2) as { result: { tools: { name: string; inputSchema: { required?: string[] } }[] } };
923
+ const lookup = res.result.tools.find((t) => t.name === "stat_owner_lookup");
924
+ expect(lookup).toBeDefined();
925
+ expect(lookup!.inputSchema.required).toContain("path");
926
+ });
927
+
928
+ test("MCP resources/read returns schema JSON", async () => {
929
+ const responses = await mcpRequest([
930
+ { jsonrpc: "2.0", id: 3, method: "resources/read", params: { uri: "argsbarg://schema" } },
931
+ ]);
932
+ const res = responses.get(3) as { result: { contents: { text: string }[] } };
933
+ const schema = JSON.parse(res.result.contents[0]!.text);
934
+ expect(schema.key).toBe("nested.ts");
935
+ });
936
+
937
+ test("MCP tools/call runs stat_owner_lookup", async () => {
938
+ const readme = join(import.meta.dir, "..", "README.md");
939
+ const responses = await mcpRequest([
940
+ {
941
+ jsonrpc: "2.0",
942
+ id: 4,
943
+ method: "tools/call",
944
+ params: {
945
+ name: "stat_owner_lookup",
946
+ arguments: { path: readme, "user-name": "test" },
947
+ },
948
+ },
949
+ ]);
950
+ const res = responses.get(4) as { result: { content: { text: string }[]; isError: boolean } };
951
+ expect(res.result.isError).toBe(false);
952
+ expect(res.result.content[0]!.text).toContain("lookup user=test");
953
+ });
954
+
955
+ test("MCP tools/call returns structuredContent for JSON stdout", async () => {
956
+ const readme = join(import.meta.dir, "..", "README.md");
957
+ const responses = await mcpRequest([
958
+ {
959
+ jsonrpc: "2.0",
960
+ id: 6,
961
+ method: "tools/call",
962
+ params: {
963
+ name: "stat_owner_lookup",
964
+ arguments: { path: readme, "user-name": "test", json: true },
965
+ },
966
+ },
967
+ ]);
968
+ const res = responses.get(6) as {
969
+ result: {
970
+ content: { text: string }[];
971
+ structuredContent?: { user: string; path: string };
972
+ isError: boolean;
973
+ };
974
+ };
975
+ expect(res.result.isError).toBe(false);
976
+ expect(res.result.structuredContent).toEqual({ user: "test", path: readme });
977
+ expect(JSON.parse(res.result.content[0]!.text.trim())).toEqual({ user: "test", path: readme });
978
+ });
979
+
980
+ test("MCP tools/call errors on missing required positional", async () => {
981
+ const responses = await mcpRequest([
982
+ {
983
+ jsonrpc: "2.0",
984
+ id: 5,
985
+ method: "tools/call",
986
+ params: { name: "stat_owner_lookup", arguments: { "user-name": "test" } },
987
+ },
988
+ ]);
989
+ const res = responses.get(5) as { result: { isError: boolean; content: { text: string }[] } };
990
+ expect(res.result.isError).toBe(true);
991
+ expect(res.result.content[0]!.text).toContain("Missing argument: path");
992
+ });
993
+
994
+ test("MCP ping returns empty result", async () => {
995
+ const responses = await mcpRequest([{ jsonrpc: "2.0", id: 99, method: "ping", params: {} }]);
996
+ const res = responses.get(99) as { result: Record<string, never> };
997
+ expect(res.result).toEqual({});
998
+ });
999
+
1000
+ test("minimal.ts mcp without opt-in fails", async () => {
1001
+ const { stderr, exitCode } = await $`bun run examples/minimal.ts mcp`.nothrow().quiet();
1002
+ expect(exitCode).toBe(1);
1003
+ expect(stderr.toString()).toContain("mcp");
1004
+ });
1005
+
1006
+ test("ctx.invocation is cli via cliRun", async () => {
1007
+ const indexPath = join(import.meta.dir, "index.ts");
1008
+ const { stdout } = await $`bun -e ${`
1009
+ import { cliRun, CliCommand } from ${JSON.stringify(indexPath)};
1010
+ const cli = { key: "t", description: "d", handler: (ctx) => console.log(ctx.invocation) };
1011
+ await cliRun(cli, []);
1012
+ `}`.quiet();
1013
+ expect(stdout.toString().trim()).toBe("cli");
1014
+ });
1015
+
1016
+ test("ctx.invocation is mcp via cliInvoke", async () => {
1017
+ let seen = "";
1018
+ const root: CliCommand = {
1019
+ key: "app",
1020
+ description: "",
1021
+ handler: (ctx) => {
1022
+ seen = ctx.invocation;
1023
+ },
1024
+ };
1025
+ cliValidateRoot(root);
1026
+ const result = await cliInvoke(root, []);
1027
+ expect(result.kind).toBe("ok");
1028
+ expect(seen).toBe("mcp");
1029
+ });
1030
+
1031
+ const enumMcpFixture: CliCommand = {
1032
+ key: "app",
1033
+ description: "",
1034
+ mcpServer: {},
1035
+ commands: [
1036
+ {
1037
+ key: "run",
1038
+ description: "Run with mode.",
1039
+ options: [
1040
+ {
1041
+ name: "mode",
1042
+ description: "Mode.",
1043
+ kind: CliOptionKind.Enum,
1044
+ choices: ["dev", "prod"],
1045
+ required: true,
1046
+ },
1047
+ ],
1048
+ handler: () => {},
1049
+ },
1050
+ ],
1051
+ };
1052
+
1053
+ test("Enum option inputSchema includes enum array", () => {
1054
+ const tools = collectMcpTools(enumMcpFixture);
1055
+ const run = tools.find((t) => t.name === "run")!;
1056
+ const schema = run.inputSchema as { properties: { mode: { enum?: string[] } } };
1057
+ expect(schema.properties.mode.enum).toEqual(["dev", "prod"]);
1058
+ });
1059
+
1060
+ test("cliInvoke rejects invalid Enum value", async () => {
1061
+ const root: CliCommand = {
1062
+ key: "app",
1063
+ description: "",
1064
+ handler: () => {},
1065
+ options: [
1066
+ {
1067
+ name: "mode",
1068
+ description: "Mode.",
1069
+ kind: CliOptionKind.Enum,
1070
+ choices: ["dev", "prod"],
1071
+ required: true,
1072
+ },
1073
+ ],
1074
+ };
1075
+ cliValidateRoot(root);
1076
+ const result = await cliInvoke(root, ["--mode", "staging"]);
1077
+ expect(result.kind).toBe("error");
1078
+ expect(result.errorMsg).toContain("not one of");
1079
+ });
1080
+
1081
+ test("cliInvoke accepts valid Enum value", async () => {
1082
+ const root: CliCommand = {
1083
+ key: "app",
1084
+ description: "",
1085
+ handler: (ctx) => {
1086
+ console.log(ctx.stringOpt("mode"));
1087
+ },
1088
+ options: [
1089
+ {
1090
+ name: "mode",
1091
+ description: "Mode.",
1092
+ kind: CliOptionKind.Enum,
1093
+ choices: ["dev", "prod"],
1094
+ required: true,
1095
+ },
1096
+ ],
1097
+ };
1098
+ cliValidateRoot(root);
1099
+ const result = await cliInvoke(root, ["--mode", "dev"]);
1100
+ expect(result.kind).toBe("ok");
1101
+ expect(result.stdout.trim()).toBe("dev");
1102
+ });
1103
+
1104
+ test("cliValidateRoot rejects Enum with no choices", () => {
1105
+ const root: CliCommand = {
1106
+ key: "app",
1107
+ description: "",
1108
+ handler: () => {},
1109
+ options: [{ name: "mode", description: "", kind: CliOptionKind.Enum, choices: [] }],
1110
+ };
1111
+ expect(() => cliValidateRoot(root)).toThrow(/requires non-empty choices/);
1112
+ });
1113
+
1114
+ test("cliValidateRoot rejects Enum with duplicate choices", () => {
1115
+ const root: CliCommand = {
1116
+ key: "app",
1117
+ description: "",
1118
+ handler: () => {},
1119
+ options: [{ name: "mode", description: "", kind: CliOptionKind.Enum, choices: ["a", "a"] }],
1120
+ };
1121
+ expect(() => cliValidateRoot(root)).toThrow(/choices must be distinct/);
1122
+ });
1123
+
1124
+ test("mcpTool.description override wins without requiresEnv suffix", () => {
1125
+ const root: CliCommand = {
1126
+ key: "app",
1127
+ description: "",
1128
+ mcpServer: {},
1129
+ commands: [
1130
+ {
1131
+ key: "x",
1132
+ description: "Leaf desc.",
1133
+ mcpTool: { description: "custom", requiresEnv: ["TOKEN"] },
1134
+ handler: () => {},
1135
+ },
1136
+ ],
1137
+ };
1138
+ const tools = collectMcpTools(root);
1139
+ expect(tools[0]!.description).toBe("custom");
1140
+ });
1141
+
1142
+ test("mcpTool.requiresEnv appended to auto description", () => {
1143
+ const root: CliCommand = {
1144
+ key: "app",
1145
+ description: "",
1146
+ mcpServer: {},
1147
+ commands: [
1148
+ {
1149
+ key: "x",
1150
+ description: "Leaf desc.",
1151
+ mcpTool: { requiresEnv: ["TOKEN"] },
1152
+ handler: () => {},
1153
+ },
1154
+ ],
1155
+ };
1156
+ const tools = collectMcpTools(root);
1157
+ expect(tools[0]!.description).toContain("[requires env: TOKEN]");
1158
+ });
1159
+
1160
+ test("cliValidateRoot rejects duplicate mcpResources URIs", () => {
1161
+ const root: CliCommand = {
1162
+ key: "app",
1163
+ description: "",
1164
+ mcpServer: {
1165
+ resources: [
1166
+ { uri: "a://1", name: "a", load: () => "a" },
1167
+ { uri: "a://1", name: "b", load: () => "b" },
1168
+ ],
1169
+ },
1170
+ commands: [{ key: "x", description: "", handler: () => {} }],
1171
+ };
1172
+ expect(() => cliValidateRoot(root)).toThrow(/URIs must be unique/);
1173
+ });
1174
+
1175
+ test("cliValidateRoot rejects resource URI matching schemaResourceUri", () => {
1176
+ const root: CliCommand = {
1177
+ key: "app",
1178
+ description: "",
1179
+ mcpServer: {
1180
+ schemaResourceUri: "custom://schema",
1181
+ resources: [{ uri: "custom://schema", name: "dup", load: () => "" }],
1182
+ },
1183
+ commands: [{ key: "x", description: "", handler: () => {} }],
1184
+ };
1185
+ expect(() => cliValidateRoot(root)).toThrow(/conflicts with the built-in schema resource/);
1186
+ });
1187
+
1188
+ test("allMcpResources includes custom resources", () => {
1189
+ const root: CliCommand = {
1190
+ key: "app",
1191
+ description: "",
1192
+ mcpServer: {
1193
+ resources: [{ uri: "test://x", name: "x", load: () => "body" }],
1194
+ },
1195
+ commands: [{ key: "leaf", description: "", handler: () => {} }],
1196
+ };
1197
+ const resources = allMcpResources(root);
1198
+ expect(resources.map((r) => r.uri)).toContain("argsbarg://schema");
1199
+ expect(resources.map((r) => r.uri)).toContain("test://x");
1200
+ });
1201
+
1202
+ test("applyShellEnv merges PATH and preserves host vars", () => {
1203
+ const origPath = process.env.PATH ?? "";
1204
+ const origHome = process.env.HOME;
1205
+ process.env.PATH = "/host/bin";
1206
+ process.env.HOME = "host-home";
1207
+ applyShellEnv({ PATH: "/shell/bin:/host/bin", HOME: "shell-home", NEWVAR: "yes" });
1208
+ expect(process.env.PATH?.startsWith("/shell/bin:")).toBe(true);
1209
+ expect(process.env.PATH).toContain("/host/bin");
1210
+ expect(process.env.HOME).toBe("host-home");
1211
+ expect(process.env.NEWVAR).toBe("yes");
1212
+ process.env.PATH = origPath;
1213
+ if (origHome === undefined) {
1214
+ delete process.env.HOME;
1215
+ } else {
1216
+ process.env.HOME = origHome;
1217
+ }
1218
+ delete process.env.NEWVAR;
1219
+ });
1220
+
1221
+ test("loadEnvFile overwrites existing keys", () => {
1222
+ const dir = mkdtempSync(join(tmpdir(), "argsbarg-env-"));
1223
+ const file = join(dir, ".env");
1224
+ writeFileSync(file, "FOO=fromfile\n", "utf8");
1225
+ process.env.FOO = "original";
1226
+ loadEnvFile(file);
1227
+ expect(process.env.FOO).toBe("fromfile");
1228
+ delete process.env.FOO;
1229
+ });
1230
+
1231
+ test("Enum completions list choices in bash script", () => {
1232
+ const root: CliCommand = {
1233
+ key: "app",
1234
+ description: "",
1235
+ commands: [
1236
+ {
1237
+ key: "run",
1238
+ description: "",
1239
+ options: [
1240
+ { name: "mode", description: "m", kind: CliOptionKind.Enum, choices: ["dev", "prod"] },
1241
+ ],
1242
+ handler: () => {},
1243
+ },
1244
+ ],
1245
+ };
1246
+ const bash = completionBashScript(root);
1247
+ expect(bash).toContain("--mode) COMPREPLY=");
1248
+ expect(bash).toContain("dev");
1249
+ expect(bash).toContain("prod");
1250
+ });
1251
+
1252
+ test("MCP resources/list includes custom resource", async () => {
1253
+ const responses = await mcpRequest(
1254
+ [{ jsonrpc: "2.0", id: 10, method: "resources/list", params: {} }],
1255
+ { script: "examples/mcp-test.ts" },
1256
+ );
1257
+ const res = responses.get(10) as { result: { resources: { uri: string }[] } };
1258
+ const uris = res.result.resources.map((r) => r.uri);
1259
+ expect(uris).toContain("argsbarg://schema");
1260
+ expect(uris).toContain("test://hello");
1261
+ });
1262
+
1263
+ test("MCP resources/read returns custom resource body", async () => {
1264
+ const responses = await mcpRequest(
1265
+ [{ jsonrpc: "2.0", id: 11, method: "resources/read", params: { uri: "test://hello" } }],
1266
+ { script: "examples/mcp-test.ts" },
1267
+ );
1268
+ const res = responses.get(11) as { result: { contents: { text: string }[] } };
1269
+ expect(res.result.contents[0]!.text).toBe("hello resource");
1270
+ });
1271
+
1272
+ test("MCP resources/read unknown URI returns error", async () => {
1273
+ const responses = await mcpRequest(
1274
+ [{ jsonrpc: "2.0", id: 12, method: "resources/read", params: { uri: "missing://nope" } }],
1275
+ { script: "examples/mcp-test.ts" },
1276
+ );
1277
+ const res = responses.get(12) as { error: { code: number } };
1278
+ expect(res.error.code).toBe(-32602);
1279
+ });
1280
+
1281
+ test("MCP requiresEnv fails when env missing", async () => {
1282
+ const responses = await mcpRequest(
1283
+ [
1284
+ {
1285
+ jsonrpc: "2.0",
1286
+ id: 13,
1287
+ method: "tools/call",
1288
+ params: { name: "echo_env", arguments: { name: "ARGS_TEST_SECRET" } },
1289
+ },
1290
+ ],
1291
+ { script: "examples/mcp-test.ts", env: { ARGS_TEST_SECRET: "" } },
1292
+ );
1293
+ const res = responses.get(13) as { result: { isError: boolean; content: { text: string }[] } };
1294
+ expect(res.result.isError).toBe(true);
1295
+ expect(res.result.content[0]!.text).toContain("ARGS_TEST_SECRET");
1296
+ });
1297
+
1298
+ test("MCP requiresEnv succeeds when env present", async () => {
1299
+ const responses = await mcpRequest(
1300
+ [
1301
+ {
1302
+ jsonrpc: "2.0",
1303
+ id: 14,
1304
+ method: "tools/call",
1305
+ params: { name: "echo_env", arguments: { name: "ARGS_TEST_SECRET" } },
1306
+ },
1307
+ ],
1308
+ { script: "examples/mcp-test.ts", env: { ARGS_TEST_SECRET: "sekrit" } },
1309
+ );
1310
+ const res = responses.get(14) as { result: { isError: boolean; content: { text: string }[] } };
1311
+ expect(res.result.isError).toBe(false);
1312
+ expect(res.result.content[0]!.text.trim()).toBe("sekrit");
1313
+ });
1314
+
1315
+ test("MCP envFile loads vars for tool handlers", async () => {
1316
+ const dir = mkdtempSync(join(tmpdir(), "argsbarg-mcp-"));
1317
+ const envFile = join(dir, "mcp.env");
1318
+ writeFileSync(envFile, "ARGS_FILE_TOKEN=file-value\n", "utf8");
1319
+ const responses = await mcpRequest(
1320
+ [
1321
+ {
1322
+ jsonrpc: "2.0",
1323
+ id: 15,
1324
+ method: "tools/call",
1325
+ params: { name: "echo_env", arguments: { name: "ARGS_FILE_TOKEN" } },
1326
+ },
1327
+ ],
1328
+ { script: "examples/mcp-test.ts", env: { ARGS_TEST_ENV_FILE: envFile, ARGS_TEST_SECRET: "present" } },
1329
+ );
1330
+ const res = responses.get(15) as { result: { isError: boolean; content: { text: string }[] } };
1331
+ expect(res.result.isError).toBe(false);
1332
+ expect(res.result.content[0]!.text.trim()).toBe("file-value");
642
1333
  });