On Windows a canonicalized project root and an env-derived one are two identity keys for one directory, and the disclosure names a project the caller cannot match #159

Closed
opened 2026-09-05 17:48:57 +02:00 by buildagent · 3 comments
Member

Found on Windows 11 / MSVC by a session running the full workspace suite at 08a6eed. Two failures, one cause. Invisible on Linux forever, because the two spellings never diverge there.

Measured

env TMP / TEMP                              E:\Temp
[System.IO.Path]::GetTempPath()             E:\Temp\
Get-Item 'E:\Temp'  .Name                   Temp
E:\ directory entry as the PARENT stores it TEMP     <-- the true on-disk case

std::env::temp_dir() yields E:\Temp from the environment variable. canonicalize() yields E:\TEMP from the filesystem, via GetFinalPathNameByHandle, which returns the case the directory entry actually holds. Windows paths are case-insensitive but case-preserving, so these are the same directory.

The code compares a canonicalized path against a non-canonicalized one and uses both as identity keys:

the_narrowest_basis_is_carried_not_the_first_by_hash        activation_offer.rs:1416
  the disclosure must name the project whose answer was actually carried
    left:  "E:\\TEMP\\.tmpAMWyUB\\alpha"
   right:  "E:\\Temp\\.tmpAMWyUB\\alpha"

two_other_projects_never_merge_and_the_basis_is_named       activation_offer.rs:775
  basis_project_root E:\TEMP\.tmprFdH16\beta names neither other project
    ({"E:\\Temp\\.tmprFdH16\\alpha": "same_file_candidate",
      "E:\\Temp\\.tmprFdH16\\beta":  "member_candidate"})

Why this is the class we care most about

The disclosure names a root the caller cannot match to anything it holds. basis_project_root exists to say which project's answer was carried; here it names a key absent from the very map it is describing. A reader following it finds nothing and cannot tell whether the basis was wrong, the map was wrong, or the field means something else.

That is worse than a wrong count. It is a pointer into a namespace the reader does not share.

Scope, stated carefully

It fires only where the environment variable's case differs from the on-disk case. That is ordinary — the reporting box is a stock setup, directory created TEMP, variable written Temp — but not universal. Two eliminations already done:

  • grep finds no to_uppercase in crates/indexer/src, so the divergence comes from canonicalization, not a deliberate fold.
  • It is not what reddened our own Windows CI: that job was green at 7b3fc7c and its single failure at 08a6eed was an unrelated TOML-forge bug, now fixed.

So: a genuine Windows defect that one box happened to expose, not a CI blocker.

The repair is a policy decision, not a patch

Decide whether a project root is stored canonicalized or as given, and apply that at ONE boundary. Comparing the two forms is the bug; either form alone is fine.

Care needed — this repo has a scar here. A key derived from canonicalize().unwrap_or(raw) moves when the disk moves, which broke the approval store once already and cost a legacy_project_key migration. #151 chose the other direction deliberately: a random workspace-id sidecar inside the tree, never re-derived from a path. Whatever is chosen here should be consistent with that, and the two should not disagree about what identifies a project.

  • #151 (project identity; chose a non-path-derived key for exactly this family of reasons)
  • #137 (linked-project package-set identity)
  • The recorded Windows shape "separator/case in ignore matching" — this is the same hazard in project-root identity rather than in matching

🤖 Generated with Claude Code

https://claude.ai/code/session_01K1zj5VcFJvJt3pQxe9259K

