deps-cli 1.1.0

CLI for running deps-lsp's dependency-health checks in CI, pre-commit, and shell workflows
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
//! Integration tests for the `check` pipeline: `walk` → `report::check_manifest` →
//! `format`/`exit`, run against real fixture manifests and real ecosystem registrations
//! (via `deps_engine::setup::register_ecosystems`) — never a fake `Ecosystem`, since none of
//! the ecosystem crates expose a public way to inject a mock registry from outside their own
//! crate (their `with_base_for_test`-style constructors are `#[cfg(test)] pub(crate)`).
//!
//! Every scenario here runs `--offline` (FR-013): [`deps_core::HttpCache::set_offline`]
//! intercepts before any real HTTP call reaches the network, so these tests are fast and
//! deterministic without needing a mock registry server. This also doubles as the SC-004
//! network-isolation test — see `test_offline_run_completes_without_network_access`, and its
//! doc comment for exactly what is (and is not) proved without a request-counting test
//! double (spec 062 review S6).
//!
//! // TODO(critic): FR-005 automated parity test vs deps-lsp handlers::diagnostics (T025)
//!
//! A live cross-tool parity check against `deps-lsp`'s own diagnostics path (FR-005/SC-001)
//! is deferred to manual verification (`.claude/rules/continuous-improvement.md`) and a
//! follow-up automated test that drives a real `deps-lsp` `ServerState` — see this PR's
//! handoff notes for why: `deps-lsp`'s `generate_diagnostics_internal` is `pub(crate)` and
//! `handle_diagnostics` needs a live `tower_lsp_server::Client`, both nontrivial to construct
//! from an external crate's test suite. **Structural non-drift is not, in fact, guaranteed
//! "today" without qualification** (correction, spec 062 review C2): both adapters call the
//! same `deps_engine::classify::*` functions and the identical `Ecosystem::generate_diagnostics`,
//! but C2 proved the *inputs* fed into that shared call can still diverge per adapter (this
//! crate was dropping `fetch_result.licenses` entirely before that fix landed) — sharing the
//! classification function does not, by itself, prove every adapter assembles its inputs
//! identically. The live parity test above is what would actually close that gap; until it
//! exists, non-drift rests on manual review of each `VersionData` assembly site matching.

// `allow-expect-in-tests` only recognizes `#[test]` bodies, not the plain helper functions
// (`offline_context`/`run_pipeline`) every test here calls.
#![allow(clippy::expect_used)]

use deps_cli::exit::{EXIT_CLEAN, exit_code};
use deps_cli::report::{CheckContext, CheckReport, FailOnPolicy, check_manifest};
use deps_cli::{format, walk};
use deps_core::osv::OsvClient;
use deps_core::policy_config::PolicyConfig;
use deps_core::{EcosystemRegistry, HttpCache};
use deps_engine::setup::{EcosystemRuntime, register_ecosystems};
use std::sync::Arc;
use std::time::Duration;

/// Builds a real, fully-registered [`EcosystemRegistry`] plus an offline [`CheckContext`] —
/// the same construction `main.rs` performs, minus config-file loading.
fn offline_context() -> (EcosystemRegistry, CheckContext) {
    let policy = PolicyConfig::default();
    let runtime = EcosystemRuntime::from_policy(&policy);
    let cache = Arc::new(HttpCache::with_policy(Arc::clone(&runtime.policy)));
    cache.set_offline(true);
    assert!(
        cache.is_offline(),
        "test setup bug: HttpCache must actually be offline before this helper is trusted"
    );
    let registry = EcosystemRegistry::new();
    let _ = register_ecosystems(&registry, Arc::clone(&cache), &runtime);

    let mut policy = policy;
    policy.network.offline = true;

    let ctx = CheckContext {
        cache: Arc::clone(&cache),
        osv: Arc::new(OsvClient::new(Arc::clone(&cache))),
        lockfile_cache: Arc::new(deps_core::lockfile::LockFileCache::new()),
        policy,
    };
    (registry, ctx)
}

