char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somsnapshot: embed shared main and worker startup in the CLI688e96e

main
22.1 KiB601 linesraw
1#[path = "common/registry.rs"]
2mod fixture;
3
4use std::fs;
5use std::path::Path;
6use std::process::{Command, Output};
7
8use fixture::Registry;
9use serde_json::json;
10
11fn run(server: &Registry, directory: &Path, args: &[&str]) -> Output {
12    let options = server.options(directory);
13    Command::new(env!("CARGO_BIN_EXE_avril"))
14        .current_dir(directory)
15        // Options alone run the entry; a leading command selects another.
16        .args(
17            args.first()
18                .is_none_or(|arg| arg.starts_with('-'))
19                .then_some("run"),
20        )
21        .args(args)
22        .arg(format!("--jsr-registry={}", options.jsr_registry))
23        .arg(format!("--cache-dir={}", options.cache.display()))
24        .arg("main.ts")
25        .output()
26        .unwrap()
27}
28
29fn success(output: &Output) -> String {
30    assert!(
31        output.status.success(),
32        "{}",
33        String::from_utf8_lossy(&output.stderr)
34    );
35    String::from_utf8(output.stdout.clone()).unwrap()
36}
37
38#[test]
39fn runs_native_jsr_graphs_with_typescript_cycles_exports_and_package_private_dependencies() {
40    let server = Registry::new();
41    server.package(
42        "@test/value",
43        "1.0.0",
44        &[("mod.ts", "export const value = 'one';")],
45        &[(".", "./mod.ts")],
46    );
47    server.package(
48        "@test/value",
49        "2.0.0",
50        &[("mod.ts", "export const value = 'two';")],
51        &[(".", "./mod.ts")],
52    );
53    server.package("@test/library", "1.0.0", &[
54        ("mod.ts", "import type {Missing} from 'npm:missing'; import {value} from 'jsr:/@test/value@^1'; import './cycle.ts'; export {value}; export const count: number = globalThis.count = (globalThis.count ?? 0) + 1; export const main = import.meta.main; export const url = import.meta.url; export const self = () => import(import.meta.url);"),
55        ("cycle.ts", "import {value} from './mod.ts'; export const read = () => value;"),
56        ("sub.mts", "export const value: number = 42;"),
57    ], &[(".", "./mod.ts"), ("./sub", "./sub.mts")]);
58    let dir = tempfile::tempdir().unwrap();
59    fs::write(dir.path().join("avril.conf"), "imports: {lib: 'jsr:@test/library@^1', 'lib/': 'jsr:@test/library@^1/', value: 'jsr:@test/value@^2'}").unwrap();
60    fs::write(dir.path().join("main.ts"), "import * as a from 'lib'; import * as b from 'jsr:@test/library@^1'; import {value} from 'value'; import {value as sub} from 'lib/sub'; console.log(a === b, a.value, value, sub, a.count, a.main, (await a.self()) === a, a.url);").unwrap();
61    let output = run(&server, dir.path(), &[]);
62    let stdout = success(&output);
63    assert_eq!(
64        stdout,
65        format!(
66            "true one two 42 1 false true {}@test/library/1.0.0/mod.ts\n",
67            server.url
68        )
69    );
70    let lock = fixture::read_lock(dir.path());
71    assert_eq!(lock["specifiers"]["jsr:@test/value@^1"], "1.0.0");
72    assert_eq!(lock["specifiers"]["jsr:@test/value@^2"], "2.0.0");
73    assert!(
74        !server
75            .requests
76            .lock()
77            .unwrap()
78            .iter()
79            .any(|path| path.contains("missing"))
80    );
81}
82
83#[test]
84fn computed_dynamic_imports_acquire_static_closures_and_share_evaluation() {
85    let server = Registry::new();
86    server.package("@test/lazy", "1.0.0", &[
87        ("mod.ts", "import {value} from './dep.ts'; export {value}; export const count = globalThis.count = (globalThis.count ?? 0) + 1; export const main = import.meta.main;"),
88        ("dep.ts", "export const value: string = 'acquired';"),
89    ], &[(".", "./mod.ts")]);
90    let mut metadata: serde_json::Value = serde_json::from_slice(
91        server
92            .files
93            .lock()
94            .unwrap()
95            .get("/@test/lazy/1.0.0_meta.json")
96            .unwrap(),
97    )
98    .unwrap();
99    metadata["moduleGraph2"] = json!({"/mod.ts": {"dependencies": [{"type": "static", "kind": "import", "specifier": "./dep.ts"}]}});
100    server.files.lock().unwrap().insert(
101        "/@test/lazy/1.0.0_meta.json".into(),
102        serde_json::to_vec(&metadata).unwrap(),
103    );
104    let dir = tempfile::tempdir().unwrap();
105    fs::write(dir.path().join("main.ts"), "const name = 'jsr:@test/' + 'lazy@1'; const timer = new Promise(resolve => setTimeout(resolve, 0)); const [a,b,c] = await Promise.all([import(name),import(name),import(name)]); await timer; console.log(a === b, b === c, a.value, a.count, a.main); try { await import('jsr:@test/missing@1'); throw new Error('success'); } catch(e) { console.log(e.message.includes('404')); } console.log((await import(name)) === a);").unwrap();
106    assert_eq!(
107        success(&run(&server, dir.path(), &[])),
108        "true true acquired 1 false\ntrue\ntrue\n"
109    );
110    let requests = server.requests.lock().unwrap();
111    for path in [
112        "/@test/lazy/meta.json",
113        "/@test/lazy/1.0.0_meta.json",
114        "/@test/lazy/1.0.0/mod.ts",
115        "/@test/lazy/1.0.0/dep.ts",
116    ] {
117        assert_eq!(
118            requests
119                .iter()
120                .filter(|request| request.as_str() == path)
121                .count(),
122            1,
123            "{requests:?}"
124        );
125    }
126}
127
128#[test]
129fn published_packages_do_not_inherit_application_maps_or_escape_their_packages() {
130    let server = Registry::new();
131    server.package(
132        "@test/bare",
133        "1.0.0",
134        &[("mod.ts", "import 'dep';")],
135        &[(".", "./mod.ts")],
136    );
137    server.package(
138        "@test/escape",
139        "1.0.0",
140        &[("mod.ts", "import '../../other/1.0.0/private.ts';")],
141        &[(".", "./mod.ts")],
142    );
143    server.package(
144        "@test/builtin",
145        "1.0.0",
146        &[("mod.ts", "import 'node:fs';")],
147        &[(".", "./mod.ts")],
148    );
149    let dir = tempfile::tempdir().unwrap();
150    fs::write(dir.path().join("avril.conf"), "imports: {dep: './dep.ts'}").unwrap();
151    fs::write(
152        dir.path().join("dep.ts"),
153        "throw new Error('inherited app map');",
154    )
155    .unwrap();
156    for (package, expected) in [
157        ("bare", "Unsupported module specifier: dep"),
158        ("escape", "escapes its registry package"),
159        ("builtin", "node: builtin modules are not supported"),
160    ] {
161        fs::write(
162            dir.path().join("main.ts"),
163            format!("import 'jsr:@test/{package}@1';"),
164        )
165        .unwrap();
166        let output = run(&server, dir.path(), &[]);
167        assert!(!output.status.success());
168        assert!(
169            String::from_utf8_lossy(&output.stderr).contains(expected),
170            "{}",
171            String::from_utf8_lossy(&output.stderr)
172        );
173    }
174    assert!(
175        !server
176            .requests
177            .lock()
178            .unwrap()
179            .iter()
180            .any(|path| path.contains("other"))
181    );
182}
183
184#[test]
185fn package_acquisition_does_not_grant_guest_network_or_filesystem_access() {
186    let server = Registry::new();
187    server.package(
188        "@test/pkg",
189        "1.0.0",
190        &[("mod.ts", "export const value = 42;")],
191        &[(".", "./mod.ts")],
192    );
193    let dir = tempfile::tempdir().unwrap();
194    fs::write(dir.path().join("main.ts"), format!("import {{value}} from 'jsr:@test/pkg@1'; import {{open,errors}} from 'avril:std'; console.log(value); try {{await fetch('{}'); throw new Error('net allowed');}} catch(e) {{console.log(e.message.includes('PermissionDenied'));}} try {{await open('./cache'); throw new Error('read allowed');}} catch(e) {{console.log(e instanceof errors.PermissionDenied);}}", server.url)).unwrap();
195    let grant = format!("--allow-js-exec={}", server.url);
196    let output = run(&server, dir.path(), &["-0", &grant]);
197    assert_eq!(success(&output), "42\ntrue\ntrue\n");
198}
199
200#[test]
201fn denied_or_revoked_remote_imports_do_not_download_code() {
202    let server = Registry::new();
203    server.package(
204        "@test/pkg",
205        "1.0.0",
206        &[("mod.ts", "export {};")],
207        &[(".", "./mod.ts")],
208    );
209    let dir = tempfile::tempdir().unwrap();
210    fs::write(
211        dir.path().join("main.ts"),
212        "await import('jsr:@test/pkg@1');",
213    )
214    .unwrap();
215    let output = run(&server, dir.path(), &["-0"]);
216    assert!(!output.status.success());
217    assert!(String::from_utf8_lossy(&output.stderr).contains("PermissionDenied: js-exec"));
218    assert!(server.requests.lock().unwrap().is_empty());
219    fs::write(dir.path().join("main.ts"), "import {pledge} from 'avril:std'; const pending = import('jsr:@test/pkg@1'); pledge({jsExec: false}); try {await pending; throw new Error('allowed');} catch(e) {console.log(e.message.includes('PermissionDenied'));}").unwrap();
220    assert_eq!(success(&run(&server, dir.path(), &[])), "true\n");
221    assert!(server.requests.lock().unwrap().is_empty());
222}
223
224#[test]
225fn cached_remote_modules_obey_live_file_denials_and_reject_encoded_aliases() {
226    let server = Registry::new();
227    server.package(
228        "@test/pkg",
229        "1.0.0",
230        &[(
231            "mod.ts",
232            "export const url = import.meta.url; export const alias = () => import('./mod%2Ets');",
233        )],
234        &[(".", "./mod.ts")],
235    );
236    let dir = tempfile::tempdir().unwrap();
237    fs::write(dir.path().join("main.ts"), "import {pledge} from 'avril:std'; const a = await import('jsr:@test/pkg@1'); try {await a.alias(); throw new Error('alias allowed');} catch(e) {console.log(e.message.includes('canonical'));} pledge({jsExec: {deny: [a.url]}}); try {await import('jsr:@test/pkg@1'); throw new Error('allowed');} catch(e) {console.log(e.message.includes('PermissionDenied'));} try {await import(a.url); throw new Error('allowed');} catch(e) {console.log(e.message.includes('PermissionDenied'));}").unwrap();
238    assert_eq!(
239        success(&run(&server, dir.path(), &[])),
240        "true\ntrue\ntrue\n"
241    );
242}
243
244#[test]
245fn metadata_version_denials_prevent_fetching_that_versions_manifest() {
246    let server = Registry::new();
247    server.package(
248        "@test/pkg",
249        "1.0.0",
250        &[("mod.ts", "export {};")],
251        &[(".", "./mod.ts")],
252    );
253    let dir = tempfile::tempdir().unwrap();
254    fs::write(dir.path().join("main.ts"), "import 'jsr:@test/pkg@1';").unwrap();
255    let deny = format!("--deny-js-exec={}@test/pkg/1.0.0/", server.url);
256    let output = run(&server, dir.path(), &[&deny]);
257    assert!(!output.status.success());
258    assert_eq!(
259        *server.requests.lock().unwrap(),
260        vec!["/@test/pkg/meta.json"]
261    );
262}
263
264#[test]
265fn tooling_prepares_without_execution_and_vendored_runs_are_offline_reproducible() {
266    let server = Registry::new();
267    server.package(
268        "@test/pkg",
269        "1.0.0",
270        &[("mod.ts", "export const value = 1;")],
271        &[(".", "./mod.ts")],
272    );
273    server.package(
274        "@test/lazy",
275        "1.0.0",
276        &[("mod.ts", "export const value = 'lazy';")],
277        &[(".", "./mod.ts")],
278    );
279    let dir = tempfile::tempdir().unwrap();
280    fs::write(dir.path().join("main.ts"), "import {value} from 'jsr:@test/pkg@^1'; console.log(value, (await import('jsr:@test/' + 'lazy@1')).value);").unwrap();
281    assert_eq!(success(&run(&server, dir.path(), &["cache"])), "");
282    let output = run(&server, dir.path(), &["info"]);
283    let graph: serde_json::Value = serde_json::from_str(&success(&output)).unwrap();
284    assert_eq!(graph["modules"].as_object().unwrap().len(), 2);
285    server.package(
286        "@test/pkg",
287        "1.1.0",
288        &[("mod.ts", "export const value = 2;")],
289        &[(".", "./mod.ts")],
290    );
291    assert_eq!(success(&run(&server, dir.path(), &["update"])), "");
292    assert_eq!(
293        success(&run(
294            &server,
295            dir.path(),
296            &["vendor", "--include=jsr:@test/lazy@1"]
297        )),
298        ""
299    );
300    assert!(dir.path().join("vendor/@test/pkg/1.1.0/mod.ts").is_file());
301    assert!(dir.path().join("vendor/@test/lazy/1.0.0/mod.ts").is_file());
302    fs::remove_dir_all(dir.path().join("cache")).unwrap();
303    server.requests.lock().unwrap().clear();
304    assert_eq!(
305        success(&run(
306            &server,
307            dir.path(),
308            &["--offline", "--frozen", "--vendor=vendor"]
309        )),
310        "2 lazy\n"
311    );
312    assert!(server.requests.lock().unwrap().is_empty());
313}
314
315#[test]
316fn remote_typescript_stacks_use_registry_source_urls_and_original_lines() {
317    let server = Registry::new();
318    server.package("@test/throws", "1.0.0", &[("mod.ts", "type Erased = {\n field: string;\n};\nexport function fail(): never {\n throw new Error('remote');\n}\n")], &[(".", "./mod.ts")]);
319    let dir = tempfile::tempdir().unwrap();
320    fs::write(
321        dir.path().join("main.ts"),
322        "import {fail} from 'jsr:@test/throws@1'; fail();",
323    )
324    .unwrap();
325    let output = run(&server, dir.path(), &[]);
326    assert!(!output.status.success());
327    assert!(
328        String::from_utf8_lossy(&output.stderr).contains(&format!(
329            "at fail ({}@test/throws/1.0.0/mod.ts:5:8)",
330            server.url
331        )),
332        "{}",
333        String::from_utf8_lossy(&output.stderr)
334    );
335}
336
337#[test]
338fn unchanged_locks_are_not_rewritten_and_guest_grants_do_not_move_the_root() {
339    let server = Registry::new();
340    server.package(
341        "@test/pkg",
342        "1.0.0",
343        &[("mod.ts", "export const value = 1;")],
344        &[(".", "./mod.ts")],
345    );
346    let dir = tempfile::tempdir().unwrap();
347    fs::create_dir(dir.path().join("src")).unwrap();
348    fs::write(dir.path().join("avril.conf"), "").unwrap();
349    fs::write(
350        dir.path().join("src/main.ts"),
351        "import {value} from 'jsr:@test/pkg@^1'; console.log(value);",
352    )
353    .unwrap();
354    let execute = |args: &[&str]| {
355        Command::new(env!("CARGO_BIN_EXE_avril"))
356            .current_dir(dir.path())
357            .args(
358                args.first()
359                    .is_none_or(|arg| arg.starts_with('-'))
360                    .then_some("run"),
361            )
362            .args(args)
363            .arg(format!("--jsr-registry={}", server.url))
364            .arg("--cache-dir=cache")
365            .arg("src/main.ts")
366            .output()
367            .unwrap()
368    };
369    assert_eq!(success(&execute(&[])), "1\n");
370    let lock = dir.path().join("avril.lock");
371    let original = fs::read(&lock).unwrap();
372    let modified = fs::metadata(&lock).unwrap().modified().unwrap();
373    server.package(
374        "@test/pkg",
375        "1.1.0",
376        &[("mod.ts", "export const value = 2;")],
377        &[(".", "./mod.ts")],
378    );
379    server.requests.lock().unwrap().clear();
380    let grant = format!("--allow-js-exec={}", server.url);
381    assert_eq!(success(&execute(&["-0", &grant])), "1\n");
382    assert_eq!(success(&execute(&["-0", &grant, "--frozen"])), "1\n");
383    assert!(!dir.path().join("src/avril.lock").exists());
384    assert_eq!(fs::read(&lock).unwrap(), original);
385    assert_eq!(fs::metadata(&lock).unwrap().modified().unwrap(), modified);
386    assert!(server.requests.lock().unwrap().is_empty());
387}
388
389#[test]
390fn vendoring_can_seed_private_computed_files_and_ignores_unusable_graph_hints() {
391    let server = Registry::new();
392    server.package(
393        "@test/private",
394        "1.0.0",
395        &[
396            (
397                "mod.ts",
398                "export const load = () => import('./' + 'hidden.ts');",
399            ),
400            ("hidden.ts", "export const value = 42;"),
401        ],
402        &[(".", "./mod.ts")],
403    );
404    let mut metadata: serde_json::Value = serde_json::from_slice(
405        server
406            .files
407            .lock()
408            .unwrap()
409            .get("/@test/private/1.0.0_meta.json")
410            .unwrap(),
411    )
412    .unwrap();
413    metadata["moduleGraph2"] = json!(["unknown hint format"]);
414    metadata["moduleGraph1"] = json!({"ignored": true});
415    server.files.lock().unwrap().insert(
416        "/@test/private/1.0.0_meta.json".into(),
417        serde_json::to_vec(&metadata).unwrap(),
418    );
419    let dir = tempfile::tempdir().unwrap();
420    fs::write(
421        dir.path().join("main.ts"),
422        "import {load} from 'jsr:@test/private@1'; console.log((await load()).value);",
423    )
424    .unwrap();
425    success(&run(
426        &server,
427        dir.path(),
428        &[
429            "vendor",
430            "--include-from=jsr:@test/private@1",
431            "--include=./hidden.ts",
432        ],
433    ));
434    assert!(
435        dir.path()
436            .join("vendor/@test/private/1.0.0/hidden.ts")
437            .exists()
438    );
439    fs::remove_dir_all(dir.path().join("cache")).unwrap();
440    server.requests.lock().unwrap().clear();
441    assert_eq!(
442        success(&run(
443            &server,
444            dir.path(),
445            &["--offline", "--frozen", "--vendor=vendor"]
446        )),
447        "42\n"
448    );
449    assert!(server.requests.lock().unwrap().is_empty());
450}
451
452#[test]
453fn named_local_packages_do_not_override_explicit_jsr_registry_requests() {
454    let server = Registry::new();
455    server.package(
456        "@test/library",
457        "1.0.0",
458        &[("mod.ts", "export const value = 'registry';")],
459        &[(".", "./mod.ts")],
460    );
461    let dir = tempfile::tempdir().unwrap();
462    fs::create_dir(dir.path().join("lib")).unwrap();
463    fs::write(
464        dir.path().join("avril.conf"),
465        "deps: [{from: './lib', name: '@test/library'}]",
466    )
467    .unwrap();
468    fs::write(
469        dir.path().join("lib/avril.conf"),
470        "name: '@test/library', version: '9.0.0', exports: './mod.ts'",
471    )
472    .unwrap();
473    fs::write(
474        dir.path().join("lib/mod.ts"),
475        "export const value = 'local';",
476    )
477    .unwrap();
478    fs::write(dir.path().join("main.ts"), "import * as local from '@test/library'; import * as remote from 'jsr:@test/library@1'; console.log(local.value, remote.value, local !== remote);").unwrap();
479    assert_eq!(
480        success(&run(&server, dir.path(), &[])),
481        "local registry true\n"
482    );
483}
484
485#[test]
486fn jsr_dependencies_bind_their_package_name_or_override_and_stay_private() {
487    let server = Registry::new();
488    server.package(
489        "@test/library",
490        "1.0.0",
491        &[
492            ("mod.ts", "export const value = 'root';"),
493            ("sub.ts", "export const value = 'sub';"),
494        ],
495        &[(".", "./mod.ts"), ("./sub", "./sub.ts")],
496    );
497    server.package(
498        "@test/value",
499        "2.0.0",
500        &[("mod.ts", "export const value = 'two';")],
501        &[(".", "./mod.ts")],
502    );
503    let dir = tempfile::tempdir().unwrap();
504    fs::create_dir(dir.path().join("local")).unwrap();
505    fs::write(
506        dir.path().join("avril.conf"),
507        "deps: ['jsr:@test/library@^1', {from: 'jsr:@test/value@^2', name: 'value'}, './local']",
508    )
509    .unwrap();
510    fs::write(
511        dir.path().join("local/avril.conf"),
512        "name: 'local', exports: './mod.ts'",
513    )
514    .unwrap();
515    fs::write(
516        dir.path().join("local/mod.ts"),
517        "export async function isolated() {try {await import('value'); return false;} catch(e) {return e.message.includes('Unsupported module specifier');}}",
518    )
519    .unwrap();
520    fs::write(dir.path().join("main.ts"), "import {value as root} from '@test/library'; import {value as sub} from '@test/library/sub'; import * as direct from 'jsr:@test/library@^1'; import {value} from 'value'; import {isolated} from 'local'; console.log(root, sub, value, direct.value === root, await isolated()); for (const name of ['@test/value', '@test/library/missing']) {try {await import(name); console.log('allowed');} catch(e) {console.log(e.message.includes(name === '@test/value' ? 'Unsupported module specifier' : 'no export'));}}").unwrap();
521    assert_eq!(
522        success(&run(&server, dir.path(), &[])),
523        "root sub two true true\ntrue\ntrue\n"
524    );
525    let lock = fixture::read_lock(dir.path());
526    assert_eq!(lock["specifiers"]["jsr:@test/library@^1"], "1.0.0");
527    let info: serde_json::Value =
528        serde_json::from_str(&success(&run(&server, dir.path(), &["info"]))).unwrap();
529    let main = &info["modules"][dir.path().join("main.ts").to_str().unwrap()];
530    assert_eq!(
531        main["package"]["dependencies"]["@test/library"],
532        "jsr:@test/library@^1"
533    );
534    assert_eq!(
535        main["package"]["dependencies"]["value"],
536        "jsr:@test/value@^2"
537    );
538    assert_eq!(
539        main["imports"]["@test/library/sub"],
540        format!("{}@test/library/1.0.0/sub.ts", server.url)
541    );
542}
543
544#[test]
545fn direct_and_binding_imports_agree_on_selection_locking_and_modules() {
546    let server = Registry::new();
547    for (version, value, sub) in [("1.0.0", "one", "sub-one"), ("1.1.0", "two", "sub-two")] {
548        server.package(
549            "@test/parity",
550            version,
551            &[
552                ("mod.ts", &format!("export const value = '{value}';")),
553                ("sub.ts", &format!("export const sub = '{sub}';")),
554            ],
555            &[(".", "./mod.ts"), ("./sub", "./sub.ts")],
556        );
557    }
558    let direct = tempfile::tempdir().unwrap();
559    fs::write(
560        direct.path().join("main.ts"),
561        "import {value} from 'jsr:@test/parity@^1'; import {sub} from 'jsr:@test/parity@^1/sub'; console.log(value, sub);",
562    )
563    .unwrap();
564    let bound = tempfile::tempdir().unwrap();
565    fs::write(
566        bound.path().join("avril.conf"),
567        "deps: ['jsr:@test/parity@^1']",
568    )
569    .unwrap();
570    fs::write(
571        bound.path().join("main.ts"),
572        "import {value} from '@test/parity'; import {sub} from '@test/parity/sub'; console.log(value, sub);",
573    )
574    .unwrap();
575    let direct_stdout = success(&run(&server, direct.path(), &[]));
576    let bound_stdout = success(&run(&server, bound.path(), &[]));
577    // The caret range resolves identically through both routes.
578    assert_eq!(direct_stdout, bound_stdout);
579    assert_eq!(direct_stdout, "two sub-two\n");
580    let direct_lock = fixture::read_lock(direct.path());
581    let bound_lock = fixture::read_lock(bound.path());
582    assert_eq!(direct_lock["specifiers"], bound_lock["specifiers"]);
583    assert_eq!(direct_lock["specifiers"]["jsr:@test/parity@^1"], "1.1.0");
584    assert_eq!(direct_lock, bound_lock);
585    let direct_info: serde_json::Value =
586        serde_json::from_str(&success(&run(&server, direct.path(), &["info"]))).unwrap();
587    let bound_info: serde_json::Value =
588        serde_json::from_str(&success(&run(&server, bound.path(), &["info"]))).unwrap();
589    let direct_main =
590        &direct_info["modules"][direct.path().join("main.ts").to_str().unwrap()]["imports"];
591    let bound_main =
592        &bound_info["modules"][bound.path().join("main.ts").to_str().unwrap()]["imports"];
593    assert_eq!(
594        direct_main["jsr:@test/parity@^1"],
595        bound_main["@test/parity"]
596    );
597    assert_eq!(
598        direct_main["jsr:@test/parity@^1/sub"],
599        bound_main["@test/parity/sub"]
600    );
601}