leindex 2.0.2

LeIndex MCP and semantic code search engine for AI tools and large codebases
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
//! Install script tests (VAL-RELEASE-019, VAL-RELEASE-020).
//!
//! These tests statically validate `install.sh` to ensure the installer
//! correctly consumes the release bundle distribution model introduced
//! by the load-dynamic ORT migration and the interactive `leindex setup`
//! command.
//!
//! Invariants tested:
//!
//!   * install.sh copies bundled ORT libs to `~/.leindex/lib/`
//!     (the GitHub Release bundle install path with pre-built `lib/`)
//!   * install.sh never copies model files from release artifacts; setup owns
//!     model provisioning
//!   * install.sh runs `leindex setup --check` after install to report
//!     neural search status, suggesting `leindex setup` when unconfigured
//!   * install.sh contains NO `ORT_LIB_PATH` or `ORT_PREFER_DYNAMIC_LINK`
//!     references (obsolete under load-dynamic)
//!   * install.sh's cargo build line uses the `onnx` (load-dynamic) feature
//!     and does NOT pass `onnx-migraphx` (the MIGraphX provider is a
//!     runtime-only concern handled by the discovered ORT library)
//!
//! The contract is documented in
//! `validation-contract.md` (Area: RELEASE, VAL-RELEASE-019/020) and
//! `architecture.md` (Section 3: Release Pipeline).

#![cfg(test)]

use std::path::PathBuf;

/// Return the absolute path to the repo root, computed from CARGO_MANIFEST_DIR
/// (the directory containing Cargo.toml of the `leindex` package under test).
fn repo_root() -> PathBuf {
    let dir = env!("CARGO_MANIFEST_DIR");
    PathBuf::from(dir)
}

/// Read install.sh as a string. Panics with a clear diagnostic if the file
/// is missing so test failures point at the actual cause.
fn install_sh() -> String {
    read_repo_file("install.sh")
}

fn read_repo_file(rel: &str) -> String {
    let path = repo_root().join(rel);
    std::fs::read_to_string(&path)
        .unwrap_or_else(|e| panic!("failed to read {}: {e}", path.display()))
}

// ============================================================================
// ORT environment variable invariants (no build-time linking)
// ============================================================================

mod ort_env_invariants {
    use super::*;

    /// No line in install.sh may set `ORT_LIB_PATH`. Under the load-dynamic
    /// ort strategy, the worker discovers and dlopens libonnxruntime at
    /// runtime via the ort_discovery module — no build-time env var is
    /// needed, and its presence would indicate an incomplete migration.
    #[test]
    fn no_ort_lib_path_references() {
        let sh = install_sh();
        assert!(
            !sh.contains("ORT_LIB_PATH"),
            "install.sh must not contain ORT_LIB_PATH (obsolete under load-dynamic)"
        );
    }

    /// No line in install.sh may set `ORT_PREFER_DYNAMIC_LINK`. This env var
    /// was used by the old download-binaries + copy-dylibs strategy and has
    /// no effect under load-dynamic. Its presence would indicate stale
    /// references from the pre-migration distribution model.
    #[test]
    fn no_ort_prefer_dynamic_link_references() {
        let sh = install_sh();
        assert!(
            !sh.contains("ORT_PREFER_DYNAMIC_LINK"),
            "install.sh must not contain ORT_PREFER_DYNAMIC_LINK (obsolete under load-dynamic)"
        );
    }
}

// ============================================================================
// Build line invariants (load-dynamic, not onnx-migraphx)
// ============================================================================

mod build_line_invariants {
    use super::*;

    /// The `cargo build` invocation in install.sh must use the `onnx`
    /// feature (which carries the `load-dynamic` ort strategy), NOT
    /// `onnx-migraphx`. The MIGraphX (AMD GPU) provider is a runtime-only
    /// concern: it is provided by the onnxruntime-migraphx pip wheel or
    /// the bundled `lib/` directory (discovered at runtime). Building
    /// with `onnx` (load-dynamic) produces a binary that can use CPU,
    /// CUDA, or MIGraphX ORT at runtime depending on which library is
    /// discovered.
    ///
    /// We check that the actual `cargo build` command line contains
    /// `--features` with `onnx` and does NOT contain `onnx-migraphx` in
    /// the feature list.
    #[test]
    fn build_line_uses_onnx_not_onnx_migraphx() {
        let sh = install_sh();

        // Find the cargo build line. It must reference the onnx feature.
        let has_cargo_build = sh
            .lines()
            .any(|line| line.contains("cargo build") && line.contains("--features"));
        assert!(
            has_cargo_build,
            "install.sh must contain a cargo build command with --features"
        );

        // The build line must use onnx (load-dynamic), not onnx-migraphx.
        // We look at the actual cargo build invocation. A comment line
        // may mention onnx-migraphx (explaining why we DON'T use it),
        // so we filter non-comment, non-empty lines that carry the build.
        let build_lines: Vec<&str> = sh
            .lines()
            .filter(|line| {
                let trimmed = line.trim_start();
                !trimmed.starts_with('#')
            })
            .filter(|line| line.contains("cargo build") && line.contains("--features"))
            .collect();

        assert!(
            !build_lines.is_empty(),
            "install.sh must have at least one non-comment cargo build line with --features"
        );

        for line in &build_lines {
            // Must use onnx feature (load-dynamic)
            assert!(
                line.contains("onnx"),
                "cargo build line must reference the 'onnx' feature (load-dynamic): {line}"
            );
            // Must NOT enable onnx-migraphx as a build feature.
            // Its presence (in any form) indicates building the provider
            // bindings, which is not needed under load-dynamic.
            assert!(
                !line.contains("onnx-migraphx"),
                "cargo build line must NOT contain onnx-migraphx (runtime-only under load-dynamic): {line}"
            );
        }
    }