/// Runs the full `walk` → `check_manifest` pipeline against `dir` and returns the assembled
/// report plus whether any manifest's registry fetch was reported unreachable.
async fn run_pipeline(dir: &std::path::Path) -> (CheckReport, bool) {
    let (registry, ctx) = offline_context();
    let outcome = walk::walk(&[dir.to_path_buf()], &registry, false, false);
    assert!(!outcome.truncated);

    let mut findings = Vec::new();
    let mut had_execution_error = false;
    for manifest in outcome.manifests {
        let content = deps_core::fs_probe::read_to_string_capped(&manifest.path, 10_000_000)
            .expect("read fixture manifest")
            .expect("fixture manifest under size cap");
        // Review finding M2: mirrors main.rs's fix — `uri_path` (not `path`) drives lockfile
        // lookup, so this harness exercises the same correct wiring `check` itself uses.
        let result = check_manifest(
            &manifest.ecosystem,
            &manifest.uri_path,
            &manifest.display_path,
            &content,
            &ctx,
        )
        .await
        .expect("check_manifest must not fail for a well-formed fixture");
        had_execution_error |= result.registry_unreachable;
        findings.extend(result.findings);
    }
    (CheckReport { findings }, had_execution_error)
}

/// SC-004's network-isolation guarantee, composed from two facts rather than one black-box
/// timing heuristic (spec 062 review S6):
///
/// 1. `offline_context`'s own assertion proves *this crate's* wiring actually calls
///    `HttpCache::set_offline(true)` — the exact class of bug this PR's own history hit once
///    (the original implementation gated only the OSV scan and forgot the registry fetch
///    path entirely).
/// 2. `HttpCache::ensure_online` — the shared gate every one of `deps-core`'s 4 send sites
///    checks before opening a socket — is deps-core's own, separately audited invariant (spec
///    062 security review F1's audit verified this empirically with a canary token), not
///    something this crate's test suite re-proves from scratch.
///
/// Together these give a real, non-heuristic proof, but neither one is a request counter: no
/// `Registry` implementation is injectable from outside its own ecosystem crate (see this
/// file's module doc), so there is no way to assert "zero calls reached a `Registry` method"
/// from here — only "zero calls reached the network", which is the property SC-004 actually
/// cares about. The bounded timeout below is a belt-and-suspenders regression signal on top of
/// those two proofs, not the proof itself: a real (accidental) network attempt against an
/// unreachable/slow host would hang well past it.
#[tokio::test]
async fn test_offline_run_completes_without_network_access() {
    let dir = tempfile::tempdir().expect("create temp dir");
    std::fs::write(
        dir.path().join("Cargo.toml"),
        "[package]\nname = \"fixture\"\nversion = \"0.1.0\"\n\n[dependencies]\nserde = \"1.0\"\n",
    )
    .expect("write fixture manifest");

    let (report, _had_error) =
        tokio::time::timeout(Duration::from_secs(10), run_pipeline(dir.path()))
            .await
            .expect("offline run must not block on network I/O");

    // `serde` has no lock-file-resolved version and offline mode never fetches, so it
    // surfaces as an unresolved-lookup finding rather than a crash or a silently empty report.
    assert!(
        !report.findings.is_empty(),
        "offline mode must still report on what it can determine"
    );
}

#[tokio::test]
async fn test_empty_directory_produces_no_findings_and_clean_exit() {
    let dir = tempfile::tempdir().expect("create temp dir");
    let (report, had_error) = run_pipeline(dir.path()).await;
    assert!(report.findings.is_empty());
    assert!(!had_error);
    assert_eq!(
        exit_code(&report, &FailOnPolicy::default_categories(), had_error),
        EXIT_CLEAN
    );
}

