3293cb5ff40c3eb18a599ce9113ed74d3d8d1607
19 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3293cb5ff4 |
feat(py): mirage mcp over stdio, and invert the mypy allowlist
Two cleanup-plan items, both Python-side.
Item 29 -- `mirage mcp`. TypeScript shipped a six-tool stdio MCP server;
Python had none, so a pip-install user could not point Cursor or Claude
Desktop at a workspace and `mirage --help` differed by distribution.
Adding the entry point alone would have duplicated the tools, because
this side kept them private inside the Claude Agent SDK integration, so
the shared layer comes first:
- agents/tool_descriptions.py -- the six strings, one copy.
- agents/tool_operations.py -- MirageToolOperations, lifted out of the
SDK server's private _MirageTools.
- agents/file_version.py -- stale-write protection, which this side
lacked entirely. TS stamps stored bytes; here the stamp covers the
rendered bytes, because this read tool has always rendered and an
edit must search what the agent was actually shown.
- agents/mcp/server.py + cli/mcp.py -- the server and `mirage mcp`.
- server/workspace_config.py -- config discovery (candidates, env
names, walk up from cwd), which Python had nowhere, so every entry
point had to be handed an explicit path.
The server is the low-level MCP Server rather than FastMCP: FastMCP does
not forward a version, and TS advertises one. Handlers are bound methods,
not decorated closures, so nothing nests.
Item 28 -- the mypy allowlist. 54 modules opted *in* to annotation
checking against 1826, so the default was unchecked and every new file
joined the unchecked side. The default is now strict, with a list of
what is not yet annotated that only shrinks. 166 annotations cleared
along the way; the remainder is named module by module.
Two real defects surfaced by the annotations, neither of them typing:
- Workspace._original_open / _original_os were invented by assignment
in lifecycle.patch_process, so unpatch without a patch raised
AttributeError. Declared, and the restore is guarded.
- sed_generic declared a non-optional writer while its own docstring
and its `write_bytes is None` branch said otherwise; the builder
passes None whenever the backend cannot write.
Tests keep the PathSpec rule instead of full strict: measured, full
strict on python/tests is 2374 errors, of which 634 are `str` where a
pydantic field declares SecretStr -- which pydantic coerces at runtime --
and most of the rest is the monkeypatched-fake pattern CLAUDE.md
sanctions. The rule that is violated for real is PathSpec, 19 times, and
scripts/check_test_pathspec.py now holds that line. One of the 19 was a
latent AttributeError: tests/e2e passes a str to s3 write_bytes, which
reads .mount_path, and the test skips without a live versioned bucket.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
0d5915457f |
feat(workspace): state doors and per-plane views (#771)
* feat(workspace): state doors and per-plane views * fix(workspace): one general session door for every variable write Codex round: shell readlink dispatches through the op door; array-shaped assignments and subscripted unsets can no longer bypass pre_session. SessionView.set is now general over variable shapes (str | ShellArray), pre_session_gate is private to the door and takes a SessionContext that carries session_id, staged declaration arrays store through the builtins, for/select loop variables write through the view, and readonly pre-checks are separated from policy-denial catches at every shell site. Also fixes export ARR=(x) printing the export listing and CI formatter drift. * test(integ): pin the state-door fixes; readonly declaration arrays abandon the line * ci(integ): key the WinFsp install on the DLL, not choco's exit code |
||
|
|
ef99849b94 |
refactor(seam): dispatcher-built CommandOpts, typed adapter ops, named ParsedCommand, one DispatchFn home (#609 items 26+23) (#772)
* wip(seam): py dispatcher builds CommandOpts; handlers take (accessor, paths, texts, opts); adapter op protocols; DispatchFn typing Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(seam): dispatcher-built CommandOpts, typed adapter ops, named ParsedCommand, one DispatchFn home (#609 items 26+23) Item 26 (T2-8 seam typing), all four pieces, plus task 23's static FlagView query-name gate: - The python dispatcher (Mount.execute_cmd) now constructs one CommandOpts per invocation and calls every handler as fn(accessor, paths, texts, opts) — the TS convention. Flags stop sharing a bag with injected context, accepts_kwarg opt-in dies, and all 72 builders, 75 bespoke wrappers and ~30 provision functions become pure wiring; the provision path builds the same bag with command/spec set. Generics that took trailing fact params (ls, find, tree, zip, tar, du, file, stat) now read opts.links/opts.mounts/... like their TS twins. - adapter.py gains per-slot op protocols (ReaddirOp/StatOp/WriteOp/...) mirroring adapter.ts, so wiring readdir where stat belongs no longer type-checks; Builder.fn/provision and CommandIO fields are typed. Surfaced and fixed real drift: hf/databricks mkdir had index before parents (a positional parents call would land in index), wget -O dropped its PathSpec value under as_str. - TS parseFlags returns a named ParsedCommand (twin of the py NamedTuple) instead of a 15-slot positional tuple; optionError takes (cmdName, parsed) like python's option_error. - DispatchFn moves to runtime/types.ts (python's home for the same protocol); the crossmount re-export inversion and the CommandDispatch duplicate are gone; 38 python files stop spelling it Callable[..., Any]. The dead CommandOpts.resource field (zero readers) is deleted along with the CrossResourceStub that existed to fill it. - Task 23: tests/commands/test_flag_query_names.py and commands/flag_query_names.test.ts fail on a FlagView query naming a dest no spec bound in the module declares, without needing the code path to run (both verified to fire on a planted typo). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(examples): redis example calls provisions with the 4-positional shape file_read_provision/head_tail_provision/metadata_provision are (accessor, paths, texts, opts) now; the direct demo calls still passed the old command= kwarg, crashing the example before the persistence section (CI examples gate). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(integ): pin the #772 fixes where the harness can reach them - find -empty on chroma + github (the wrappers used to drop the flag, which would flood every path; -empty now rides find_generic), plus a -not -empty composition case on each so the pin has positive output - mkdir -p gains databricks/databricks-prefix (zero prior mkdir coverage on that backend; the param-order bug itself is only reachable positionally, so MkdirOp+mypy is its real gate) - wget -O / curl -o were already pinned by integ/resources/http Both hosts green: py chroma+github 83 ok, ts all five targets 4070 ok. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: bytecii <bytecii@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ae36ac41ee |
test(integ): report-and-continue pins + builder wiring gate
postgres/mongodb mixed good+missing operand facets, history cross-mount continue, test_builders_declare_only_dispatcher_params ratchet; history ls/tree/stat/find and dify find/cat wrappers ride the generics. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
836047e286 |
refactor(commands): builders become wiring; generics own the flags (T2-2)
26 builders forward **flags wholesale; each generic parses once into a frozen struct via a spec-bound FlagView (ls/du/find/sed/tar/tree + 20 mechanical). rm/rmdir/ln/touch are builder-is-implementation and read the bag through FlagView inline, mirroring the TS builders. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
cd47bb87a4 |
fix(traversal): cross a nested mount correctly in du, find, ls -R and tree (#755)
* fix(traversal): cross a nested mount correctly in du, find, ls -R and tree A mount nested inside another mount's tree is invisible to the parent backend's readdir, so the parent's keys under that path are shadowed. Six bugs followed from that, all fixed here in both languages. - du counted the shadowed keys. The generic now opts into MountView and prunes leaves under a descendant mount after the link merge, then recomputes the total from the survivors. - du -c printed one total row per mount. The bytes-level re-total is extracted as merge_du_totals so the executor fan-out reuses the same code, with -h humanizing once at the merge instead of per block. - Fan-out sub-runs never received LinkView, so symlinks vanished from find/du/tree/grep -r exactly when a nested mount existed. - A line whose operands span mounts never fanned out inside an operand, because the cross-mount runner executes one single-mount native run per operand. run_with_fanout wraps that seam; it forwards resolve_hint as well as stdin, which the STREAM strategy needs. - ls -R was filtered a line at a time. Its format is PATH: plus bare names, so the path-first filter dropped the shadowed header and kept its entries, and ate the blank separator. It is filtered a group at a time now. - tree is no longer fanned out at all: its output is one document, so a per-mount block printed a second root, drawing and summary. It crosses inside the generic through a new injected fact, readdir_path, a dispatcher-backed listing offered beside stat_path. Pinned against GNU coreutils 9.7 and tree 2.2.1 on debian:stable-slim. * fix(traversal): fold nested du into ancestors, and stop tree disclosing an ungranted mount Three review findings, all reproduced before fixing. tree crossed a boundary from the mount table alone: a crossing entry's row is synthesized as a directory without asking any backend, so the dispatcher never saw it and could not refuse it. `tree /base` drew and counted an ungranted `/base/private` while ls, find and du all hid it. The child-mount lookup is now session-filtered, the way ls filters the same fact. du keeps the raw list on purpose: an ungranted mount still shadows the parent's keys. du concatenated per-mount blocks, so every ancestor row carried only the parent backend's own bytes. Pinned on coreutils 9.7 over a real tmpfs mount, GNU folds: `du base` prints `7 base/inner` then `17 base`, and `du -s base` prints one row. The blocks are now merged at the leaf level and the tree derived by the same rollup a single-mount run uses, so folding, post-order, --max-depth and -a come from one implementation. rollup grows a `dirs` hint so an empty mount keeps GNU's `0` row, and the mount roots are stat'ed rather than assumed to be directories, because /.bash_history is a mount serving a single file. tree's cross-mount stat threw a bare Error, which isWalkError does not recognize, so a vanished entry rejected the whole run instead of being skipped. It throws a stamped enoent now, matching python's FileNotFoundError. The three nested integ truths encoded the pre-fix output: ls -R had no blank line between groups (GNU 9.7 prints one) and tree showed two roots and two summaries (real tree 2.2.1 over a mount prints one document). |
||
|
|
b88ed40119 |
fix(find): classify the start point once, above every backend (#696)
* fix(find): classify the start point once, above every backend `find` asked the backend to walk a start point that is not a directory, and each answered differently: a Graph 404, a silently empty listing, or ENOTDIR. The reported symptom was `find -L <cross-mount link>`, but the operand rewrite was correct all along: `find /data/a.txt`, with no symlink anywhere, failed identically. Every native find op listed `<start>/children` unconditionally and hardcoded the start as `kind="d"`. Extends the shape #688 built for symlink start points so the start point is classified once and every backend inherits the answer: a symlink goes to link_results, a non-directory is reported and walked no further, a directory walks. The stat rides the op dispatcher as a new `stat_path` fact rather than a threaded callable, for the reason #688 recorded when the same threading left `hf` behind, and because the router resolves `find -L` into another mount before the command runs. Only a positive non-directory answer short-circuits: a stat that sees nothing is not proof of absence when a backend's directories are implicit. Also fixed, found on the way: - `find /d \( -name x \)` classified `(` and `)` as PATH operands, giving find two phantom start points, hidden because the native path reads paths[0] and drops the rest - `tree <file>` diverged three ways by backend; GNU prints the marker, counts the file and exits 0 - Python's missing-operand message printed the path twice instead of `No such file or directory` Fixes #684, which this change needed: gdrive was the one native-find backend that could not be tested at all, so `find` there reached the real Drive API. Widens the fake's patch targets to resolve/tree/rename, teaches it the `name=` filter, and makes an unresolvable target a hard error. The e2e s3 fake needed `list_objects_v2` for the same reason. Pinned against GNU findutils 4.10.0 and tree 2.2.1 on debian:stable-slim before changing behavior. Mirrored in Python and TypeScript. * fix(find): decide absence above the backend, not per backend `find <missing>` exited 1 only on the three backends that wire a stat into find, and `tree <missing>` rendered GNU's marker on twelve of eighteen targets. Both were the same gap: a backend could not tell "absent" from "a directory that exists only as its children", so neither command could either. The rule already existed in the repo. `test -d` has always asked both channels a backend can answer on, and only both coming back empty means nothing is there. `resolve_path_stat` / `resolvePathStat` is now that one implementation, shared by the start-point probe and `path_kind`, so a traversal and `test -d` cannot disagree about whether a path exists. Measured on 18 targets before changing anything: an implicit directory answers, a missing path does not, and an empty `mkdir`'d directory answers everywhere except hf, whose `mkdir` creates nothing and which `root_create_mkdir` already excludes for that reason. Two pre-existing fixture bugs the uniform rule exposed, both of which a silent `find` had been hiding: - TypeScript's integration fixture seeded `store.files` without the ancestor directories, a store no write can produce. RAM keeps directories explicitly, so `ls /data/logs` printed nothing and `test -d /data/logs` was false there already. The python mirror fixture has always `mkdir`'d each ancestor; now both do. - The nextcloud exclusion was missing from `tree_start_missing`'s target list because it needs a real server, which is what turned `integ-data` and `integ-ts` red. Verified against a local `nextcloud:30-apache`, the image CI uses. |
||
|
|
37d734b67e |
feat(integ): jaeger backend, real-server observability integ, cross-backend contract (#643)
* feat(integ): jaeger backend, real-server observability integ, cross-backend contract
Add a jaeger backend and put both observability backends under a real server
in integ, then extend the shared read-only contract across nine backends.
Jaeger backend (python + typescript):
- service-scoped tree: /services/<name>/{operations.json,traces/<id>.json}.
Jaeger's search API requires a service, so there is no listable /traces.
- always sends an explicit microsecond window: `lookback` is ignored by the
query API, so without start/end the search silently returns nothing.
- an unknown service answers 200 with an empty list, so existence is checked
against the service list rather than inferred from an empty listing.
Langfuse fixes found by running against a real self-hosted instance:
- prompt versions were unlistable: the list endpoint returns a `versions`
array, and reading a scalar `version` collapsed each prompt to one 0.json.
- dataset runs rendered as an indented document under a .jsonl name.
- a 404 leaked the raw SDK error instead of ENOENT; a 500 still propagates.
- stat accepted any plausible path without checking it exists.
- typescript requested dataset runs under /v2/, a hard 404 on a real server.
- typescript applied a hidden 7-day window that hid traces cat could serve.
- an unrecognized path resolved to scope "root", so the grep/rg push-down
answered a missing file with every trace in the mount and exit 0.
GNU alignment:
- du reported a missing operand as size 0 with exit 0; now reports it and
exits 1 while keeping output for the operands that exist.
- tree wrote a malformed line to stderr where GNU writes nothing.
ENOENT alignment across backends:
- trello, linear, slack and email readdir returned [] for an unrecognized
path, so ls and tree reported a bogus path as real but empty.
- email selected an unvalidated IMAP folder, leaking "command SEARCH illegal
in state AUTH" to the caller.
- email and slack hand-rolled an ENOENT whose message carried an "ENOENT: "
prefix, and slack's dropped the mount prefix.
- trello typescript readBytes lacked the virtual path python passes, so the
message reported the mount-relative path.
Integ:
- resources/observability/ holds langfuse (135 cases), jaeger (99) and the
36-case shared contract, which runs on nine read-only backends via a new
{mount} token so one case can cover backends with different mount paths.
- targets gain a facet, and the runners gain --facet, so CI runs one backend
family per job: observability, project, email, chat, dify, mem0, core.
- langfuse runs against a six-container compose stack, jaeger against a
single container seeded over OTLP.
Two contract cases stay scoped away from email and gmail: their grep/rg push
down to a server-side search that reports "no matches" for a path that does
not exist. Six typescript-only ENOENT prefix sites remain in box, discord,
gdocs, gdrive, gsheets and gslides.
Also includes the pre-existing session-mode integ migration that was already
in the tree: session_modes scripts replaced by session/modes.json.
* fix(jaeger,langfuse): CodeQL ReDoS, eslint findings, formatter pass
- stat used a /\/+$/ trailing-slash regex in jaeger and langfuse, which
CodeQL flags as js/polynomial-redos on a path from library input. Both now
use the existing loop-based rstripSlash. Python already used str.rstrip.
- errorMessage stringified an unknown errors[0].msg, which could render as
[object Object]; now requires a non-empty string, mirroring python's
truthiness check.
- readdir tested pattern against undefined, which its type excludes.
- drop an unused fixture constant in read.test.ts.
* fix(integ): shell-attributed redirect golden, regenerate specs for jaeger
The ro_write_refused contract case expected "echo: <path>: Operation not
supported", but #635 (which landed in main after this branch was cut)
attributes a failed redirect target to the shell rather than the command, so
the correct stderr has no command prefix. It is the only contract case using a
redirect; mkdir, rm and tee take file operands and keep their prefixes.
Regenerate spec/ for the new jaeger resource (154 one-line additions across
both generators). CI runs gen_specs.py and gen-specs.ts as a separate drift
step, not through pre-commit, so a local pre-commit run does not catch this.
* fix(ci): build mirage-browser in the observability and facet integ jobs
The typescript runner imports mirage-browser statically for the opfs target,
so its dist must exist even in a job that runs no browser target. Both new
jobs hand-roll their setup and built only core and node, so every typescript
host step died with ERR_MODULE_NOT_FOUND. The pre-existing jobs get the build
from the integ-battery-setup action, which is why only these two were hit.
This was masked in the first run: the typescript step is skipped when the
python step fails, so the stale golden hid it.
* fix(jaeger,du): codex review, service-scoped reads and a real request deadline
read() served a trace fetched by id through any service directory, so
/services/<other>/traces/<id>.json returned content that stat and ls both
report absent. It now asserts the service exists and that the trace's own
process table names it. Membership comes from the trace document, not the
service listing, because that listing is windowed and limited and would hide a
trace that really does belong.
The typescript jaeger transport ignored requestTimeout: the config field, the
snake-case mapping and the transport option all existed, but nothing read them
and fetch ran with no deadline, so a stalled endpoint hung the command forever.
Threaded through as seconds, matching python's httpx timeout and dify's
existing requestTimeout convention.
du's operand stat caught every exception and reported it as
"No such file or directory", so an auth failure or a backend bug printed a
wrong reason and a partial total. Narrowed to isMissingPath, matching python's
(FileNotFoundError, ValueError). The test mock rejected with a bare
Error('ENOENT') rather than a stamped FsError, which is what let this pass.
Moved the jaeger resource's command and op imports to module scope, per the
repo's import rule. Verified no cycle: the registry and workspace still import
cleanly. This also matches the typescript resource, which already imports
JAEGER_COMMANDS at module scope.
Integ gains a cat and a stat through a foreign service, the pair that proves
the two agree.
|
||
|
|
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>
|
||
|
|
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> |
||
|
|
5e4e3aa685 |
test(integ): bash parameter-expansion, array, and quoting coverage + expansion-layer fixes (#560)
* test(integ): bash parameter-expansion, array, and quoting cases
100 new GNU-pinned cases (debian:stable-slim bash 5.2): bash/param.json
(52), bash/array.json (31), bash/var.json +9, bash/glob.json +8, seq
500600-500907, all 12 targets (read-only).
Product fixes that rode along (Python & TS + unit tests):
- expand_braces refactor: parse-to-struct + injected expand_child
callback, so operand args expand (${x:-$d}, ${x:-$(cmd)}, ${f%$ext});
pattern operands keep glob chars literal via a dollar-ref pass.
- ${var/pat/rep} is now glob-based longest-match, with /# and /%
anchors and case-mod patterns (${v^^[el]}).
- Substring offsets and array subscripts evaluate as arithmetic
(${v:1+1}, ${a[i+1]}) through the shared evaluator.
- Arrays: negative indices, ${a[@]:o:l} slices, ${!a[@]}, per-element
ops, bare $a, a+=()/v+=/a[i]= forms, literals word-split cmdsubs and
resolve globs; bad negative subscript aborts the line like bash.
- Shared fnmatch treats [^...] as negation (bash/glibc): new
mirage/utils/fnmatch.py swept over 10 call sites; TS classBody fix.
- Adjacent ${} expansions in dquotes no longer lose the separating
space (it hides in the next node's leading token).
Deliberate divergences documented in the PR plan: ${v:$o} spelling
(tree-sitter grammar limit; ${v:o} works), quoted "${a[@]:1:2}" word
count, dense arrays, IFS-unaware "$*", no stderr warning on negative
OOB reads.
Both hosts pass all 1081 JSON cases (ram+disk python, ram TS).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(integ): pin case/find caret-class negation and local indirection
Close the probe-only gaps: case patterns and find -name share the
fnmatch [^...] fix, and ${!ref} resolves function locals. 4 cases
(case_caret_class, case_bang_class, find_name_caret_class,
param_indirect_local), GNU-pinned via docker.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: bytecii <bytecii@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
1c193bc4e8 |
Optional-typing sweep: path-only generic ops, NULL_INDEX, no bare generics (#541)
* refactor(types): stdin annotations use the ByteSource alias * refactor(generics): injected ops are path-only, accessor+index bind at the wrapper Generic commands no longer take accessor (or a dead index) just to thread them back into injected callables. Builders and bespoke wrappers bind both via bound_op (None-passthrough) or partial for write-side ops, mirroring the TS builders' closures. call_*/resolve_pattern/relay helpers and the cache read-through wrappers gain path-first forms; CommandIO-level ops keep the raw (accessor, path, index) shape. * refactor(index): NULL_INDEX everywhere, no index-is-None branches index is a required IndexCacheStore on every op and wrapper; callers with no real index pass NULL_INDEX (the null object built for exactly this). Dead is-None guards drop out of trello/linear/github_ci/google readdir+stat, CacheManager, and the github wrappers. * style(types): parameterize every bare generic, gate with disallow_any_generics dict -> dict[str, Any] (payloads) / dict[str, object] (flag bags), Callable -> Callable[..., Any], plus list/tuple/Awaitable/Pattern/ Task/Future/Token/AsyncMongoClient. The mypy gate now carries disallow_any_generics so bare generics cannot come back. * style: pre-commit formatting + parameterize main's new bare dicts * fix: post-merge gate fixes + make security barriers analyzer-recognizable - resolve_within_root: startswith(root + sep) guard instead of commonpath (same semantics; the shape CodeQL models as a path-injection barrier) - cli table output: trimEnd() instead of the polynomial /\s+$/ trim - example proxies: re-encode validated endpoint segments before URL construction (recognized SSRF barrier; no-op for the allowed charset) - parameterize bare generics arriving from #540 (the new mypy gate caught them) * fix(server): plain startswith barrier in resolve_within_root |
||
|
|
31340ad9ca |
refactor(shell): raw_path owns word spelling; respell lazy drain errors
- relative_spec: one constructor for typed word + cwd -> PathSpec - raw_path is total (defaults to virtual); PathSpec.display deleted - word_text/wordText: single str-or-spelling coercion helper - rebase_display -> rebase_raw, nullable param dropped - lazy-stream drain errors respell operands as typed via ExecutionNode.paths (TS drain now also formats GNU lines) - integ: relspell battery (walker labels, error spelling, ./ glob, du trailing slash) |
||
|
|
4da17edb3f |
refactor(paths): pure-virtual PathSpec, mount stamps resource_path
- rename original -> virtual, as_typed -> raw_path - remove prefix field and strip_prefix/key properties - add required resource_path stamped by the mount at dispatch - add strip_mount/mount_key/rekey/mount_prefix_of in utils/key_prefix - mirror in TypeScript (resourcePath, rawPath, mountKey et al) |
||
|
|
e80af8fe0b | feat: GNU-aligned cwd/env, subshell isolation, and relative-path display | ||
|
|
2671ad7517 |
fix(commands): terminate generated record output with newlines (#147)
* fix(commands): terminate generated record output with newlines
Add a shared record-output helper for commands that build logical text rows, and use it across affected find/grep/rg/ls/tree/stat/du/md5/cmp/file/wc call sites.
This keeps byte-preserving commands untouched while making generated command records end with POSIX-style trailing newlines. Update focused generic, RAM/Redis, and mocked Discord tests for the new output contract.
* fix(commands): terminate du and rm verbose record outputs
* test(commands): update assertions for terminated command output
The newline-termination fix makes find/grep/rg/ls/stat/tree/wc/du
emit a trailing newline on the last record. These assertions still encoded
the old separator-style output (no final newline) and failed in CI:
- dify/discord/github/workspace: expect a trailing "\n"
- notion: switch to splitlines() so the terminator doesn't add an empty
member to the compared set
- github wc: rstrip the newline before splitting on tab
- s3 integration: expect the blank separator line that `echo >>` always
intended — the missing grep terminator had previously swallowed it
Test-only; no production code changed.
* fix(commands): terminate grep/rg -c count output
* refactor: use format_records at remaining grep/rg/wc sites; add header to output.py
DRY-only: every changed site is guaranteed non-empty (or guards the empty
case separately), so output is byte-identical. discord stderr helper uses
format_optional_records to preserve None-on-empty.
---------
Co-authored-by: Zecheng Zhang <zechengzhang97@gmail.com>
|
||
|
|
0ac9cecca9 |
refactor(commands): consolidate cloud backend wrappers (s3/gdrive/google workspaces) (#70)
* refactor(commands): consolidate s3/gdrive/gdocs/gsheets/gslides/gmail wrappers Apply the same thin-wrapper + generic-dispatch pattern that landed for ram/disk/redis in #68. Each cloud backend wrapper now does just three things: glob-resolve paths, inject backend-specific callables (read_bytes / read_stream / readdir_fn / stat_fn / mkdir_fn), and pass through stdin/options to the generic implementation. Backends touched (84 wrappers, net -2487 lines): - s3: 34 wrappers - gdrive: 33 wrappers - gdocs / gsheets / gslides / gmail: 4 each (basename, dirname, realpath, tree) Kept resource-specific (per "use resource-specific when generic doesn't fit" guidance): - s3/du, s3/find: custom S3 listing semantics - gdrive/du, gdrive/find, gdrive/sed: no core/gdrive/du, find, or write - gdocs/gsheets/gslides/gmail jq, nl, find: no core/stream or core/find - Folder-based commands (cat/, head/, tail/, ls/, stat/, file/, cut/, grep/, wc/): dispatch over format variants (parquet/feather/etc.) - gws_* domain-specific Google commands: no generic equivalent For Google backends that only expose `read` (no `read_bytes`), the wrappers alias `from mirage.core.<b>.read import read as read_bytes`. This makes gdrive's special-format handling (.gdoc.json / .gsheet.json / .gslide.json -> JSON bytes) transparent to the generic commands: they see "just bytes" regardless of source format. gdrive/__init__.py renamed `sort_cmd` import to `sort` for consistency with other backends. Streaming semantics preserved verbatim: read_stream callables continue to chunk-iterate S3 GetObject responses, backpressure propagates through multi-stage pipes (verified end-to-end on s3 and gcs -- `cat | tr | grep | head -n 1` downloads only 8 KB of a 10 MB object). Two pre-existing example bugs fixed while verifying: - examples/python/s3/s3.py: Workspace.load was passing only /s3/ override when the workspace had both /s3/ and /deep/ mounts. Added the missing /deep/ override. - examples/python/gcs/gcs.py: called ws.reset() which never existed on Workspace. Replaced with ws.cache.clear() (which is what the surrounding code clearly intended given the cache.get() assertions). Also extended the example with streaming + multi-stage pipe backpressure demonstrations. .gitignore: added docs/learnings/ alongside docs/plans/ for local-only notes. Tests: 6203 passed, 266 skipped (no agent tests run). Pre-commit: all hooks pass. * fix(gdrive): thread index into delegated command closures (gdrive/gdocs/gsheets/gslides/gmail) * test(integ): add moto-based S3/GCS cross-backend integ harness * refactor(commands): promote index to an explicit param in backend command wrappers * refactor(filetype): redirect ls/file for parquet/orc/feather to shared core handlers * refactor(commands): delegate cloud/s3 content commands to generic layer - thread index through generic ls/stat so delegated cloud stat works in walk - migrate gdocs/gdrive/gmail/gsheets/gslides/s3 cat/head/wc/ls/stat/jq/nl off inlined logic - remove dead package-shadowed cat/head modules * test(integ): add streaming byte-accounting cases to s3/gcs harness Clear cache, run, and report bytes pulled from the backend for early-exit commands (head -c/-n, grep -m) vs a full read, per mount. Output is deterministic (no timing) and identical across s3/gcs, proving parity. * refactor(integ): iterate s3/gcs cases over a single MOUNTS loop * fix(commands): use index fast-path for stat in tree/grep/rg - generic tree threads index to stat (uniform with ls); cloud tree wrappers stop binding index in the stat partial - s3 grep/rg bind index into call_stat so per-path stat hits the index - integ/s3.py asserts API-call counts: ls -l and tree do 0 HeadObject, proving stats resolve from the index readdir populated * fix(ls): use index fast-path for filetype rendering in directory ls -l render_long_entry passed a prefix-less str to the filetype handler, so cloud handlers hit the wrong key and silently fell back to standard format while wasting HeadObject/GetObject calls. Build a prefixed PathSpec and thread index through; s3 ls filetype handlers now stat via the index. Adds parquet/orc/feather cases to the s3/gcs integ harness. * chore(commands): remove stray no-op expression statements Drop leftover bare statements: 'index' in email/rg.py and 'accessor.store' in ram/redis stat provision. No behavior change. |
||
|
|
38a9689afa |
refactor(commands): delegate backend builtins to shared generic layer (#77)
* refactor(commands): delegate backend builtins to shared generic layer Route per-backend basename, dirname, cut, sort and friends through the generic/*.py implementations with injected I/O callables, add exclude and verbose handling to generic tar, and fix core read/readdir/stat mount-prefix handling. Adds path_helper and utils/lines shared helpers; threads the index cache through github sort's read. * test(integ): regenerate truth.txt for corrected cut/ls -R/grep -l/find output * refactor: move gnu_basename/gnu_dirname to utils/path * test(utils): add unit tests for gnu_basename/gnu_dirname/resolve_path |
||
|
|
870016c5b4 |
refactor(commands): generic dedup + cross-backend integ harness (#68)
* feat(utils): add stream/bytes conversion helpers
* feat(commands): add generic cat as async iterator
* refactor(commands): ram cat wrapper uses generic
* test(commands): expand generic cat coverage; fix $ on trailing partial line
Old _number_lines_stream had loose tests that missed three POSIX edge cases:
multi-digit line number alignment (%6d vs literal 5-space prefix), trailing
newline preservation, and $-marker placement on partial last lines. Added
unit coverage for all three, plus combined-flag scenarios and a
one-byte-at-a-time chunking test.
The trailing-$ test caught a real bug: my generic was emitting "hello$" for
`cat -E` on input without a final \n, but BSD/GNU cat only emit $ before \n.
Fixed by dropping the suffix from the trailing-partial-line branch.
* test(ram): add 1:1 test file for ram cat wrapper
Establishes src↔test correspondence (every src file has a matching test_*.py)
for the ram cat wrapper. Covers the wrapper's behavior end-to-end via
Workspace.execute, including regression tests for the two POSIX bug fixes
that landed via the generic refactor:
- cat -n multi-digit line number alignment (lines 1-12)
- no-trailing-newline preservation for both cat and cat -n
Plus multi-file concatenation, empty input, and the all-blank-lines edge case.
* refactor(commands): disk cat wrapper uses generic + 1:1 tests
The active disk cat is at disk/cat/cat.py (the disk/cat package shadows the
top-level disk/cat.py file, which is dead code). Wires through generic_cat,
preserves CachableAsyncIterator/IOResult shape, drops _number_lines_stream.
Tests cover the same POSIX edge cases as ram cat: multi-digit alignment,
no-trailing-newline preservation, empty input, all-blank-lines.
* refactor(commands): redis cat wrapper uses generic + 1:1 tests
Structurally identical to ram cat migration. Tests use the same REDIS_URL
skip pattern as tests/core/redis/conftest.py — they run when REDIS_URL is
set in the environment, skip otherwise.
* fix(commands): preserve cachable identity in cat wrappers for cache to populate
Bug: wrapping cachable in generic_cat unconditionally caused stdout and
io.reads[path] to reference different objects. mount.py:_wrap then wrapped
them separately, and wrap_cachable_streams only updated stdout when
identity matched (stdout is stream). The cache cachable was never iterated
by stdout materialization, so the background drain saw an exhausted source
and cached empty bytes.
Fix: only wrap with generic_cat when a flag (currently just -n) actually
requires line processing. For the plain 'cat path' case, return cachable
directly — matching the pre-refactor behavior and keeping stdout identical
to the value in io.reads[path].
Tests caught: test_cache_hit_serves_from_ram, test_grep_uses_cache,
test_fuse_read_uses_cache_when_populated (workspace cache layer).
* feat(commands): add generic head with -c fast path and -n line-buffered path
* refactor(commands): ram head wrapper uses generic + 1:1 tests
* refactor(commands): disk head wrapper uses generic + 1:1 tests
* refactor(commands): redis head wrapper uses generic + 1:1 tests
* test(commands): backfill generic cat/head coverage missing from old helpers
Audited tests against legacy head_helper/cat_helper test files. Added:
cat:
- empty input passthrough (no flags)
- binary passthrough across full 0..255 byte range
- show_ends only marks newlines, not other binary bytes
head:
- n=1 single-line output
- empty input (with and without -c)
- single line no newline with default n
- c=negative emits nothing
* refactor(commands): consolidate ram/disk/redis logic into generic modules
Phases K through Z extracted shared per-command logic into
mirage/commands/builtin/generic/<name>.py modules that accept VFS
callables (read_bytes, read_stream, write_bytes, stat, readdir, mkdir)
via dependency injection. Per-backend wrappers (ram, disk, redis) are
now thin shims that resolve globs, supply backend-specific callables,
and delegate to the generic.
Scope:
- 57 generic modules
- 170 backend wrappers reduced to shims
- 67 new generic-level tests under tests/commands/builtin/generic/
- Net -7,772 lines (178 files changed: +1,891 / -9,663)
Bug fixes converged during refactor (no backward compat needed):
- disk/md5 emitted only the digest; now matches coreutils `{digest} {path}`
- disk/sort and disk/nl only operated on paths[0]; now process all paths
- disk/awk lacked BEGIN/END/accumulator support; converged on ram/redis
- disk/unzip flattened archive directory structure; now preserves it
- disk/patch used offset-splice algorithm; converged on lockstep walk
(closer to real patch(1) semantics)
- rg failed to auto-recurse on bare directories; now matches ripgrep
- file command silently swallowed read errors; now logs to logger.debug
Cleanup:
- Deleted 3 dead top-level head.py files shadowed by head/ packages in
ram/disk/redis (Python package always wins over .py at same level)
- Removed 4 stray __init__.py files under tests/ per CLAUDE.md rule
- Replaced inline string literals with enums (FindType, LsSortBy)
- Removed bare `try/except: pass` for mkdir using idempotent
mkdir(parents=True) instead
* test(integ): add cross-backend equivalence harness with 240 cases
New /integ folder runs identical command suite across ram/disk/redis
and diffs each backend's output against committed integ/truth.txt
canonical fixture. CI workflow (.github/workflows/test_integ.yml)
boots a redis service and runs all three backends.
Also fixes bugs surfaced by running the suite:
- disk/cat: chain multiple files (was processing only paths[0])
- sha256sum: use original path string; -c mode resolves mount prefix
- md5: support stdin when no paths given
- jq: iterate per-record on .jsonl files (real jq semantics)
- xxd -r: strip offset prefix and ASCII suffix before unhexlify
- find -mindepth/-maxdepth: off-by-one across ram/disk/redis/s3
- find -o vs -or: handle both spellings
- awk: \$NF/\$NR resolve via field indirection; bare condition
programs (e.g. '\$2 > 28') treated as condition with default
print \$0; && and || compound conditions; BEGIN block executes;
END can read NR
- diff: default to GNU normal format (1,5c1,3 with < / > / ---);
-u flag required for unified output
- cmp: use 'char N' instead of 'byte N' (matches GNU/BSD)
Extracts:
- DiffOpTag StrEnum (equal/delete/insert/replace) in diff_types.py
- AwkCmpOp / AwkBoolOp / AwkBlock / AwkBuiltin StrEnums plus
FIELD_PREFIX / PRINT_STMT / CMP_OP_PATTERN constants in
generic/awk_types.py
* fix(integ): preserve trailing whitespace in truth.txt + redis find test
The trailing-whitespace pre-commit hook stripped tabs at line-end in
integ/truth.txt, which broke the diff against backends (paste pads
shorter columns with trailing tabs). Exclude integ/truth.txt from
end-of-file-fixer, mixed-line-ending, and trailing-whitespace hooks.
Also fix tests/core/redis/test_find.py {maxdepth, mindepth} that
asserted the previously-buggy depth math (matches the existing
ram/disk test updates).
|