14 Commits

Author SHA1 Message Date
bytecii 73f5bd7c21 chore: cleanup items 31+32 — nested-function rule, layout, CI audit, ls facets
Closes the two remaining small items of Block E. Both were largely
recon: much of what they filed had already been fixed, and the part
that had not turned up a real GNU divergence.

Item 31 (T3-7), rule/doc reconciliation
---------------------------------------

The `object`-parameter half is already done: #825's mypy inversion took
the package from the filed 28 down to 7, of which 3 are docstring prose
and the remaining 4 are protocol methods (`__contains__`, `pop`) whose
signatures typeshed dictates, all already listed in the no-object gate.
`utils/errors.py` reads `str | PathSpec` today, and `builders/sed.py` no
longer holds flag values at all -- item 22 moved that to the generic.

That left the nested-function rule, which the plan asked to decide
before gating. Measured: 39 nested defs, and **every one of them closes
over its enclosing scope** -- 38 by free variable, and `sed_helper._repl`
by binding through parameter defaults, the loop-variable idiom, with a
comment saying so. A flat "do not nest" is a rule the architecture
cannot keep (op factories, provision builders, the read-through cache,
every decorator's wrapper), so gating it as written would have meant 39
standing violations.

So the rule now says what it was reaching for: a nested def must capture
the scope around it, or it belongs at module level. That is mechanical
and enforceable -- `tests/test_nested_functions_are_closures.py` reads
free variables from `symtable` and parameter defaults from the AST. It
passes on the tree as-is and fires on a helper that reads only its own
arguments.

Layout: the eight test-only TS directories are flattened
(`ram/{cat,cut,grep,head,ls,tail,wc}/`, `ssh/ls/`), `awk_helper.ts` moves
up beside its nine siblings, and the `provision.ts` / `_provision.ts`
split is settled on Python's convention -- `_provision` for a backend's
own, bare `provision` for the shared ones (cli, generic_bind). That last
one was not cosmetic: Python already had `_provision.py` for github,
gmail, redis and email, so renaming **closed 4 real parity divergences**
(layout baseline 296 -> 292). The `findEval`/`findParse` line was
already done in #827.

Item 32 (T3-8), test/CI tidiness
--------------------------------

The two asymmetric `ls` conformance matrices are raised to symmetry, and
they pass: python `[ram,disk,redis]` vs typescript `[ram]` was stale
caution, not a divergence. Both runners now reject an asymmetric matrix
at load time unless the case carries a `divergence` key explaining why --
a case is a parity claim, and narrowing one side reads as coverage while
the side that still lists the backend goes green. Loading the corpus
under the new assertion proves no other case was asymmetric. The README
documented the override as "not yet needed"; it exists now.

Adds the `ts-audit` job, mirroring `test_python.yml`'s exactly (same
`continue-on-error`, same out-of-gate placement). Nothing ran `pnpm
audit` for the TS tree while `typescript/package.json` carried 27
hand-written CVE overrides that only a person remembering to check kept
current.

Integ facets for `ls`: `-A`, `-d`, `-r`, `-S`, `-h`, all pinned against
GNU coreutils 9.7 in docker. `-S` uses regular files only, because GNU
sorts a directory by its inode size while mirage counts it as 0 -- a
divergence CLAUDE.md documents deliberately.

The `-h` facet found a real bug
-------------------------------

`ls -h` disagreed with GNU three ways, and because the flag had zero
cross-backend coverage nothing caught it. GNU prints a count below one
unit with no suffix (`24`); mirage printed `24B`. GNU rounds *up* to the
precision shown (1025 bytes is `1.1K`); mirage gave `1.0K`. GNU drops
the decimal once the value reaches ten (`10K`); mirage gave `10.0K`.

One shared engine replaces both formatters in both languages -- GNU runs
`-h` and `-H` through one `human_readable`, and so do we now. Rounding
up can carry past the base (1048575 ceils to 1024K, which GNU shows as
`1.0M`), so the unit is re-chosen after rounding rather than once up
front. 21 GNU-read points are pinned as a table in each language.