#[tokio::test]
async fn test_multi_ecosystem_fixture_tree_discovers_both_manifests() {
    let dir = tempfile::tempdir().expect("create temp dir");
    std::fs::write(
        dir.path().join("Cargo.toml"),
        "[package]\nname = \"fixture\"\n\n[dependencies]\nserde = \"1.0\"\n",
    )
    .expect("write cargo manifest");
    std::fs::write(
        dir.path().join("package.json"),
        r#"{"name":"fixture","dependencies":{"left-pad":"1.0.0"}}"#,
    )
    .expect("write npm manifest");

    let (report, _had_error) = run_pipeline(dir.path()).await;
    let ecosystems: std::collections::HashSet<_> =
        report.findings.iter().map(|f| f.ecosystem).collect();
    assert!(ecosystems.contains(&deps_core::EcosystemId::Cargo));
    assert!(ecosystems.contains(&deps_core::EcosystemId::Npm));
}

#[tokio::test]
async fn test_table_and_json_formatters_render_the_same_pipeline_output() {
    let dir = tempfile::tempdir().expect("create temp dir");
    std::fs::write(
        dir.path().join("Cargo.toml"),
        "[package]\nname = \"fixture\"\n\n[dependencies]\nserde = \"1.0\"\n",
    )
    .expect("write fixture manifest");

    let (report, _had_error) = run_pipeline(dir.path()).await;

    let table = format::table::render(&report);
    assert!(table.contains("Cargo.toml"));

    let json = format::json::render(&report).expect("json render must succeed");
    let document: format::json::ReportDocument =
        serde_json::from_str(&json).expect("json must round-trip");
    assert_eq!(document.schema_version, format::json::SCHEMA_VERSION);
    assert_eq!(document.findings.len(), report.findings.len());
}

#[tokio::test]
async fn test_sarif_formatter_renders_the_same_pipeline_output() {
    // Directory name needs percent-encoding (`#` reads as a URI fragment separator, space is
    // invalid in a bare URI-reference) — the repro this test guards against (spec 062 review S2/B3).
    let dir = tempfile::tempdir().expect("create temp dir");
    let manifest_dir = dir.path().join("weird dir#name");
    std::fs::create_dir(&manifest_dir).expect("create nested fixture dir");
    std::fs::write(
        manifest_dir.join("Cargo.toml"),
        "[package]\nname = \"fixture\"\n\n[dependencies]\nserde = \"1.0\"\n",
    )
    .expect("write fixture manifest");

    let (report, _had_error) = run_pipeline(dir.path()).await;
    assert!(
        !report.findings.is_empty(),
        "serde 1.0 must produce at least one finding"
    );

    let sarif = format::sarif::to_sarif(&report);
    let results = sarif.runs[0]
        .results
        .as_ref()
        .expect("results must be present");
    assert_eq!(results.len(), report.findings.len());

    for result in results {
        let uri = result
            .locations
            .as_ref()
            .expect("locations must be present")[0]
            .physical_location
            .as_ref()
            .expect("physicalLocation must be present")
            .artifact_location
            .as_ref()
            .expect("artifactLocation must be present")
            .uri
            .as_ref()
            .expect("uri must be present");
        assert!(
            !uri.contains('#'),
            "a literal '#' in {uri:?} would be read as a URI fragment separator"
        );
        assert!(
            !uri.contains(' '),
            "a literal space in {uri:?} is not a valid URI-reference"
        );
        assert!(
            !uri.contains('\\'),
            "{uri:?} must use '/' separators, not the platform's own (possibly '\\\\') display form"
        );
        assert!(
            !std::path::Path::new(uri).is_absolute(),
            "{uri:?} must stay relative to the walked root, matching table/json's own display_path"
        );
    }

    let json = format::sarif::render(&report).expect("sarif render must succeed");
    let parsed: serde_json::Value = serde_json::from_str(&json).expect("sarif must round-trip");
    assert_eq!(parsed["version"], "2.1.0");
}