Found on Windows 11 / MSVC by a session running the full workspace suite at `08a6eed`. Two failures, one cause. **Invisible on Linux forever**, because the two spellings never diverge there. ## Measured ``` env TMP / TEMP E:\Temp [System.IO.Path]::GetTempPath() E:\Temp\ Get-Item 'E:\Temp' .Name Temp E:\ directory entry as the PARENT stores it TEMP <-- the true on-disk case ``` `std::env::temp_dir()` yields `E:\Temp` from the environment variable. `canonicalize()` yields `E:\TEMP` from the filesystem, via `GetFinalPathNameByHandle`, which returns the case the directory entry actually holds. Windows paths are case-**insensitive** but case-**preserving**, so these are the same directory. The code compares a canonicalized path against a non-canonicalized one **and uses both as identity keys**: ``` the_narrowest_basis_is_carried_not_the_first_by_hash activation_offer.rs:1416 the disclosure must name the project whose answer was actually carried left: "E:\\TEMP\\.tmpAMWyUB\\alpha" right: "E:\\Temp\\.tmpAMWyUB\\alpha" two_other_projects_never_merge_and_the_basis_is_named activation_offer.rs:775 basis_project_root E:\TEMP\.tmprFdH16\beta names neither other project ({"E:\\Temp\\.tmprFdH16\\alpha": "same_file_candidate", "E:\\Temp\\.tmprFdH16\\beta": "member_candidate"}) ``` ## Why this is the class we care most about **The disclosure names a root the caller cannot match to anything it holds.** `basis_project_root` exists to say *which project's answer was carried*; here it names a key absent from the very map it is describing. A reader following it finds nothing and cannot tell whether the basis was wrong, the map was wrong, or the field means something else. That is worse than a wrong count. It is a pointer into a namespace the reader does not share. ## Scope, stated carefully It fires only where the environment variable's case differs from the on-disk case. That is ordinary — the reporting box is a stock setup, directory created `TEMP`, variable written `Temp` — but **not universal**. Two eliminations already done: - `grep` finds no `to_uppercase` in `crates/indexer/src`, so the divergence comes from canonicalization, not a deliberate fold. - It is **not** what reddened our own Windows CI: that job was green at `7b3fc7c` and its single failure at `08a6eed` was an unrelated TOML-forge bug, now fixed. So: a genuine Windows defect that one box happened to expose, not a CI blocker. ## The repair is a policy decision, not a patch Decide whether a project root is stored **canonicalized** or **as given**, and apply that at ONE boundary. *Comparing the two forms is the bug; either form alone is fine.* **Care needed** — this repo has a scar here. A key derived from `canonicalize().unwrap_or(raw)` **moves when the disk moves**, which broke the approval store once already and cost a `legacy_project_key` migration. #151 chose the other direction deliberately: a random `workspace-id` sidecar inside the tree, never re-derived from a path. Whatever is chosen here should be consistent with that, and the two should not disagree about what identifies a project. ## Related - #151 (project identity; chose a non-path-derived key for exactly this family of reasons) - #137 (linked-project package-set identity) - The recorded Windows shape "separator/case in ignore matching" — this is the same hazard in **project-root identity** rather than in matching 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01K1zj5VcFJvJt3pQxe9259K
Author
Member

Triage 2026-09-06 at f6a878a: CLOSING — and, unusually for a Windows-only defect, there is real Windows evidence, not just a Linux green.

Verified against master, not against a lane report.

The repair, exactly as this issue specified it

The issue asked for a decision, not a patch: "Decide whether a project root is stored canonicalized or as given, and apply that at ONE boundary. Comparing the two forms is the bug; either form alone is fine."

That is what landed, in 8e6bcac: approval::recorded_project_root, crates/indexer/src/approval.rs:922, whose doc-comment opens "THE ONE SPELLING A PROJECT ROOT IS STORED, COMPARED AND DISCLOSED IN — #159, named so that nobody has to re-derive it."

Three things in that doc are worth quoting back, because they are the parts that make this a close rather than a patch:

  1. The chosen answer is canonicalize() + approval::strip_verbatim, in that order. The verbatim strip is not incidental — on Windows canonicalize() returns \\?\E:\proj, so normalising for case and stopping there just moves the mismatch onto a second axis.
  2. It does not disagree with #151. The doc states the distinction explicitly: #151 chose a random workspace-id sidecar as a project's identity, deliberately never re-derived from a path; this function is about the spelling a path is stored, compared and shown in. Different questions — the first must be path-independent, the second must have exactly one answer. That is the consistency this issue asked for.
  3. project_key is deliberately NOT routed through it, and the reason is written at approval::key_input: its input is what every approval record already on an operator's disk is filed under, and changing it would rename their grants out of existence. So this normalises what is stored and compared, never what is hashed — which is precisely the canonicalize().unwrap_or(raw) scar this issue warned about, avoided rather than repeated.

find_callers gives one production call site — approval.rs:2433, inside ApprovalRecord::empty, which used to restate the logic inline — plus four test sites.

The two failing tests

