返回 CodeWhale
dsh_tests.rs
根目录 / crates / tui / src / plugins / install / dsh_tests.rs
1 //! Conformance corpus for native DSH bundle import. The cases mirror the
2 //! bundle-mode contracts of `scripts/test_convert_plugin.py` (converter
3 //! 0.10.1); the pinned upstream package is shared with that suite.
4
5 use super::*;
6 use serde_json::json;
7 use std::path::PathBuf;
8
9 const MCP: &str = "@deepseek-ai/dsh-mcp-client";
10 const SKILLS: &str = "@deepseek-ai/dsh-skill-filesystem";
11
12 struct Fixture {
13 _root: tempfile::TempDir,
14 bundle: PathBuf,
15 output: PathBuf,
16 }
17
18 fn fixture(patch: Option<&str>, manifest: Option<Json>) -> Fixture {
19 let root = tempfile::tempdir().unwrap();
20 let bundle = root.path().join("dsh-bundle");
21 fs::create_dir(&bundle).unwrap();
22 let mut package = json!({
23 "name": "@demo/tools-dsh",
24 "version": "1.2.3",
25 "dsh": {"bundle": {"patch": "./cordis.patch.yml"}},
26 });
27 if let Some(Json::Object(extra)) = manifest {
28 for (key, value) in extra {
29 package[key] = value;
30 }
31 }
32 fs::write(bundle.join("package.json"), package.to_string()).unwrap();
33 if let Some(patch) = patch {
34 fs::write(bundle.join("cordis.patch.yml"), patch).unwrap();
35 }
36 let output = root.path().join("out");
37 Fixture {
38 _root: root,
39 bundle,
40 output,
41 }
42 }
43
44 fn docs_row(extra: &str) -> String {
45 format!(
46 " - id: docs-entry\n name: '{MCP}'\n{extra} config:\n serverName: docs\n transport: streamable-http\n url: https://docs.example.invalid/mcp\n"
47 )
48 }
49
50 fn servers(output: &Path) -> Json {
51 let text = fs::read_to_string(output.join("mcp.json")).unwrap();
52 serde_json::from_str::<Json>(&text).unwrap()["mcpServers"].clone()
53 }
54
55 fn receipt(output: &Path) -> String {
56 fs::read_to_string(output.join("CONVERSION.md")).unwrap()
57 }
58
59 fn refused(fixture: &Fixture, needle: &str) {
60 let error = convert_package(&fixture.bundle, &fixture.output)
61 .unwrap_err()
62 .to_string();
63 assert!(error.contains(needle), "expected `{needle}` in: {error}");
64 assert!(
65 !fixture.output.exists(),
66 "a refusal leaves no partial output"
67 );
68 }
69
70 #[test]
71 fn evaluates_patches_and_skips_foreign_rows() {
72 let f = fixture(
73 Some(&format!(
74 "- insert:\n - id: docs-entry\n name: '{MCP}'\n config:\n serverName: docs\n transport: streamable-http\n url: https://docs.example.invalid/mcp\n toolCallTimeoutMs: 19000\n - id: skin\n name: '@deepseek-ai/dsh-client-ui-theme'\n config: {{hue: 4}}\n- id: docs-entry\n disabled: true\n- id: ghost\n disabled: true\n"
75 )),
76 None,
77 );
78 let conversion = convert_package(&f.bundle, &f.output).unwrap();
79 assert_eq!(conversion.plugin_name, "tools-dsh");
80 assert_eq!(
81 servers(&f.output)["docs"],
82 json!({"type": "streamable-http", "url": "https://docs.example.invalid/mcp",
83 "extensions": {"net.codewhale": {"execute_timeout": 19, "disabled": true}}})
84 );
85 let text = receipt(&f.output);
86 for fact in ["@demo/tools-dsh@1.2.3", "skin", "ghost"] {
87 assert!(text.contains(fact), "{fact}: {text}");
88 }
89 let manifest: Json =
90 serde_json::from_str(&fs::read_to_string(f.output.join("plugin.json")).unwrap()).unwrap();
91 assert_eq!(manifest["name"], "tools-dsh");
92 assert_eq!(manifest["version"], "1.2.3");
93 assert_eq!(
94 manifest["extensions"]["net.codewhale"]["capabilities"]["network_hosts"],
95 json!(["docs.example.invalid"])
96 );
97 }
98
99 #[test]
100 fn environment_expressions_are_never_evaluated_and_host_paths_never_copied() {
101 let f = fixture(
102 Some(&format!(
103 "- insert:\n - id: local-entry\n name: '{MCP}'\n config:\n serverName: localdocs\n transport: stdio\n command: !!js process.execPath\n args:\n - !!js process.env.DSH_UNSET_7391 || '/tmp/server.mjs'\n - id: env-url\n name: '{MCP}'\n config:\n serverName: envdocs\n transport: streamable-http\n url: !!js '`https://example.invalid/${{process.env.DSH_CANARY}}/mcp`'\n{}",
104 docs_row("")
105 )),
106 None,
107 );
108 convert_package(&f.bundle, &f.output).unwrap();
109 let names: Vec<_> = servers(&f.output)
110 .as_object()
111 .unwrap()
112 .keys()
113 .cloned()
114 .collect();
115 assert_eq!(names, ["docs"]);
116 assert!(!f.output.join("mcp").exists());
117 let text = receipt(&f.output);
118 assert!(
119 text.contains("local-entry") && text.contains("reads an environment value"),
120 "{text}"
121 );
122 assert!(text.contains("No environment variable values were resolved"));
123 }
124
125 #[test]
126 fn a_relative_entry_missing_from_the_package_is_skipped_not_searched_for() {
127 let f = fixture(
128 Some(&format!(
129 "- insert:\n - id: local-entry\n name: '{MCP}'\n config:\n serverName: localdocs\n transport: stdio\n command: !!js process.execPath\n args: ['./server.mjs']\n"
130 )),
131 None,
132 );
133 refused(&f, "No portable components");
134 }
135
136 #[test]
137 fn disabled_ancestry_disables_children_and_is_receipted() {
138 let f = fixture(
139 Some(&format!(
140 "- insert:\n - id: outer\n group: true\n disabled: true\n config:\n - id: inner\n group: true\n config:\n - id: docs-entry\n name: '{MCP}'\n config: {{serverName: docs, transport: streamable-http, url: 'https://docs.example.invalid/mcp'}}\n - id: skills-row\n name: '{SKILLS}'\n config: {{customSkillDirs: [pack-skills]}}\n - id: live-entry\n name: '{MCP}'\n config: {{serverName: live, transport: streamable-http, url: 'https://live.example.invalid/mcp'}}\n"
141 )),
142 None,
143 );
144 let skill = f.bundle.join("pack-skills/guide");
145 fs::create_dir_all(&skill).unwrap();
146 fs::write(
147 skill.join("SKILL.md"),
148 "---\nname: guide\ndescription: Bundled skill\n---\nBody.\n",
149 )
150 .unwrap();
151 convert_package(&f.bundle, &f.output).unwrap();
152 let servers = servers(&f.output);
153 assert_eq!(
154 servers["docs"]["extensions"]["net.codewhale"],
155 json!({"disabled": true})
156 );
157 assert_eq!(servers["live"]["extensions"]["net.codewhale"], json!({}));
158 assert!(!f.output.join("skills").exists());
159 let text = receipt(&f.output);
160 assert!(text.contains("disabled by ancestor `outer`"), "{text}");
161 assert!(text.contains("skipped-disabled"), "{text}");
162 }
163
164 #[test]
165 fn unresolved_conditional_gates_refuse_but_foreign_gates_are_skipped() {
166 let gated_group = format!(
167 "- insert:\n - id: gate\n name: '@deepseek-ai/cordis-plugin-group'\n group: true\n disabled: !!js process.env.DISABLE_THIS\n config:\n - id: docs-entry\n name: '{MCP}'\n config: {{serverName: docs, transport: streamable-http, url: 'https://docs.example.invalid/mcp'}}\n"
168 );
169 let gated_row = format!(
170 "- insert:\n - id: docs-entry\n name: '{MCP}'\n disabled: !!js process.env.DISABLE_THIS\n config: {{serverName: docs, transport: streamable-http, url: 'https://docs.example.invalid/mcp'}}\n"
171 );
172 let string_gate = format!(
173 "- insert:\n - id: docs-entry\n name: '{MCP}'\n disabled: 'yes'\n config: {{serverName: docs, transport: streamable-http, url: 'https://docs.example.invalid/mcp'}}\n"
174 );
175 for patch in [gated_group, gated_row, string_gate] {
176 refused(&fixture(Some(&patch), None), "conditional or non-boolean");
177 }
178 let control = fixture(
179 Some(&format!(
180 "- insert:\n - id: tool-bash\n name: '@deepseek-ai/dsh-tool-bash'\n disabled: !!js process.platform === 'win32'\n{}",
181 docs_row("")
182 )),
183 None,
184 );
185 convert_package(&control.bundle, &control.output).unwrap();
186 let text = receipt(&control.output);
187 assert!(
188 text.contains("tool-bash")
189 && text.contains("conditional `disabled` gate was not evaluated"),
190 "{text}"
191 );
192 }
193
194 #[test]
195 fn ordered_layers_replace_config_and_record_provenance() {
196 let f = fixture(
197 Some(&format!(
198 "- insert:\n - id: docs-entry\n name: '{MCP}'\n config: {{serverName: docs, transport: streamable-http, url: 'https://first.example.invalid/mcp', toolCallTimeoutMs: 19000}}\n"
199 )),
200 Some(
201 json!({"dsh": {"bundle": {"patch": ["./cordis.patch.yml", "./presets/overlay.patch.yml"]}}}),
202 ),
203 );
204 fs::create_dir(f.bundle.join("presets")).unwrap();
205 fs::write(
206 f.bundle.join("presets/overlay.patch.yml"),
207 "- id: docs-entry\n disabled: true\n config: {serverName: docs, transport: streamable-http, url: 'https://second.example.invalid/mcp'}\n",
208 )
209 .unwrap();
210 convert_package(&f.bundle, &f.output).unwrap();
211 assert_eq!(
212 servers(&f.output)["docs"],
213 json!({"type": "streamable-http", "url": "https://second.example.invalid/mcp",
214 "extensions": {"net.codewhale": {"disabled": true}}})
215 );
216 let text = receipt(&f.output);
217 assert!(text.contains("applied in declaration order"));
218 assert!(text.contains(&format!("Converter version {CONVERTER_VERSION}")));
219 assert!(text.contains("Source package: @demo/tools-dsh@1.2.3"));
220 assert!(text.contains(&sha256_hex(
221 &fs::read(f.bundle.join("package.json")).unwrap()
222 )));
223 for relative in ["./cordis.patch.yml", "./presets/overlay.patch.yml"] {
224 let content = fs::read(f.bundle.join(relative.trim_start_matches("./"))).unwrap();
225 assert!(
226 text.contains(&format!(
227 "{relative} (sha256 {}, {} bytes)",
228 sha256_hex(&content),
229 content.len()
230 )),
231 "{relative}: {text}"
232 );
233 }
234 let structured: Json =
235 serde_json::from_str(&fs::read_to_string(f.output.join("CONVERSION.json")).unwrap())
236 .unwrap();
237 assert_eq!(structured["schema"], "codewhale.plugin-conversion.v1");
238 }
239
240 #[test]
241 fn skipped_patch_operations_are_structured_manual_ports() {
242 let f = fixture(
243 Some(&format!("- insert:\n{}", docs_row(""))),
244 Some(json!({"dsh": {"bundle": {"patch": ["./cordis.patch.yml", "./overlay.yml"]}}})),
245 );
246 fs::write(
247 f.bundle.join("overlay.yml"),
248 "- {id: missing-group, insert: []}\n- {disabled: true}\n- {id: missing-row, disabled: true}\n- {id: docs-entry, name: wrong-package, disabled: true}\n",
249 )
250 .unwrap();
251 convert_package(&f.bundle, &f.output).unwrap();
252 let structured: Json =
253 serde_json::from_str(&fs::read_to_string(f.output.join("CONVERSION.json")).unwrap())
254 .unwrap();
255 let skipped = structured["required_manual_ports"].as_array().unwrap();
256 assert_eq!(
257 skipped
258 .iter()
259 .map(|row| row["row"].clone())
260 .collect::<Vec<_>>(),
261 [
262 json!("missing-group"),
263 Json::Null,
264 json!("missing-row"),
265 json!("docs-entry")
266 ]
267 );
268 assert_eq!(
269 skipped
270 .iter()
271 .map(|row| row["patch"].clone())
272 .collect::<Vec<_>>(),
273 [json!(1), json!(2), json!(3), json!(4)]
274 );
275 for row in skipped {
276 assert_eq!(
277 (row["kind"].as_str(), row["layer"].as_str()),
278 (Some("patch"), Some("./overlay.yml"))
279 );
280 assert!(receipt(&f.output).contains(row["reason"].as_str().unwrap()));
281 }
282 assert!(
283 servers(&f.output)["docs"]["extensions"]["net.codewhale"]
284 .get("disabled")
285 .is_none()
286 );
287 }
288
289 #[test]
290 fn patch_layer_list_is_bounded_and_contained() {
291 let portable = format!("- insert:\n{}", docs_row(""));
292 let attempt = |declared: Json, files: &[(&str, String)]| {
293 let f = fixture(None, Some(json!({"dsh": {"bundle": {"patch": declared}}})));
294 for (name, text) in files {
295 let destination = f.bundle.join(name);
296 fs::create_dir_all(destination.parent().unwrap()).unwrap();
297 fs::write(destination, text).unwrap();
298 }
299 f
300 };
301 let ok = attempt(json!(["./a.yml"]), &[("a.yml", portable.clone())]);
302 convert_package(&ok.bundle, &ok.output).unwrap();
303 refused(
304 &attempt(
305 json!(["./a.yml", "./a.yml"]),
306 &[("a.yml", portable.clone())],
307 ),
308 "listed once",
309 );
310 refused(
311 &attempt(json!(["a.yml", "./a.yml"]), &[("a.yml", portable.clone())]),
312 "listed once",
313 );
314 refused(&attempt(json!(["/tmp/a.yml"]), &[]), "relative path");
315 refused(&attempt(json!(["C:\\a.yml"]), &[]), "relative path");
316 refused(
317 &attempt(
318 json!(["./a.yml", "../outside.yml"]),
319 &[("a.yml", portable.clone())],
320 ),
321 "relative path",
322 );
323 refused(&attempt(json!(["./missing.yml"]), &[]), "inside the bundle");
324 refused(&attempt(json!([]), &[]), "non-empty ordered list");
325 let many: Vec<String> = (0..65).map(|i| format!("./layer-{i}.yml")).collect();
326 refused(&attempt(json!(many), &[]), "At most 64");
327 refused(
328 &attempt(
329 json!(["./a.yml", "./b.yml"]),
330 &[
331 ("a.yml", format!("[]\n{}", " ".repeat(1024 * 1024 - 3))),
332 ("b.yml", "[]\n".into()),
333 ],
334 ),
335 "aggregate patch limit",
336 );
337 }
338
339 #[test]
340 fn pinned_upstream_multifile_package_parses_without_promoting_rows() {
341 let bundle = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../scripts/fixtures/dsh-web-app");
342 let upstream: Json =
343 serde_json::from_str(&fs::read_to_string(bundle.join("UPSTREAM.json")).unwrap()).unwrap();
344 assert_eq!(
345 upstream["commit"],
346 "00102833dfaee1da9f48a3a8eae9d34005a75218"
347 );
348 for (relative, digest) in upstream["files"].as_object().unwrap() {
349 assert_eq!(
350 &json!(sha256_hex(&fs::read(bundle.join(relative)).unwrap())),
351 digest,
352 "{relative}"
353 );
354 }
355 let package = Package::open(&bundle).unwrap();
356 let loaded = load_bundle(&package).unwrap();
357 assert_eq!(loaded.layers.len(), 5);
358 assert!(!loaded.entries.is_empty());
359 let mut components = Components::default();
360 components.walk(&package, &loaded.entries, None).unwrap();
361 assert!(
362 components.servers.is_empty()
363 && components.hosts.is_empty()
364 && components.skill_dirs.is_empty()
365 );
366 assert!(
367 components
368 .outcomes
369 .iter()
370 .all(|o| !o.outcome.starts_with("converted"))
371 );
372 assert!(
373 components
374 .notes
375 .iter()
376 .any(|note| note.contains("conditional"))
377 );
378 }
379
380 #[test]
381 fn group_children_and_relative_stdio_entries_convert_from_the_declared_cwd() {
382 let f = fixture(
383 Some(&format!(
384 "- insert:\n - {{id: grouped, group: true, config: []}}\n- id: grouped\n insert:\n - id: in-group\n name: '{MCP}'\n config: {{serverName: inner, transport: stdio, command: node, args: [server.mjs], cwd: packaged}}\n"
385 )),
386 None,
387 );
388 fs::write(f.bundle.join("server.mjs"), "wrong source").unwrap();
389 fs::create_dir(f.bundle.join("packaged")).unwrap();
390 fs::write(f.bundle.join("packaged/server.mjs"), "correct source").unwrap();
391 let conversion = convert_package(&f.bundle, &f.output).unwrap();
392 assert!(conversion.requires_node);
393 assert_eq!(conversion.local_servers, ["inner"]);
394 assert_eq!(servers(&f.output)["inner"]["cwd"], "mcp/inner");
395 assert_eq!(
396 fs::read_to_string(f.output.join("mcp/inner/server.mjs")).unwrap(),
397 "correct source"
398 );
399 }
400
401 #[test]
402 fn custom_skill_dirs_import_and_disabled_skills_are_omitted() {
403 let build = |disabled: &str| {
404 let f = fixture(
405 Some(&format!(
406 "- insert:\n - id: skills-row\n name: '{SKILLS}'\n{disabled} config: {{customSkillDirs: [pack-skills]}}\n{}",
407 docs_row("")
408 )),
409 None,
410 );
411 let skill = f.bundle.join("pack-skills/guide");
412 fs::create_dir_all(&skill).unwrap();
413 fs::write(
414 skill.join("SKILL.md"),
415 "---\nname: guide\ndescription: Bundled skill\n---\nBody.\n",
416 )
417 .unwrap();
418 fs::write(skill.join("notes.txt"), "companion").unwrap();
419 f
420 };
421 let enabled = build("");
422 let conversion = convert_package(&enabled.bundle, &enabled.output).unwrap();
423 assert_eq!(conversion.skills, ["guide"]);
424 let skill = fs::read_to_string(enabled.output.join("skills/guide/SKILL.md")).unwrap();
425 assert!(
426 skill.starts_with("---\nname: guide\ndescription: |-\n Bundled skill\n---"),
427 "{skill}"
428 );
429 assert_eq!(
430 fs::read_to_string(enabled.output.join("skills/guide/notes.txt")).unwrap(),
431 "companion"
432 );
433 let disabled = build(" disabled: true\n");
434 assert!(
435 convert_package(&disabled.bundle, &disabled.output)
436 .unwrap()
437 .skills
438 .is_empty()
439 );
440 let text = receipt(&disabled.output);
441 assert!(
442 text.contains("skipped-disabled") && text.contains("preserved by omission"),
443 "{text}"
444 );
445 }
446
447 #[test]
448 fn js_is_never_executed_and_unlowerable_rows_are_recorded() {
449 let sentinel = tempfile::tempdir().unwrap().path().join("expression-ran");
450 let f = fixture(
451 Some(&format!(
452 "- insert:\n - id: bad\n name: '{MCP}'\n config:\n serverName: bad\n transport: streamable-http\n url: !!js require('node:fs').writeFileSync('{}', 'ran')\n - id: ok\n name: '{MCP}'\n config: {{serverName: ok, transport: streamable-http, url: 'https://ok.example.invalid/mcp'}}\n",
453 sentinel.display()
454 )),
455 None,
456 );
457 convert_package(&f.bundle, &f.output).unwrap();
458 assert!(!sentinel.exists());
459 let names: Vec<_> = servers(&f.output)
460 .as_object()
461 .unwrap()
462 .keys()
463 .cloned()
464 .collect();
465 assert_eq!(names, ["ok"]);
466 assert!(receipt(&f.output).contains("bad"));
467 }
468
469 #[test]
470 fn manifests_must_declare_contained_patches() {
471 refused(&fixture(None, None), "inside the bundle");
472 refused(
473 &fixture(
474 None,
475 Some(json!({"dsh": {"bundle": {"patch": "../outside.yml"}}})),
476 ),
477 "relative path",
478 );
479 let plain = fixture(None, None);
480 fs::write(
481 plain.bundle.join("package.json"),
482 json!({"name": "plain"}).to_string(),
483 )
484 .unwrap();
485 refused(&plain, "dsh.bundle.patch");
486 assert!(!is_dsh_package(&plain.bundle));
487 assert!(is_dsh_package(&fixture(None, None).bundle));
488 }
489
490 #[test]
491 fn policy_and_dependency_fields_never_widen_activation() {
492 for field in [
493 "inject: [approvals]",
494 "intercept: {tools: true}",
495 "isolate: {tools: private}",
496 "unknownGate: true",
497 ] {
498 for disabled in ["false", "true"] {
499 let rows = [
500 format!(
501 " - {{id: docs-entry, name: '{MCP}', {field}, disabled: {disabled}, config: {{serverName: docs, transport: streamable-http, url: 'https://docs.example.invalid/mcp'}}}}\n"
502 ),
503 format!(
504 " - {{id: skills, name: '{SKILLS}', {field}, disabled: {disabled}, config: {{customSkillDirs: [skills]}}}}\n"
505 ),
506 format!(
507 " - {{id: group, group: true, {field}, disabled: {disabled}, config: []}}\n"
508 ),
509 ];
510 for row in rows {
511 refused(
512 &fixture(Some(&format!("- insert:\n{row}")), None),
513 "unsupported entry policy or dependency",
514 );
515 }
516 }
517 }
518 }
519
520 #[test]
521 fn skipped_stdio_rows_never_copy_their_source() {
522 let f = fixture(
523 Some(&format!(
524 "- insert:\n{} - id: unportable\n name: '{MCP}'\n config: {{serverName: local, transport: stdio, command: node, args: [server.mjs], cwd: packaged, env: {{TOKEN: canary-9f2}}}}\n",
525 docs_row("")
526 )),
527 None,
528 );
529 fs::create_dir(f.bundle.join("packaged")).unwrap();
530 fs::write(
531 f.bundle.join("packaged/server.mjs"),
532 "throw new Error('never run');\n",
533 )
534 .unwrap();
535 convert_package(&f.bundle, &f.output).unwrap();
536 assert!(!f.output.join("mcp").exists());
537 let structured = fs::read_to_string(f.output.join("CONVERSION.json")).unwrap();
538 let parsed: Json = serde_json::from_str(&structured).unwrap();
539 assert_eq!(parsed["required_manual_ports"][0]["row"], "unportable");
540 assert!(!structured.contains("canary-9f2"));
541 }
542
543 #[test]
544 fn a_duplicate_server_cannot_discard_a_disabled_row() {
545 let f = fixture(
546 Some(&format!(
547 "- insert:\n{}{}",
548 docs_row(""),
549 docs_row(" disabled: true\n").replace("id: docs-entry", "id: disabled-copy")
550 )),
551 None,
552 );
553 refused(&f, "Duplicate MCP server");
554 }
555
556 #[test]
557 fn literal_lowering_never_evaluates_or_changes_escaped_strings() {
558 for expression in [
559 "'https://example.invalid/mcp'",
560 "`https://example.invalid/mcp`",
561 ] {
562 assert_eq!(
563 lower_js(expression, "url").unwrap(),
564 "https://example.invalid/mcp"
565 );
566 }
567 assert_eq!(lower_js("process.execPath", "command").unwrap(), "node");
568 for expression in [
569 r"'https://example.invalid/\\x41'",
570 "`x` + `y`",
571 "process.env.TOKEN",
572 "process.env.TOKEN || 'literal'",
573 "`${process.env.TOKEN}`",
574 "process.env['TOKEN']",
575 ] {
576 assert!(lower_js(expression, "url").is_err(), "{expression}");
577 }
578 }
579
580 #[test]
581 fn data_parsing_is_closed() {
582 assert!(
583 parse_json(r#"{"a": 1, "a": 2}"#).is_err(),
584 "duplicate JSON keys"
585 );
586 assert!(
587 parse_yaml("a: 1\na: 2\n", false).is_err(),
588 "duplicate YAML keys"
589 );
590 assert!(parse_yaml("a: &x 1\nb: *x\n", false).is_err(), "aliases");
591 assert!(parse_yaml("a: !!str 1\n", false).is_err(), "explicit tags");
592 assert!(
593 parse_yaml("a: !!js process.execPath\n", false).is_err(),
594 "js without permission"
595 );
596 assert_eq!(
597 parse_yaml("a: !!js process.execPath\n", true)
598 .unwrap()
599 .get("a"),
600 Some(&Value::Js("process.execPath".into()))
601 );
602 assert!(parse_yaml("- !!js x\n", true).unwrap() == Value::Seq(vec![Value::Js("x".into())]));
603 assert!(
604 parse_yaml("!!js [a]\n", true).is_err(),
605 "tagged collections"
606 );
607 assert!(
608 parse_yaml("a: 1\n---\nb: 2\n", false).is_err(),
609 "multiple documents"
610 );
611 assert!(parse_yaml("{__jsExpr: x}\n", false).is_err());
612 assert!(
613 parse_yaml(&format!("{}1{}", "[".repeat(33), "]".repeat(33)), false).is_err(),
614 "depth"
615 );
616 assert_eq!(
617 parse_yaml("{a: yes, b: true, c: 0x10, d: 1.5, e: ~, f: '1'}\n", false).unwrap(),
618 Value::Map(vec![
619 ("a".into(), Value::Str("yes".into())),
620 ("b".into(), Value::Bool(true)),
621 ("c".into(), Value::Int(16)),
622 ("d".into(), Value::Float(1.5)),
623 ("e".into(), Value::Null),
624 ("f".into(), Value::Str("1".into())),
625 ])
626 );
627 }
628
629 #[test]
630 fn endpoints_are_literal_https_or_loopback_with_canonical_hosts() {
631 for (url, host) in [
632 ("https://docs.example.invalid/mcp", "docs.example.invalid"),
633 (
634 "https://Docs.Example.Invalid:8443/mcp",
635 "docs.example.invalid",
636 ),
637 ("http://127.0.0.1:9000/mcp", "127.0.0.1"),
638 ("http://[::1]:9000/mcp", "[::1]"),
639 ("https://10.0.0.1/mcp", "10.0.0.1"),
640 ] {
641 assert_eq!(endpoint_host(url).unwrap(), host, "{url}");
642 }
643 for url in [
644 "http://docs.example.invalid/mcp",
645 "https://user:secret@docs.example.invalid/mcp",
646 "https://docs.example.invalid/mcp?token=x",
647 "https://docs.example.invalid/mcp#x",
648 "https://127.1/mcp",
649 "https://0x7f.0.0.1/mcp",
650 "https://docs.example.invalid:0/mcp",
651 "https://docs.example.invalid/${x}",
652 "ftp://docs.example.invalid/mcp",
653 ] {
654 assert!(endpoint_host(url).is_err(), "{url}");
655 }
656 }
657
658 #[test]
659 fn links_inside_the_package_are_refused() {
660 #[cfg(unix)]
661 {
662 let f = fixture(Some(&format!("- insert:\n{}", docs_row(""))), None);
663 let outside = tempfile::tempdir().unwrap();
664 fs::write(outside.path().join("layer.yml"), "[]\n").unwrap();
665 std::os::unix::fs::symlink(
666 outside.path().join("layer.yml"),
667 f.bundle.join("linked.yml"),
668 )
669 .unwrap();
670 fs::write(
671 f.bundle.join("package.json"),
672 json!({"name": "@demo/tools-dsh", "dsh": {"bundle": {"patch": ["./linked.yml"]}}})
673 .to_string(),
674 )
675 .unwrap();
676 refused(&f, "links or reparse points");
677 }
678 }
679
680 #[test]
681 fn the_converted_bundle_is_a_valid_native_plugin() {
682 let f = fixture(Some(&format!("- insert:\n{}", docs_row(""))), None);
683 convert_package(&f.bundle, &f.output).unwrap();
684 let manifest =
685 crate::plugins::agent_plugin::resolve_manifest_path(&f.output).expect("manifest");
686 let validated =
687 crate::plugins::manifest::PluginManifest::validate_from_path(&manifest).unwrap();
688 assert_eq!(validated.manifest.plugin.name, "tools-dsh");
689 }
690
691 #[test]
692 fn derived_names_are_native_plugin_names() {
693 assert_eq!(
694 derived_plugin_name("@deepseek-ai/bundle-web-app").as_deref(),
695 Some("bundle-web-app")
696 );
697 assert_eq!(
698 derived_plugin_name("Tools_DSH").as_deref(),
699 Some("tools-dsh")
700 );
701 assert_eq!(derived_plugin_name("@x/--"), None);
702 }
703
704 #[test]
705 fn raw_agent_presets_import_emits_exact_native_entries_and_catalog() {
706 let source = Path::new(env!("CARGO_MANIFEST_DIR"))
707 .join("tests/fixtures/extension_host/raw-agent-presets/source");
708 let root = tempfile::tempdir().unwrap();
709 let output = root.path().join("raw-preset-output");
710 let converted = convert_package(&source, &output).unwrap();
711 assert!(converted.requires_native);
712 assert_eq!(converted.native_rows, ["@deepseek-ai/dsh-agent-presets"]);
713 let manifest: Json =
714 serde_json::from_slice(&fs::read(output.join("plugin.json")).unwrap()).unwrap();
715 assert_eq!(
716 manifest["extensions"]["net.codewhale"]["native"]["paths"],
717 json!(["native/presets/a.mjs", "native/presets/b.mjs"])
718 );
719 assert!(
720 !output.join("native/index.mjs").exists(),
721 "no unselected global composition consumer remains"
722 );
723 let catalog: Json =
724 serde_json::from_slice(&fs::read(output.join("native/presets.json")).unwrap()).unwrap();
725 assert_eq!(catalog["default"], "a");
726 for row in catalog["presets"].as_array().unwrap() {
727 let entry = row["entry"]["path"].as_str().unwrap();
728 let bytes = fs::read(output.join(entry)).unwrap();
729 assert_eq!(row["entry"]["sha256"], sha256_hex(&bytes));
730 let data = crate::plugins::native_presets::metadata_from_bytes(&bytes).unwrap();
731 assert_eq!(data.id, row["id"].as_str().unwrap());
732 }
733 assert!(
734 crate::plugins::manifest::PluginManifest::validate_from_path(&output.join("plugin.json"))
735 .is_ok()
736 );
737 }
738
739 #[test]
740 fn contained_bare_modules_never_walk_ambient_packages_or_escape_exports() {
741 let f = fixture(None, None);
742 let package = Package::open(&f.bundle).unwrap();
743 assert!(
744 presets::contained_module(&package, "js-yaml").is_err(),
745 "installed host dependency is not a package receipt"
746 );
747 let dir = f.bundle.join("node_modules/@demo/profile");
748 fs::create_dir_all(&dir).unwrap();
749 fs::write(dir.join("package.json"), r#"{"name":"@demo/profile","type":"module","exports":{".":{"require":"./not.cjs","import":"./index.mjs"},"./escape":"../../../outside.mjs"}}"#).unwrap();
750 fs::write(dir.join("index.mjs"), "export function apply() {}\n").unwrap();
751 assert_eq!(
752 presets::contained_module(&package, "@demo/profile").unwrap(),
753 dir.join("index.mjs").canonicalize().unwrap()
754 );
755 assert!(presets::contained_module(&package, "@demo/profile/escape").is_err());
756 assert!(presets::contained_module(&package, "@demo/profile/../../outside").is_err());
757 fs::write(dir.join("package.json"),r#"{"name":"@demo/profile","type":"module","exports":{"node":null,"import":"./index.mjs"}}"#).unwrap();
758 assert!(
759 presets::contained_module(&package, "@demo/profile").is_err(),
760 "matched null export must not fall through"
761 );
762 }
763
764 #[test]
765 fn contained_bare_native_row_imports_but_an_unresolved_sibling_never_installs_a_partial_graph() {
766 let build = |mixed: bool| {
767 let f = fixture(
768 Some(&format!(
769 "- insert:\n - {{id: native-row, name: '@demo/profile'}}\n{}",
770 if mixed {
771 " - {id: missing, name: absent-package}\n"
772 } else {
773 ""
774 }
775 )),
776 None,
777 );
778 let dir = f.bundle.join("node_modules/@demo/profile");
779 fs::create_dir_all(&dir).unwrap();
780 fs::write(
781 dir.join("package.json"),
782 r#"{"name":"@demo/profile","type":"module","exports":"./index.mjs"}"#,
783 )
784 .unwrap();
785 fs::write(dir.join("index.mjs"),"export const inject=['prompt'];export function apply(ctx){ctx.prompt.registerSection({id:'profile',text:'actual closed bare module'})}\n").unwrap();
786 f
787 };
788 let valid = build(false);
789 let converted = convert_package(&valid.bundle, &valid.output).unwrap();
790 assert!(converted.requires_native);
791 assert_eq!(converted.native_rows, ["@demo/profile"]);
792 let spec: Json =
793 serde_json::from_slice(&fs::read(valid.output.join("native/composition.json")).unwrap())
794 .unwrap();
795 assert_eq!(spec["modules"][0]["name"], "@demo/profile");
796 assert_eq!(
797 spec["modules"][0]["path"],
798 "node_modules/@demo/profile/index.mjs"
799 );
800 assert!(
801 crate::plugins::manifest::PluginManifest::validate_from_path(
802 &valid.output.join("plugin.json")
803 )
804 .is_ok()
805 );
806 refused(&build(true), "no partial graph");
807 }
808
809 #[test]
810 fn raw_catalog_broken_rows_remain_structured_manual_ports_beside_healthy_entries() {
811 let source = Path::new(env!("CARGO_MANIFEST_DIR"))
812 .join("tests/fixtures/extension_host/raw-agent-presets/source");
813 let root = tempfile::tempdir().unwrap();
814 let bundle = root.path().join("source");
815 fs::create_dir(&bundle).unwrap();
816 walk_files(&source, |path, is_dir| {
817 let target = bundle.join(path.strip_prefix(&source).unwrap());
818 if is_dir {
819 fs::create_dir_all(target)?;
820 } else {
821 fs::create_dir_all(target.parent().unwrap())?;
822 fs::copy(path, target)?;
823 }
824 Ok(())
825 })
826 .unwrap();
827 fs::create_dir_all(bundle.join("presets/broken")).unwrap();
828 fs::write(
829 bundle.join("presets/broken/agent.cordis.yml"),
830 "[invalid yaml\n",
831 )
832 .unwrap();
833 let output = root.path().join("converted");
834 let converted = convert_package(&bundle, &output).unwrap();
835 assert!(converted.requires_native);
836 let manual: Vec<_> = converted
837 .outcomes
838 .iter()
839 .filter(|row| row.kind == "native-preset")
840 .collect();
841 assert_eq!(manual.len(), 1);
842 assert_eq!(manual[0].row.as_deref(), Some("broken"));
843 assert!(manual[0].needs_manual_port());
844 let catalog: Json =
845 serde_json::from_slice(&fs::read(output.join("native/presets.json")).unwrap()).unwrap();
846 let broken = catalog["presets"]
847 .as_array()
848 .unwrap()
849 .iter()
850 .find(|row| row["id"] == "broken")
851 .unwrap();
852 assert!(broken.get("broken").is_some());
853 assert!(broken.get("entry").is_none());
854 assert_eq!(
855 catalog["presets"]
856 .as_array()
857 .unwrap()
858 .iter()
859 .filter(|row| row.get("entry").is_some())
860 .count(),
861 2
862 );
863 let receipt: Json =
864 serde_json::from_slice(&fs::read(output.join("CONVERSION.json")).unwrap()).unwrap();
865 assert!(
866 receipt["required_manual_ports"]
867 .as_array()
868 .unwrap()
869 .iter()
870 .any(|row| row["row"] == "broken")
871 );
872 }
873
873 lines RUST