#[tokio::test]
async fn test_walk_paths_default_to_current_directory_semantics_via_single_file() {
    // Exercises the "explicit manifest path, not a directory" branch of `walk::walk` end to
    // end (FR-002's routing applies identically either way).
    let dir = tempfile::tempdir().expect("create temp dir");
    let manifest = dir.path().join("Cargo.toml");
    std::fs::write(&manifest, "[package]\nname = \"fixture\"\n").expect("write fixture manifest");

    let (registry, ctx) = offline_context();
    let outcome = walk::walk(std::slice::from_ref(&manifest), &registry, false, false);
    assert_eq!(outcome.manifests.len(), 1);

    let content = deps_core::fs_probe::read_to_string_capped(&manifest, 10_000_000)
        .expect("read manifest")
        .expect("under size cap");
    let result = check_manifest(
        &outcome.manifests[0].ecosystem,
        &manifest,
        &manifest,
        &content,
        &ctx,
    )
    .await
    .expect("check_manifest must succeed");
    // `Cargo.toml` with no `[dependencies]` produces no findings at all.
    assert!(result.findings.is_empty());
}

#[tokio::test]
async fn test_sarif_formatter_relativizes_an_absolute_single_file_path() {
    // An explicit file path routes through `walk::walk`'s `root.is_file()` branch, which passes
    // the absolute path through as `display_path` unchanged (spec 062 review R1: `manifest_uri`
    // must not leak it into `artifactLocation.uri` as-is).
    let dir = tempfile::tempdir().expect("create temp dir");
    let manifest = dir.path().join("Cargo.toml");
    assert!(
        manifest.is_absolute(),
        "test setup bug: fixture path must be absolute"
    );
    std::fs::write(
        &manifest,
        "[package]\nname = \"fixture\"\n\n[dependencies]\nserde = \"1.0\"\n",
    )
    .expect("write fixture manifest");

    let (registry, ctx) = offline_context();
    let outcome = walk::walk(std::slice::from_ref(&manifest), &registry, false, false);
    assert_eq!(outcome.manifests.len(), 1);
    assert!(
        outcome.manifests[0].display_path.is_absolute(),
        "test setup bug: this must exercise the absolute-display_path branch"
    );

    let content = deps_core::fs_probe::read_to_string_capped(&manifest, 10_000_000)
        .expect("read manifest")
        .expect("under size cap");
    let result = check_manifest(
        &outcome.manifests[0].ecosystem,
        &manifest,
        &manifest,
        &content,
        &ctx,
    )
    .await
    .expect("check_manifest must succeed");
    assert!(
        !result.findings.is_empty(),
        "serde 1.0 must produce at least one finding"
    );

    let report = CheckReport {
        findings: result.findings,
    };
    let sarif = format::sarif::to_sarif(&report);
    let results = sarif.runs[0]
        .results
        .as_ref()
        .expect("results must be present");
    for result in results {
        let uri = result
            .locations
            .as_ref()
            .expect("locations must be present")[0]
            .physical_location
            .as_ref()
            .expect("physicalLocation must be present")
            .artifact_location
            .as_ref()
            .expect("artifactLocation must be present")
            .uri
            .as_ref()
            .expect("uri must be present");
        assert!(
            !std::path::Path::new(uri).is_absolute(),
            "{uri:?} must not stay absolute — it leaks local machine path structure into a \
             document meant to be uploaded to GitHub code scanning"
        );
    }
}

