|
1 | | -import { test, expect } from "bun:test" |
| 1 | +import { test, expect, mock, afterEach } from "bun:test" |
2 | 2 | import { Config } from "../../src/config/config" |
3 | 3 | import { Instance } from "../../src/project/instance" |
| 4 | +import { Auth } from "../../src/auth" |
4 | 5 | import { tmpdir } from "../fixture/fixture" |
5 | 6 | import path from "path" |
6 | 7 | import fs from "fs/promises" |
@@ -913,3 +914,234 @@ test("permission config preserves key order", async () => { |
913 | 914 | }, |
914 | 915 | }) |
915 | 916 | }) |
| 917 | + |
| 918 | +// MCP config merging tests |
| 919 | + |
| 920 | +test("project config can override MCP server enabled status", async () => { |
| 921 | + await using tmp = await tmpdir({ |
| 922 | + init: async (dir) => { |
| 923 | + // Simulates a base config (like from remote .well-known) with disabled MCP |
| 924 | + await Bun.write( |
| 925 | + path.join(dir, "opencode.jsonc"), |
| 926 | + JSON.stringify({ |
| 927 | + $schema: "https://opencode.ai/config.json", |
| 928 | + mcp: { |
| 929 | + jira: { |
| 930 | + type: "remote", |
| 931 | + url: "https://jira.example.com/mcp", |
| 932 | + enabled: false, |
| 933 | + }, |
| 934 | + wiki: { |
| 935 | + type: "remote", |
| 936 | + url: "https://wiki.example.com/mcp", |
| 937 | + enabled: false, |
| 938 | + }, |
| 939 | + }, |
| 940 | + }), |
| 941 | + ) |
| 942 | + // Project config enables just jira |
| 943 | + await Bun.write( |
| 944 | + path.join(dir, "opencode.json"), |
| 945 | + JSON.stringify({ |
| 946 | + $schema: "https://opencode.ai/config.json", |
| 947 | + mcp: { |
| 948 | + jira: { |
| 949 | + type: "remote", |
| 950 | + url: "https://jira.example.com/mcp", |
| 951 | + enabled: true, |
| 952 | + }, |
| 953 | + }, |
| 954 | + }), |
| 955 | + ) |
| 956 | + }, |
| 957 | + }) |
| 958 | + await Instance.provide({ |
| 959 | + directory: tmp.path, |
| 960 | + fn: async () => { |
| 961 | + const config = await Config.get() |
| 962 | + // jira should be enabled (overridden by project config) |
| 963 | + expect(config.mcp?.jira).toEqual({ |
| 964 | + type: "remote", |
| 965 | + url: "https://jira.example.com/mcp", |
| 966 | + enabled: true, |
| 967 | + }) |
| 968 | + // wiki should still be disabled (not overridden) |
| 969 | + expect(config.mcp?.wiki).toEqual({ |
| 970 | + type: "remote", |
| 971 | + url: "https://wiki.example.com/mcp", |
| 972 | + enabled: false, |
| 973 | + }) |
| 974 | + }, |
| 975 | + }) |
| 976 | +}) |
| 977 | + |
| 978 | +test("MCP config deep merges preserving base config properties", async () => { |
| 979 | + await using tmp = await tmpdir({ |
| 980 | + init: async (dir) => { |
| 981 | + // Base config with full MCP definition |
| 982 | + await Bun.write( |
| 983 | + path.join(dir, "opencode.jsonc"), |
| 984 | + JSON.stringify({ |
| 985 | + $schema: "https://opencode.ai/config.json", |
| 986 | + mcp: { |
| 987 | + myserver: { |
| 988 | + type: "remote", |
| 989 | + url: "https://myserver.example.com/mcp", |
| 990 | + enabled: false, |
| 991 | + headers: { |
| 992 | + "X-Custom-Header": "value", |
| 993 | + }, |
| 994 | + }, |
| 995 | + }, |
| 996 | + }), |
| 997 | + ) |
| 998 | + // Override just enables it, should preserve other properties |
| 999 | + await Bun.write( |
| 1000 | + path.join(dir, "opencode.json"), |
| 1001 | + JSON.stringify({ |
| 1002 | + $schema: "https://opencode.ai/config.json", |
| 1003 | + mcp: { |
| 1004 | + myserver: { |
| 1005 | + type: "remote", |
| 1006 | + url: "https://myserver.example.com/mcp", |
| 1007 | + enabled: true, |
| 1008 | + }, |
| 1009 | + }, |
| 1010 | + }), |
| 1011 | + ) |
| 1012 | + }, |
| 1013 | + }) |
| 1014 | + await Instance.provide({ |
| 1015 | + directory: tmp.path, |
| 1016 | + fn: async () => { |
| 1017 | + const config = await Config.get() |
| 1018 | + expect(config.mcp?.myserver).toEqual({ |
| 1019 | + type: "remote", |
| 1020 | + url: "https://myserver.example.com/mcp", |
| 1021 | + enabled: true, |
| 1022 | + headers: { |
| 1023 | + "X-Custom-Header": "value", |
| 1024 | + }, |
| 1025 | + }) |
| 1026 | + }, |
| 1027 | + }) |
| 1028 | +}) |
| 1029 | + |
| 1030 | +test("local .opencode config can override MCP from project config", async () => { |
| 1031 | + await using tmp = await tmpdir({ |
| 1032 | + init: async (dir) => { |
| 1033 | + // Project config with disabled MCP |
| 1034 | + await Bun.write( |
| 1035 | + path.join(dir, "opencode.json"), |
| 1036 | + JSON.stringify({ |
| 1037 | + $schema: "https://opencode.ai/config.json", |
| 1038 | + mcp: { |
| 1039 | + docs: { |
| 1040 | + type: "remote", |
| 1041 | + url: "https://docs.example.com/mcp", |
| 1042 | + enabled: false, |
| 1043 | + }, |
| 1044 | + }, |
| 1045 | + }), |
| 1046 | + ) |
| 1047 | + // Local .opencode directory config enables it |
| 1048 | + const opencodeDir = path.join(dir, ".opencode") |
| 1049 | + await fs.mkdir(opencodeDir, { recursive: true }) |
| 1050 | + await Bun.write( |
| 1051 | + path.join(opencodeDir, "opencode.json"), |
| 1052 | + JSON.stringify({ |
| 1053 | + $schema: "https://opencode.ai/config.json", |
| 1054 | + mcp: { |
| 1055 | + docs: { |
| 1056 | + type: "remote", |
| 1057 | + url: "https://docs.example.com/mcp", |
| 1058 | + enabled: true, |
| 1059 | + }, |
| 1060 | + }, |
| 1061 | + }), |
| 1062 | + ) |
| 1063 | + }, |
| 1064 | + }) |
| 1065 | + await Instance.provide({ |
| 1066 | + directory: tmp.path, |
| 1067 | + fn: async () => { |
| 1068 | + const config = await Config.get() |
| 1069 | + expect(config.mcp?.docs?.enabled).toBe(true) |
| 1070 | + }, |
| 1071 | + }) |
| 1072 | +}) |
| 1073 | + |
| 1074 | +test("project config overrides remote well-known config", async () => { |
| 1075 | + const originalFetch = globalThis.fetch |
| 1076 | + let fetchedUrl: string | undefined |
| 1077 | + const mockFetch = mock((url: string | URL | Request) => { |
| 1078 | + const urlStr = url.toString() |
| 1079 | + if (urlStr.includes(".well-known/opencode")) { |
| 1080 | + fetchedUrl = urlStr |
| 1081 | + return Promise.resolve( |
| 1082 | + new Response( |
| 1083 | + JSON.stringify({ |
| 1084 | + config: { |
| 1085 | + mcp: { |
| 1086 | + jira: { |
| 1087 | + type: "remote", |
| 1088 | + url: "https://jira.example.com/mcp", |
| 1089 | + enabled: false, |
| 1090 | + }, |
| 1091 | + }, |
| 1092 | + }, |
| 1093 | + }), |
| 1094 | + { status: 200 }, |
| 1095 | + ), |
| 1096 | + ) |
| 1097 | + } |
| 1098 | + return originalFetch(url) |
| 1099 | + }) |
| 1100 | + globalThis.fetch = mockFetch as unknown as typeof fetch |
| 1101 | + |
| 1102 | + const originalAuthAll = Auth.all |
| 1103 | + Auth.all = mock(() => |
| 1104 | + Promise.resolve({ |
| 1105 | + "https://example.com": { |
| 1106 | + type: "wellknown" as const, |
| 1107 | + key: "TEST_TOKEN", |
| 1108 | + token: "test-token", |
| 1109 | + }, |
| 1110 | + }), |
| 1111 | + ) |
| 1112 | + |
| 1113 | + try { |
| 1114 | + await using tmp = await tmpdir({ |
| 1115 | + git: true, |
| 1116 | + init: async (dir) => { |
| 1117 | + // Project config enables jira (overriding remote default) |
| 1118 | + await Bun.write( |
| 1119 | + path.join(dir, "opencode.json"), |
| 1120 | + JSON.stringify({ |
| 1121 | + $schema: "https://opencode.ai/config.json", |
| 1122 | + mcp: { |
| 1123 | + jira: { |
| 1124 | + type: "remote", |
| 1125 | + url: "https://jira.example.com/mcp", |
| 1126 | + enabled: true, |
| 1127 | + }, |
| 1128 | + }, |
| 1129 | + }), |
| 1130 | + ) |
| 1131 | + }, |
| 1132 | + }) |
| 1133 | + await Instance.provide({ |
| 1134 | + directory: tmp.path, |
| 1135 | + fn: async () => { |
| 1136 | + const config = await Config.get() |
| 1137 | + // Verify fetch was called for wellknown config |
| 1138 | + expect(fetchedUrl).toBe("https://example.com/.well-known/opencode") |
| 1139 | + // Project config (enabled: true) should override remote (enabled: false) |
| 1140 | + expect(config.mcp?.jira?.enabled).toBe(true) |
| 1141 | + }, |
| 1142 | + }) |
| 1143 | + } finally { |
| 1144 | + globalThis.fetch = originalFetch |
| 1145 | + Auth.all = originalAuthAll |
| 1146 | + } |
| 1147 | +}) |
0 commit comments