Blast radius, all re-pinned: five integ cases that had recorded the `B`
suffix (du, discord, langfuse, email, gmail), and the du fan-out tests.
Those last ones needed care rather than a new number: they exist to
prove the total is humanized once instead of twice, and their 1500+1500
stops discriminating under correct rounding, since 3000 bytes and two
`1.5K` readings both render `3.0K`. They now use 1025+1025, where
single-rounding gives `2.1K` and double-rounding would give `2.2K`.

Verified: pre-commit clean, integ 8054/0 on ram+disk+redis in both
languages, conformance green in both, and the layout, spec, barrel,
docs, case-target and PathSpec gates all pass.

Not covered, still open on item 32: integ facets for `ls -t` (mtime
order is not stable across backends without a seeded fixture), mktemp,
gzip, the checksum `--check` companions, `df -H/-k/-a`, and `cmp
-n/-b/-i`; and the T2-7 helper unit-suite mirroring. Item 31 leaves the
provision *presence* sets unreconciled (Python has lancedb/qdrant, TS has
trello) -- that is a question about which backends should provision at
all, not about naming.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 06:57:08 -07:00
bytecii 099aa4b35e fix(readdir): ENOENT vs ENOTDIR for a missing path (py+ts, 6 backends) (#637)
* fix(readdir): ENOENT vs ENOTDIR for a missing path (py+ts, 6 backends)

`ls` on a missing path reported the wrong errno string on several
backends, and Python/TypeScript disagreed on the same backend.

GNU pin (`docker run --rm debian:stable-slim`):

    ls /nope             -> No such file or directory
    ls /data/nope        -> No such file or directory
    ls /data/nope/deeper -> No such file or directory   (however deep)
    ls /data/a.txt/x     -> Not a directory             (component is a file)
    ls /data/a.txt/      -> Not a directory

The rule: ENOTDIR only when a path component exists and is not a
directory; a component that does not exist is ENOENT, at any depth.

Every affected site was wrong, in both directions:

| backend  | before                                     |
|----------|--------------------------------------------|
| py disk  | always ENOTDIR (+ leaked the real fs path) |
| py ram   | always ENOENT (missed genuine ENOTDIR)     |
| py redis | always ENOENT                              |
| ts ram   | always ENOTDIR                             |
| ts disk  | mapped ENOENT -> enotdir explicitly        |
| ts opfs  | mapped NotFoundError -> enotdir            |

Store-backed backends (ram, redis) have no kernel to draw the line, so
they now ask a new shared helper — `readdir_error` / `readdirError` —
which walks the ancestors and returns ENOTDIR at the first existing
non-directory component, else ENOENT. Kernel/API-backed backends already
had the right signal and just needed to stop squashing it: disk lets
listdir raise and restamps onto the PathSpec (which also fixes the
real-fs-path leak in the Python error), opfs keeps NotFoundError->ENOENT
distinct from TypeMismatchError->ENOTDIR.

Blast radius is stderr bytes only: no caller distinguishes the two codes
after a readdir (glob_walk, condition operators, chroma/dify walk,
find_action_dispatch, wasm/fs, monty all catch them together). Three
tests pinned the old behavior and were corrected.

Coverage: 6 cross-backend cases in integ/unix/nf/ls.json (ram, disk,
redis, opfs — `nf/` had cat/grep/head/stat/tail/wc but no ls, which is
the gap this lived in), 5 in conformance/cases/ls.json, plus helper and
per-backend unit tests on both sides. The new cases pin exit 1, matching
this tree's `ls` exit and the existing convention in
integ/resources/slack/ls.json; they will need the same bump as the other
ls cases when the 0/1/2 exit-code split lands.

Verified: pre-commit clean; python + TS integ 1886/0 (ram) and 1874/0
(disk) on both hosts; full TS suites and targeted Python suites pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(readdir): stop the errno walk at the first missing component

readdir_error / readdirError scanned every ancestor for a file and
reported ENOTDIR if it found one at any depth. A flat store can hold a
key whose parent is not a directory (RAM/Redis rename writes the
destination key without adding its ancestors), so `ls /missing/a.txt/x`
found the orphan `/missing/a.txt` and said "Not a directory" while
`ls /missing` said "No such file or directory" for the same tree.

Both helpers now take an is_dir / isDir probe and stop where the kernel
stops resolving: the first component that is neither a directory nor a
file is ENOENT. A file component is still ENOTDIR.

* fix(tree/grep/rg): keep walking when readdir reports ENOTDIR

Splitting the readdir errno into ENOENT/ENOTDIR left the python tree
and grep/rg walkers behind: they caught only
`(FileNotFoundError, ValueError)`, and NotADirectoryError is a sibling
of FileNotFoundError under OSError, not a subclass. So on RAM/Redis:

- `tree /a.txt/x` escaped the walk and returned exit 1 with no body,
  where `tree /nope` still printed GNU's `[error opening dir]` + exit 2
- `grep -rl foo /a.txt/x /real` aborted and dropped /real's results,
  where the same line with `/nope` warned and kept them
- `rg -l foo /a.txt/x /real` did the same

GNU pins (debian:stable-slim): `tree /d/a.txt/x` prints the same body
and exits 2 as `tree /d/nope`; `grep -rl foo /d/a.txt/x /d/real` warns
and still prints `/d/real/b.txt`.

Both cases now route through one named `WALK_ERRORS` tuple (FS_ERRORS
plus the ValueError store backends raise for "not a directory"), so a
future errno split cannot desync these catch sites again. TypeScript
already used a broad catch and was unaffected; python now matches it.

---------

Co-authored-by: bytecii <bytecii@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Zecheng Zhang <zechengzhang97@gmail.com>
2026-07-25 21:38:23 -07:00
bytecii 4807b23630 fix(ls): GNU per-directory headers for multi-operand listings (py+ts) (#638)
`ls a b` flattened every operand's entries into one unseparated list
instead of printing GNU's `a:` / `b:` blocks. Pinned the real rule with
docker (debian:stable-slim, coreutils 9.7): headers are keyed on the
*operand count*, not on how many directories were listed —

  ls a          -> bare listing, no header
  ls a b        -> `a:` block, blank line, `b:` block
  ls /nope a    -> `a:` (2 operands, one of them failed)
  ls -R a       -> `a:` (recursive always heads)

and non-directory operands print first as one unheaded block, with all
operands sorted together before that split.

Both the non-recursive and `-R` paths now share one `probe_operand` /
`probeOperand` that lists an operand and reports whether it turned out
to be a directory, so unifying them fixed three more divergences found
alongside the reported one:

- operands were listed in command-line order, interleaved: `ls b zfile
  a mfile` printed `g.txt /zfile f.txt sub /mfile`
- `-d` did not sort its operands (`ls -d b a` -> `b a`)
- `ls -R zfile a` emitted a bogus `zfile:` header with `zfile` as its
  own entry

`ls -R` on a directory whose readdir fails no longer prints an empty
header, and the TS path now type-checks a failed-readdir operand before
treating it as a file row (Python already did).

The exit-code rule is unchanged in meaning but no longer keyed on
stdout being empty: it asks whether any operand listed, so a header
line cannot flip the code. That keeps this independent of the in-flight
ls exit 0/1/2 work.

Verified: 41 Python + 16 TS unit cases with byte-exact stdout, and both
integ hosts at 1882 passed / 0 failed on `--target ram` (all 7 pinned ls
surfaces unchanged, including the 24-operand `ls /data/*.txt` glob).

Cross-mount `ls` (operands on different mounts) still prints flat — the
FANOUT strategy runs one native ls per operand, so each run correctly
sees a single operand. Closing that needs operand count threaded
through the executor and still cannot produce GNU's global operand
sort, so it is left out here.

Co-authored-by: bytecii <bytecii@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Zecheng Zhang <zechengzhang97@gmail.com>
2026-07-25 18:59:51 -07:00
bytecii 7d1c518184 fix(ls): GNU exit-code seriousness split (0/1/2) in py+ts (#636)
Pre-commit / pre-commit (push) Has been cancelled
CLI exit codes / changes (push) Has been cancelled
CLI exit codes / Python CLI (push) Has been cancelled
CLI exit codes / TypeScript CLI (push) Has been cancelled
CLI exit codes / Cross-language snapshot interop (push) Has been cancelled
CLI exit codes / cli-gate (push) Has been cancelled
Test (Install) / changes (push) Has been cancelled
Test (Install) / python-install (push) Has been cancelled
Test (Install) / ts-install (push) Has been cancelled
Test (Install) / test-install-gate (push) Has been cancelled
Integ / changes (push) Has been cancelled
Integ / integ (push) Has been cancelled
Integ / integ-ts (push) Has been cancelled
Integ / integ-shared-py (push) Has been cancelled
Integ / integ-shared-ts (push) Has been cancelled
Integ / integ-shared-parity (push) Has been cancelled
Integ / integ-database (push) Has been cancelled
Integ / integ-database-ts (push) Has been cancelled
Integ / integ-data (push) Has been cancelled
Integ / integ-fuse (push) Has been cancelled
Integ / integ-fuse-windows (push) Has been cancelled
Integ / runtime-py (push) Has been cancelled
Integ / runtime-ts (push) Has been cancelled
Integ / integ-gate (push) Has been cancelled
Test (Python) / changes (push) Has been cancelled
Test (Python) / test (push) Has been cancelled
Test (Python) / import-isolation (deepagents, openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Python) / import-isolation (deepagents, pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Test (Python) / runtime (push) Has been cancelled
Test (Python) / audit (push) Has been cancelled
Test (Python) / test-python-gate (push) Has been cancelled
Test (TypeScript) / changes (push) Has been cancelled
Test (TypeScript) / test (push) Has been cancelled
Test (TypeScript) / python-fs-shim (push) Has been cancelled
Test (TypeScript) / test-typescript-gate (push) Has been cancelled
* fix(ls): GNU exit-code seriousness split (0/1/2) in py+ts

`ls` returned 1 for an inaccessible command-line operand where GNU
coreutils returns 2, and — worse — masked the failure entirely when any
other operand succeeded: `ls /data/nope /data` exited 0.

The rule was `1 if warnings and not results else 0`, i.e. nonzero only
when *every* operand failed. GNU instead ratchets a status upward
(`set_exit_status`): 0 ok, 1 minor problem (trouble met below an
operand), 2 serious trouble (a command-line operand could not be
accessed); serious always wins, minor only upgrades a clean run.

Pinned against GNU coreutils 9.7 (debian:stable-slim), including the
non-root cases that reach the minor-problem path:

  ls /nope                          -> 2
  ls /nope /ok       (either order) -> 2, still lists /ok
  unreadable dir as operand         -> 2
  ls -R, unreadable subdirectory    -> 1, keeps parent output
  ls -l, unstattable entries        -> 1, lists the siblings
  minor + serious together          -> 2

Warnings now carry a `serious` flag and are collapsed by
`exit_status_for` / `exitStatusFor`. Seriousness comes from a
`command_line_arg` / `commandLineArg` parameter threaded through
`walk`/`walk_grouped` (`walkGrouped`), false on recursion; a per-entry
stat failure inside a listed directory is always minor.

Three fixes were load-bearing for the split:

* Both executors gated `ls` child-mount and symlink injection on exit
  code == 0. A minor problem still lists the directory, so exit 1 would
  have silently dropped the injected rows; the gate is now
  `!= LS_FAILURE`. (Injection already bails for multi-operand and for
  -R/-d, and usage errors return early via UsageError, so the affected
  surface is a single-operand listing with a minor warning.)
* Python's minor path was unreachable: `walk` caught only
  FileNotFoundError/ValueError/NotADirectoryError, so a PermissionError
  escaped the command and discarded stdout. Now `(OSError, ValueError)`,
  matching TypeScript.
* TypeScript's `listDir` used `Promise.all`, so one failing child stat
  killed the whole listing and surfaced as an operand failure — which
  would have exited 2 where Python exits 1. Now `Promise.allSettled`
  with per-entry warnings, matching Python's tolerance.

Also fixes a Python-only bug where `walk_grouped` appended a group
unconditionally, so `ls -R /nope` printed a bogus `/nope:` header on
stdout (TypeScript was already correct).

Pinned surfaces: integ/resources/slack/ls.json (slack_ls_archived_missing)
1 -> 2, and five hard-pinned unit tests. conformance/cases/ls.json had no
error coverage, so two cases were added; they assert the status via
`2>/dev/null; echo rc=$?` because the stderr text itself diverges for an
unrelated pre-existing reason (Python+disk and TypeScript+RAM report
"Not a directory" where GNU and Python+RAM say "No such file or
directory") — tracked separately.

Verified: 26 Python + 14 TypeScript unit tests; integ 1611 passed / 0
failed on both hosts; conformance green on both; both hosts byte-identical
across all 12 pinned scenarios.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(ls): mem0 golden, -R separator, review nits

- integ/resources/mem0/basic.json: mem0_readdir_of_file_enotdir asserted
  exit 1 for an inaccessible command-line operand; GNU returns 2. This
  was the second nonzero-ls golden in the repo and the only CI failure.
- The -R blank-line separator was keyed on the operand index, so a first
  operand that failed (rendering no group) still made the next one emit a
  leading blank. It now keys on what was actually emitted, matching GNU
  and the TypeScript path.
- Test denial stubs moved to module scope (no nested functions) and the
  new docstrings carry Args types.

---------

Co-authored-by: bytecii <bytecii@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Zecheng Zhang <zechengzhang97@gmail.com>
2026-07-25 16:07:32 -07:00
Zecheng Zhang 90a20bd169 fix: stop masking backend failures as missing paths, widen TS conformance (#633)
Three fixes from an audit of the TypeScript side.

1. WorkspaceFS.exists/isDir/isFile caught every exception and returned
   false, so an auth failure, timeout, or backend bug read back as "this
   path does not exist". Python already had the right answer: it swallows
   only (FileNotFoundError, ValueError), where ValueError is the registry's
   "no mount matches path". Added noMount() plus an isMissingPath()
   predicate and applied it to the three probes, to pathExists/isDirectory
   in the copy helpers, and to the same pattern in realpath and df, both of
   which had already diverged from their narrow Python twins.

2. LanceDB blob decoding called the Node global Buffer inside the
   runtime-agnostic core package, which breaks in browsers. It now uses the
   existing decodeBase64 helper, matching its qdrant sibling. The two gmail
   base64url helpers moved onto the shared helpers as well.

3. TypeScript conformance only ran against RAM while Python ran RAM, disk
   and Redis. The runner now covers all three with fresh backend state per
   case, and all 34 cases pass on every backend. Teardown is split so
   clearing Redis happens before Workspace.close() destroys the client,
   rather than silently opening a second connection per case.

Also wired integ/runners/parity.py into CI as integ-shared-parity and added
it to the gate; it existed but nothing ran it. Locally it compares 5386
case/target pairs with no mismatches. Added conformance/** to the python and
typescript workflow path filters, which did not trigger those suites before.

The test stubs that faked backend errors as new Error("not found") or
new Error("ENOENT"), with the code in the message instead of on the error,
now use enoent() like real backends do.
2026-07-24 23:02:00 -07:00
bytecii 426090484a feat(stat): full GNU stat -c directive set + agent-aware ownership (#609 Tier 1) (#626)
Pre-commit / pre-commit (push) Has been cancelled
CLI exit codes / changes (push) Has been cancelled
Test (Install) / changes (push) Has been cancelled
Integ / changes (push) Has been cancelled
Test (Python) / changes (push) Has been cancelled
Test (TypeScript) / changes (push) Has been cancelled
CLI exit codes / Python CLI (push) Has been cancelled
CLI exit codes / TypeScript CLI (push) Has been cancelled
CLI exit codes / Cross-language snapshot interop (push) Has been cancelled
CLI exit codes / cli-gate (push) Has been cancelled
Test (Install) / python-install (push) Has been cancelled
Test (Install) / ts-install (push) Has been cancelled
Test (Install) / test-install-gate (push) Has been cancelled
Integ / integ (push) Has been cancelled
Integ / integ-ts (push) Has been cancelled
Integ / integ-shared-py (push) Has been cancelled
Integ / integ-shared-ts (push) Has been cancelled
Integ / integ-database (push) Has been cancelled
Integ / integ-database-ts (push) Has been cancelled
Integ / integ-data (push) Has been cancelled
Integ / integ-fuse (push) Has been cancelled
Integ / integ-fuse-windows (push) Has been cancelled
Integ / runtime-py (push) Has been cancelled
Integ / runtime-ts (push) Has been cancelled
Integ / integ-gate (push) Has been cancelled
Test (Python) / test (push) Has been cancelled
Test (Python) / import-isolation (deepagents, openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Python) / import-isolation (deepagents, pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Test (Python) / runtime (push) Has been cancelled
Test (Python) / audit (push) Has been cancelled
Test (Python) / test-python-gate (push) Has been cancelled
Test (TypeScript) / test (push) Has been cancelled
Test (TypeScript) / python-fs-shim (push) Has been cancelled
Test (TypeScript) / test-typescript-gate (push) Has been cancelled
2026-07-23 19:08:49 -07:00
bytecii a584eef138 feat(sort): full GNU -k KEYDEF grammar (py+ts) (#607)
Replace single-field `-k N` with the complete GNU sort key spec,
mirrored across Python and TypeScript:

- Field ranges and character offsets: -k F1[.C1][opts][,F2[.C2][opts]]
- Field-N-to-EOL default (`-k2` spans field 2 to end of line, not just
  field 2 -- the largest prior silent divergence)
- Multiple -k keys applied in order (spec `-k` is now repeatable)
- Per-key modifiers n/g/h/V/M/f/r/b; any per-key modifier (incl. -b)
  makes a key ignore all global ordering options (GNU key_init rule),
  while globals still drive the last-resort whole-line compare
- Last-resort whole-line comparison, disabled by -s
- GNU field model: leading blanks belong to the following field; -b
  skips them; -t is a clean split
- Invalid field 0 -> exit 2 with GNU-matching stderr

Pinned against GNU coreutils 9.7. New cross-language
conformance/cases/sort.json validates py/ts parity.

Co-authored-by: bytecii <bytecii@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 22:39:53 -07:00
bytecii 0a431ca5f4 test(integ): PR5+PR6 unix/crossmount flag coverage + GNU shell fixes (mv/rm/mktemp/split/seq/join/tree/readlink) (#589)
* test(integ): PR5 unix write-family + long-tail + archive flag coverage

Adds 70 GNU-pinned integ cases (seq 601000-601351) for previously
untested unix flags, plus the product fixes the coverage surfaced
(Python + TypeScript, with unit tests):

- mv -v: emit GNU "renamed 'src' -> 'dst'" (was cp's arrow form)
- rm -rv/-dv: depth-first per-entry verbose + "removed directory" for
  dirs; rm -d on non-empty now GNU "cannot remove ...: Directory not
  empty" (shared utils/verbose helper; generic + s3 + gridfs; gridfs
  now collects errors instead of aborting)
- mktemp /path/tmpl.XXXX: honor an explicit path template's own
  directory (was prepending /tmp)
- split -l: TS read the wrong flag key (AMBIGUOUS_NAMES renames -l to
  args_l) so it never segmented by lines
- seq -w: was a value option that ate the start operand; now boolean,
  with width = widest formatted value (handles negatives)

Cases: cp/mv/ln/rm/tee/mkdir/touch/mktemp(descoped)/split/csplit (A),
cat/sort/diff/cmp/jq/uniq/nl/cut/join/comm/tail/base64/seq/sha256sum/
realpath/expand/unexpand/date (B), gzip/gunzip/zip/unzip/expr/shuf (C).
Green on py ram/disk + ts ram (1520/1520); PR5-A also py s3.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(shell): join -o/-e field mapping + jq -s coverage (py & ts)

join -o FILENUM.FIELD indexed the key-stripped rest, so `1.2` pointed at
field 3 and paired `-o` output came out empty; -e was also ignored in the
-o path. Map against the full 1-based field list (key included) and fill
missing fields with the -e value. Mirrored py & ts; native/unit tests
updated to GNU-correct output.

jq -s already slurps jsonl/stdin correctly; added integ coverage.

7 new integ cases (join -o/-e/-v/-1-2, jq -s); green py ram/disk + ts ram.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(shell): GNU-format tree output + integ coverage (py & ts)

tree emitted unicode box-drawing, no root line, no summary, and -L was
off by one. Rewrite to match GNU (C-locale ASCII):

- root path as the first line
- ASCII branches (|--, `--, |   ) matching the docker oracle
- blank line + "N directories, M files" summary (singular when 1; the
  file count is omitted under -d); the root counts as a directory once
  it has a listed entry (an empty root reports 0)
- -L N shows N levels (was N+1)
- a missing root prints "<path>  [error opening dir]" and exits 2

Mirrored py & ts; updated the unicode/format unit tests across generic,
ram, disk, databricks; refreshed the tree.json / nl.json cases and added
5 GNU-pinned integ cases (basic, -L, -d, -a, -I). Green py ram/disk +
ts ram (1530/1530).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(shell): readlink -f/-e/-m canonicalize symlinks (py & ts)

readlink -f/-e/-m fell through to the mount command, which only
normpath'd the operand and never resolved the link. Route every
readlink through the namespace handler and, for -f/-e/-m, resolve the
full symlink chain (namespace.follow) and normalize — GNU realpath-
style — so `readlink -f link` yields the canonical target (chains and
relative targets included). Plain readlink still prints the raw target.

Mirrored py & ts; 6 GNU-pinned integ cases (readlink.json: -f, chain,
relative, -e, -m, -f -n). Green py ram/disk + ts ram (1536/1536).

Documented divergence: -f/-e do not yet fail on a missing intermediate
component (existence checks need async in the sync handler).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(integ): PR6 crossmount redirect/archive/read-routing cases

6 GNU-pinned crossmount cases spanning /data + /data2: redirect > and
>> across mounts, gzip-then-cp-then-zcat archive across mounts, and
jq/tree/xxd routing to the second mount. Green py ram/disk + ts ram
(1542/1542).

Documented divergences (mirage rejects cross-mount, GNU allows; not
added as cases): split/csplit output prefix on the other mount and tar
across mounts ("paths span multiple mounts ... not supported");
find | xargs -I ( -I unsupported by design).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test: update backend tree tests for GNU-format output

The tree rewrite (ee617c6d) adds a root line + summary, so the discord
and slack backend tree tests (exact line count/position) and two
generic-test docstrings needed refreshing to the ASCII format.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* style: yapf-format native/test_join.py

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(integ): regenerate s3/onedrive/dify truth for GNU tree format

The tree rewrite (ee617c6d) changed the legacy per-backend scripts'
tree output; regenerate the truth fixtures I can run locally
(s3 via moto, onedrive in-process, dify). Only the tree blocks
changed (root line + ASCII branches + summary).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(integ): deprecate legacy tree cases; JSON harness owns tree

The GNU tree rewrite (ee617c6d) changed the tree output pinned by the
legacy per-backend scripts. Rather than maintain those brittle golden
fixtures, drop the redundant `tree` case from the legacy scripts — the
declarative JSON harness now owns tree coverage (integ/unix/tree.json:
basic + -L/-d/-a/-I/-P, plus crossmount routing).

- Removed the tree/tree_table case from chroma, qdrant, lancedb, dify,
  notion, s3, onedrive (py + ts where present) and the matching
  `=== *tree* ===` sections from their truth files (s3/onedrive/dify
  regenerated; chroma/qdrant/lancedb/notion/s3_ts section-pruned).
- Kept each script's unique backend coverage (e.g. s3.py's
  gcs/minio/seaweedfs endpoints + columnar rendering) — only the
  generic, now-duplicated tree case is dropped.
- Added tree -P to integ/unix/tree.json to complete the flag set.

Green py ram/disk + ts ram (1543/1543).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: resolve CI failures from the tree rewrite + seq spec drift

Full-matrix CI surfaced the tree rewrite's remaining golden/behavior
impact:

- rm -rv on dropbox/box/gdrive printed `removed directory '.../sub/'`
  (object stores return dir paths with a trailing slash) — strip it in
  removal_lines (py & ts); GNU never prints a trailing slash.
- Regenerate the seq -w spec snapshots (spec/{python,typescript}) —
  value_kind text -> none (the "Spec drift" gate).
- Update the tree goldens the rewrite touched: conformance tree_subdir,
  slack_tree_day, and the integ/truth redis replay line, to GNU format.
- redis/test_tree.py -L assertion + native_join.test.ts -o assertion
  (mirror the fixes already applied to ram/disk / the python native
  join test).
- Curate hf/hf-prefix out of the empty-dir tree cases (object stores
  don't persist empty directories) and ssh out of cp -R tree cases
  (recursive-copy limitation).

Green py ram/disk + ts ram (1543); py/ts unit + conformance green
locally (redis/service targets verified in CI).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(opfs): rm -v/-dv verbose + -d empty-check, mkdir -v (browser)

The opfs (browser) rm/mkdir had bespoke implementations that ignored
-v entirely and skipped rm -d's empty-check (giving `rm: Directory not
empty` without the operand). Mirror the fix already applied to the
generic/s3/gridfs rm: per-entry verbose via cpWalk + removalLines,
`removed directory` for dirs, GNU `cannot remove '<path>': Directory
not empty`, and mkdir -v. Green on ts --target opfs (1552/1552).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: bytecii <bytecii@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 20:44:56 -07:00
Zecheng Zhang 823da1bb1c fix(find): GNU coreutils divergences and parser hardening (#312)
- emit the start path at depth 0; implement -empty
- real expression grammar parser (-not/-o/-a, parentheses) replacing
  the dead _extract_not_name path; shared predicate-tree evaluator
- unknown predicates and invalid -size/-maxdepth/-mtime args exit 1
- validate -type letters; bound parser recursion depth
- migrate onedrive find to the generic; remove dead find_predicate_error
  and _extract_* helpers
- fan-out mount synthesis and child-mount -maxdepth honor the parsed
  expression tree
- chroma/dify: derive entry kind so -type works under the tree (was
  hardcoded to file)
- emit the start path for an empty directory across the object/API
  backends (s3 marker, nextcloud/hf scan base, onedrive stat); ssh/
  nextcloud/hf compute is_empty for -empty; fix nextcloud/hf metadata
  shadowing in the size/mtime path
- align integ truth for the now-correct -name OR and -not output
- mirror all changes across Python and TypeScript

Directory handling under -size is tracked separately in #318.
2026-06-15 03:13:52 -07:00
Zecheng Zhang 30677c1555 Merge origin/main; keep bare-octal cmp -l; conformance stat %n expects operand path 2026-06-11 00:45:24 -07:00
sonhmai 73aa275361 style(conformance): format backend guide 2026-06-11 13:28:29 +07:00
sonhmai 1d5ae3d69d feat(conformance): add readme 2026-06-11 13:27:35 +07:00
sonhmai 29db1d0eff fix(commands): terminate cmp and wc stdin output 2026-06-11 13:27:35 +07:00
sonhmai 1ed17a66b4 Add cross-language command conformance spec and runners (#151)
One declarative JSON spec (conformance/) describing command behavior as
seeds plus cases with byte-exact expected stdout, stderr, and exit code.
Two thin runners consume the same files: pytest against ram/disk/redis
and vitest against ram. Each case declares an explicit backend matrix;
a listed backend that lacks the command fails rather than skips, and a
case matching no backend is a load-time error.
2026-06-11 13:27:07 +07:00