alef 0.103.6

Opinionated polyglot binding generator for Rust libraries
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
//! Integration coverage for [`repair_missing_cfg_binding_features`] against the shape that
//! motivated it: a Ruby and an Elixir native manifest that already declare
//! `native-http`/`opendal-cache`/`wasm-http`, generated source that forwards `#[cfg(feature =
//! "tokenizer")]` / `#[cfg(feature = "tower")]` for three functions
//! (`count_tokens`/`count_request_tokens`/`record_cost_usd`), and a core crate that genuinely
//! declares both `tokenizer` and `tower`.

use super::*;
use crate::core::ir::FunctionDef;
use std::collections::BTreeSet;

const EXISTING_MANIFEST: &str = "[package]\nname = \"sample-core-rb\"\nversion = \"0.1.0\"\n\n[features]\n\
     default = [\"native-http\"]\n\
     native-http = [\"sample-core/native-http\"]\n\
     opendal-cache = [\"sample-core/opendal-cache\"]\n\
     wasm-http = [\"sample-core/wasm-http\"]\n\n\
     [dependencies]\nmagnus = \"0.7\"\n";

fn sample_config(ws_root: &std::path::Path) -> ResolvedCrateConfig {
    let mut config = test_config();
    config.workspace_root = Some(ws_root.to_path_buf());
    config.name = "sample-core".to_string();
    config.sources = vec![PathBuf::from("crates/sample-core/src/lib.rs")];
    config
}