    /// The build line must use `load-dynamic` compatible features. We verify
    /// that it does not pull in download-binaries or copy-dylibs (which would
    /// re-introduce build-time ORT linking).
    #[test]
    fn build_line_does_not_use_download_binaries_or_copy_dylibs() {
        let sh = install_sh();
        let build_lines: Vec<&str> = sh
            .lines()
            .filter(|line| {
                let trimmed = line.trim_start();
                !trimmed.starts_with('#')
            })
            .filter(|line| line.contains("cargo build") && line.contains("--features"))
            .collect();

        for line in &build_lines {
            assert!(
                !line.contains("download-binaries"),
                "cargo build line must not enable download-binaries: {line}"
            );
            assert!(
                !line.contains("copy-dylibs"),
                "cargo build line must not enable copy-dylibs: {line}"
            );
        }
    }
}

// ============================================================================
// Bundle consumption: runtime libraries and setup-owned model provisioning
// ============================================================================

mod bundle_consumption {
    use super::*;

    /// install.sh must copy bundled ORT runtime libraries from the release
    /// bundle's `lib/` directory to `~/.leindex/lib/`. This is the
    /// zero-setup path: the GitHub Release bundle includes pre-built ORT
    /// `.so` files, and installing them to `~/.leindex/lib/` means the
    /// worker's discovery chain finds ORT without requiring `pip install`
    /// or `leindex setup`.
    ///
    /// We verify install.sh contains a function or logic block that copies
    /// files matching `libonnxruntime` to the LEINDEX_HOME `lib/` subdirectory.
    #[test]
    fn copies_bundled_ort_libs_to_leindex_lib() {
        let sh = install_sh();

        // There must be a function that installs ORT libraries.
        assert!(
            sh.contains("install_ort_libraries"),
            "install.sh must define an install_ort_libraries function"
        );

        // It must target the LEINDEX_HOME lib/ subdirectory. The discovery
        // chain searches ~/.leindex/lib/ (from ort_discovery.rs).
        assert!(
            sh.contains("LEINDEX_HOME") && sh.contains("/lib"),
            "install.sh must reference LEINDEX_HOME and a lib/ subdirectory for ORT install"
        );

        // It must look for libonnxruntime files.
        assert!(
            sh.contains("libonnxruntime"),
            "install.sh must look for libonnxruntime shared library files"
        );
    }

    /// Model files are never part of release bundles. The installer must leave
    /// model provisioning to `leindex setup`.
    #[test]
    fn does_not_copy_models_from_release_bundles() {
        let sh = install_sh();

        assert!(
            !sh.contains("install_model_assets"),
            "install.sh must not define a bundled model installer"
        );
        assert!(
            !sh.contains("qwen3-embed-0.6b"),
            "install.sh must not carry a release-bundled model filename"
        );
    }

    /// install.sh should prefer the pre-built release bundle (fast path,
    /// no Rust toolchain needed) before falling back to building from
    /// source. This is the distribution model described in architecture.md
    /// Section 3: install.sh downloads or builds the runtime bundle.
    #[test]
    fn prefers_release_bundle_download() {
        let sh = install_sh();

        // There must be logic to download/extract the release bundle.
        assert!(
            sh.contains("try_install_from_release_bundle")
                || sh.contains("release bundle")
                || sh.contains("download_bundle"),
            "install.sh should attempt to download the pre-built release bundle"
        );
    }

    #[test]
    fn release_bundle_path_runs_before_rust_gate() {
        let sh = install_sh();
        let bundle_idx = sh
            .find("try_install_from_release_bundle")
            .expect("install.sh must try release bundle install");
        let rust_idx = sh
            .find("Rust is not installed or is too old")
            .expect("install.sh must still validate Rust before source build fallback");
        assert!(
            bundle_idx < rust_idx,
            "pre-built bundle install must be attempted before prompting for Rust"
        );
    }
}

