AprilNEA/OpenLogi · error

must be a non-symlink directory

Error message

{asset} must be a non-symlink directory

What it means

`require_directory` demands that a given asset path exists, is a real directory, and is not a symbolic link, using `symlink_metadata` so symlinks are detected even when they point at directories. This protects the fixture layout from symlink-based substitutions that could break integrity guarantees of published fixtures.

Solutions

  1. Replace the symlink with a real directory: `rm <asset> && mkdir <asset>` (copy contents back if needed).
  2. Point the asset path at the actual directory rather than a link to it.
  3. Verify with `ls -la` / `test -d <asset> && ! test -L <asset>` before running the command.

Example fix

// before (assets/cases -> ../shared/cases symlink)
ln -s ../shared/cases out/mx-master/cases
// after
rm out/mx-master/cases && mkdir out/mx-master/cases && cp ../shared/cases/* out/mx-master/cases/
Defensive patterns

Strategy: validation

Validate before calling

[ -d "$ASSET" ] && [ ! -L "$ASSET" ] || { echo "$ASSET must be a real directory" >&2; exit 1; }

Prevention

When it happens

Trigger: Any of run, publish_manifest_and_finish, or validate_resumable_layout checks an asset path that (a) is a symlink to a directory, or (b) is not a directory at all (a regular file, or nonexistent — the latter surfaces as the wrapped `could not inspect {asset}` context error instead).

Common situations: Checking out fixture trees where directories were replaced by symlinks (e.g. shared fixtures via ln -s); pointing an asset path at a file; a packaging step that replaced a directory with a link.

Related errors


AI-assisted analysis of AprilNEA/OpenLogi@e846e6f4b4 (2026-09-13). Data as JSON: /api/errors/a51dff0f2e9d58c5. Report an issue: GitHub.

Appendix: source

Thrown at crates/openlogi-cli/src/cmd/fixture/contribute.rs:486

    if state.fixture_id != args.id
        || state.profile_id != format!("{}-profile", args.id)
        || state.profile_name != args.name
    {
        bail!("--id and --name must match the in-progress contribution");
    }
    Ok(())
}

fn read_json<T: serde::de::DeserializeOwned>(path: &Path, asset: &str) -> Result<T> {
    let bytes = fs::read(path).with_context(|| format!("could not read {asset}"))?;
    serde_json::from_slice(&bytes).with_context(|| format!("could not parse {asset}"))
}

fn require_directory(path: &Path, asset: &str) -> Result<()> {
    let metadata =
        fs::symlink_metadata(path).with_context(|| format!("could not inspect {asset}"))?;
    if metadata.file_type().is_symlink() || !metadata.is_dir() {
        bail!("{asset} must be a non-symlink directory");
    }
    Ok(())
}

fn require_regular_file(path: &Path, asset: &str) -> Result<()> {
    let metadata =
        fs::symlink_metadata(path).with_context(|| format!("could not inspect {asset}"))?;
    if metadata.file_type().is_symlink() || !metadata.is_file() {
        bail!("{asset} must be a non-symlink regular file");
    }
    Ok(())
}

fn reject_symlink(path: &Path, asset: &str) -> Result<()> {
    match fs::symlink_metadata(path) {
        Ok(metadata) if metadata.file_type().is_symlink() => {
            bail!("{asset} output must not be a symlink")
        }

View on GitHub (pinned to e846e6f4b4)