Both named in this issue still exist and are now routed through the helper:

  • two_other_projects_never_merge_and_the_basis_is_named — crates/indexer/tests/activation_offer.rs:780
  • the_narrowest_basis_is_carried_not_the_first_by_hash — crates/indexer/tests/activation_offer.rs:1414

with approval::recorded_project_root(tmp.path()) at :183, :1500 and :1602, and crates/mcp-server/tests/activation_offer_e2e.rs:213.

Linux run (expected to be trivially green here — recorded for completeness):

$ export CARGO_INCREMENTAL=0
$ cargo test -p code-index-indexer --test activation_offer \
    the_narrowest_basis_is_carried_not_the_first_by_hash
test the_narrowest_basis_is_carried_not_the_first_by_hash ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 25 filtered out; finished in 0.06s
EXIT=0

Why I am not treating that Linux green as the proof

This issue is explicit that the defect is "invisible on Linux forever", so a Linux pass grades nothing. And this repo has a recorded scar exactly here — a #[cfg(not(windows))] identity body leaves the Windows rule ungraded. recorded_project_root carries no cfg gate: it canonicalizes on every platform, so the Windows arm is the same code the Linux arm runs, and it is exercised by the same tests.

The real evidence is a green native-Windows CI job on a commit that contains the fix. From /api/v1/repos/h-dv/code-index/actions/tasks:

run sha job status
603 095eba6 fmt + clippy + build + test (windows) success

and git merge-base --is-ancestor 8e6bcac 095eba6 → true, as is 8e6bcac..f6a878a. That job runs cargo test --workspace --no-fail-fast (.forgejo/workflows/ci-windows.yml:268), which includes crates/indexer/tests/activation_offer.rs. So both tests named in this issue have actually executed on Windows, with the fix, and passed.

Residual, stated plainly

The native-Windows job is red on master right now (run 608 at f6a878a, 15 s) — but it dies in the Free disk pre-flight step (ci-windows.yml:117) before compiling anything, which is #179 and is owned elsewhere. It is not a regression of this fix, and run 603 is the closest green ancestor that contains it. If #179's pre-flight turns out to have been masking a later failure in that job, this should be reopened — but nothing currently suggests that, since 603 ran the full suite.

🤖 Triage lane, 2026-09-06, master f6a878a

