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
26.5 KiB693 linesraw
1use std::fs;
2use std::path::Path;
3use std::process::{Command, Output};
4
5fn files(root: &Path, files: &[(&str, &str)]) {
6    for (name, source) in files {
7        let path = root.join(name);
8        fs::create_dir_all(path.parent().unwrap()).unwrap();
9        fs::write(path, source).unwrap();
10    }
11}
12
13fn run(root: &Path, entry: &str, args: &[&str]) -> Output {
14    Command::new(env!("CARGO_BIN_EXE_avril"))
15        .current_dir(root)
16        // Options alone run the entry; a leading command selects another.
17        .args(
18            args.first()
19                .is_none_or(|arg| arg.starts_with('-'))
20                .then_some("run"),
21        )
22        .args(args)
23        .arg(entry)
24        .output()
25        .unwrap()
26}
27
28fn success(output: &Output) -> &str {
29    assert!(
30        output.status.success(),
31        "{}",
32        String::from_utf8_lossy(&output.stderr)
33    );
34    std::str::from_utf8(&output.stdout).unwrap()
35}
36
37#[test]
38fn renamed_scoped_and_self_exports_share_identity_with_relative_imports() {
39    let dir = tempfile::tempdir().unwrap();
40    files(
41        dir.path(),
42        &[
43            (
44                "app/avril.conf",
45                "name: '@app/demo', deps: [{from: '../math', name: 'math'}, {from: '../math', name: 'math-copy'}, {from: '../math', name: '@app/math'}]",
46            ),
47            (
48                "app/main.ts",
49                "import * as a from 'math'; import * as b from 'math-copy'; import * as c from '../math/mod.ts'; import * as d from '@app/math'; import * as v from 'math/vector'; const name = 'math'; console.log(a === b, b === c, c === d, v === await import(name + '/vector'), a.self === v, a.count, v.count); try {await import('@original/math');} catch(e) {console.log(e.message.includes('Unsupported module specifier'));}",
50            ),
51            (
52                "math/avril.conf",
53                "name: '@original/math', version: '0.1.0', description: 'Shared math utilities', exports: {'.': './mod.ts', './vector': './src/vector.ts'}",
54            ),
55            (
56                "math/mod.ts",
57                "import * as self from '@original/math/vector'; export {self}; export const count = globalThis.roots = (globalThis.roots ?? 0) + 1;",
58            ),
59            (
60                "math/src/vector.ts",
61                "export const count = globalThis.vectors = (globalThis.vectors ?? 0) + 1;",
62            ),
63        ],
64    );
65    assert_eq!(
66        success(&run(dir.path(), "app/main.ts", &[])),
67        "true true true true true 1 1\ntrue\n"
68    );
69    assert_eq!(success(&run(&dir.path().join("math"), "mod.ts", &[])), "");
70}
71
72#[test]
73fn identical_declared_names_in_different_directories_keep_distinct_self_bindings() {
74    let dir = tempfile::tempdir().unwrap();
75    files(
76        dir.path(),
77        &[
78            (
79                "app/avril.conf",
80                "deps: [{from: '../v1', name: 'math'}, {from: '../v2', name: 'math-next'}, {from: '../format', name: '@app/format'}]",
81            ),
82            (
83                "app/main.ts",
84                "import {value as first} from 'math'; import {value as second} from 'math-next'; import {format} from '@app/format'; console.log(first, second, format);",
85            ),
86            (
87                "v1/avril.conf",
88                "name: '@acme/math', version: '1.0.0', exports: {'.': './mod.ts', './inner': './inner.ts'}",
89            ),
90            (
91                "v2/avril.conf",
92                "name: '@acme/math', version: '2.0.0', exports: {'.': './mod.ts', './inner': './inner.ts'}",
93            ),
94            ("v1/mod.ts", "export {value} from '@acme/math/inner';"),
95            ("v2/mod.ts", "export {value} from '@acme/math/inner';"),
96            ("v1/inner.ts", "export const value = 1;"),
97            ("v2/inner.ts", "export const value = 2;"),
98            ("format/avril.conf", "exports: './mod.ts'"),
99            ("format/mod.ts", "export const format = 'unnamed';"),
100        ],
101    );
102    assert_eq!(
103        success(&run(dir.path(), "app/main.ts", &[])),
104        "1 2 unnamed\n"
105    );
106}
107
108#[test]
109fn dependencies_and_explicit_maps_are_private_to_the_owning_package() {
110    let dir = tempfile::tempdir().unwrap();
111    files(
112        dir.path(),
113        &[
114            (
115                "app/avril.conf",
116                "deps: [{from: '../math', name: 'math'}], imports: {numeric: './app-number.ts', consumerOnly: './app-number.ts'}",
117            ),
118            (
119                "app/main.ts",
120                "import {value, isolated} from 'math'; import {value as appValue} from 'numeric'; console.log(value, appValue, await isolated());",
121            ),
122            ("app/app-number.ts", "export const value = 'app';"),
123            (
124                "math/avril.conf",
125                "exports: './mod.ts', deps: ['../numeric'], imports: {helper: './helper.ts'}",
126            ),
127            (
128                "math/mod.ts",
129                "import {value as n} from 'number-implementation'; import {suffix} from 'helper'; export const value = n + suffix; export async function isolated() {try {await import('consumerOnly'); return false;} catch(e) {return e.message.includes('Unsupported module specifier');}}",
130            ),
131            ("math/helper.ts", "export const suffix = '-math';"),
132            (
133                "numeric/avril.conf",
134                "name: 'number-implementation', exports: './mod.ts'",
135            ),
136            ("numeric/mod.ts", "export const value = 'numeric';"),
137        ],
138    );
139    assert_eq!(
140        success(&run(dir.path(), "app/main.ts", &[])),
141        "numeric-math app true\n"
142    );
143    files(
144        dir.path(),
145        &[("app/main.ts", "import 'number-implementation';")],
146    );
147    let output = run(dir.path(), "app/main.ts", &[]);
148    assert!(!output.status.success());
149    assert!(
150        String::from_utf8_lossy(&output.stderr)
151            .contains("Unsupported module specifier: number-implementation")
152    );
153}
154
155#[test]
156fn cyclic_package_headers_and_source_graphs_do_not_scan_unused_dependencies() {
157    let dir = tempfile::tempdir().unwrap();
158    files(
159        dir.path(),
160        &[
161            (
162                "app/avril.conf",
163                "deps: ['../a', '../b', {from: '../missing', name: 'unused'}]",
164            ),
165            (
166                "app/main.ts",
167                "import {cycle} from '@cycle/a'; import {leafB} from '@cycle/b/leaf'; console.log(cycle(), leafB());",
168            ),
169            (
170                "a/avril.conf",
171                "name: '@cycle/a', exports: './mod.ts', deps: ['../b']",
172            ),
173            (
174                "a/mod.ts",
175                "import {leafB} from '@cycle/b/leaf'; export const a = 'a'; export function cycle() {return leafB();}",
176            ),
177            (
178                "b/avril.conf",
179                "name: '@cycle/b', exports: {'./leaf': './leaf.ts'}, deps: ['../a']",
180            ),
181            (
182                "b/leaf.ts",
183                "import {a} from '@cycle/a'; export function leafB() {return a + 'b';}",
184            ),
185        ],
186    );
187    assert_eq!(success(&run(dir.path(), "app/main.ts", &[])), "ab ab\n");
188}
189
190#[test]
191fn package_imports_have_no_private_file_fallback_but_relative_files_remain_available() {
192    let dir = tempfile::tempdir().unwrap();
193    files(
194        dir.path(),
195        &[
196            ("app/avril.conf", "deps: [{from: '../math', name: 'math'}]"),
197            (
198                "app/main.js",
199                "import {privateSelf} from 'math'; console.log(await privateSelf());",
200            ),
201            ("math/avril.conf", "name: '@acme/math', exports: './mod.js'"),
202            (
203                "math/mod.js",
204                "export async function privateSelf() {try {await import('@acme/math/private.js'); return false;} catch(e) {return e.message.includes('no export');}}",
205            ),
206            ("math/private.js", "console.log('private relative file');"),
207        ],
208    );
209    assert_eq!(success(&run(dir.path(), "app/main.js", &[])), "true\n");
210    for specifier in [
211        "math/private.js",
212        "math/../private.js",
213        "math/%2e%2e/private.js",
214        "math/",
215    ] {
216        files(
217            dir.path(),
218            &[("app/main.js", &format!("import '{specifier}';"))],
219        );
220        let output = run(dir.path(), "app/main.js", &[]);
221        assert!(!output.status.success(), "{specifier}");
222        assert!(String::from_utf8_lossy(&output.stderr).contains("no export"));
223        assert!(output.stdout.is_empty());
224    }
225    files(
226        dir.path(),
227        &[("app/main.js", "import '../math/private.js';")],
228    );
229    assert_eq!(
230        success(&run(dir.path(), "app/main.js", &[])),
231        "private relative file\n"
232    );
233}
234
235#[test]
236fn declared_dependency_requires_its_own_config_and_nonempty_exports() {
237    let dir = tempfile::tempdir().unwrap();
238    files(
239        dir.path(),
240        &[
241            (
242                "avril.conf",
243                "exports: './main.js', deps: [{from: './lib', name: 'lib'}]",
244            ),
245            ("main.js", "import 'lib';"),
246            ("lib/mod.js", "throw Error('not exported');"),
247        ],
248    );
249    let output = run(dir.path(), "main.js", &[]);
250    assert!(!output.status.success());
251    assert!(String::from_utf8_lossy(&output.stderr).contains("must contain avril.conf"));
252    files(dir.path(), &[("lib/avril.conf", "name: 'original'")]);
253    let output = run(dir.path(), "main.js", &[]);
254    assert!(!output.status.success());
255    assert!(String::from_utf8_lossy(&output.stderr).contains("has no exports"));
256    files(
257        dir.path(),
258        &[("lib/avril.conf", "exports: {'./sub': './mod.js'}")],
259    );
260    let output = run(dir.path(), "main.js", &[]);
261    assert!(!output.status.success());
262    assert!(String::from_utf8_lossy(&output.stderr).contains("no export \".\""));
263}
264
265#[test]
266fn export_files_and_cached_dependency_headers_obey_live_execution_caps() {
267    let dir = tempfile::tempdir().unwrap();
268    files(
269        dir.path(),
270        &[
271            ("app/avril.conf", "deps: ['../lib']"),
272            ("lib/avril.conf", "name: 'lib', exports: './mod.js'"),
273            ("lib/mod.js", "export const value = 1;"),
274        ],
275    );
276    for denied in ["lib/avril.conf", "lib/mod.js", "lib"] {
277        files(
278            dir.path(),
279            &[(
280                "app/main.js",
281                &format!(
282                    "import {{pledge}} from 'avril:std'; await import('lib'); pledge({{jsExec: {{deny: ['{denied}']}}}}); try {{await import('lib'); console.log('allowed');}} catch(e) {{console.log(e.message.includes('PermissionDenied'));}}"
283                ),
284            )],
285        );
286        assert_eq!(
287            success(&run(dir.path(), "app/main.js", &[])),
288            "true\n",
289            "{denied}"
290        );
291    }
292}
293
294#[test]
295fn denied_dependencies_do_not_disclose_directory_or_config_existence() {
296    let dir = tempfile::tempdir().unwrap();
297    files(
298        dir.path(),
299        &[
300            ("app/avril.conf", "deps: [{from: '../secret', name: 'lib'}]"),
301            ("app/main.js", "import 'lib';"),
302        ],
303    );
304    let args = ["-0", "--allow-js-exec=app"];
305    let missing = run(dir.path(), "app/main.js", &args);
306    files(dir.path(), &[("secret/avril.conf", "not: valid: syntax")]);
307    let present = run(dir.path(), "app/main.js", &args);
308    assert!(!missing.status.success());
309    assert_eq!(missing.stderr, present.stderr);
310    assert!(String::from_utf8_lossy(&missing.stderr).contains("PermissionDenied: js-exec lib"));
311    let args = [
312        "-0",
313        "--allow-js-exec=app",
314        "--allow-js-exec=secret",
315        "--deny-js-exec=secret/avril.conf",
316    ];
317    fs::remove_file(dir.path().join("secret/avril.conf")).unwrap();
318    let missing = run(dir.path(), "app/main.js", &args);
319    files(dir.path(), &[("secret/avril.conf", "not: valid: syntax")]);
320    let present = run(dir.path(), "app/main.js", &args);
321    assert_eq!(missing.stderr, present.stderr);
322    assert!(String::from_utf8_lossy(&missing.stderr).contains("PermissionDenied: js-exec lib"));
323}
324
325#[cfg(unix)]
326#[test]
327fn exported_symlinks_are_confined_and_directory_aliases_preserve_identity() {
328    use std::os::unix::fs::symlink;
329    let dir = tempfile::tempdir().unwrap();
330    files(
331        dir.path(),
332        &[
333            (
334                "app/avril.conf",
335                "deps: [{from: '../lib', name: 'lib'}, {from: '../link', name: 'renamed'}]",
336            ),
337            (
338                "app/main.js",
339                "import * as a from 'lib'; import * as b from 'renamed'; console.log(a === b);",
340            ),
341            ("lib/avril.conf", "exports: './public.js'"),
342            ("lib/inside.js", "export const value = 1;"),
343            ("outside.js", "console.log('escaped');"),
344        ],
345    );
346    symlink("lib", dir.path().join("link")).unwrap();
347    symlink("inside.js", dir.path().join("lib/public.js")).unwrap();
348    assert_eq!(success(&run(dir.path(), "app/main.js", &[])), "true\n");
349    fs::remove_file(dir.path().join("lib/public.js")).unwrap();
350    symlink("../outside.js", dir.path().join("lib/public.js")).unwrap();
351    let output = run(dir.path(), "app/main.js", &[]);
352    assert!(!output.status.success());
353    assert!(String::from_utf8_lossy(&output.stderr).contains("Export target escapes its package"));
354    assert!(String::from_utf8_lossy(&output.stderr).contains("avril.conf:1:"));
355    assert!(output.stdout.is_empty());
356}
357
358#[cfg(unix)]
359#[test]
360fn dependency_config_symlinks_and_world_writable_headers_are_rejected() {
361    use std::os::unix::fs::{PermissionsExt, symlink};
362    // Path-only entries read the dependency's name at startup, outside guest grants.
363    for deps in ["[{from: '../lib', name: 'lib'}]", "['../lib']"] {
364        let dir = tempfile::tempdir().unwrap();
365        files(
366            dir.path(),
367            &[
368                ("app/avril.conf", &format!("deps: {deps}")),
369                ("app/main.js", "import 'lib';"),
370                ("lib/other.conf", "name: 'lib', exports: './mod.js'"),
371                ("lib/mod.js", "console.log('untrusted');"),
372            ],
373        );
374        let config = dir.path().join("lib/avril.conf");
375        symlink("other.conf", &config).unwrap();
376        let present = run(dir.path(), "app/main.js", &[]);
377        fs::remove_file(&config).unwrap();
378        symlink("missing", &config).unwrap();
379        let missing = run(dir.path(), "app/main.js", &[]);
380        assert!(!present.status.success(), "{deps}");
381        assert_eq!(present.stderr, missing.stderr, "{deps}");
382        assert!(String::from_utf8_lossy(&present.stderr).contains("non-symlink"));
383        fs::remove_file(&config).unwrap();
384        fs::write(&config, "name: 'lib', exports: './mod.js'").unwrap();
385        fs::set_permissions(&config, fs::Permissions::from_mode(0o666)).unwrap();
386        let output = run(dir.path(), "app/main.js", &[]);
387        assert!(!output.status.success(), "{deps}");
388        assert!(String::from_utf8_lossy(&output.stderr).contains("Untrusted config"));
389        assert!(output.stdout.is_empty());
390        fs::set_permissions(&config, fs::Permissions::from_mode(0o644)).unwrap();
391        let lib = dir.path().join("lib");
392        fs::set_permissions(&lib, fs::Permissions::from_mode(0o777)).unwrap();
393        let output = run(dir.path(), "app/main.js", &[]);
394        assert!(!output.status.success(), "{deps}");
395        assert!(String::from_utf8_lossy(&output.stderr).contains("Untrusted dependency directory"));
396        assert!(output.stdout.is_empty());
397        fs::set_permissions(&lib, fs::Permissions::from_mode(0o755)).unwrap();
398        assert_eq!(success(&run(dir.path(), "app/main.js", &[])), "untrusted\n");
399    }
400}
401
402#[test]
403fn info_reports_metadata_and_per_referrer_export_bindings() {
404    let dir = tempfile::tempdir().unwrap();
405    files(
406        dir.path(),
407        &[
408            (
409                "app/avril.conf",
410                "name: '@app/demo', deps: ['../math', {from: '../math', name: 'math'}], imports: {helper: './helper.ts'}",
411            ),
412            (
413                "app/main.ts",
414                "import {value} from 'math'; import {typed} from '@original/math'; export const result = value + typed; throw Error('must not execute');",
415            ),
416            ("app/helper.ts", "export const value = 'app';"),
417            (
418                "math/avril.conf",
419                "name: '@original/math', version: '0.1.0', description: 'Shared utilities', exports: {'.': './mod.ts', './types': './types.d.ts'}, imports: {helper: './helper.ts'}",
420            ),
421            (
422                "math/mod.ts",
423                "import {value} from 'helper'; import type {Value} from '@original/math/types'; export const typed: Value = value; export {value};",
424            ),
425            ("math/helper.ts", "export const value = 1;"),
426            ("math/types.d.ts", "export type Value = number;"),
427        ],
428    );
429    let output = run(dir.path(), "app/main.ts", &["info"]);
430    let graph: serde_json::Value = serde_json::from_str(success(&output)).unwrap();
431    let main = &graph["modules"][dir.path().join("app/main.ts").to_str().unwrap()];
432    let library = &graph["modules"][dir.path().join("math/mod.ts").to_str().unwrap()];
433    assert_eq!(
434        main["imports"]["math"],
435        dir.path().join("math/mod.ts").to_str().unwrap()
436    );
437    assert_eq!(main["package"]["name"], "@app/demo");
438    for binding in ["math", "@original/math"] {
439        assert_eq!(
440            main["imports"][binding],
441            dir.path().join("math/mod.ts").to_str().unwrap()
442        );
443        assert_eq!(
444            main["package"]["dependencies"][binding],
445            dir.path().join("math").to_str().unwrap()
446        );
447    }
448    assert_eq!(library["package"]["name"], "@original/math");
449    assert_eq!(library["package"]["version"], "0.1.0");
450    assert_eq!(library["package"]["description"], "Shared utilities");
451    assert_eq!(
452        library["imports"]["helper"],
453        dir.path().join("math/helper.ts").to_str().unwrap()
454    );
455}
456
457#[test]
458fn failed_dependency_headers_can_be_repaired_without_poisoning_later_imports() {
459    let dir = tempfile::tempdir().unwrap();
460    files(
461        dir.path(),
462        &[
463            ("avril.conf", "deps: [{from: './lib', name: 'lib'}]"),
464            (
465                "main.js",
466                "import {writeTextFile} from 'avril:fs'; try {await import('lib');} catch(e) {console.log(e.message.includes('avril.conf'));} await writeTextFile('./lib/avril.conf', \"exports: './mod.js'\"); console.log((await import('lib')).value);",
467            ),
468            ("lib/mod.js", "export const value = 42;"),
469        ],
470    );
471    assert_eq!(success(&run(dir.path(), "main.js", &[])), "true\n42\n");
472    files(dir.path(), &[("lib/avril.conf", "exports: false")]);
473    assert_eq!(success(&run(dir.path(), "main.js", &[])), "true\n42\n");
474}
475
476#[test]
477fn exported_sources_keep_their_nearest_nested_package_ownership() {
478    let dir = tempfile::tempdir().unwrap();
479    files(
480        dir.path(),
481        &[
482            (
483                "app/avril.conf",
484                "deps: [{from: '../outer', name: 'outer'}]",
485            ),
486            (
487                "app/main.ts",
488                "import {value, isolated} from 'outer'; console.log(value, await isolated());",
489            ),
490            (
491                "outer/avril.conf",
492                "name: '@original/outer', exports: './inner/mod.ts', imports: {helper: './outer-helper.ts'}",
493            ),
494            ("outer/outer-helper.ts", "export const value = 'outer';"),
495            (
496                "outer/inner/avril.conf",
497                "name: '@original/inner', exports: {'./value': './value.ts'}",
498            ),
499            (
500                "outer/inner/mod.ts",
501                "export {value} from '@original/inner/value'; export async function isolated() {try {await import('helper'); return false;} catch(e) {return e.message.includes('Unsupported module specifier');}}",
502            ),
503            ("outer/inner/value.ts", "export const value = 'inner';"),
504        ],
505    );
506    assert_eq!(
507        success(&run(dir.path(), "app/main.ts", &[])),
508        "inner true\n"
509    );
510}
511
512#[test]
513fn ordinary_absence_snapshots_require_a_fresh_loader_to_establish_a_new_package() {
514    let dir = tempfile::tempdir().unwrap();
515    files(
516        dir.path(),
517        &[
518            ("avril.conf", "deps: [{from: './lib', name: 'lib'}]"),
519            (
520                "main.js",
521                "import * as relative from './lib/mod.js'; import {writeTextFile} from 'avril:fs'; await writeTextFile('./lib/avril.conf', \"exports: './mod.js'\"); try {console.log('same', (await import('lib')) === relative);} catch(e) {console.log('snapshot', e.message.includes('must contain avril.conf'));}",
522            ),
523            ("lib/mod.js", "export {value} from './helper.js';"),
524            ("lib/helper.js", "export const value = 1;"),
525        ],
526    );
527    assert_eq!(success(&run(dir.path(), "main.js", &[])), "snapshot true\n");
528    assert_eq!(success(&run(dir.path(), "main.js", &[])), "same true\n");
529}
530
531#[test]
532fn path_only_dependencies_bind_their_declared_names_and_overrides_do_not() {
533    let dir = tempfile::tempdir().unwrap();
534    files(
535        dir.path(),
536        &[
537            (
538                "app/avril.conf",
539                "deps: ['../math', {from: '../math', name: 'math'}]",
540            ),
541            (
542                "app/main.ts",
543                "import * as a from '@acme/math'; import * as b from 'math'; import * as v from '@acme/math/vector'; import * as w from 'math/vector'; console.log(a === b, v === w, a.self === v, a.value);",
544            ),
545            (
546                "math/avril.conf",
547                "name: '@acme/math', exports: {'.': './mod.ts', './vector': './vector.ts'}",
548            ),
549            (
550                "math/mod.ts",
551                "import * as self from '@acme/math/vector'; export {self}; export const value = 'math';",
552            ),
553            ("math/vector.ts", "export const vector = true;"),
554        ],
555    );
556    assert_eq!(
557        success(&run(dir.path(), "app/main.ts", &[])),
558        "true true true math\n"
559    );
560    files(
561        dir.path(),
562        &[("app/avril.conf", "deps: [{from: '../math', name: 'math'}]")],
563    );
564    let output = run(dir.path(), "app/main.ts", &[]);
565    assert!(!output.status.success());
566    assert!(
567        String::from_utf8_lossy(&output.stderr)
568            .contains("Unsupported module specifier: @acme/math")
569    );
570}
571
572#[test]
573fn path_only_dependency_names_must_be_declared_and_unique() {
574    let dir = tempfile::tempdir().unwrap();
575    files(
576        dir.path(),
577        &[
578            ("app/main.js", "console.log('ran');"),
579            ("v1/avril.conf", "name: '@acme/math', exports: './mod.js'"),
580            ("v2/avril.conf", "name: '@acme/math', exports: './mod.js'"),
581            ("anonymous/avril.conf", "exports: './mod.js'"),
582            ("broken/avril.conf", "name: false"),
583        ],
584    );
585    for (deps, message) in [
586        ("['../anonymous']", "declares no name"),
587        ("['../missing']", "Cannot read the name of dependency"),
588        ("['../v2/../broken']", "Expected a string"),
589        (
590            "['../v1', '../v2']",
591            "Duplicate dependency binding \"@acme/math\"",
592        ),
593    ] {
594        files(dir.path(), &[("app/avril.conf", &format!("deps: {deps}"))]);
595        let output = run(dir.path(), "app/main.js", &[]);
596        let stderr = String::from_utf8_lossy(&output.stderr);
597        assert!(!output.status.success(), "{deps}");
598        assert!(output.stdout.is_empty(), "{deps}");
599        assert!(stderr.contains(message), "{deps}: {stderr}");
600        let consumer = format!("app{}avril.conf:1:", std::path::MAIN_SEPARATOR);
601        assert!(stderr.contains(&consumer), "{deps}: {stderr}");
602    }
603}
604
605#[test]
606fn root_bindings_are_read_at_startup_and_nested_bindings_when_their_config_loads() {
607    let dir = tempfile::tempdir().unwrap();
608    files(
609        dir.path(),
610        &[
611            (
612                "app/avril.conf",
613                "deps: ['../lib', {from: '../mid', name: 'mid'}]",
614            ),
615            (
616                "app/main.js",
617                "import {writeTextFile} from 'avril:fs'; await writeTextFile('../lib/avril.conf', \"name: 'renamed', exports: './mod.js'\"); await writeTextFile('../leaf/avril.conf', \"name: 'leaf', exports: './mod.js'\"); console.log((await import('original')).value, (await import('mid')).value); try {await import('renamed');} catch(e) {console.log(e.message.includes('Unsupported module specifier'));}",
618            ),
619            ("lib/avril.conf", "name: 'original', exports: './mod.js'"),
620            ("lib/mod.js", "export const value = 'lib';"),
621            ("mid/avril.conf", "exports: './mod.js', deps: ['../leaf']"),
622            (
623                "mid/mod.js",
624                "import {value as leaf} from 'leaf'; export const value = 'mid-' + leaf;",
625            ),
626            ("leaf/mod.js", "export const value = 'leaf';"),
627        ],
628    );
629    let output = Command::new(env!("CARGO_BIN_EXE_avril"))
630        .arg("run")
631        .current_dir(dir.path().join("app"))
632        .arg("main.js")
633        .output()
634        .unwrap();
635    assert_eq!(success(&output), "lib mid-leaf\ntrue\n");
636}
637
638#[test]
639fn name_discovery_ignores_execution_grants_but_loading_does_not() {
640    let dir = tempfile::tempdir().unwrap();
641    files(
642        dir.path(),
643        &[
644            ("app/avril.conf", "deps: ['../lib']"),
645            (
646                "app/main.js",
647                "for (const name of ['@acme/lib', 'undeclared']) {try {console.log((await import(name)).value);} catch(e) {console.log(e.message);}}",
648            ),
649            ("lib/avril.conf", "name: '@acme/lib', exports: './mod.js'"),
650            ("lib/mod.js", "export const value = 'lib';"),
651        ],
652    );
653    assert_eq!(
654        success(&run(
655            dir.path(),
656            "app/main.js",
657            &["-0", "--allow-js-exec=app"]
658        )),
659        "PermissionDenied: js-exec @acme/lib\nUnsupported module specifier: undeclared\n"
660    );
661    assert_eq!(
662        success(&run(
663            dir.path(),
664            "app/main.js",
665            &["-0", "--allow-js-exec=app", "--allow-js-exec=lib"]
666        )),
667        "lib\nUnsupported module specifier: undeclared\n"
668    );
669    assert_eq!(
670        success(&run(
671            dir.path(),
672            "app/main.js",
673            &[
674                "-0",
675                "--allow-js-exec=app",
676                "--allow-js-exec=lib",
677                "--deny-js-exec=lib/avril.conf"
678            ]
679        )),
680        "PermissionDenied: js-exec @acme/lib\nUnsupported module specifier: undeclared\n"
681    );
682    files(
683        dir.path(),
684        &[(
685            "app/main.js",
686            "import {pledge} from 'avril:std'; pledge({jsExec: {deny: ['lib/avril.conf']}}); try {await import('@acme/lib'); console.log('allowed');} catch(e) {console.log(e.message);}",
687        )],
688    );
689    assert_eq!(
690        success(&run(dir.path(), "app/main.js", &[])),
691        "PermissionDenied: js-exec @acme/lib\n"
692    );
693}