@mandujs/core 0.9.46 → 0.10.0
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/package.json +1 -1
- package/src/brain/doctor/config-analyzer.ts +498 -0
- package/src/brain/doctor/index.ts +10 -0
- package/src/change/snapshot.ts +46 -1
- package/src/change/types.ts +13 -0
- package/src/config/index.ts +8 -2
- package/src/config/mcp-ref.ts +348 -0
- package/src/config/mcp-status.ts +348 -0
- package/src/config/metadata.test.ts +308 -0
- package/src/config/metadata.ts +293 -0
- package/src/config/symbols.ts +144 -0
- package/src/contract/index.ts +26 -25
- package/src/contract/protection.ts +364 -0
- package/src/error/domains.ts +265 -0
- package/src/error/index.ts +25 -13
- package/src/filling/filling.ts +88 -6
- package/src/guard/analyzer.ts +7 -2
- package/src/guard/config-guard.ts +281 -0
- package/src/guard/decision-memory.test.ts +293 -0
- package/src/guard/decision-memory.ts +532 -0
- package/src/guard/healing.test.ts +259 -0
- package/src/guard/healing.ts +874 -0
- package/src/guard/index.ts +119 -0
- package/src/guard/negotiation.test.ts +282 -0
- package/src/guard/negotiation.ts +975 -0
- package/src/guard/semantic-slots.test.ts +379 -0
- package/src/guard/semantic-slots.ts +796 -0
- package/src/index.ts +2 -0
- package/src/lockfile/generate.ts +259 -0
- package/src/lockfile/index.ts +186 -0
- package/src/lockfile/lockfile.test.ts +410 -0
- package/src/lockfile/types.ts +184 -0
- package/src/lockfile/validate.ts +308 -0
- package/src/runtime/security.ts +155 -0
- package/src/runtime/server.ts +318 -256
- package/src/utils/differ.test.ts +342 -0
- package/src/utils/differ.ts +482 -0
- package/src/utils/hasher.test.ts +326 -0
- package/src/utils/hasher.ts +319 -0
- package/src/utils/index.ts +29 -0
- package/src/utils/safe-io.ts +188 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 설정 Diff 시스템 테스트
|
|
3
|
+
*
|
|
4
|
+
* @see docs/plans/08_ont-run_adoption_plan.md - 섹션 7.1
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, it } from "bun:test";
|
|
8
|
+
import {
|
|
9
|
+
diffConfig,
|
|
10
|
+
formatConfigDiff,
|
|
11
|
+
summarizeDiff,
|
|
12
|
+
hasConfigChanges,
|
|
13
|
+
} from "./differ.js";
|
|
14
|
+
|
|
15
|
+
describe("diffConfig", () => {
|
|
16
|
+
it("should detect added MCP servers", () => {
|
|
17
|
+
const oldConfig = { mcpServers: {} };
|
|
18
|
+
const newConfig = {
|
|
19
|
+
mcpServers: {
|
|
20
|
+
sequential: { command: "npx", args: ["-y", "@mcp/sequential"] },
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const diff = diffConfig(oldConfig, newConfig);
|
|
25
|
+
|
|
26
|
+
expect(diff.hasChanges).toBe(true);
|
|
27
|
+
expect(diff.mcpServers.added).toContain("sequential");
|
|
28
|
+
expect(diff.mcpServers.removed).toHaveLength(0);
|
|
29
|
+
expect(diff.mcpServers.modified).toHaveLength(0);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("should detect removed MCP servers", () => {
|
|
33
|
+
const oldConfig = {
|
|
34
|
+
mcpServers: {
|
|
35
|
+
context7: { command: "npx", args: ["-y", "@mcp/context7"] },
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
const newConfig = { mcpServers: {} };
|
|
39
|
+
|
|
40
|
+
const diff = diffConfig(oldConfig, newConfig);
|
|
41
|
+
|
|
42
|
+
expect(diff.hasChanges).toBe(true);
|
|
43
|
+
expect(diff.mcpServers.removed).toContain("context7");
|
|
44
|
+
expect(diff.mcpServers.added).toHaveLength(0);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("should detect modified MCP servers", () => {
|
|
48
|
+
const oldConfig = {
|
|
49
|
+
mcpServers: {
|
|
50
|
+
api: { url: "http://old-url.com", port: 3000 },
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
const newConfig = {
|
|
54
|
+
mcpServers: {
|
|
55
|
+
api: { url: "http://new-url.com", port: 3000 },
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const diff = diffConfig(oldConfig, newConfig);
|
|
60
|
+
|
|
61
|
+
expect(diff.hasChanges).toBe(true);
|
|
62
|
+
expect(diff.mcpServers.modified).toHaveLength(1);
|
|
63
|
+
expect(diff.mcpServers.modified[0].name).toBe("api");
|
|
64
|
+
expect(diff.mcpServers.modified[0].changes.url).toEqual({
|
|
65
|
+
old: "http://old-url.com",
|
|
66
|
+
new: "http://new-url.com",
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("should detect modified project config values", () => {
|
|
71
|
+
const oldConfig = { port: 3000, debug: false };
|
|
72
|
+
const newConfig = { port: 3001, debug: false };
|
|
73
|
+
|
|
74
|
+
const diff = diffConfig(oldConfig, newConfig);
|
|
75
|
+
|
|
76
|
+
expect(diff.hasChanges).toBe(true);
|
|
77
|
+
expect(diff.projectConfig.modified).toHaveLength(1);
|
|
78
|
+
expect(diff.projectConfig.modified[0]).toEqual({
|
|
79
|
+
key: "port",
|
|
80
|
+
old: 3000,
|
|
81
|
+
new: 3001,
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("should detect added project config keys", () => {
|
|
86
|
+
const oldConfig = { port: 3000 };
|
|
87
|
+
const newConfig = { port: 3000, newFeature: true };
|
|
88
|
+
|
|
89
|
+
const diff = diffConfig(oldConfig, newConfig);
|
|
90
|
+
|
|
91
|
+
expect(diff.hasChanges).toBe(true);
|
|
92
|
+
expect(diff.projectConfig.added).toContain("newFeature");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("should detect removed project config keys", () => {
|
|
96
|
+
const oldConfig = { port: 3000, deprecated: true };
|
|
97
|
+
const newConfig = { port: 3000 };
|
|
98
|
+
|
|
99
|
+
const diff = diffConfig(oldConfig, newConfig);
|
|
100
|
+
|
|
101
|
+
expect(diff.hasChanges).toBe(true);
|
|
102
|
+
expect(diff.projectConfig.removed).toContain("deprecated");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("should return hasChanges=false when configs are identical", () => {
|
|
106
|
+
const config = {
|
|
107
|
+
port: 3000,
|
|
108
|
+
mcpServers: {
|
|
109
|
+
api: { url: "http://example.com" },
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const diff = diffConfig(config, { ...config });
|
|
114
|
+
|
|
115
|
+
expect(diff.hasChanges).toBe(false);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("should handle empty configs", () => {
|
|
119
|
+
const diff = diffConfig({}, {});
|
|
120
|
+
|
|
121
|
+
expect(diff.hasChanges).toBe(false);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("should handle nested object changes", () => {
|
|
125
|
+
const oldConfig = {
|
|
126
|
+
features: {
|
|
127
|
+
islands: true,
|
|
128
|
+
ssr: { enabled: true, streaming: false },
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
const newConfig = {
|
|
132
|
+
features: {
|
|
133
|
+
islands: true,
|
|
134
|
+
ssr: { enabled: true, streaming: true },
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const diff = diffConfig(oldConfig, newConfig);
|
|
139
|
+
|
|
140
|
+
expect(diff.hasChanges).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("should handle array changes", () => {
|
|
144
|
+
const oldConfig = { plugins: ["a", "b"] };
|
|
145
|
+
const newConfig = { plugins: ["a", "b", "c"] };
|
|
146
|
+
|
|
147
|
+
const diff = diffConfig(oldConfig, newConfig);
|
|
148
|
+
|
|
149
|
+
expect(diff.hasChanges).toBe(true);
|
|
150
|
+
expect(diff.projectConfig.modified[0].key).toBe("plugins");
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe("formatConfigDiff", () => {
|
|
155
|
+
it("should format diff with colors by default", () => {
|
|
156
|
+
const diff = diffConfig(
|
|
157
|
+
{ port: 3000 },
|
|
158
|
+
{ port: 3001 }
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
const formatted = formatConfigDiff(diff);
|
|
162
|
+
|
|
163
|
+
expect(formatted).toContain("변경 감지");
|
|
164
|
+
expect(formatted).toContain("port");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("should format diff without colors when disabled", () => {
|
|
168
|
+
const diff = diffConfig(
|
|
169
|
+
{ port: 3000 },
|
|
170
|
+
{ port: 3001 }
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
const formatted = formatConfigDiff(diff, { color: false });
|
|
174
|
+
|
|
175
|
+
expect(formatted).toContain("변경 감지");
|
|
176
|
+
// ANSI 코드가 없어야 함
|
|
177
|
+
expect(formatted).not.toContain("\x1b[");
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("should redact secrets by default in formatted diff", () => {
|
|
181
|
+
const oldConfig = {
|
|
182
|
+
mcpServers: {
|
|
183
|
+
api: { url: "http://api.com", token: "old-secret-123" },
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
const newConfig = {
|
|
187
|
+
mcpServers: {
|
|
188
|
+
api: { url: "http://api.com", token: "new-secret-456" },
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const diff = diffConfig(oldConfig, newConfig);
|
|
193
|
+
const formatted = formatConfigDiff(diff, { verbose: true });
|
|
194
|
+
|
|
195
|
+
expect(formatted).toContain("***");
|
|
196
|
+
expect(formatted).not.toContain("old-secret-123");
|
|
197
|
+
expect(formatted).not.toContain("new-secret-456");
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("should show secrets when showSecrets is true", () => {
|
|
201
|
+
const oldConfig = {
|
|
202
|
+
mcpServers: {
|
|
203
|
+
api: { token: "old-secret" },
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
const newConfig = {
|
|
207
|
+
mcpServers: {
|
|
208
|
+
api: { token: "new-secret" },
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const diff = diffConfig(oldConfig, newConfig);
|
|
213
|
+
const formatted = formatConfigDiff(diff, {
|
|
214
|
+
verbose: true,
|
|
215
|
+
showSecrets: true,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
expect(formatted).toContain("old-secret");
|
|
219
|
+
expect(formatted).toContain("new-secret");
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("should redact various sensitive key patterns", () => {
|
|
223
|
+
const sensitiveKeys = [
|
|
224
|
+
"token",
|
|
225
|
+
"secret",
|
|
226
|
+
"password",
|
|
227
|
+
"api_key",
|
|
228
|
+
"apikey",
|
|
229
|
+
"authorization",
|
|
230
|
+
"access_token",
|
|
231
|
+
"private_key",
|
|
232
|
+
];
|
|
233
|
+
|
|
234
|
+
for (const key of sensitiveKeys) {
|
|
235
|
+
const oldConfig = { mcpServers: { test: { [key]: "old-value" } } };
|
|
236
|
+
const newConfig = { mcpServers: { test: { [key]: "new-value" } } };
|
|
237
|
+
|
|
238
|
+
const diff = diffConfig(oldConfig, newConfig);
|
|
239
|
+
const formatted = formatConfigDiff(diff, { verbose: true });
|
|
240
|
+
|
|
241
|
+
expect(formatted).toContain("***");
|
|
242
|
+
expect(formatted).not.toContain("old-value");
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("should show no changes message when identical", () => {
|
|
247
|
+
const config = { port: 3000 };
|
|
248
|
+
const diff = diffConfig(config, config);
|
|
249
|
+
const formatted = formatConfigDiff(diff);
|
|
250
|
+
|
|
251
|
+
expect(formatted).toContain("변경사항 없음");
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
describe("summarizeDiff", () => {
|
|
256
|
+
it("should summarize diff changes", () => {
|
|
257
|
+
const diff = diffConfig(
|
|
258
|
+
{ port: 3000, mcpServers: { a: {} } },
|
|
259
|
+
{ port: 3001, mcpServers: { b: {} } }
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
const summary = summarizeDiff(diff);
|
|
263
|
+
|
|
264
|
+
expect(summary).toContain("MCP 서버");
|
|
265
|
+
expect(summary).toContain("설정");
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("should return no changes message when identical", () => {
|
|
269
|
+
const config = { port: 3000 };
|
|
270
|
+
const diff = diffConfig(config, config);
|
|
271
|
+
|
|
272
|
+
expect(summarizeDiff(diff)).toBe("변경사항 없음");
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
describe("hasConfigChanges", () => {
|
|
277
|
+
it("should return true when configs differ", () => {
|
|
278
|
+
expect(hasConfigChanges({ a: 1 }, { a: 2 })).toBe(true);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("should return false when configs are identical", () => {
|
|
282
|
+
expect(hasConfigChanges({ a: 1 }, { a: 1 })).toBe(false);
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
describe("real-world scenarios", () => {
|
|
287
|
+
it("should handle complex mandu config diff", () => {
|
|
288
|
+
const oldConfig = {
|
|
289
|
+
name: "my-project",
|
|
290
|
+
port: 3000,
|
|
291
|
+
mcpServers: {
|
|
292
|
+
sequential: {
|
|
293
|
+
command: "npx",
|
|
294
|
+
args: ["-y", "@anthropic/sequential-mcp"],
|
|
295
|
+
},
|
|
296
|
+
magic: {
|
|
297
|
+
command: "npx",
|
|
298
|
+
args: ["-y", "@21st/magic-mcp"],
|
|
299
|
+
env: { API_KEY: "secret-key-123" },
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
features: {
|
|
303
|
+
islands: true,
|
|
304
|
+
ssr: true,
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const newConfig = {
|
|
309
|
+
name: "my-project",
|
|
310
|
+
port: 3001, // 변경
|
|
311
|
+
mcpServers: {
|
|
312
|
+
sequential: {
|
|
313
|
+
command: "npx",
|
|
314
|
+
args: ["-y", "@anthropic/sequential-mcp"],
|
|
315
|
+
},
|
|
316
|
+
// magic 삭제
|
|
317
|
+
context7: {
|
|
318
|
+
// 추가
|
|
319
|
+
command: "npx",
|
|
320
|
+
args: ["-y", "@context7/mcp"],
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
features: {
|
|
324
|
+
islands: true,
|
|
325
|
+
ssr: true,
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const diff = diffConfig(oldConfig, newConfig);
|
|
330
|
+
|
|
331
|
+
expect(diff.hasChanges).toBe(true);
|
|
332
|
+
expect(diff.mcpServers.added).toContain("context7");
|
|
333
|
+
expect(diff.mcpServers.removed).toContain("magic");
|
|
334
|
+
expect(diff.projectConfig.modified.find((m) => m.key === "port")).toBeDefined();
|
|
335
|
+
|
|
336
|
+
// 포맷 테스트
|
|
337
|
+
const formatted = formatConfigDiff(diff, { color: false, verbose: true });
|
|
338
|
+
expect(formatted).toContain("context7");
|
|
339
|
+
expect(formatted).toContain("magic");
|
|
340
|
+
expect(formatted).toContain("port");
|
|
341
|
+
});
|
|
342
|
+
});
|