## Triage 2026-09-06 at `f6a878a`: CLOSING — and, unusually for a Windows-only defect, **there is real Windows evidence, not just a Linux green.** Verified against master, not against a lane report. ### The repair, exactly as this issue specified it The issue asked for a decision, not a patch: *"Decide whether a project root is stored **canonicalized** or **as given**, and apply that at ONE boundary. Comparing the two forms is the bug; either form alone is fine."* That is what landed, in `8e6bcac`: **`approval::recorded_project_root`**, `crates/indexer/src/approval.rs:922`, whose doc-comment opens *"THE ONE SPELLING A PROJECT ROOT IS STORED, COMPARED AND DISCLOSED IN — #159, named so that nobody has to re-derive it."* Three things in that doc are worth quoting back, because they are the parts that make this a close rather than a patch: 1. **The chosen answer is `canonicalize()` + `approval::strip_verbatim`, in that order.** The verbatim strip is not incidental — on Windows `canonicalize()` returns `\\?\E:\proj`, so normalising for case and stopping there just moves the mismatch onto a second axis. 2. **It does not disagree with #151.** The doc states the distinction explicitly: #151 chose a random `workspace-id` sidecar as a project's *identity*, deliberately never re-derived from a path; this function is about the *spelling* a path is stored, compared and shown in. Different questions — the first must be path-independent, the second must have exactly one answer. That is the consistency this issue asked for. 3. **`project_key` is deliberately NOT routed through it**, and the reason is written at `approval::key_input`: its input is what every approval record already on an operator's disk is filed under, and changing it would rename their grants out of existence. So this normalises what is **stored and compared**, never what is **hashed** — which is precisely the `canonicalize().unwrap_or(raw)` scar this issue warned about, avoided rather than repeated. `find_callers` gives one production call site — `approval.rs:2433`, inside `ApprovalRecord::empty`, which used to restate the logic inline — plus four test sites. ### The two failing tests Both named in this issue still exist and are now routed through the helper: - `two_other_projects_never_merge_and_the_basis_is_named` — `crates/indexer/tests/activation_offer.rs:780` - `the_narrowest_basis_is_carried_not_the_first_by_hash` — `crates/indexer/tests/activation_offer.rs:1414` with `approval::recorded_project_root(tmp.path())` at `:183`, `:1500` and `:1602`, and `crates/mcp-server/tests/activation_offer_e2e.rs:213`. Linux run (expected to be trivially green here — recorded for completeness): ``` $ export CARGO_INCREMENTAL=0 $ cargo test -p code-index-indexer --test activation_offer \ the_narrowest_basis_is_carried_not_the_first_by_hash test the_narrowest_basis_is_carried_not_the_first_by_hash ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 25 filtered out; finished in 0.06s EXIT=0 ``` ### Why I am not treating that Linux green as the proof This issue is explicit that the defect is **"invisible on Linux forever"**, so a Linux pass grades nothing. And this repo has a recorded scar exactly here — *a `#[cfg(not(windows))]` identity body leaves the Windows rule ungraded*. `recorded_project_root` carries **no cfg gate**: it canonicalizes on every platform, so the Windows arm is the same code the Linux arm runs, and it is exercised by the same tests. **The real evidence is a green native-Windows CI job on a commit that contains the fix.** From `/api/v1/repos/h-dv/code-index/actions/tasks`: | run | sha | job | status | |---|---|---|---| | 603 | `095eba6` | `fmt + clippy + build + test (windows)` | **success** | and `git merge-base --is-ancestor 8e6bcac 095eba6` → true, as is `8e6bcac`..`f6a878a`. That job runs `cargo test --workspace --no-fail-fast` (`.forgejo/workflows/ci-windows.yml:268`), which includes `crates/indexer/tests/activation_offer.rs`. So both tests named in this issue have **actually executed on Windows, with the fix, and passed**. ### Residual, stated plainly The native-Windows job is **red on master right now** (run 608 at `f6a878a`, 15 s) — but it dies in the **Free disk pre-flight** step (`ci-windows.yml:117`) before compiling anything, which is **#179** and is owned elsewhere. It is not a regression of this fix, and run 603 is the closest green ancestor that contains it. If #179's pre-flight turns out to have been masking a later failure in that job, this should be reopened — but nothing currently suggests that, since 603 ran the full suite. 🤖 Triage lane, 2026-09-06, master `f6a878a`
Author
Member

REOPENING — I closed this an hour ago and I was wrong. There is a second boundary, it is verified in source, and this repo's own comment explains why it matters.

Correcting my own close rather than leaving it standing. Everything in the comment above about approval::recorded_project_root is accurate and still holds; what it missed is that this issue's rule applies to comparison as well as to storage, and comparison has three implementations, not one.

The three implementations of "does this recorded root name this directory"

site fallback when canonicalize() fails
crates/indexer/src/approval.rs:2630 roots_match deepest_real(recorded) == deepest_real(dir) — strips verbatim ✅
crates/daemon/src/lifecycle.rs:711 root_matches _ => recorded == dir — raw ❌
crates/daemon/src/rpc_index.rs:810 root_matches _ => r == expected — raw ❌

Read at f6a878a and unchanged at 45cf6e4 (that commit touches only CI and test files):

// crates/daemon/src/lifecycle.rs:711
pub(crate) fn root_matches(recorded: &str, dir: &Path) -> bool {
    let recorded = Path::new(recorded);
    match (recorded.canonicalize(), dir.canonicalize()) {
        (Ok(a), Ok(b)) => a == b,
        _ => recorded == dir,          // <-- the two forms, compared
    }
}

rpc_index.rs:810 is the same shape behind an Option.

Why this is this issue and not a new one

approval.rs already fixed exactly this, and says so. Its comment at :965-970 records that its own last arm used to have this shape and what it cost:

"an unmounted share, a deleted tree… this one used to not [strip], so a project whose directory was unavailable compared its STRIPPED recorded root against an UNSTRIPPED live one and roots_match called the operator's own project foreign."

That is precisely the failure this issue defines — "Comparing the two forms is the bug" — and the two daemon copies still have the pre-fix shape. Since recorded_project_root now guarantees the stored spelling is canonical-and-stripped, the recorded side is always normalised while the live side on that arm is raw, so the mismatch is now systematic rather than incidental.

