char/avril

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

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

Charlotte Somdeslop: consolidate public type and registry test utils7325e8c

main
54.9 KiB1824 linesraw
1#[path = "common/registry.rs"]
2mod fixture;
3
4use fixture::{Registry, failure, manifest, success, write};
5use std::fs;
6use std::path::{Path, PathBuf};
7use std::process::{Command, Output};
8
9fn run(dir: &Path, args: &[&str], entry: &str) -> Output {
10    Command::new(env!("CARGO_BIN_EXE_avril"))
11        .arg("run")
12        .current_dir(dir)
13        .args(args)
14        .arg(entry)
15        .output()
16        .unwrap()
17}
18
19#[test]
20fn local_cjs_executes_once_with_shared_identity() {
21    let dir = tempfile::tempdir().unwrap();
22    write(
23        dir.path(),
24        "main.cjs",
25        r#"
26        const a1 = require('./a.cjs');
27        const a2 = require('./a.cjs');
28        console.log(a1 === a2, globalThis.count, a1.foo);
29        import('./a.cjs').then((ns) => console.log(ns.default === a1, globalThis.count));
30        "#,
31    );
32    write(
33        dir.path(),
34        "a.cjs",
35        "globalThis.count = (globalThis.count ?? 0) + 1; exports.foo = 7;",
36    );
37    assert_eq!(
38        success(&run(dir.path(), &[], "main.cjs")),
39        "true 1 7\ntrue 1\n"
40    );
41}
42
43#[test]
44fn esm_default_and_named_import_of_cjs() {
45    let dir = tempfile::tempdir().unwrap();
46    write(
47        dir.path(),
48        "main.js",
49        r#"
50        import def, { foo, bar } from './a.cjs';
51        import * as ns from './a.cjs';
52        console.log(def.foo, foo, bar, ns.bar, def === ns.default);
53        "#,
54    );
55    write(
56        dir.path(),
57        "a.cjs",
58        "exports.foo = 1; module.exports.bar = 2; module.exports.foo = 10;",
59    );
60    assert_eq!(
61        success(&run(dir.path(), &[], "main.js")),
62        "10 10 2 2 true\n"
63    );
64}
65
66#[test]
67fn function_primitive_and_reassigned_exports() {
68    let dir = tempfile::tempdir().unwrap();
69    write(
70        dir.path(),
71        "main.cjs",
72        r#"
73        console.log(require('./prim.cjs'));
74        const fn_ = require('./fn.cjs');
75        console.log(typeof fn_, fn_(), fn_.extra);
76        console.log(require('./reassign.cjs').foo);
77        "#,
78    );
79    write(dir.path(), "prim.cjs", "module.exports = 42;");
80    write(
81        dir.path(),
82        "fn.cjs",
83        "module.exports = function() { return 3; }; module.exports.extra = 5;",
84    );
85    write(
86        dir.path(),
87        "reassign.cjs",
88        "exports.a = 1; module.exports = { foo: 9 };",
89    );
90    assert_eq!(
91        success(&run(dir.path(), &[], "main.cjs")),
92        "42\nfunction 3 5\n9\n"
93    );
94    write(
95        dir.path(),
96        "main2.js",
97        r#"
98        import prim from './prim.cjs';
99        import fn_, { extra } from './fn.cjs';
100        console.log(prim, fn_(), extra);
101        "#,
102    );
103    assert_eq!(success(&run(dir.path(), &[], "main2.js")), "42 3 5\n");
104}
105
106#[test]
107fn cycles_observe_partial_and_replaced_exports() {
108    let dir = tempfile::tempdir().unwrap();
109    write(
110        dir.path(),
111        "main.cjs",
112        "console.log(JSON.stringify(require('./a.cjs')));",
113    );
114    write(
115        dir.path(),
116        "a.cjs",
117        r#"
118        exports.partial = 'a-partial';
119        const b = require('./b.cjs');
120        module.exports.done = 'a-done';
121        module.exports.bSeen = b.partial;
122        "#,
123    );
124    write(
125        dir.path(),
126        "b.cjs",
127        r#"
128        exports.partial = 'b-partial';
129        const a = require('./a.cjs');
130        module.exports = { done: 'b-done', aSeen: a.partial };
131        "#,
132    );
133    // `b` replaces its exports during the cycle, dropping `partial`; `a`
134    // observes the replaced object, so `bSeen` is absent (undefined omitted).
135    assert_eq!(
136        success(&run(dir.path(), &[], "main.cjs")),
137        "{\"partial\":\"a-partial\",\"done\":\"a-done\"}\n"
138    );
139}
140
141#[test]
142fn lazy_conditional_trycatch_and_failed_retry() {
143    let dir = tempfile::tempdir().unwrap();
144    write(
145        dir.path(),
146        "main.cjs",
147        r#"
148        globalThis.executed = false;
149        if (false) { require('./lazy.cjs'); }
150        try { require('./missing.cjs'); } catch (e) { console.log('missing'); }
151        try { require('./bad.cjs'); } catch (e) { console.log('bad1', e.message); }
152        try { require('./bad.cjs'); } catch (e) { console.log('bad2', e.message); }
153        console.log('lazy', globalThis.executed);
154        "#,
155    );
156    write(
157        dir.path(),
158        "lazy.cjs",
159        "globalThis.executed = true; module.exports = 1;",
160    );
161    write(dir.path(), "bad.cjs", "throw new Error('boom');");
162    let out = success(&run(dir.path(), &[], "main.cjs"));
163    assert!(out.contains("missing"), "{out}");
164    assert!(
165        out.contains("bad1 boom") && out.contains("bad2 boom"),
166        "{out}"
167    );
168    assert!(out.contains("lazy false"), "{out}");
169}
170
171#[test]
172fn computed_and_aliased_require_and_resolve_has_no_side_effects() {
173    let dir = tempfile::tempdir().unwrap();
174    write(
175        dir.path(),
176        "main.cjs",
177        r#"
178        const r = require;
179        const base = './a';
180        const mod = r(base + '.cjs');
181        const alias = r('./a.cjs');
182        console.log(mod === alias, mod.foo);
183        const p = require.resolve('./lazy.cjs');
184        console.log(p.endsWith('lazy.cjs'), globalThis.lazy === undefined);
185        "#,
186    );
187    write(dir.path(), "a.cjs", "exports.foo = 11;");
188    write(
189        dir.path(),
190        "lazy.cjs",
191        "globalThis.lazy = true; module.exports = 1;",
192    );
193    assert_eq!(
194        success(&run(dir.path(), &[], "main.cjs")),
195        "true 11\ntrue true\n"
196    );
197}
198
199#[test]
200fn json_require_and_import_rejection() {
201    let dir = tempfile::tempdir().unwrap();
202    write(dir.path(), "data.json", "{\"a\": 5}");
203    write(
204        dir.path(),
205        "main.cjs",
206        "console.log(require('./data.json').a);",
207    );
208    assert_eq!(success(&run(dir.path(), &[], "main.cjs")), "5\n");
209    write(dir.path(), "main2.js", "import './data.json';");
210    assert!(
211        failure(&run(dir.path(), &[], "main2.js")).contains("JSON"),
212        "import JSON stays unsupported"
213    );
214    write(dir.path(), "bad.json", "{oops");
215    write(
216        dir.path(),
217        "main3.cjs",
218        "try { require('./bad.json'); } catch (e) { console.log(e.name); }",
219    );
220    assert_eq!(success(&run(dir.path(), &[], "main3.cjs")), "SyntaxError\n");
221}
222
223#[test]
224fn wrapper_this_return_filename_dirname_and_module_fields() {
225    let dir = tempfile::tempdir().unwrap();
226    write(
227        dir.path(),
228        "main.cjs",
229        r#"
230        console.log(this === exports, typeof require, module.id.endsWith('main.cjs'));
231        console.log(module.filename.endsWith('main.cjs'), module.loaded === false);
232        console.log(__filename.endsWith('main.cjs'), __dirname.length > 0);
233        return;
234        "#,
235    );
236    // Top-level `return` is legal inside the wrapper; reaching here means it parsed.
237    assert_eq!(
238        success(&run(dir.path(), &[], "main.cjs")),
239        "true function true\ntrue true\ntrue true\n"
240    );
241    write(
242        dir.path(),
243        "shebang.cjs",
244        "#!/usr/bin/env node\nmodule.exports = 3;",
245    );
246    write(
247        dir.path(),
248        "main2.cjs",
249        "console.log(require('./shebang.cjs'));",
250    );
251    assert_eq!(success(&run(dir.path(), &[], "main2.cjs")), "3\n");
252}
253
254#[test]
255fn import_from_cjs_uses_correct_referrer() {
256    let dir = tempfile::tempdir().unwrap();
257    write(dir.path(), "esm.js", "export const x = 9;");
258    write(dir.path(), "sub/inner.cjs", "module.exports = 1;");
259    write(
260        dir.path(),
261        "main.cjs",
262        r#"
263        import('./esm.js').then((m) => console.log('dyn', m.x));
264        import('./sub/inner.cjs').then((m) => console.log('cjs-dyn', m.default));
265        "#,
266    );
267    let out = success(&run(dir.path(), &[], "main.cjs"));
268    assert!(out.contains("dyn 9"), "{out}");
269    assert!(out.contains("cjs-dyn 1"), "{out}");
270}
271
272#[test]
273fn require_esm_is_explicitly_unsupported() {
274    let dir = tempfile::tempdir().unwrap();
275    write(dir.path(), "esm.js", "export const x = 1;");
276    write(
277        dir.path(),
278        "main.cjs",
279        "try { require('./esm.js'); } catch (e) { console.log(e.message.slice(0, 20)); }",
280    );
281    let out = success(&run(dir.path(), &[], "main.cjs"));
282    assert!(out.contains("require() of ESM"), "{out}");
283}
284
285#[test]
286fn extension_and_directory_lookup() {
287    let dir = tempfile::tempdir().unwrap();
288    write(dir.path(), "sub/bar.json", "{\"b\": 2}");
289    write(
290        dir.path(),
291        "main.cjs",
292        "console.log(require('./sub/bar').b, require('./sub/bar.json').b);",
293    );
294    assert_eq!(success(&run(dir.path(), &[], "main.cjs")), "2 2\n");
295    write(dir.path(), "sub/foo.js", "export const x = 1;");
296    write(
297        dir.path(),
298        "main2.cjs",
299        "try { require('./sub/foo'); } catch (e) { console.log(e.message.slice(0, 16)); }",
300    );
301    assert!(
302        success(&run(dir.path(), &[], "main2.cjs")).contains("require() of ESM"),
303        ".js stays ESM when required"
304    );
305    fs::create_dir_all(dir.path().join("mydir")).unwrap();
306    fs::write(
307        dir.path().join("mydir/package.json"),
308        "{\"main\": \"./entry.cjs\"}",
309    )
310    .unwrap();
311    write(dir.path(), "mydir/entry.cjs", "module.exports = 99;");
312    write(dir.path(), "main3.cjs", "console.log(require('./mydir'));");
313    assert_eq!(success(&run(dir.path(), &[], "main3.cjs")), "99\n");
314    fs::create_dir_all(dir.path().join("idx")).unwrap();
315    write(dir.path(), "idx/index.cjs", "module.exports = 100;");
316    write(dir.path(), "main4.cjs", "console.log(require('./idx'));");
317    assert_eq!(success(&run(dir.path(), &[], "main4.cjs")), "100\n");
318    fs::write(dir.path().join("bad.node"), "binary").unwrap();
319    write(
320        dir.path(),
321        "main5.cjs",
322        "try { require('./bad'); } catch (e) { console.log(e.message.slice(0, 13)); }",
323    );
324    assert!(
325        success(&run(dir.path(), &[], "main5.cjs")).contains("Native addons"),
326        ".node rejects"
327    );
328}
329
330#[test]
331fn malformed_syntax_is_guest_error() {
332    let dir = tempfile::tempdir().unwrap();
333    write(dir.path(), "bad.cjs", "const = =");
334    write(
335        dir.path(),
336        "main.cjs",
337        "try { require('./bad.cjs'); } catch (e) { console.log(e.name); }",
338    );
339    assert_eq!(success(&run(dir.path(), &[], "main.cjs")), "SyntaxError\n");
340}
341
342#[test]
343fn cached_require_respects_pledge_and_transitive_denial() {
344    let dir = tempfile::tempdir().unwrap();
345    write(dir.path(), "a.cjs", "module.exports = 42;");
346    write(
347        dir.path(),
348        "b.cjs",
349        "const a = require('./a.cjs'); module.exports = a + 1;",
350    );
351    write(
352        dir.path(),
353        "main.js",
354        r#"
355        import { pledge } from 'avril:std';
356        import b from './b.cjs';
357        console.log(b);
358        pledge({jsExec:{deny:['./a.cjs']}});
359        try { await import('./b.cjs'); console.log('unexpected'); }
360        catch (e) { console.log('denied'); }
361        "#,
362    );
363    let output = Command::new(env!("CARGO_BIN_EXE_avril"))
364        .arg("run")
365        .current_dir(dir.path())
366        .arg("main.js")
367        .output()
368        .unwrap();
369    let out = success(&output);
370    assert_eq!(out, "43\ndenied\n");
371}
372
373#[test]
374fn npm_cjs_default_and_named_import_and_require() {
375    let server = Registry::new();
376    server.publish_npm(
377        "cjslib",
378        "1.0.0",
379        &[
380            (
381                "package.json",
382                &manifest("cjslib", "1.0.0", serde_json::json!({"type": "commonjs"})),
383            ),
384            ("index.js", "exports.foo = 1; module.exports.bar = 2;"),
385        ],
386    );
387    let dir = tempfile::tempdir().unwrap();
388    fs::write(dir.path().join("avril.conf"), "deps: ['npm:cjslib@1']").unwrap();
389    write(
390        dir.path(),
391        "main.js",
392        r#"
393        import def, { foo, bar } from 'npm:cjslib@1';
394        console.log(def.foo, foo, bar);
395        "#,
396    );
397    assert_eq!(
398        success(&server.run_npm(dir.path(), &["run"], "main.js")),
399        "1 1 2\n"
400    );
401    write(
402        dir.path(),
403        "main2.cjs",
404        r#"
405        const lib = require('npm:cjslib@1');
406        console.log(lib.foo, lib.bar);
407        const again = require('npm:cjslib@1');
408        console.log(lib === again);
409        "#,
410    );
411    assert_eq!(
412        success(&server.run_npm(dir.path(), &["run"], "main2.cjs")),
413        "1 2\ntrue\n"
414    );
415    // Offline frozen rerun uses the pinned tree without network.
416    assert_eq!(
417        success(&server.run_npm(dir.path(), &["run", "--offline", "--frozen"], "main2.cjs")),
418        "1 2\ntrue\n"
419    );
420}
421
422#[test]
423fn npm_module_entry_and_commonjs_main_keep_distinct_formats() {
424    let server = Registry::new();
425    server.publish_npm(
426        "dual-entry",
427        "1.0.0",
428        &[
429            (
430                "package.json",
431                r#"{"name":"dual-entry","version":"1.0.0","module":"./esm.js","main":"./main.js"}"#,
432            ),
433            (
434                "esm.js",
435                "import main from './main.js'; export default { value: main.value + 1 };",
436            ),
437            ("main.js", "exports.value = 41;"),
438        ],
439    );
440    let dir = tempfile::tempdir().unwrap();
441    write(dir.path(), "avril.conf", "deps: ['npm:dual-entry@1']");
442    write(
443        dir.path(),
444        "main.js",
445        r#"
446        import direct from 'npm:dual-entry@1/esm.js';
447        import entry from 'npm:dual-entry@1';
448        import required from './require.cjs';
449        const dynamic = (await import('npm:dual-entry@1')).default;
450        console.log(direct === entry, entry.value, required.value, required.denied, dynamic === entry);
451        "#,
452    );
453    write(
454        dir.path(),
455        "require.cjs",
456        r#"
457        exports.value = require('npm:dual-entry@1').value;
458        try {
459            require('npm:dual-entry@1/esm.js');
460        } catch (error) {
461            exports.denied = error.message.startsWith('require() of ESM');
462        }
463        "#,
464    );
465    for args in [vec!["run"], vec!["run", "--offline", "--frozen"]] {
466        assert_eq!(
467            success(&server.run_npm(dir.path(), &args, "main.js")),
468            "true 42 41 true true\n"
469        );
470    }
471}
472
473#[test]
474fn npm_conditional_exports_choose_require_branch() {
475    let server = Registry::new();
476    server.publish_npm(
477        "cond",
478        "1.0.0",
479        &[
480            (
481                "package.json",
482                &manifest(
483                    "cond",
484                    "1.0.0",
485                    serde_json::json!({"exports": {".": {"require": "./c.cjs", "import": "./e.mjs"}}}),
486                ),
487            ),
488            ("c.cjs", "module.exports = 'cjs';"),
489            ("e.mjs", "export default 'esm';"),
490        ],
491    );
492    let dir = tempfile::tempdir().unwrap();
493    fs::write(dir.path().join("avril.conf"), "deps: ['npm:cond@1']").unwrap();
494    write(
495        dir.path(),
496        "main.js",
497        "import def from 'npm:cond@1'; console.log(def);",
498    );
499    assert_eq!(
500        success(&server.run_npm(dir.path(), &["run"], "main.js")),
501        "esm\n"
502    );
503    write(
504        dir.path(),
505        "main2.cjs",
506        "console.log(require('npm:cond@1'));",
507    );
508    assert_eq!(
509        success(&server.run_npm(dir.path(), &["run"], "main2.cjs")),
510        "cjs\n"
511    );
512}
513
514#[test]
515fn npm_nested_types_and_relative_lookup() {
516    let server = Registry::new();
517    server.publish_npm(
518        "nested",
519        "1.0.0",
520        &[
521            (
522                "package.json",
523                &manifest("nested", "1.0.0", serde_json::json!({"type": "commonjs"})),
524            ),
525            ("index.js", "module.exports = require('./sub/util');"),
526            ("sub/util.js", "module.exports = 5;"),
527            ("sub/data.json", "{\"v\": 6}"),
528            ("esm/package.json", "{\"type\": \"module\"}"),
529            ("esm/mod.js", "export default 7;"),
530        ],
531    );
532    let dir = tempfile::tempdir().unwrap();
533    fs::write(dir.path().join("avril.conf"), "deps: ['npm:nested@1']").unwrap();
534    write(
535        dir.path(),
536        "main.cjs",
537        r#"
538        console.log(require('npm:nested@1'));
539        console.log(require('npm:nested@1/sub/data.json').v);
540        "#,
541    );
542    assert_eq!(
543        success(&server.run_npm(dir.path(), &["run"], "main.cjs")),
544        "5\n6\n"
545    );
546    write(
547        dir.path(),
548        "main2.js",
549        "import mod from 'npm:nested@1/esm/mod.js'; console.log(mod);",
550    );
551    assert_eq!(
552        success(&server.run_npm(dir.path(), &["run"], "main2.js")),
553        "7\n"
554    );
555}
556
557#[test]
558fn npm_json_require_and_unprepared_require_fails_clearly() {
559    let server = Registry::new();
560    server.publish_npm(
561        "data",
562        "1.0.0",
563        &[
564            (
565                "package.json",
566                &manifest(
567                    "data",
568                    "1.0.0",
569                    serde_json::json!({"exports": {".": "./data.json"}}),
570                ),
571            ),
572            ("data.json", "{\"a\": 8}"),
573        ],
574    );
575    let dir = tempfile::tempdir().unwrap();
576    fs::write(dir.path().join("avril.conf"), "deps: ['npm:data@1']").unwrap();
577    write(
578        dir.path(),
579        "main.cjs",
580        "console.log(require('npm:data@1').a);",
581    );
582    assert_eq!(
583        success(&server.run_npm(dir.path(), &["run"], "main.cjs")),
584        "8\n"
585    );
586    // Unprepared new npm acquisition via sync require fails without network.
587    let dir2 = tempfile::tempdir().unwrap();
588    write(
589        dir2.path(),
590        "main.cjs",
591        "try { require('npm:data@1'); } catch (e) { console.log(e.message); }",
592    );
593    let out = success(&server.run_npm(dir2.path(), &["run"], "main.cjs"));
594    assert!(out.contains("not prepared"), "{out}");
595}
596
597#[test]
598fn npm_require_esm_is_unsupported_and_native_rejects() {
599    let server = Registry::new();
600    server.publish_npm(
601        "esmlib",
602        "1.0.0",
603        &[
604            (
605                "package.json",
606                &manifest("esmlib", "1.0.0", serde_json::json!({"type": "module"})),
607            ),
608            ("index.js", "export default 1;"),
609        ],
610    );
611    server.publish_npm(
612        "nativelib",
613        "1.0.0",
614        &[
615            (
616                "package.json",
617                &manifest(
618                    "nativelib",
619                    "1.0.0",
620                    serde_json::json!({"exports": {".": "./binding.node"}}),
621                ),
622            ),
623            ("binding.node", "binary"),
624        ],
625    );
626    let dir = tempfile::tempdir().unwrap();
627    fs::write(
628        dir.path().join("avril.conf"),
629        "deps: ['npm:esmlib@1', 'npm:nativelib@1']",
630    )
631    .unwrap();
632    write(
633        dir.path(),
634        "main.cjs",
635        "try { require('npm:esmlib@1'); } catch (e) { console.log(e.message.slice(0, 16)); }",
636    );
637    assert!(
638        success(&server.run_npm(dir.path(), &["run"], "main.cjs")).contains("require() of ESM"),
639        "require(ESM) is explicitly unsupported"
640    );
641    write(
642        dir.path(),
643        "main2.cjs",
644        "try { require('npm:nativelib@1'); } catch (e) { console.log(e.message.slice(0, 13)); }",
645    );
646    assert!(
647        success(&server.run_npm(dir.path(), &["run"], "main2.cjs")).contains("Native addons"),
648        "native rejects"
649    );
650}
651
652#[test]
653fn reentry_getter_no_panic_and_single_read() {
654    let dir = tempfile::tempdir().unwrap();
655    write(
656        dir.path(),
657        "a.cjs",
658        r#"
659        let n = 0;
660        Object.defineProperty(module, 'exports', { get() {
661            if (++n === 3) return require('./b.cjs');
662            return { n };
663        }});
664        "#,
665    );
666    write(dir.path(), "b.cjs", "module.exports = 42;");
667    write(
668        dir.path(),
669        "main.cjs",
670        "require('./a.cjs'); console.log('second'); console.log(JSON.stringify(require('./a.cjs'))); console.log(require('./a.cjs'));",
671    );
672    assert_eq!(
673        success(&run(dir.path(), &[], "main.cjs")),
674        "second\n{\"n\":2}\n42\n"
675    );
676    write(
677        dir.path(),
678        "single.cjs",
679        "let n = 0; Object.defineProperty(module, 'exports', { get() { return ++n; } });",
680    );
681    write(
682        dir.path(),
683        "main2.cjs",
684        "console.log(require('./single.cjs'));",
685    );
686    assert_eq!(success(&run(dir.path(), &[], "main2.cjs")), "1\n");
687}
688
689#[test]
690fn require_args_are_strings_not_coerced() {
691    let dir = tempfile::tempdir().unwrap();
692    write(dir.path(), "a.cjs", "module.exports = 1;");
693    write(
694        dir.path(),
695        "main.cjs",
696        r#"
697        for (const bad of [42, {}, null]) {
698            try { require(bad); console.log('coerced'); }
699            catch (e) { console.log(e.name); }
700        }
701        try { require.resolve(); } catch (e) { console.log(e.name); }
702        "#,
703    );
704    assert_eq!(
705        success(&run(dir.path(), &[], "main.cjs")),
706        "TypeError\nTypeError\nTypeError\nTypeError\n"
707    );
708}
709
710#[test]
711fn prototype_pollution_does_not_fire_on_creation() {
712    let dir = tempfile::tempdir().unwrap();
713    write(dir.path(), "a.cjs", "module.exports = 7;");
714    write(
715        dir.path(),
716        "main.cjs",
717        r#"
718        Object.defineProperty(Object.prototype, 'exports', { set(v) { throw new Error('setter fired'); } });
719        Object.defineProperty(Object.prototype, 'resolve', { set(v) { throw new Error('resolve setter'); } });
720        console.log(require('./a.cjs'));
721        "#,
722    );
723    assert_eq!(success(&run(dir.path(), &[], "main.cjs")), "7\n");
724}
725
726#[test]
727fn json_semantics_match_v8_not_serde() {
728    let dir = tempfile::tempdir().unwrap();
729    write(dir.path(), "big.json", "{\"n\":1e999}");
730    write(dir.path(), "bom.json", "\u{FEFF}{\"a\":1}");
731    fs::write(
732        dir.path().join("deep.json"),
733        format!("{{\"a\":{}}}", "[".repeat(50) + &"]".repeat(50)),
734    )
735    .unwrap();
736    write(dir.path(), "bad.json", "{oops");
737    write(
738        dir.path(),
739        "main.cjs",
740        r#"
741        console.log(require('./big.json').n);
742        console.log(require('./bom.json').a);
743        console.log(typeof require('./deep.json').a);
744        try { require('./bad.json'); } catch (e) { console.log(e.name); }
745        "#,
746    );
747    assert_eq!(
748        success(&run(dir.path(), &[], "main.cjs")),
749        "Infinity\n1\nobject\nSyntaxError\n"
750    );
751}
752
753#[test]
754fn cjs_source_maps_preserve_throw_lines() {
755    let dir = tempfile::tempdir().unwrap();
756    write(
757        dir.path(),
758        "a.cjs",
759        "exports.ok = 1;\nthrow new Error('boom');\n",
760    );
761    write(
762        dir.path(),
763        "main.cjs",
764        "try { require('./a.cjs'); } catch (e) { console.log(e.stack.split('\\n')[1]); }",
765    );
766    let out = success(&run(dir.path(), &[], "main.cjs"));
767    assert!(out.contains("a.cjs:2:"), "{out}");
768    write(
769        dir.path(),
770        "b.cts",
771        "const x: number = 1;\nthrow new Error('cts-boom');\n",
772    );
773    write(
774        dir.path(),
775        "main2.cjs",
776        "try { require('./b.cts'); } catch (e) { console.log(e.stack.split('\\n')[1]); }",
777    );
778    let out2 = success(&run(dir.path(), &[], "main2.cjs"));
779    assert!(out2.contains("b.cts:2:"), "{out2}");
780}
781
782#[test]
783fn local_dotted_basename_and_trailing_slash() {
784    let dir = tempfile::tempdir().unwrap();
785    write(dir.path(), "a.b.json", "{\"v\":3}");
786    write(dir.path(), "main.cjs", "console.log(require('./a.b').v);");
787    assert_eq!(success(&run(dir.path(), &[], "main.cjs")), "3\n");
788}
789
790#[test]
791fn esm_intermediary_does_not_hide_revoked_cjs() {
792    let dir = tempfile::tempdir().unwrap();
793    write(dir.path(), "a.cjs", "module.exports = 1;");
794    write(
795        dir.path(),
796        "mid.js",
797        "export { default } from './a.cjs'; export { default as v } from './a.cjs';",
798    );
799    write(
800        dir.path(),
801        "main.js",
802        r#"
803        import { pledge } from 'avril:std';
804        import { v } from './mid.js';
805        console.log(v);
806        pledge({jsExec:{deny:['./a.cjs']}});
807        try { await import('./mid.js'); console.log('unexpected'); }
808        catch (e) { console.log('denied'); }
809        "#,
810    );
811    let output = Command::new(env!("CARGO_BIN_EXE_avril"))
812        .arg("run")
813        .current_dir(dir.path())
814        .arg("main.js")
815        .output()
816        .unwrap();
817    assert_eq!(success(&output), "1\ndenied\n");
818}
819
820#[test]
821fn bare_and_aliased_npm_require() {
822    let server = Registry::new();
823    server.publish_npm(
824        "cjslib",
825        "1.0.0",
826        &[
827            (
828                "package.json",
829                &manifest("cjslib", "1.0.0", serde_json::json!({"type": "commonjs"})),
830            ),
831            ("index.js", "module.exports = 21;"),
832        ],
833    );
834    server.publish_npm(
835        "alib",
836        "1.0.0",
837        &[
838            (
839                "package.json",
840                &manifest("alib", "1.0.0", serde_json::json!({"type": "commonjs"})),
841            ),
842            ("index.js", "module.exports = 22;"),
843        ],
844    );
845    let dir = tempfile::tempdir().unwrap();
846    fs::write(
847        dir.path().join("avril.conf"),
848        "deps: ['npm:cjslib@1', {from: 'npm:alib@1', name: 'ui'}]",
849    )
850    .unwrap();
851    write(
852        dir.path(),
853        "main.cjs",
854        "console.log(require('cjslib'), require('ui'));",
855    );
856    assert_eq!(
857        success(&server.run_npm(dir.path(), &["run"], "main.cjs")),
858        "21 22\n"
859    );
860}
861
862#[test]
863fn fresh_cjs_only_offline_and_cache() {
864    let server = Registry::new();
865    server.publish_npm(
866        "cjslib",
867        "1.0.0",
868        &[
869            (
870                "package.json",
871                &manifest("cjslib", "1.0.0", serde_json::json!({"type": "commonjs"})),
872            ),
873            ("index.js", "module.exports = 33;"),
874        ],
875    );
876    let dir = tempfile::tempdir().unwrap();
877    fs::write(dir.path().join("avril.conf"), "deps: ['npm:cjslib@1']").unwrap();
878    write(
879        dir.path(),
880        "main.cjs",
881        "console.log(require('npm:cjslib@1'));",
882    );
883    assert_eq!(
884        success(&server.run_npm(dir.path(), &["run"], "main.cjs")),
885        "33\n"
886    );
887    assert_eq!(
888        success(&server.run_npm(dir.path(), &["run", "--offline", "--frozen"], "main.cjs")),
889        "33\n"
890    );
891    let cache_out = server.run_npm(dir.path(), &["cache"], "main.cjs");
892    assert!(
893        cache_out.status.success(),
894        "{}",
895        String::from_utf8_lossy(&cache_out.stderr)
896    );
897    assert_eq!(
898        success(&server.run_npm(dir.path(), &["run", "--offline", "--frozen"], "main.cjs")),
899        "33\n"
900    );
901}
902
903#[test]
904fn npm_legacy_main_without_extension_and_subdir() {
905    let server = Registry::new();
906    server.publish_npm(
907        "leg",
908        "1.0.0",
909        &[
910            (
911                "package.json",
912                &manifest(
913                    "leg",
914                    "1.0.0",
915                    serde_json::json!({"type": "commonjs", "main": "lib/start"}),
916                ),
917            ),
918            ("lib/start.js", "module.exports = 44;"),
919            ("lib/index.js", "module.exports = 45;"),
920            ("sub.js", "module.exports = 46;"),
921        ],
922    );
923    let dir = tempfile::tempdir().unwrap();
924    fs::write(dir.path().join("avril.conf"), "deps: ['npm:leg@1']").unwrap();
925    write(
926        dir.path(),
927        "main.cjs",
928        "console.log(require('npm:leg@1')); console.log(require('npm:leg@1/sub'));",
929    );
930    assert_eq!(
931        success(&server.run_npm(dir.path(), &["run"], "main.cjs")),
932        "44\n46\n"
933    );
934    server.publish_npm(
935        "legdir",
936        "1.0.0",
937        &[
938            (
939                "package.json",
940                &manifest(
941                    "legdir",
942                    "1.0.0",
943                    serde_json::json!({"type": "commonjs", "main": "lib"}),
944                ),
945            ),
946            ("lib/index.js", "module.exports = 47;"),
947        ],
948    );
949    let dir2 = tempfile::tempdir().unwrap();
950    fs::write(dir2.path().join("avril.conf"), "deps: ['npm:legdir@1']").unwrap();
951    write(
952        dir2.path(),
953        "main.cjs",
954        "console.log(require('npm:legdir@1'));",
955    );
956    assert_eq!(
957        success(&server.run_npm(dir2.path(), &["run"], "main.cjs")),
958        "47\n"
959    );
960}
961
962#[test]
963fn cached_cjs_require_after_pledge_denies() {
964    let dir = tempfile::tempdir().unwrap();
965    write(dir.path(), "a.cjs", "module.exports = 1;");
966    write(
967        dir.path(),
968        "main.cjs",
969        r#"
970        console.log(require('./a.cjs'));
971        import('avril:std').then(({ pledge }) => {
972            pledge({ jsExec: { deny: ['./a.cjs'] } });
973            try { console.log(require('./a.cjs')); } catch (e) { console.log('denied'); }
974        });
975        "#,
976    );
977    let out = success(&run(dir.path(), &[], "main.cjs"));
978    assert_eq!(out, "1\ndenied\n");
979}
980
981#[test]
982fn named_getter_transitive_denial() {
983    let dir = tempfile::tempdir().unwrap();
984    write(dir.path(), "b.cjs", "module.exports = 42;");
985    write(
986        dir.path(),
987        "a.cjs",
988        "Object.defineProperty(exports, 'foo', { enumerable: true, get() { return require('./b.cjs'); } });",
989    );
990    write(
991        dir.path(),
992        "main.js",
993        r#"
994        import { pledge } from 'avril:std';
995        import { foo } from './a.cjs';
996        console.log(foo);
997        pledge({jsExec:{deny:['./b.cjs']}});
998        try { await import('./a.cjs'); console.log('unexpected'); }
999        catch (e) { console.log('denied'); }
1000        "#,
1001    );
1002    let output = Command::new(env!("CARGO_BIN_EXE_avril"))
1003        .arg("run")
1004        .current_dir(dir.path())
1005        .arg("main.js")
1006        .output()
1007        .unwrap();
1008    assert_eq!(success(&output), "42\ndenied\n");
1009}
1010
1011#[test]
1012fn npm_cache_path_needs_virtual_authority() {
1013    let server = Registry::new();
1014    server.publish_npm(
1015        "cjslib",
1016        "1.0.0",
1017        &[
1018            (
1019                "package.json",
1020                &manifest("cjslib", "1.0.0", serde_json::json!({"type": "commonjs"})),
1021            ),
1022            ("index.js", "module.exports = 1;"),
1023        ],
1024    );
1025    let dir = tempfile::tempdir().unwrap();
1026    fs::write(dir.path().join("avril.conf"), "deps: ['npm:cjslib@1']").unwrap();
1027    write(
1028        dir.path(),
1029        "main.cjs",
1030        "console.log(require('npm:cjslib@1'));",
1031    );
1032    // Local-only grant cannot authorize npm virtual files.
1033    let out = Command::new(env!("CARGO_BIN_EXE_avril"))
1034        .arg("run")
1035        .current_dir(dir.path())
1036        .arg("-0")
1037        .arg("--allow-js-exec=./")
1038        .arg(format!("--jsr-registry={}", server.url))
1039        .arg(format!("--npm-registry={}", server.url))
1040        .arg(format!(
1041            "--cache-dir={}",
1042            dir.path().join("cache").display()
1043        ))
1044        .arg("main.cjs")
1045        .output()
1046        .unwrap();
1047    assert!(
1048        !out.status.success(),
1049        "local grant must not authorize npm cache"
1050    );
1051    assert!(
1052        String::from_utf8_lossy(&out.stderr).contains("PermissionDenied"),
1053        "{}",
1054        String::from_utf8_lossy(&out.stderr)
1055    );
1056}
1057
1058#[test]
1059fn npm_private_exports_and_imports_reject() {
1060    let server = Registry::new();
1061    server.publish_npm(
1062        "priv",
1063        "1.0.0",
1064        &[
1065            (
1066                "package.json",
1067                &manifest(
1068                    "priv",
1069                    "1.0.0",
1070                    serde_json::json!({
1071                        "type": "commonjs",
1072                        "exports": { ".": "./index.js" },
1073                        "imports": { "#u": "./utils.js" }
1074                    }),
1075                ),
1076            ),
1077            ("index.js", "module.exports = require('#u');"),
1078            ("utils.js", "module.exports = 55;"),
1079            ("secret.js", "module.exports = 99;"),
1080        ],
1081    );
1082    let dir = tempfile::tempdir().unwrap();
1083    fs::write(dir.path().join("avril.conf"), "deps: ['npm:priv@1']").unwrap();
1084    write(
1085        dir.path(),
1086        "main.cjs",
1087        "console.log(require('npm:priv@1'));",
1088    );
1089    assert_eq!(
1090        success(&server.run_npm(dir.path(), &["run"], "main.cjs")),
1091        "55\n"
1092    );
1093    write(
1094        dir.path(),
1095        "main2.cjs",
1096        "try { require('npm:priv@1/secret.js'); } catch (e) { console.log('blocked'); }",
1097    );
1098    assert_eq!(
1099        success(&server.run_npm(dir.path(), &["run"], "main2.cjs")),
1100        "blocked\n"
1101    );
1102}
1103
1104#[test]
1105fn npm_imports_choose_require_branch() {
1106    let server = Registry::new();
1107    server.publish_npm(
1108        "imp",
1109        "1.0.0",
1110        &[
1111            (
1112                "package.json",
1113                &manifest(
1114                    "imp",
1115                    "1.0.0",
1116                    serde_json::json!({
1117                        "type": "commonjs",
1118                        "imports": { "#x": { "require": "./c.cjs", "import": "./e.mjs" } }
1119                    }),
1120                ),
1121            ),
1122            ("index.js", "module.exports = require('#x');"),
1123            ("c.cjs", "module.exports = 'c';"),
1124            ("e.mjs", "export default 'e';"),
1125        ],
1126    );
1127    let dir = tempfile::tempdir().unwrap();
1128    fs::write(dir.path().join("avril.conf"), "deps: ['npm:imp@1']").unwrap();
1129    write(dir.path(), "main.cjs", "console.log(require('npm:imp@1'));");
1130    assert_eq!(
1131        success(&server.run_npm(dir.path(), &["run"], "main.cjs")),
1132        "c\n"
1133    );
1134}
1135
1136#[test]
1137fn import_map_mapped_require() {
1138    let dir = tempfile::tempdir().unwrap();
1139    fs::write(
1140        dir.path().join("avril.conf"),
1141        "imports: { a: './a.cjs', dir: './mydir', alias: './b.cjs' }",
1142    )
1143    .unwrap();
1144    write(dir.path(), "a.cjs", "module.exports = 42;");
1145    write(dir.path(), "b.cjs", "module.exports = 43;");
1146    fs::create_dir_all(dir.path().join("mydir")).unwrap();
1147    fs::write(
1148        dir.path().join("mydir/package.json"),
1149        "{\"main\":\"./entry.cjs\"}",
1150    )
1151    .unwrap();
1152    write(dir.path(), "mydir/entry.cjs", "module.exports = 44;");
1153    write(
1154        dir.path(),
1155        "main.cjs",
1156        "console.log(require('a'), require('dir'), require('alias'));",
1157    );
1158    assert_eq!(success(&run(dir.path(), &[], "main.cjs")), "42 44 43\n");
1159    // Symlink identity: symlinked file resolves to canonical target.
1160    #[cfg(unix)]
1161    {
1162        std::os::unix::fs::symlink(dir.path().join("a.cjs"), dir.path().join("link.cjs")).unwrap();
1163        write(
1164            dir.path(),
1165            "main2.cjs",
1166            "console.log(require('./link.cjs') === require('./a.cjs'));",
1167        );
1168        assert_eq!(success(&run(dir.path(), &[], "main2.cjs")), "true\n");
1169    }
1170    let dir2 = tempfile::tempdir().unwrap();
1171    fs::write(
1172        dir2.path().join("avril.conf"),
1173        "imports: { bad: 'ext:foo', nd: 'node:fs' }",
1174    )
1175    .unwrap();
1176    write(
1177        dir2.path(),
1178        "main.cjs",
1179        "try { require('bad'); } catch (e) { console.log('denied-ext'); } try { require('nd'); } catch (e) { console.log('denied-node'); }",
1180    );
1181    assert_eq!(
1182        success(&run(dir2.path(), &[], "main.cjs")),
1183        "denied-ext\ndenied-node\n"
1184    );
1185}
1186
1187#[test]
1188fn local_trailing_slash_directory() {
1189    let dir = tempfile::tempdir().unwrap();
1190    fs::create_dir_all(dir.path().join("idx")).unwrap();
1191    write(dir.path(), "idx/index.cjs", "module.exports = 100;");
1192    write(dir.path(), "main.cjs", "console.log(require('./idx/'));");
1193    assert_eq!(success(&run(dir.path(), &[], "main.cjs")), "100\n");
1194}
1195
1196#[test]
1197fn denied_getter_not_executed() {
1198    let dir = tempfile::tempdir().unwrap();
1199    write(dir.path(), "b.cjs", "module.exports = 1;");
1200    write(
1201        dir.path(),
1202        "a.cjs",
1203        r#"
1204        const b = require('./b.cjs');
1205        globalThis.count = 0;
1206        Object.defineProperty(module, 'exports', { enumerable: true, get() { globalThis.count++; return { b }; } });
1207        "#,
1208    );
1209    write(dir.path(), "require.cjs", "module.exports = require;");
1210    write(
1211        dir.path(),
1212        "main.js",
1213        r#"
1214        import { pledge } from 'avril:std';
1215        const r = (await import('./require.cjs')).default;
1216        console.log(r('./a.cjs').b, globalThis.count);
1217        pledge({jsExec:{deny:['./b.cjs']}});
1218        try { r('./a.cjs'); console.log('unexpected'); }
1219        catch (e) { console.log('denied', globalThis.count); }
1220        "#,
1221    );
1222    let output = Command::new(env!("CARGO_BIN_EXE_avril"))
1223        .arg("run")
1224        .current_dir(dir.path())
1225        .arg("main.js")
1226        .output()
1227        .unwrap();
1228    assert_eq!(success(&output), "1 1\ndenied 1\n");
1229}
1230
1231#[test]
1232fn getter_invokes_pledge() {
1233    let dir = tempfile::tempdir().unwrap();
1234    write(dir.path(), "b.cjs", "module.exports = 2;");
1235    write(
1236        dir.path(),
1237        "a.cjs",
1238        r#"
1239        const cached = require('./b.cjs');
1240        Object.defineProperty(module, 'exports', { enumerable: true, get() {
1241            globalThis.pledge({ jsExec: { deny: ['./b.cjs'] } });
1242            return cached;
1243        }});
1244        "#,
1245    );
1246    write(
1247        dir.path(),
1248        "named.cjs",
1249        r#"
1250        const cached = require('./b.cjs');
1251        Object.defineProperty(exports, 'foo', { enumerable: true, get() {
1252            globalThis.pledge({ jsExec: { deny: ['./b.cjs'] } });
1253            return cached;
1254        }});
1255        "#,
1256    );
1257    write(
1258        dir.path(),
1259        "main.js",
1260        r#"
1261        import { pledge } from 'avril:std';
1262        globalThis.pledge = pledge;
1263        try { (await import('./a.cjs')).default; console.log('unexpected'); }
1264        catch (e) { console.log('denied-module'); }
1265        globalThis.pledge = pledge;
1266        try { (await import('./named.cjs')).foo; console.log('unexpected'); }
1267        catch (e) { console.log('denied-named'); }
1268        "#,
1269    );
1270    let output = Command::new(env!("CARGO_BIN_EXE_avril"))
1271        .arg("run")
1272        .current_dir(dir.path())
1273        .arg("main.js")
1274        .output()
1275        .unwrap();
1276    assert_eq!(success(&output), "denied-module\ndenied-named\n");
1277}
1278
1279#[test]
1280fn npm_root_and_trailing_require() {
1281    let server = Registry::new();
1282    server.publish_npm(
1283        "rootpkg",
1284        "1.0.0",
1285        &[
1286            (
1287                "package.json",
1288                &manifest(
1289                    "rootpkg",
1290                    "1.0.0",
1291                    serde_json::json!({"type": "commonjs", "main": "./index.js"}),
1292                ),
1293            ),
1294            ("index.js", "module.exports = { value: 61 };"),
1295            ("helper.cjs", "module.exports = require('./');"),
1296        ],
1297    );
1298    let dir = tempfile::tempdir().unwrap();
1299    fs::write(dir.path().join("avril.conf"), "deps: ['npm:rootpkg@1']").unwrap();
1300    write(
1301        dir.path(),
1302        "main.cjs",
1303        "const h = require('npm:rootpkg@1/helper.cjs'); const r = require('npm:rootpkg@1'); console.log(h === r, h.value);",
1304    );
1305    assert_eq!(
1306        success(&server.run_npm(dir.path(), &["run"], "main.cjs")),
1307        "true 61\n"
1308    );
1309    server.publish_npm(
1310        "dirpkg",
1311        "1.0.0",
1312        &[
1313            (
1314                "package.json",
1315                &manifest("dirpkg", "1.0.0", serde_json::json!({"type": "commonjs"})),
1316            ),
1317            ("index.js", "module.exports = require('./sub/');"),
1318            ("sub/package.json", "{\"main\": \"./inner.js\"}"),
1319            ("sub/inner.js", "module.exports = 63;"),
1320        ],
1321    );
1322    let dir2 = tempfile::tempdir().unwrap();
1323    fs::write(dir2.path().join("avril.conf"), "deps: ['npm:dirpkg@1']").unwrap();
1324    write(
1325        dir2.path(),
1326        "main.cjs",
1327        "console.log(require('npm:dirpkg@1'));\n        console.log(require('npm:dirpkg@1/sub/'));",
1328    );
1329    assert_eq!(
1330        success(&server.run_npm(dir2.path(), &["run"], "main.cjs")),
1331        "63\n63\n"
1332    );
1333}
1334
1335#[test]
1336fn npm_malformed_and_tampered_metadata_fail() {
1337    let server = Registry::new();
1338    server.publish_npm(
1339        "badmeta",
1340        "1.0.0",
1341        &[
1342            (
1343                "package.json",
1344                &manifest("badmeta", "1.0.0", serde_json::json!({"type": "commonjs"})),
1345            ),
1346            ("index.js", "module.exports = require('./sub');"),
1347            ("sub/package.json", "{oops"),
1348            ("sub/index.js", "module.exports = 1;"),
1349        ],
1350    );
1351    let dir = tempfile::tempdir().unwrap();
1352    fs::write(dir.path().join("avril.conf"), "deps: ['npm:badmeta@1']").unwrap();
1353    write(
1354        dir.path(),
1355        "main.cjs",
1356        "console.log(require('npm:badmeta@1'));",
1357    );
1358    // Malformed nested metadata fails when consumed, not via fallback.
1359    assert!(
1360        failure(&server.run_npm(dir.path(), &["run"], "main.cjs")).contains("Invalid nested"),
1361        "malformed nested package.json must propagate"
1362    );
1363    // Post-acquisition tampering fails verification on use.
1364    let server2 = Registry::new();
1365    server2.publish_npm(
1366        "tamper",
1367        "1.0.0",
1368        &[
1369            (
1370                "package.json",
1371                &manifest("tamper", "1.0.0", serde_json::json!({"type": "commonjs"})),
1372            ),
1373            ("index.js", "module.exports = 1;"),
1374        ],
1375    );
1376    let dir3 = tempfile::tempdir().unwrap();
1377    fs::write(dir3.path().join("avril.conf"), "deps: ['npm:tamper@1']").unwrap();
1378    write(
1379        dir3.path(),
1380        "main.cjs",
1381        "console.log(require('npm:tamper@1'));",
1382    );
1383    assert_eq!(
1384        success(&server2.run_npm(dir3.path(), &["run"], "main.cjs")),
1385        "1\n"
1386    );
1387    // Corrupt one materialized file; next use must fail integrity, not fallback.
1388    fn find_index(dir: &Path) -> Option<PathBuf> {
1389        let mut stack = vec![dir.to_owned()];
1390        while let Some(d) = stack.pop() {
1391            for e in fs::read_dir(&d).ok()? {
1392                let e = e.ok()?;
1393                let path = e.path();
1394                if path.is_dir() {
1395                    stack.push(path);
1396                } else if path.file_name().and_then(|n| n.to_str()) == Some("index.js") {
1397                    return Some(path);
1398                }
1399            }
1400        }
1401        None
1402    }
1403    let tampered_path =
1404        find_index(&dir3.path().join(".avril/npm")).expect("expected materialized index.js");
1405    fs::write(&tampered_path, "module.exports = 2; // tampered").unwrap();
1406    assert!(
1407        failure(&server2.run_npm(dir3.path(), &["run"], "main.cjs")).contains("integrity"),
1408        "tampered npm file must fail verification"
1409    );
1410}
1411
1412#[test]
1413fn npm_mapping_leakage_and_peer_cjs_identity() {
1414    let server = Registry::new();
1415    server.publish_npm(
1416        "peer-dep",
1417        "1.0.0",
1418        &[
1419            (
1420                "package.json",
1421                &manifest("peer-dep", "1.0.0", serde_json::json!({"type": "commonjs"})),
1422            ),
1423            ("index.js", "module.exports = { tag: 'peer' };"),
1424        ],
1425    );
1426    server.publish_npm(
1427        "has-peer",
1428        "1.0.0",
1429        &[
1430            (
1431                "package.json",
1432                &manifest(
1433                    "has-peer",
1434                    "1.0.0",
1435                    serde_json::json!({
1436                        "type": "commonjs",
1437                        "peerDependencies": { "peer-dep": "1" }
1438                    }),
1439                ),
1440            ),
1441            ("index.js", "module.exports = require('peer-dep');"),
1442        ],
1443    );
1444    let dir = tempfile::tempdir().unwrap();
1445    fs::write(
1446        dir.path().join("avril.conf"),
1447        "deps: ['npm:has-peer@1', 'npm:peer-dep@1'], imports: { mapped: './local.cjs' }",
1448    )
1449    .unwrap();
1450    write(dir.path(), "local.cjs", "module.exports = 72;");
1451    write(
1452        dir.path(),
1453        "main.cjs",
1454        "console.log(require('npm:has-peer@1').tag, require('mapped')); console.log(require('npm:has-peer@1') === require('npm:peer-dep@1'));",
1455    );
1456    assert_eq!(
1457        success(&server.run_npm(dir.path(), &["run"], "main.cjs")),
1458        "peer 72\ntrue\n"
1459    );
1460    // npm-owned file must not use project import map: publish npm file requiring 'mapped' should fail (not a declared dep).
1461    server.publish_npm(
1462        "leaky",
1463        "1.0.0",
1464        &[
1465            (
1466                "package.json",
1467                &manifest("leaky", "1.0.0", serde_json::json!({"type": "commonjs"})),
1468            ),
1469            ("index.js", "module.exports = require('mapped');"),
1470        ],
1471    );
1472    let dir2 = tempfile::tempdir().unwrap();
1473    fs::write(
1474        dir2.path().join("avril.conf"),
1475        "deps: ['npm:leaky@1'], imports: { mapped: './local.cjs' }",
1476    )
1477    .unwrap();
1478    write(dir2.path(), "local.cjs", "module.exports = 1;");
1479    write(
1480        dir2.path(),
1481        "main.cjs",
1482        "try { require('npm:leaky@1'); console.log('unexpected'); } catch (e) { console.log('blocked'); }",
1483    );
1484    assert_eq!(
1485        success(&server.run_npm(dir2.path(), &["run"], "main.cjs")),
1486        "blocked\n"
1487    );
1488}
1489
1490#[test]
1491fn cts_erased_positions_and_shebang() {
1492    let dir = tempfile::tempdir().unwrap();
1493    write(
1494        dir.path(),
1495        "a.cts",
1496        "#!/usr/bin/env node\ntype A = { x: number };\nenum E { A, B }\nconst v: A = { x: 1 };\nthrow new Error('cts-boom');\n",
1497    );
1498    write(
1499        dir.path(),
1500        "main.cjs",
1501        "try { require('./a.cts'); } catch (e) { console.log(e.stack.split('\\n')[1]); }",
1502    );
1503    let out = success(&run(dir.path(), &[], "main.cjs"));
1504    assert!(out.contains("a.cts:5:"), "{out}");
1505}
1506
1507#[test]
1508fn cjs_snapshot_stable_across_mutation() {
1509    let dir = tempfile::tempdir().unwrap();
1510    write(dir.path(), "a.cjs", "exports.foo = 1;");
1511    write(
1512        dir.path(),
1513        "mutate.js",
1514        r#"
1515        import { writeTextFile } from 'avril:fs';
1516        await writeTextFile('./a.cjs', 'exports.bar = 2;');
1517        "#,
1518    );
1519    write(
1520        dir.path(),
1521        "main.js",
1522        r#"
1523        import './mutate.js';
1524        import def, { foo } from './a.cjs';
1525        console.log(def.foo, foo, 'bar' in def);
1526        "#,
1527    );
1528    // Link stores foo from prepared snapshot; mutate rewrites before CJS eval,
1529    // but eval uses prepared snapshot + stored names, so no fatal and foo survives.
1530    assert_eq!(success(&run(dir.path(), &[], "main.js")), "1 1 false\n");
1531}
1532
1533#[test]
1534fn fresh_cache_in_distinct_project_offline() {
1535    let server = Registry::new();
1536    server.publish_npm(
1537        "cjslib2",
1538        "1.0.0",
1539        &[
1540            (
1541                "package.json",
1542                &manifest("cjslib2", "1.0.0", serde_json::json!({"type": "commonjs"})),
1543            ),
1544            ("index.js", "module.exports = 77;"),
1545        ],
1546    );
1547    let dir = tempfile::tempdir().unwrap();
1548    fs::write(dir.path().join("avril.conf"), "deps: ['npm:cjslib2@1']").unwrap();
1549    write(
1550        dir.path(),
1551        "main.cjs",
1552        "console.log(require('npm:cjslib2@1'));",
1553    );
1554    let cache_out = server.run_npm(dir.path(), &["cache"], "main.cjs");
1555    assert!(
1556        cache_out.status.success(),
1557        "{}",
1558        String::from_utf8_lossy(&cache_out.stderr)
1559    );
1560    assert_eq!(
1561        success(&server.run_npm(dir.path(), &["run", "--offline", "--frozen"], "main.cjs")),
1562        "77\n"
1563    );
1564}
1565
1566#[test]
1567fn npm_absolute_paths_share_identity_and_block_escape() {
1568    let server = Registry::new();
1569    server.publish_npm(
1570        "abspkg",
1571        "1.0.0",
1572        &[
1573            (
1574                "package.json",
1575                &manifest("abspkg", "1.0.0", serde_json::json!({"type": "commonjs"})),
1576            ),
1577            ("index.js", "module.exports = { v: 81 };"),
1578            ("data.js", "module.exports = { v: 82 };"),
1579            (
1580                "caller.js",
1581                "const rel = require('./data.js'); const viaResolve = require(require.resolve('./data.js')); const viaDir = require(__dirname + '/data.js'); let blocked = false; try { require('/etc/passwd'); } catch (_) { blocked = true; } console.log(rel === viaResolve, rel === viaDir, rel.v, blocked); module.exports = { rel, viaResolve };",
1582            ),
1583        ],
1584    );
1585    let dir_a = tempfile::tempdir().unwrap();
1586    fs::write(dir_a.path().join("avril.conf"), "deps: ['npm:abspkg@1']").unwrap();
1587    write(
1588        dir_a.path(),
1589        "main.cjs",
1590        "const a = require('npm:abspkg@1/data.js'); const p = require.resolve('npm:abspkg@1/data.js'); const b = require(p); console.log(a === b, a.v);",
1591    );
1592    assert_eq!(
1593        success(&server.run_npm(dir_a.path(), &["run"], "main.cjs")),
1594        "true 82\n"
1595    );
1596    write(
1597        dir_a.path(),
1598        "main2.cjs",
1599        "const c = require('npm:abspkg@1/caller.js'); console.log(c.rel.v);",
1600    );
1601    // Within-instance require.resolve('./data') + __dirname absolute share identity; outside absolute blocked.
1602    assert_eq!(
1603        success(&server.run_npm(dir_a.path(), &["run"], "main2.cjs")),
1604        "true true 82 true\n82\n"
1605    );
1606}
1607
1608#[test]
1609fn npm_cached_absolute_after_pledge() {
1610    let server = Registry::new();
1611    server.publish_npm(
1612        "pledgeabs",
1613        "1.0.0",
1614        &[
1615            (
1616                "package.json",
1617                &manifest(
1618                    "pledgeabs",
1619                    "1.0.0",
1620                    serde_json::json!({"type": "commonjs"}),
1621                ),
1622            ),
1623            ("index.js", "module.exports = require('./data.js');"),
1624            ("data.js", "module.exports = { v: 83 };"),
1625        ],
1626    );
1627    let dir = tempfile::tempdir().unwrap();
1628    fs::write(dir.path().join("avril.conf"), "deps: ['npm:pledgeabs@1']").unwrap();
1629    write(dir.path(), "require.cjs", "module.exports = require;");
1630    write(
1631        dir.path(),
1632        "main.js",
1633        r#"
1634        import { pledge } from 'avril:std';
1635        const r = (await import('./require.cjs')).default;
1636        const first = r('npm:pledgeabs@1/data.js');
1637        const abs = r.resolve('npm:pledgeabs@1/data.js');
1638        console.log(first.v, first === r(abs));
1639        pledge({ jsExec: false });
1640        try { r(abs); console.log('unexpected'); }
1641        catch (e) { console.log('denied'); }
1642        "#,
1643    );
1644    assert_eq!(
1645        success(&server.run_npm(dir.path(), &["run"], "main.js")),
1646        "83 true\ndenied\n"
1647    );
1648}
1649
1650#[test]
1651fn npm_dot_main_and_nested_dot_fallback() {
1652    let server = Registry::new();
1653    server.publish_npm(
1654        "dotmain",
1655        "1.0.0",
1656        &[
1657            (
1658                "package.json",
1659                &manifest(
1660                    "dotmain",
1661                    "1.0.0",
1662                    serde_json::json!({"type": "commonjs", "main": "."}),
1663                ),
1664            ),
1665            ("index.js", "module.exports = 91;"),
1666        ],
1667    );
1668    server.publish_npm(
1669        "dotdir",
1670        "1.0.0",
1671        &[
1672            (
1673                "package.json",
1674                &manifest("dotdir", "1.0.0", serde_json::json!({"type": "commonjs"})),
1675            ),
1676            ("index.js", "module.exports = require('./sub');"),
1677            ("sub/package.json", "{\"main\": \".\"}"),
1678            ("sub/index.js", "module.exports = 92;"),
1679        ],
1680    );
1681    let dir = tempfile::tempdir().unwrap();
1682    fs::write(
1683        dir.path().join("avril.conf"),
1684        "deps: ['npm:dotmain@1', 'npm:dotdir@1']",
1685    )
1686    .unwrap();
1687    write(
1688        dir.path(),
1689        "main.cjs",
1690        "console.log(require('npm:dotmain@1'), require('npm:dotdir@1'));",
1691    );
1692    assert_eq!(
1693        success(&server.run_npm(dir.path(), &["run"], "main.cjs")),
1694        "91 92\n"
1695    );
1696}
1697
1698#[test]
1699fn npm_same_graph_tampered_metadata_fails_not_fallback() {
1700    let server = Registry::new();
1701    server.publish_npm(
1702        "metagraph2",
1703        "1.0.0",
1704        &[
1705            (
1706                "package.json",
1707                &manifest("metagraph2", "1.0.0", serde_json::json!({"type": "commonjs"})),
1708            ),
1709            (
1710                "index.js",
1711                r#"module.exports.run = () => { const pkg = require.resolve('./sub/package.json'); globalThis.syncWrite(pkg, '{oops'); return require('./sub'); };"#,
1712            ),
1713            ("sub/package.json", "{\"main\": \"./inner.js\"}"),
1714            ("sub/inner.js", "module.exports = 1;"),
1715            ("sub/index.js", "module.exports = 2;"),
1716        ],
1717    );
1718    let dir = tempfile::tempdir().unwrap();
1719    fs::write(dir.path().join("avril.conf"), "deps: ['npm:metagraph2@1']").unwrap();
1720    write(
1721        dir.path(),
1722        "main.js",
1723        r#"
1724        import { writeTextFile } from 'avril:fs/sync';
1725        globalThis.syncWrite = writeTextFile;
1726        const pkg = (await import('npm:metagraph2@1')).default;
1727        try { pkg.run(); console.log('unexpected'); } catch (e) { console.log(e.message.includes('Invalid nested') || e.message.includes('integrity') ? 'failed' : 'other:' + e.message); }
1728        "#,
1729    );
1730    assert_eq!(
1731        success(&server.run_npm(dir.path(), &["run"], "main.js")),
1732        "failed\n"
1733    );
1734}
1735
1736#[test]
1737fn local_symlink_npm_metadata_denied_and_tamper_rejected() {
1738    let server = Registry::new();
1739    server.publish_npm(
1740        "metapkg",
1741        "1.0.0",
1742        &[
1743            (
1744                "package.json",
1745                &manifest(
1746                    "metapkg",
1747                    "1.0.0",
1748                    serde_json::json!({"type": "commonjs", "main": "./entry.js"}),
1749                ),
1750            ),
1751            ("entry.js", "module.exports = 91;"),
1752        ],
1753    );
1754    let dir = tempfile::tempdir().unwrap();
1755    fs::write(dir.path().join("avril.conf"), "deps: ['npm:metapkg@1']").unwrap();
1756    write(
1757        dir.path(),
1758        "main.cjs",
1759        "console.log(require('npm:metapkg@1'));",
1760    );
1761    assert_eq!(
1762        success(&server.run_npm(dir.path(), &["run"], "main.cjs")),
1763        "91\n"
1764    );
1765    fn find_manifest(root: &Path) -> Option<PathBuf> {
1766        let mut stack = vec![root.join(".avril/npm")];
1767        while let Some(d) = stack.pop() {
1768            for e in fs::read_dir(&d).ok()? {
1769                let e = e.ok()?;
1770                let path = e.path();
1771                if path.is_dir() {
1772                    stack.push(path);
1773                } else if path.file_name().and_then(|n| n.to_str()) == Some("package.json")
1774                    && fs::read_to_string(&path).ok()?.contains("\"metapkg\"")
1775                {
1776                    return Some(path);
1777                }
1778            }
1779        }
1780        None
1781    }
1782    let manifest_path = find_manifest(dir.path()).expect("expected materialized npm manifest");
1783    #[cfg(unix)]
1784    {
1785        fs::create_dir_all(dir.path().join("aliased")).unwrap();
1786        std::os::unix::fs::symlink(&manifest_path, dir.path().join("aliased/package.json"))
1787            .unwrap();
1788        write(
1789            dir.path(),
1790            "main2.cjs",
1791            "try { console.log(require('./aliased')); } catch (e) { console.log(e.message.includes('PermissionDenied') || e.message.includes('not owned') ? 'denied' : 'other:' + e.message); }",
1792        );
1793        // No npm deps in config so prepare succeeds with local-only grant;
1794        // aliased metadata read must still deny (no virtual authority / unowned).
1795        fs::write(dir.path().join("avril.conf"), "deps: []").unwrap();
1796        let out = Command::new(env!("CARGO_BIN_EXE_avril"))
1797            .arg("run")
1798            .current_dir(dir.path())
1799            .arg("-0")
1800            .arg("--allow-js-exec=./")
1801            .arg(format!("--jsr-registry={}", server.url))
1802            .arg(format!("--npm-registry={}", server.url))
1803            .arg(format!(
1804                "--cache-dir={}",
1805                dir.path().join("cache").display()
1806            ))
1807            .arg("main2.cjs")
1808            .output()
1809            .unwrap();
1810        assert_eq!(success(&out), "denied\n");
1811        // Restore dep so npm instance is prepared; tampered manifest must fail integrity, not fallback.
1812        fs::write(dir.path().join("avril.conf"), "deps: ['npm:metapkg@1']").unwrap();
1813        fs::write(&manifest_path, "{\"name\":\"metapkg\",\"version\":\"1.0.0\",\"main\":\"./entry.js\",\"tampered\":true}").unwrap();
1814        write(
1815            dir.path(),
1816            "main3.cjs",
1817            "console.log(require('./aliased'));",
1818        );
1819        assert!(
1820            failure(&server.run_npm(dir.path(), &["run"], "main3.cjs")).contains("integrity"),
1821            "tampered aliased metadata must fail verification"
1822        );
1823    }
1824}