/// Reviewer follow-up #5: exercises the *real* wiring in `main.rs` (warnings →
/// `had_execution_error` → `exit_code`) via the actual built binary, not `run_pipeline`'s
/// simplified reimplementation above — `run_pipeline` hardcodes `walk::walk(..., false)` and
/// never touches `main.rs::run_check`'s own warning/exit-code logic, so a regression there
/// could pass every other test in this file while silently reopening a fail-open gap.
#[test]
fn test_respect_gitignore_flag_reaches_the_real_exit_code_wiring() {
    let dir = tempfile::tempdir().expect("create temp dir");
    std::fs::create_dir(dir.path().join(".git")).expect("create .git marker");
    std::fs::write(dir.path().join(".gitignore"), "Cargo.toml\n").expect("write gitignore");
    std::fs::write(
        dir.path().join("Cargo.toml"),
        "[package]\nname = \"fixture\"\nversion = \"0.1.0\"\n",
    )
    .expect("write fixture manifest");

    let exe = env!("CARGO_BIN_EXE_deps-cli");
    let output = std::process::Command::new(exe)
        .args(["check", "--offline", "--respect-gitignore"])
        .arg(dir.path())
        .output()
        .expect("run the real deps-cli binary");

    let stderr = String::from_utf8_lossy(&output.stderr);
    assert_eq!(
        output.status.code(),
        Some(2),
        "a manifest excluded by --respect-gitignore must exit 2 (execution error), not 0 — \
         stderr: {stderr}"
    );
    assert!(
        stderr.contains("excluded from the scan"),
        "must warn about the excluded manifest, stderr: {stderr}"
    );
}

/// Companion to the above: the same fixture under the default (`respect_gitignore: false`)
/// mode must find the manifest and exit clean through the real binary too.
#[test]
fn test_default_mode_ignores_gitignore_through_the_real_binary_and_exits_clean() {
    let dir = tempfile::tempdir().expect("create temp dir");
    std::fs::create_dir(dir.path().join(".git")).expect("create .git marker");
    std::fs::write(dir.path().join(".gitignore"), "Cargo.toml\n").expect("write gitignore");
    std::fs::write(
        dir.path().join("Cargo.toml"),
        "[package]\nname = \"fixture\"\nversion = \"0.1.0\"\n",
    )
    .expect("write fixture manifest");

    let exe = env!("CARGO_BIN_EXE_deps-cli");
    let output = std::process::Command::new(exe)
        .args(["check", "--offline"])
        .arg(dir.path())
        .output()
        .expect("run the real deps-cli binary");

    assert_eq!(
        output.status.code(),
        Some(0),
        "stderr: {}",
        String::from_utf8_lossy(&output.stderr)
    );
}