And the tree already knows the copies exist: approval.rs:2628 names "The daemon's lifecycle::root_matches rule, which cannot be called from here" — noted, not closed.

Second, smaller residual

The serde doors de_recorded_root (approval.rs:927) and de_recorded_root_opt (:935) apply recorded_root = strip_verbatim only, no canonicalize. So a record written by a pre-v0.26 build keeps its env-cased spelling when read back. This is acknowledged in-source at :830 — "This fixes what THIS build writes and nothing that is already on disk" — so it is a known bound rather than an oversight, but on Windows it means a read root and a freshly minted root can still differ in case.

What is NOT a residual, so nobody re-litigates it

project_key / key_input (approval.rs:721) is deliberately not routed through recorded_project_root, with the migration reason written at :690-720: its input is what every approval record already on an operator's disk is filed under. That is a documented decision, and correct.

Also settled and not to be redone: the Windows arm is graded, not cfg-skipped. approval.rs:812-819 explicitly refuses the #[cfg(not(windows))]-identity shape this repo has been bitten by, and activation.rs:1913/1969/2021 run on both platform arms. activation_offer.rs:1600 even reproduces the case divergence on Linux via a symlink, with an assert_ne! proving the two spellings really differ. Native-Windows CI ran green at 095eba6 with the fix in.

What closing this now requires

Fold lifecycle::root_matches and rpc_index::root_matches onto the same rule approval::roots_match uses — one clause, not three copies — or state in-source why the daemon's two cannot reach it and grade the fallback arm directly. Given how this project prefers generic mechanisms over a fix per site, the first is the one that matches the rest of the tree.

Reopened.

🤖 Triage lane, 2026-09-06, master 45cf6e4

## REOPENING — I closed this an hour ago and I was wrong. There is a second boundary, it is verified in source, and this repo's own comment explains why it matters. Correcting my own close rather than leaving it standing. Everything in the comment above about `approval::recorded_project_root` is accurate and still holds; what it missed is that this issue's rule applies to **comparison** as well as to storage, and comparison has three implementations, not one. ### The three implementations of "does this recorded root name this directory" | site | fallback when `canonicalize()` fails | |---|---| | `crates/indexer/src/approval.rs:2630` `roots_match` | `deepest_real(recorded) == deepest_real(dir)` — strips verbatim ✅ | | `crates/daemon/src/lifecycle.rs:711` `root_matches` | `_ => recorded == dir` — **raw** ❌ | | `crates/daemon/src/rpc_index.rs:810` `root_matches` | `_ => r == expected` — **raw** ❌ | Read at `f6a878a` and unchanged at `45cf6e4` (that commit touches only CI and test files): ```rust // crates/daemon/src/lifecycle.rs:711 pub(crate) fn root_matches(recorded: &str, dir: &Path) -> bool { let recorded = Path::new(recorded); match (recorded.canonicalize(), dir.canonicalize()) { (Ok(a), Ok(b)) => a == b, _ => recorded == dir, // <-- the two forms, compared } } ``` `rpc_index.rs:810` is the same shape behind an `Option`. ### Why this is this issue and not a new one **`approval.rs` already fixed exactly this, and says so.** Its comment at `:965-970` records that its own last arm used to have this shape and what it cost: > *"an unmounted share, a deleted tree… this one used to not [strip], so a project whose directory was unavailable compared its STRIPPED recorded root against an UNSTRIPPED live one and `roots_match` called the operator's own project foreign."* That is precisely the failure this issue defines — *"Comparing the two forms is the bug"* — and the two daemon copies still have the pre-fix shape. Since `recorded_project_root` now guarantees the stored spelling is canonical-and-stripped, the recorded side is *always* normalised while the live side on that arm is raw, so the mismatch is now systematic rather than incidental. And the tree already knows the copies exist: `approval.rs:2628` names *"The daemon's `lifecycle::root_matches` rule, which cannot be called from here"* — noted, not closed. ### Second, smaller residual The serde doors `de_recorded_root` (`approval.rs:927`) and `de_recorded_root_opt` (`:935`) apply `recorded_root` = **strip_verbatim only, no canonicalize**. So a record written by a pre-v0.26 build keeps its env-cased spelling when read back. This is acknowledged in-source at `:830` — *"This fixes what THIS build writes and nothing that is already on disk"* — so it is a known bound rather than an oversight, but on Windows it means a *read* root and a *freshly minted* root can still differ in case. ### What is NOT a residual, so nobody re-litigates it `project_key` / `key_input` (`approval.rs:721`) is deliberately not routed through `recorded_project_root`, with the migration reason written at `:690-720`: its input is what every approval record already on an operator's disk is filed under. That is a documented decision, and correct. Also settled and not to be redone: the Windows arm is **graded**, not cfg-skipped. `approval.rs:812-819` explicitly refuses the `#[cfg(not(windows))]`-identity shape this repo has been bitten by, and `activation.rs:1913/1969/2021` run on both platform arms. `activation_offer.rs:1600` even reproduces the case divergence **on Linux** via a symlink, with an `assert_ne!` proving the two spellings really differ. Native-Windows CI ran green at `095eba6` with the fix in. ### What closing this now requires Fold `lifecycle::root_matches` and `rpc_index::root_matches` onto the same rule `approval::roots_match` uses — one clause, not three copies — or state in-source why the daemon's two cannot reach it and grade the fallback arm directly. Given how this project prefers generic mechanisms over a fix per site, the first is the one that matches the rest of the tree. Reopened. 🤖 Triage lane, 2026-09-06, master `45cf6e4`
Author
Member