// ============================================================================
// Post-install setup check
// ============================================================================

mod setup_check {
    use super::*;

    /// install.sh must run `leindex setup --check` after install to report
    /// the neural search configuration status. If neural search is not
    /// configured, it should suggest `leindex setup`. This bridges the
    /// installer to the interactive setup wizard introduced in Milestone 2.
    #[test]
    fn runs_setup_check_after_install() {
        let sh = install_sh();

        // Must invoke `leindex setup --check` (or via the binary path).
        assert!(
            sh.contains("setup --check") || sh.contains("setup\", \"--check"),
            "install.sh must run 'leindex setup --check' after install"
        );
    }

    /// After running the check, install.sh must suggest `leindex setup`
    /// when neural search is not configured. This provides actionable
    /// guidance for the user to enable semantic search.
    #[test]
    fn suggests_leindex_setup_when_unset() {
        let sh = install_sh();

        assert!(
            sh.contains("leindex setup"),
            "install.sh should mention 'leindex setup' for enabling neural search"
        );
    }
}

// ============================================================================
// Version parity
// ============================================================================

mod version_parity {
    use super::*;

    /// install.sh SCRIPT_VERSION must match the Cargo.toml version.
    /// Per AGENTS.md, all published surfaces must stay in version lock-step.
    #[test]
    fn script_version_matches_cargo_toml() {
        let cargo_toml_path = repo_root().join("Cargo.toml");
        let cargo_toml = std::fs::read_to_string(&cargo_toml_path)
            .unwrap_or_else(|e| panic!("failed to read {}: {e}", cargo_toml_path.display()));

        let cargo_version = cargo_toml
            .lines()
            .find_map(|line| {
                let trimmed = line.trim();
                trimmed
                    .strip_prefix("version = ")
                    .map(|v| v.trim_matches('"'))
            })
            .expect("Cargo.toml must have a top-level version = \"...\" line");

        let sh = install_sh();
        let script_version = sh
            .lines()
            .find_map(|line| {
                let trimmed = line.trim();
                trimmed
                    .strip_prefix("readonly SCRIPT_VERSION=")
                    .or_else(|| trimmed.strip_prefix("SCRIPT_VERSION="))
                    .map(|v| v.trim_matches('"'))
            })
            .expect("install.sh must define SCRIPT_VERSION=\"...\"");

        assert_eq!(
            cargo_version, script_version,
            "install.sh SCRIPT_VERSION ({script_version}) must match Cargo.toml version ({cargo_version})"
        );

        let ps1 = read_repo_file("install.ps1");
        assert!(
            ps1.contains(&format!(r#"$ScriptVersion = "{cargo_version}""#))
                && ps1.contains(&format!(r#"$ExpectedVersion = "{cargo_version}""#)),
            "install.ps1 ScriptVersion and ExpectedVersion must match Cargo.toml version ({cargo_version})"
        );

        let macos = read_repo_file("install_macos.sh");
        assert!(
            macos.contains(&format!(r#"readonly SCRIPT_VERSION="{cargo_version}""#)),
            "install_macos.sh SCRIPT_VERSION must match Cargo.toml version ({cargo_version})"
        );
    }

    #[test]
    fn install_sh_accepts_all_published_macos_bundles() {
        let sh = install_sh();
        assert!(
            !sh.contains("aarch64 only"),
            "install.sh must not reject macOS x86_64 when release.yml publishes macos-x86_64"
        );
        assert!(
            sh.contains(r#"asset_name="${os_name}-${arch}""#),
            "install.sh should derive the bundle asset name from normalized OS and architecture"
        );
    }
}

// ============================================================================
// Exit handling and model asset failure propagation
// ============================================================================

mod exit_handling {
    use super::*;

    #[test]
    fn setup_check_preserves_exit_status_without_or_true() {
        let script = install_sh();
        assert!(
            !script.contains(r#"setup_output=$("$binary" setup --check 2>&1) || true"#),
            "run_setup_check must not use `|| true` before capturing setup_exit"
        );
        assert!(
            script.contains("set +e") && script.contains("set -e"),
            "run_setup_check should temporarily disable errexit, capture status, then restore errexit"
        );
        assert!(
            script.contains("setup_exit=$?"),
            "run_setup_check must capture setup --check exit status"
        );
    }

    #[test]
    fn verify_installation_does_not_require_repo_cargo_toml() {
        let script = install_sh();
        assert!(
            script.contains(r#"if [[ -f "Cargo.toml" ]]"#)
                && script.contains(r#"expected_version="$SCRIPT_VERSION""#),
            "verify_installation must fall back to SCRIPT_VERSION when install.sh runs outside a repo checkout"
        );
    }

    #[test]
    fn install_paths_do_not_copy_model_assets() {
        let script = install_sh();
        assert!(
            !script.contains("install_model_assets"),
            "source and bundle install paths must leave model provisioning to setup"
        );
        assert!(
            !script.contains("BUNDLE_DIR/models") && !script.contains("qwen3-embed-0.6b"),
            "installer must not expect model files in release bundles"
        );
    }
}

// ============================================================================
// Syntax and daemon packaging invariants
// ============================================================================

/// `install.sh` must be valid bash. An orphaned `else` with an empty body once
/// shipped after the worker-binary removal and made the universal installer
/// unusable, which no text-matching test could notice.
#[test]
fn test_install_sh_is_syntactically_valid_bash() {
    let path = repo_root().join("install.sh");
    let output = match std::process::Command::new("bash")
        .arg("-n")
        .arg(&path)
        .output()
    {
        Ok(output) => output,
        // No bash on this host (e.g. a bare Windows runner): nothing to check.
        Err(_) => return,
    };
    assert!(
        output.status.success(),
        "bash -n install.sh failed:\n{}",
        String::from_utf8_lossy(&output.stderr)
    );
}

/// The single-binary migration retired the separate worker; none of its
/// variables may survive, because `install.sh` runs under `set -u`.
#[test]
fn test_install_sh_has_no_retired_worker_variables() {
    let script = install_sh();
    for retired in ["worker_binary", "worker_install_path", "worker_bin"] {
        assert!(
            !script.contains(retired),
            "install.sh still references retired variable `{retired}`"
        );
    }
}

/// `leindex` starts the daemon from a sibling `leindexd`, so both installers
/// (source build and release bundle) must place it beside the client.
#[test]
fn test_install_sh_installs_the_daemon_for_source_and_bundle_paths() {
    let script = install_sh();
    assert!(
        script.contains("install_daemon_binary \"target/release/${DAEMON_SLUG}\""),
        "source-build path must install leindexd"
    );
    assert!(
        script.contains("install_daemon_binary \"${bundle_dir}/bin/${DAEMON_SLUG}\""),
        "release-bundle path must install leindexd"
    );
}

/// The release archive must carry `leindexd` beside `leindex` on Unix.
#[test]
fn test_release_workflow_bundles_the_daemon() {
    let workflow = read_repo_file(".github/workflows/release.yml");
    assert!(
        workflow.contains("\"$BUNDLE_DIR/bin/leindexd\""),
        "release bundle must copy leindexd into bin/"
    );
    assert!(
        workflow.contains("for bin in leindex leindexd; do"),
        "the binary verification loop must cover leindexd"
    );
}

/// The npm installer copies `leindexd` next to `leindex` and must not touch
/// identifiers it never declared (an undefined `srcMain` once threw a
/// ReferenceError for every bundle install).
#[test]
fn test_npm_installer_installs_the_daemon() {
    let installer = read_repo_file("packages/npm-leindex-mcp/install.js");
    assert!(
        installer.contains("'leindexd'"),
        "npm installer must install leindexd"
    );
    assert!(
        !installer.contains("srcMain"),
        "npm installer references an undeclared `srcMain`"
    );
}

/// The worker is not a separate binary anymore (single `leindex` binary,
/// hidden re-exec worker mode). A worker-name alias equal to the main
/// binary made the cargo fallback copy `bin/leindex` onto itself (a
/// possible 0-byte binary after a "successful" install) and never copied
/// `leindexd`; the dead `hasWorker` reinstall trigger went with it.
#[test]
fn test_npm_installer_has_no_self_copying_worker_alias() {
    let installer = read_repo_file("packages/npm-leindex-mcp/install.js");
    assert!(
        !installer.contains("getWorkerBinaryName"),
        "the retired worker-name alias must not exist"
    );
    assert!(
        !installer.contains("hasWorker"),
        "hasWorker was permanently true (worker === main binary) and is dead code"
    );
    assert!(
        installer.contains("cargoDaemon"),
        "the cargo fallback must link the daemon (leindexd) into the package"
    );
}

/// The macOS source install must place `leindexd` beside `leindex` and must
/// not reference the retired `leindex-embed` binary (its build produces
/// none, so every install printed a false "neural search unavailable"
/// warning and never installed the daemon). Prose explaining the retirement
/// is intentionally allowed (same convention as install.sh's guard).
#[test]
fn test_install_macos_installs_the_daemon_not_the_retired_worker() {
    let script = read_repo_file("install_macos.sh");
    assert!(
        script.contains("target/release/leindexd"),
        "macOS source install must install leindexd"
    );
    assert!(
        !script.contains("release/leindex-embed"),
        "the retired leindex-embed binary must not be installed"
    );
    assert!(
        !script.contains("worker_binary"),
        "no worker-binary install block may remain"
    );
}