/// Review finding M2: a manifest symlinked from directory A (which has its own `Cargo.lock`)
/// to a target manifest in directory B (which has none) must find A's lockfile during
/// `check_manifest`'s lockfile/in-use-version discovery, not B's absence of one — matching
/// US-002's own shared-manifest-symlinked-into-multiple-package-directories scenario, where
/// each symlinked location has its own adjacent lockfile.
///
/// Exercises the exact mechanism `check_manifest` -> `load_resolved_versions` relies on
/// (`Ecosystem::lockfile_provider().locate_lockfile`) directly against the URIs
/// [`DiscoveredManifest::uri_path`]/[`DiscoveredManifest::path`] actually produce, rather than
/// observing an indirect effect through findings (which would need a populated/mocked registry
/// to show a version difference).
#[cfg(unix)]
#[tokio::test]
async fn test_follow_symlinks_lockfile_lookup_uses_symlinks_directory_not_targets() {
    // A and B are both subdirectories of one walked root — the symlink's target must stay
    // inside the walked root (FR-004) for `--follow-symlinks` to resolve and route it at all;
    // A/B being two independent, unrelated temp directories would make the target a root
    // escape instead, an unrelated scenario this test isn't exercising.
    let root = tempfile::tempdir().expect("create walked root");
    let dir_a = root.path().join("a");
    let dir_b = root.path().join("b");
    std::fs::create_dir(&dir_a).expect("mkdir a");
    std::fs::create_dir(&dir_b).expect("mkdir b");

    std::fs::write(
        dir_b.join("manifest-data"),
        "[package]\nname = \"x\"\nversion = \"0.1.0\"\n\n[dependencies]\nonce_cell = \"1\"\n",
    )
    .expect("write target manifest in B");
    std::fs::write(
        dir_a.join("Cargo.lock"),
        "version = 4\n\n[[package]]\nname = \"once_cell\"\nversion = \"1.0.0\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\n",
    )
    .expect("write A's Cargo.lock");
    std::os::unix::fs::symlink(dir_b.join("manifest-data"), dir_a.join("Cargo.toml"))
        .expect("symlink A/Cargo.toml -> B/Cargo.toml");

    let (registry, ctx) = offline_context();
    let outcome = walk::walk(&[root.path().to_path_buf()], &registry, false, true);
    assert_eq!(
        outcome.manifests.len(),
        1,
        "ignored_manifests: {:?}, walk_errors: {:?}",
        outcome.ignored_manifests,
        outcome.walk_errors
    );
    let manifest = &outcome.manifests[0];

    // Sanity check on the fields M2's fix relies on: `path` (content read, canonicalized by
    // the C1/S3 containment check) resolves to B's real file; `uri_path` (lockfile lookup)
    // stays A's encountered symlink path, not canonicalized — matching `route_path`'s
    // semantics (see `route_file`'s doc).
    assert_eq!(
        manifest.path.canonicalize().expect("canonicalize path"),
        dir_b
            .join("manifest-data")
            .canonicalize()
            .expect("canonicalize B/Cargo.toml")
    );
    assert_eq!(manifest.uri_path, dir_a.join("Cargo.toml"));

    let lockfile_provider = manifest
        .ecosystem
        .lockfile_provider()
        .expect("cargo ecosystem must have a lock file provider");

    let uri_path_uri = url::Url::from_file_path(&manifest.uri_path).expect("uri_path to file uri");
    let path_uri = url::Url::from_file_path(&manifest.path).expect("path to file uri");

    assert_eq!(
        lockfile_provider.locate_lockfile(&uri_path_uri),
        Some(dir_a.join("Cargo.lock")),
        "lockfile lookup driven by uri_path must find A's Cargo.lock"
    );
    assert_eq!(
        lockfile_provider.locate_lockfile(&path_uri),
        None,
        "lockfile lookup driven by the resolved target path (B) must find nothing — B has no \
         Cargo.lock; if this were Some, check_manifest would silently anchor lockfile lookup \
         at the wrong directory"
    );

    // End-to-end: exercise the exact call `main.rs` makes (`manifest.uri_path`, per M2's fix)
    // and confirm A's lockfile actually gets parsed into the cache — the load-bearing
    // assertion that would catch a regression reverting `main.rs`'s argument choice, not just
    // the underlying `locate_lockfile` mechanism checked above.
    let content = std::fs::read_to_string(&manifest.path).expect("read manifest content");
    check_manifest(
        &manifest.ecosystem,
        &manifest.uri_path,
        &manifest.display_path,
        &content,
        &ctx,
    )
    .await
    .expect("check_manifest must succeed with the correct (uri_path) wiring");
    assert_eq!(
        ctx.lockfile_cache.len(),
        1,
        "check_manifest called with uri_path must have found and cached A's Cargo.lock"
    );

    // Simulates the M2 bug (passing the resolved target path instead of uri_path) against a
    // fresh cache — must find and cache nothing, since B has no Cargo.lock.
    let buggy_ctx = CheckContext {
        cache: Arc::clone(&ctx.cache),
        osv: Arc::clone(&ctx.osv),
        lockfile_cache: Arc::new(deps_core::lockfile::LockFileCache::new()),
        policy: ctx.policy.clone(),
    };
    check_manifest(
        &manifest.ecosystem,
        &manifest.path,
        &manifest.display_path,
        &content,
        &buggy_ctx,
    )
    .await
    .expect("check_manifest must still succeed (absence of a lock file is not an error)");
    assert_eq!(
        buggy_ctx.lockfile_cache.len(),
        0,
        "check_manifest called with the resolved target path (the bug M2 fixes) must find no \
         lockfile at all, proving the two wirings genuinely diverge"
    );
}