fn sample_api() -> ApiSurface {
    ApiSurface {
        crate_name: "sample_core".to_string(),
        functions: vec![
            FunctionDef {
                name: "count_tokens".to_string(),
                rust_path: "sample_core::count_tokens".to_string(),
                cfg: Some(r#"feature = "tokenizer""#.to_string()),
                ..Default::default()
            },
            FunctionDef {
                name: "count_request_tokens".to_string(),
                rust_path: "sample_core::count_request_tokens".to_string(),
                cfg: Some(r#"feature = "tokenizer""#.to_string()),
                ..Default::default()
            },
            FunctionDef {
                name: "record_cost_usd".to_string(),
                rust_path: "sample_core::record_cost_usd".to_string(),
                cfg: Some(r#"feature = "tower""#.to_string()),
                ..Default::default()
            },
        ],
        ..test_api()
    }
}

fn write_core_crate_manifest(ws_root: &std::path::Path) {
    let core_dir = ws_root.join("crates").join("sample-core");
    std::fs::create_dir_all(&core_dir).expect("create core crate dir");
    std::fs::write(
        core_dir.join("Cargo.toml"),
        "[package]\nname = \"sample-core\"\n\n[features]\n\
         default = []\nnative-http = []\nopendal-cache = []\nwasm-http = []\ntokenizer = []\ntower = []\n",
    )
    .expect("write core Cargo.toml");
}

fn write_existing_manifest(config: &ResolvedCrateConfig, relative: &Path) -> PathBuf {
    let manifest_path = crate::codegen::cfg::resolve_against_workspace_root(config, relative);
    std::fs::create_dir_all(manifest_path.parent().expect("manifest has a parent")).expect("create manifest dir");
    std::fs::write(&manifest_path, EXISTING_MANIFEST).expect("write existing manifest");
    manifest_path
}

/// Reproduces the sample-core incident end to end through the real orchestration entry point: both
/// the Ruby and Elixir manifests already exist, declare unrelated features, and are missing the
/// two features the generated source actually forwards. After repair, both manifests declare
/// `tokenizer` and `tower`, forwarded to the core crate the same way the pre-existing rows are,
/// and every pre-existing line survives.
#[test]
fn repair_adds_missing_features_to_both_ruby_and_elixir_manifests() {
    let dir = tempfile::tempdir().expect("tempdir");
    let ws_root = dir.path();
    let config = sample_config(ws_root);
    write_core_crate_manifest(ws_root);

    let ruby_relative = ruby_native_manifest_path(&config);
    let elixir_relative = PathBuf::from(elixir_native_crate_dir(&config)).join("Cargo.toml");
    let ruby_manifest = write_existing_manifest(&config, &ruby_relative);
    let elixir_manifest = write_existing_manifest(&config, &elixir_relative);

    let api = sample_api();
    let repaired = repair_missing_cfg_binding_features(&api, &config, &[Language::Ruby, Language::Elixir]);

    assert_eq!(
        repaired.into_iter().collect::<BTreeSet<_>>(),
        BTreeSet::from([ruby_manifest.clone(), elixir_manifest.clone()]),
        "both manifests must be reported as repaired"
    );

    for manifest in [&ruby_manifest, &elixir_manifest] {
        let content = std::fs::read_to_string(manifest).expect("read repaired manifest");
        assert!(
            content.contains(r#"tokenizer = ["sample-core/tokenizer"]"#),
            "{}: tokenizer must be forwarded, got:\n{content}",
            manifest.display()
        );
        assert!(
            content.contains(r#"tower = ["sample-core/tower"]"#),
            "{}: tower must be forwarded, got:\n{content}",
            manifest.display()
        );
        assert!(
            content.contains(r#"default = ["native-http", "tokenizer", "tower"]"#),
            "{}: declaring a feature is not enough -- it must also be enabled by default, or \
             #[cfg(feature = \"...\")] stays false and the compile-out this repair exists to fix \
             recurs silently, got:\n{content}",
            manifest.display()
        );
        for preserved in [
            r#"native-http = ["sample-core/native-http"]"#,
            r#"opendal-cache = ["sample-core/opendal-cache"]"#,
            r#"wasm-http = ["sample-core/wasm-http"]"#,
            "[dependencies]",
            "magnus = \"0.7\"",
        ] {
            assert!(
                content.contains(preserved),
                "{}: `{preserved}` must survive the repair, got:\n{content}",
                manifest.display()
            );
        }
    }
}

#[test]
fn repair_preserves_configured_wrapper_default_for_all_native_manifests_after_projection() {
    let dir = tempfile::tempdir().expect("tempdir");
    let ws_root = dir.path();
    let mut config = sample_config(ws_root);
    config.wrapper_default_features = vec!["formula-recognition".to_string()];
    write_core_crate_manifest(ws_root);
    let core_manifest = ws_root.join("crates/sample-core/Cargo.toml");
    let mut core = std::fs::read_to_string(&core_manifest).expect("read core Cargo.toml");
    core.push_str("formula-recognition = []\n");
    std::fs::write(&core_manifest, core).expect("extend core Cargo.toml");

    let native_manifests = [
        (Language::Python, PathBuf::from("crates/sample-core-py/Cargo.toml")),
        (Language::Node, PathBuf::from("crates/sample-core-node/Cargo.toml")),
        (Language::Ruby, ruby_native_manifest_path(&config)),
        (Language::Php, PathBuf::from("crates/sample-core-php/Cargo.toml")),
        (
            Language::Elixir,
            PathBuf::from(elixir_native_crate_dir(&config)).join("Cargo.toml"),
        ),
    ];
    let manifest_paths: BTreeSet<PathBuf> = native_manifests
        .iter()
        .map(|(_, relative)| write_existing_manifest(&config, relative))
        .collect();
    let api = ApiSurface {
        crate_name: "sample_core".to_string(),
        functions: vec![FunctionDef {
            name: "rust_only_formula".to_string(),
            rust_path: "sample_core::rust_only_formula".to_string(),
            cfg: Some(r#"feature = "formula-recognition""#.to_string()),
            binding_excluded: true,
            ..Default::default()
        }],
        ..test_api()
    };

    let languages: Vec<Language> = native_manifests.iter().map(|(language, _)| *language).collect();
    let repaired = repair_missing_cfg_binding_features(&api, &config, &languages);

    assert_eq!(repaired.into_iter().collect::<BTreeSet<_>>(), manifest_paths);
    for manifest in manifest_paths {
        let content = std::fs::read_to_string(&manifest).expect("read repaired manifest");
        assert!(
            content.contains(r#"formula-recognition = ["sample-core/formula-recognition"]"#),
            "{} must forward the configured wrapper feature:\n{content}",
            manifest.display()
        );
        assert!(
            content.contains(r#"default = ["native-http", "formula-recognition"]"#),
            "{} must default-enable the configured wrapper feature:\n{content}",
            manifest.display()
        );
    }
}

#[test]
fn repair_refuses_conflicting_invalid_and_unknown_wrapper_defaults_without_writing() {
    use crate::core::config::languages::RubyConfig;

    for (feature, excluded) in [
        ("formula-recognition", true),
        ("", false),
        ("dep:escape", false),
        ("unknown-feature", false),
        ("default", false),
        ("extension-module", false),
    ] {
        let dir = tempfile::tempdir().expect("tempdir");
        let ws_root = dir.path();
        let mut config = sample_config(ws_root);
        config.wrapper_default_features = vec![feature.to_string()];
        if excluded {
            config.ruby = Some(
                toml::from_str::<RubyConfig>(
                    "gem_name = \"sample_core\"\nexcluded_default_features = [\"formula-recognition\"]\n",
                )
                .expect("Ruby config"),
            );
        }
        write_core_crate_manifest(ws_root);
        let core_manifest = ws_root.join("crates/sample-core/Cargo.toml");
        let mut core = std::fs::read_to_string(&core_manifest).expect("read core Cargo.toml");
        core.push_str("formula-recognition = []\nextension-module = []\n");
        std::fs::write(&core_manifest, core).expect("extend core Cargo.toml");
        let ruby_relative = ruby_native_manifest_path(&config);
        let ruby_manifest = write_existing_manifest(&config, &ruby_relative);

        let repaired = repair_missing_cfg_binding_features(&ApiSurface::default(), &config, &[Language::Ruby]);

        assert!(repaired.is_empty(), "invalid feature `{feature}` must not be repaired");
        assert_eq!(
            std::fs::read_to_string(ruby_manifest).expect("read untouched manifest"),
            EXISTING_MANIFEST,
            "invalid feature `{feature}` must not alter an existing manifest"
        );
    }
}

#[test]
fn repair_ignores_exclusions_for_inactive_languages() {
    use crate::core::config::languages::RubyConfig;

    let dir = tempfile::tempdir().expect("tempdir");
    let ws_root = dir.path();
    let mut config = sample_config(ws_root);
    config.languages = vec![Language::Python];
    config.wrapper_default_features = vec!["formula-recognition".to_string()];
    config.ruby = Some(
        toml::from_str::<RubyConfig>(
            "gem_name = \"sample_core\"\nexcluded_default_features = [\"formula-recognition\"]\n",
        )
        .expect("Ruby config"),
    );
    write_core_crate_manifest(ws_root);
    let core_manifest = ws_root.join("crates/sample-core/Cargo.toml");
    let mut core = std::fs::read_to_string(&core_manifest).expect("read core Cargo.toml");
    core.push_str("formula-recognition = []\n");
    std::fs::write(&core_manifest, core).expect("extend core Cargo.toml");
    let python_relative = PathBuf::from("crates/sample-core-py/Cargo.toml");
    let python_manifest = write_existing_manifest(&config, &python_relative);

    let repaired = repair_missing_cfg_binding_features(&ApiSurface::default(), &config, &[Language::Python]);

    assert_eq!(repaired, vec![python_manifest.clone()]);
    let content = std::fs::read_to_string(python_manifest).expect("read repaired manifest");
    assert!(
        content.contains(r#"formula-recognition = ["sample-core/formula-recognition"]"#),
        "Python must still forward the configured default:\n{content}"
    );
}

/// Same incident, but for the Dart FRB bridge crate's manifest (alef #154: liter-llm's
/// `packages/dart/rust/Cargo.toml` never picked up `tokenizer`/`tower` after its `lib.rs`
/// started forwarding those gates for `count_tokens`/`count_request_tokens`/`record_cost_usd`).
/// Dart was missing from `managed_manifests` entirely -- this is the regression test for adding
/// it back, not a duplicate of the Ruby/Elixir case above: Dart's forwarding rows key off
/// `dart_core_dep_key` (the crate name with `-` replaced by `_`, absent a `core_crate_override`),
/// not the raw crate name Ruby/Elixir use, so `sample-core` must forward as `sample_core/<feature>`
/// here -- proving the per-language dependency key threaded through `managed_manifests` is
/// actually used, not just the Ruby/Elixir default carried over unchanged.
#[test]
fn repair_adds_missing_features_to_dart_manifest() {
    let dir = tempfile::tempdir().expect("tempdir");
    let ws_root = dir.path();
    let config = sample_config(ws_root);
    write_core_crate_manifest(ws_root);

    let dart_relative = crate::backends::dart::gen_rust_crate::dart_native_manifest_path(&config);
    let dart_manifest = write_existing_manifest(&config, &dart_relative);

    let api = sample_api();
    let repaired = repair_missing_cfg_binding_features(&api, &config, &[Language::Dart]);

    assert_eq!(
        repaired,
        vec![dart_manifest.clone()],
        "the Dart manifest must be reported as repaired"
    );

    let content = std::fs::read_to_string(&dart_manifest).expect("read repaired manifest");
    assert!(
        content.contains(r#"tokenizer = ["sample_core/tokenizer"]"#),
        "tokenizer must be forwarded to the underscored dependency key, got:\n{content}"
    );
    assert!(
        content.contains(r#"tower = ["sample_core/tower"]"#),
        "tower must be forwarded to the underscored dependency key, got:\n{content}"
    );
    assert!(
        content.contains(r#"default = ["native-http", "tokenizer", "tower"]"#),
        "declaring a feature is not enough -- it must also be enabled by default, got:\n{content}"
    );
    for preserved in [
        r#"native-http = ["sample-core/native-http"]"#,
        r#"opendal-cache = ["sample-core/opendal-cache"]"#,
        r#"wasm-http = ["sample-core/wasm-http"]"#,
        "[dependencies]",
        "magnus = \"0.7\"",
    ] {
        assert!(
            content.contains(preserved),
            "`{preserved}` must survive the repair, got:\n{content}"
        );
    }
}

/// The Dart manifest must come out byte-identical whether or not this repair runs after the
/// backend wrote it -- `alef all` had no repair call at all while `alef generate`/`alef scaffold`
/// both made one, so one source tree produced two different `[features]` tables depending on
/// which command was invoked.
///
/// Both halves of the divergence are staged here, because each one alone reproduces it:
///
/// * `hidden_gate` is `binding_excluded`, so `project_binding_api` drops it before any backend
///   sees the IR and `internal-only` gates nothing the generated crate contains. The repair is
///   handed the raw, unprojected surface, so without the matching projection it forwards a
///   feature name no emitted file mentions.
/// * `heic` is in `excluded_default_features`, so the backend deliberately writes its forwarding
///   row while keeping it out of `default`. Without honouring that list the repair reads the
///   absence as a defect and pushes the name straight back in, inverting the exclusion.
///
/// Mode A is the backend's own output on the projected surface (what `alef all` leaves on disk);
/// mode B is that same file after the repair runs on the raw surface (what `alef generate` and
/// `alef scaffold` leave on disk). They must agree. ~keep
#[test]
fn dart_manifest_is_identical_with_and_without_the_repair_pass() {
    use crate::core::config::languages::DartConfig;

    let dir = tempfile::tempdir().expect("tempdir");
    let ws_root = dir.path();
    let mut config = sample_config(ws_root);
    config.dart = Some(DartConfig {
        excluded_default_features: vec!["heic".to_string()],
        ..Default::default()
    });
    let core_dir = ws_root.join("crates").join("sample-core");
    std::fs::create_dir_all(&core_dir).expect("create core crate dir");
    std::fs::write(
        core_dir.join("Cargo.toml"),
        "[package]\nname = \"sample-core\"\n\n[features]\n\
         default = []\ntokenizer = []\ntower = []\nheic = []\ninternal-only = []\n",
    )
    .expect("write core Cargo.toml");

    let mut api = sample_api();
    api.functions.push(FunctionDef {
        name: "decode_heic".to_string(),
        rust_path: "sample_core::decode_heic".to_string(),
        cfg: Some(r#"feature = "heic""#.to_string()),
        ..Default::default()
    });
    api.functions.push(FunctionDef {
        name: "hidden_gate".to_string(),
        rust_path: "sample_core::hidden_gate".to_string(),
        cfg: Some(r#"feature = "internal-only""#.to_string()),
        binding_excluded: true,
        ..Default::default()
    });

    let mut projected = api.clone();
    projected.functions.retain(|function| !function.binding_excluded);

    let dart_relative = crate::backends::dart::gen_rust_crate::dart_native_manifest_path(&config);
    let rust_dir = dart_relative
        .parent()
        .expect("the Dart manifest has a parent")
        .to_string_lossy()
        .into_owned();
    let source_crate_name = config.name.replace('-', "_");
    let mode_a =
        crate::backends::dart::gen_rust_crate::emit_cargo_toml(&rust_dir, &projected, &config, &source_crate_name)
            .content;

    let manifest_path = crate::codegen::cfg::resolve_against_workspace_root(&config, &dart_relative);
    std::fs::create_dir_all(manifest_path.parent().expect("manifest has a parent")).expect("create manifest dir");
    std::fs::write(&manifest_path, &mode_a).expect("write the backend's own manifest");

    let repaired = repair_missing_cfg_binding_features(&api, &config, &[Language::Dart]);
    let mode_b = std::fs::read_to_string(&manifest_path).expect("read the manifest after the repair pass");

    // Control: the fixture must actually exercise both halves, or an equality assertion over two
    // identical no-op paths would pass while proving nothing. ~keep
    assert!(
        mode_a.contains(r#"heic = ["sample_core/heic"]"#),
        "control: the backend must forward the excluded feature, got:\n{mode_a}"
    );
    let default_line = mode_a
        .lines()
        .find(|line| line.starts_with("default = ["))
        .expect("control: the backend must emit a default array");
    assert!(
        !default_line.contains("\"heic\""),
        "control: the backend must keep the excluded feature out of default, got: {default_line}"
    );
    assert!(
        !mode_a.contains("internal-only"),
        "control: a binding_excluded function's gate must not reach the backend's manifest, got:\n{mode_a}"
    );

    let added: Vec<&str> = mode_b.lines().filter(|line| !mode_a.contains(*line)).collect();
    assert!(
        added.is_empty(),
        "the repair pass added lines the backend's own manifest does not have, so `alef all` and \
         `alef generate` disagree on this file: {added:#?}"
    );
    assert_eq!(
        mode_b, mode_a,
        "the Dart manifest must not depend on whether the repair pass ran -- `alef all` omits it \
         while `alef generate`/`alef scaffold` run it, so any difference here is one source tree \
         producing two different manifests"
    );
    assert!(
        repaired.is_empty(),
        "the repair must be a no-op on a manifest the backend just wrote, but it rewrote: {repaired:?}"
    );
}

/// The Ruby and Elixir counterpart of the invariant above, and the regression guard on narrowing
/// this repair's input surface: both scaffolders are handed the raw, unprojected surface, so the
/// tables they write are a superset of anything the now-projected repair can reference. The
/// repair must stay the permanent no-op it already is for these two -- a change that made it
/// touch either manifest would rewrite committed consumer files this task's fix is required not
/// to disturb. The fixture includes a `binding_excluded` gate and an `excluded_default_features`
/// name for the same reason as the Dart case: both are what a naive input surface gets wrong. ~keep
#[test]
fn ruby_and_elixir_manifests_are_untouched_by_the_repair_pass() {
    use crate::core::config::languages::RubyConfig;

    let dir = tempfile::tempdir().expect("tempdir");
    let ws_root = dir.path();
    let mut config = sample_config(ws_root);
    // `RubyConfig` has no `Default`, and a struct literal here would break on every future
    // field; deserializing the one line that matters is what its own scaffolder tests do. ~keep
    config.ruby = Some(
        toml::from_str::<RubyConfig>("gem_name = \"sample_core\"\nexcluded_default_features = [\"heic\"]\n")
            .expect("ruby config fixture"),
    );
    let core_dir = ws_root.join("crates").join("sample-core");
    std::fs::create_dir_all(&core_dir).expect("create core crate dir");
    std::fs::write(
        core_dir.join("Cargo.toml"),
        "[package]\nname = \"sample-core\"\n\n[features]\n\
         default = []\ntokenizer = []\ntower = []\nheic = []\ninternal-only = []\n",
    )
    .expect("write core Cargo.toml");

    let mut api = sample_api();
    api.functions.push(FunctionDef {
        name: "decode_heic".to_string(),
        rust_path: "sample_core::decode_heic".to_string(),
        cfg: Some(r#"feature = "heic""#.to_string()),
        ..Default::default()
    });
    api.functions.push(FunctionDef {
        name: "hidden_gate".to_string(),
        rust_path: "sample_core::hidden_gate".to_string(),
        cfg: Some(r#"feature = "internal-only""#.to_string()),
        binding_excluded: true,
        ..Default::default()
    });

    let scaffolded = crate::scaffold::scaffold_ruby_cargo(&api, &config)
        .expect("ruby cargo scaffold")
        .into_iter()
        .chain(crate::scaffold::scaffold_elixir_cargo(&api, &config).expect("elixir cargo scaffold"))
        .filter(|file| file.path.file_name().is_some_and(|name| name == "Cargo.toml"))
        .collect::<Vec<_>>();
    assert_eq!(
        scaffolded.len(),
        2,
        "control: both the Ruby and the Elixir native manifest must be produced, got: {:?}",
        scaffolded.iter().map(|file| file.path.clone()).collect::<Vec<_>>()
    );

    let mut before: Vec<(PathBuf, String)> = Vec::new();
    for file in &scaffolded {
        let path = crate::codegen::cfg::resolve_against_workspace_root(&config, &file.path);
        std::fs::create_dir_all(path.parent().expect("manifest has a parent")).expect("create manifest dir");
        std::fs::write(&path, &file.content).expect("write the scaffolder's own manifest");
        before.push((path, file.content.clone()));
    }

    let repaired = repair_missing_cfg_binding_features(&api, &config, &[Language::Ruby, Language::Elixir]);

    assert!(
        repaired.is_empty(),
        "the repair must not touch a Ruby or Elixir manifest their own scaffolders just wrote, \
         but it rewrote: {repaired:?}"
    );
    for (path, original) in before {
        assert_eq!(
            std::fs::read_to_string(&path).expect("read manifest after the repair pass"),
            original,
            "{} must be byte-for-byte unchanged by the repair pass",
            path.display()
        );
    }
}

/// A language absent from the requested set must not have its manifest touched, even though it
/// is equally missing the feature -- scaffolding one language must never write another's files.
#[test]
fn repair_skips_a_manifest_for_a_language_not_requested() {
    let dir = tempfile::tempdir().expect("tempdir");
    let ws_root = dir.path();
    let config = sample_config(ws_root);
    write_core_crate_manifest(ws_root);

    let elixir_relative = PathBuf::from(elixir_native_crate_dir(&config)).join("Cargo.toml");
    let elixir_manifest = write_existing_manifest(&config, &elixir_relative);

    let api = sample_api();
    let repaired = repair_missing_cfg_binding_features(&api, &config, &[Language::Ruby]);

    assert!(
        repaired.is_empty(),
        "requesting only Ruby must not touch the Elixir manifest, but repaired: {repaired:?}"
    );
    let content = std::fs::read_to_string(&elixir_manifest).expect("read untouched manifest");
    assert_eq!(
        content, EXISTING_MANIFEST,
        "the skipped-language manifest must be byte-for-byte unchanged"
    );
}

/// A manifest that has not been scaffolded yet (no file on disk) is left for the ordinary
/// scaffold-creation path, which already derives the correct `[features]` table from
/// `collect_cfg_features`; this repair has nothing to patch and must not error or create one.
#[test]
fn repair_is_a_no_op_when_no_manifest_exists_yet() {
    let dir = tempfile::tempdir().expect("tempdir");
    let ws_root = dir.path();
    let config = sample_config(ws_root);
    write_core_crate_manifest(ws_root);

    let api = sample_api();
    let repaired = repair_missing_cfg_binding_features(&api, &config, &[Language::Ruby, Language::Elixir]);

    assert!(
        repaired.is_empty(),
        "an unscaffolded crate has no manifest to repair, got: {repaired:?}"
    );
    let ruby_relative = ruby_native_manifest_path(&config);
    let ruby_manifest = crate::codegen::cfg::resolve_against_workspace_root(&config, &ruby_relative);
    assert!(
        !ruby_manifest.exists(),
        "this repair must never create a manifest that was never scaffolded"
    );
}

/// The manifest path is config-derived (`[crates.output] ruby` feeds `package_dir`, the crate
/// name feeds the `ext/<crate>_rb` segment), and this repair writes with a plain `fs::write`,
/// which follows a symlinked ancestor just as the scaffold migrations' temporary files do. So a
/// lexically-innocent relative manifest lands outside the workspace the moment one of its
/// existing ancestor directories is a symlink -- and a repository can ship that symlink in its
/// own tracked tree.
///
/// Unix-only because staging the escape needs `std::os::unix::fs::symlink`; the check itself is
/// not gated. The ordinary-directory counterpart is
/// `repair_adds_missing_features_to_both_ruby_and_elixir_manifests` above, which is what proves
/// this refusal is narrow rather than a blanket rejection of every pre-existing manifest. ~keep
#[test]
#[cfg(unix)]
fn repair_refuses_a_manifest_reached_through_a_symlinked_ancestor() {
    let dir = tempfile::tempdir().expect("tempdir");
    let ws_root = dir.path().join("workspace");
    let outside = dir.path().join("outside");
    let config = sample_config(&ws_root);
    write_core_crate_manifest(&ws_root);

    let ruby_relative = ruby_native_manifest_path(&config);
    let linked_parent = ws_root.join(ruby_relative.parent().expect("manifest has a parent"));
    std::fs::create_dir_all(linked_parent.parent().expect("native dir has a parent")).expect("create ext dir");
    std::fs::create_dir(&outside).expect("create outside dir");
    std::fs::write(outside.join("Cargo.toml"), EXISTING_MANIFEST).expect("write outside manifest");
    std::os::unix::fs::symlink(&outside, &linked_parent).expect("symlink");

    let api = sample_api();
    let repaired = repair_missing_cfg_binding_features(&api, &config, &[Language::Ruby]);

    assert!(
        repaired.is_empty(),
        "a manifest reached through a symlinked ancestor must not be repaired, got: {repaired:?}"
    );
    assert_eq!(
        std::fs::read_to_string(outside.join("Cargo.toml")).expect("outside manifest"),
        EXISTING_MANIFEST,
        "the repair rewrote a manifest outside the workspace root"
    );
}