FIXED, merged as a80eb61. Verified in the tree at 1d81180: lifecycle.rs:745 and rpc_index.rs:821 both delegate to code_index_indexer::approval::roots_match, which is now pub and the only rule.

The three implementations agreed on the easy arm and differed on the one that matters: where either side fails to canonicalize, the two daemon copies compared raw strings while the rule compares deepest-real forms. Since recorded_project_root guarantees the stored side is canonical-and-stripped, the raw comparison was systematically wrong rather than incidentally — the daemon calling its own database foreign. rpc_index keeps only what is genuinely its own: None means unverifiable, which is not a mismatch.

The mutation that proves it is the fold and not a clause: un-folding the daemon copy back to its pre-fix body reproduces the same RED.

Two things found on the way, both worth more than the fix:

  1. A stale doc comment is invisible to every gate we have. root_matches's doc had come unglued and was sitting on digest_of — an FNV-1a hash documented as a path comparator. Rustdoc is happy: a doc comment is syntactically valid wherever it lands. Restored.
  2. The test that grades this broke Windows, and that is now fixed separately in 1d81180. It planted a directory symlink with a bare .unwrap(), and a symlink is a privilege on Windows — the CI account has neither Developer Mode nor elevation. The knowledge now lives once, in code_index_test_support::symlink, returning a three-state outcome; and the test grades on Windows via NTFS case-insensitivity rather than skipping, because a skip that keeps the suite green is the same vacuity trap in a new costume.

Closing.

FIXED, merged as `a80eb61`. Verified in the tree at `1d81180`: `lifecycle.rs:745` and `rpc_index.rs:821` both delegate to `code_index_indexer::approval::roots_match`, which is now `pub` and the only rule. The three implementations agreed on the easy arm and differed on the one that matters: where either side fails to canonicalize, the two daemon copies compared **raw strings** while the rule compares deepest-real forms. Since `recorded_project_root` guarantees the stored side is canonical-and-stripped, the raw comparison was **systematically** wrong rather than incidentally — the daemon calling its own database foreign. `rpc_index` keeps only what is genuinely its own: `None` means unverifiable, which is not a mismatch. The mutation that proves it is the *fold* and not a clause: un-folding the daemon copy back to its pre-fix body reproduces the same RED. Two things found on the way, both worth more than the fix: 1. **A stale doc comment is invisible to every gate we have.** `root_matches`'s doc had come unglued and was sitting on `digest_of` — an FNV-1a hash documented as a path comparator. Rustdoc is happy: a doc comment is syntactically valid wherever it lands. Restored. 2. **The test that grades this broke Windows**, and that is now fixed separately in `1d81180`. It planted a directory symlink with a bare `.unwrap()`, and a symlink is a *privilege* on Windows — the CI account has neither Developer Mode nor elevation. The knowledge now lives once, in `code_index_test_support::symlink`, returning a three-state outcome; and the test grades on Windows via NTFS case-insensitivity rather than skipping, because a skip that keeps the suite green is the same vacuity trap in a new costume. Closing.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
h-dv/code-index#159
No description provided.