ebec2d6926
* refactor(ts): four shared factories mirroring the Python originals (#609 item 24 / T2-9) TypeScript had grown four copy-drift sites where Python already has one shared module. Each is extracted onto the Python shape, so the two trees line up module-for-module. builtins/shared.ts (twin of workspace/executor/builtins/shared.py). Three independent errorResult()s with three different signatures — capacity.ts hardcoded the 'df' command, links.ts hardcoded exit 1, only metadata.ts took both — so a GNU exit-code fix could not be shared. They collapse onto result/ok/fail/finish, plus operandText/absPath/splitFlags/splitValueFlags/ expandOperands. Drops links.ts's re-implemented abs+splitFlags and capacity.ts's import of splitValueFlags from metadata.ts (df reaching into chmod/touch). `Result` moves here from scope.ts, where Python keeps it in shared.py; the other 12 builtins change only their import line. The chmod/chown/chgrp/touch handlers each tracked an exitCode variable that was only ever set to 1 next to an errors.push, so finish() derives it from `errors` exactly as Python does and the variable is gone. workspace/executor/interpreter.ts. python/handle.ts and js/handle.ts were the same 130 lines apart from the label, the unavailable-error class, the payload flag, Python's -m guard and -x, and js's .mjs module flag. makeInterpreterHandler takes those as a spec; both handle.ts keep their path, their exported name and their signature, so callers and runtime.test.ts's dynamic import are untouched. The .mjs check moves into the js wrapper, which is where Python's js.py keeps it. Kept in workspace/executor rather than beside commands/builtin/general/ interpreter.ts because no commands/ module imports workspace/ today and this should not be the first. makeRm. generic/rm_command.ts already existed and gcal already used it; the gdocs/gsheets/gslides bodies were verified character-identical to it modulo indentation, so the three 79-line files become 20-line bindings. core/google/tree_ops.ts (twin of core/google/tree_ops.py). makeStat and makeUnlink over a backend's readdir; the six modules stay as real files exporting `stat`/`unlink`, so the barrel and the provision/io importers do not move. The three local eisdir() copies give way to the shared one in utils/errors.ts: isFsError keys on `code`, which is unchanged, and its message (the bare virtual path) is what Python's IsADirectoryError(raw) already produced. No behavior change intended. -995/+147 across the 26 edited files. * fix(js): honor the explicit `-` stdin operand, like python3 and node `js - a b` read a file literally named `-` and failed with "js: /-: No such file", while `python3 - a b` on the same tree read the program from stdin. Python has no such split: resolve_source's STDIN_OPERAND branch (interpreter.py) is unconditional, so both interpreters honor the operand, and js.py inherits it by taking the default Argv0Rules. Real `node -` agrees — it reads the program from stdin and keeps the following words as argv. Verified against Python by calling resolve_source directly for the js call shape: `js - a b` yields mode=stdin, args=['a', 'b'], no script path. Both quickjs hosts then build scriptArgs the same way (an absent prog leaves the args alone), so the operand lands identically. Found while extracting the shared interpreter handler in the previous commit; kept separate because it is a behavior change, not a move. The unit test fails without this fix (exit 1, not 0). The integ case pins py=ts. * test(ts): mirror the Python suites for the two new shared modules Python has tests/workspace/executor/builtins/test_shared.py and tests/core/google/test_tree_ops.py; TypeScript had neither twin, and both modules are now shared chokepoints rather than per-command code. shared.test.ts ports test_shared.py case for case: the result triple (ok/fail/finish, including finish keeping a carried IOResult's writes and deriving the exit code from `errors`), operandText/absPath, the three splitFlags cases, the three splitValueFlags cases, and the expandOperands glob expansion over a real RAM mount. tree_ops.test.ts covers makeStat over a stub readdir: the synthetic owned/shared roots answer as directories without a readdir round trip, an indexed document is served straight from the index, a miss warms the parent and then raises ENOENT, and a call with no index raises ENOENT without reaching the backend. It deliberately does not port test_tree_ops.py's one case — that a readdir raising a non-fs error propagates out of stat. TypeScript swallows it in a bare catch and reports ENOENT instead, which is a real pre-existing divergence in the T1-G "honest errors" family; pinning current TS behavior here would entrench it. Filed separately. * refactor(ts): keep the interpreter opt interfaces module-private Nothing outside interpreter.ts imports them — both handle.ts wrappers pass object literals — so knip reported them as unused exports. * fix(google): only an absent parent may collapse into ENOENT (py+ts) Every Drive-family stat/read/unlink warms a cold index by listing the parent, then retries. Python narrows that call to FileNotFoundError, so an auth failure or a dropped connection reaches the caller; TypeScript swallowed it in a bare `catch {}` and reported "No such file" instead. Five TS sites did this (google/tree_ops stat+unlink, gdrive stat, gdrive read, gdocs/gsheets/gslides read) — seven catch blocks in all. tree_ops.unlink was wrong in the other direction: Python called readdir completely unguarded, so removing a path under a missing directory reported the parent rather than the operand. GNU names the operand: $ rm nodir/file.txt rm: cannot remove 'nodir/file.txt': No such file or directory (pinned against coreutils in debian:stable-slim). Both sides now use the shape Python's make_stat already had: swallow a not-found parent, let everything else through. TypeScript spells it with the existing isEnoent(), the twin of `except FileNotFoundError`. gdrive/readdir warms a parent unguarded in both languages — already aligned, left alone. Tests fail without the fix. Two of them mirror Python suites that had pinned this since before the TS port (test_stat_propagates_parent_ refresh_failure, test_read_propagates_parent_refresh_failure); the tree_ops pair is new on both sides. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(cache): one entry_or_warm for every cold-index lookup (py+ts) Twelve call sites across eight backends had grown their own copy of "ask the index, list the parent if cold, retry, else ENOENT", each with its own idea of what a failed listing means. They now share one function in cache/index/warm, so exactly one catch in the tree decides that. The previous commit narrowed seven of those catches by hand; this makes the narrowing structural instead, and sweeps the five that commit did not reach: box/read (x2) bare catch, py narrowed -> divergence gmail/read bare catch, py narrowed -> divergence dropbox/read (x2) bare catch, py caught (FileNotFoundError, DropboxApiError) -- but readdir already turns the API's 409 for a missing path into ENOENT, so that second arm only ever swallowed live API failures. Python had the same bug here, from the other side. gdrive/readdir warms a parent unguarded in BOTH languages; already aligned, deliberately left alone. Also folds eight private eisdir() copies into the shared helper. The copies built `EISDIR: ${p}` and never stamped virtualPath, and execute.ts renders errorVirtualPath(err) straight into stderr, so a directory read printed cat: EISDIR: /docs/owned: Is a directory where Python printed the GNU form. Two TS tests asserted on that message with /EISDIR/ and so had been holding the bug in place; they now assert the stamped code and virtualPath, which is what Python's IsADirectoryError(virtual) carries. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(integ): pin the Drive-family cold-lookup error paths on both hosts Five cases over the gapps and gdrive targets, all curated by running the battery and reading the real output rather than guessing the strings: gapp_cat_dir_is_gnu cat /docs/owned -> Is a directory gapp_cat_missing_parent cat .../nope/x.gdoc.json -> names the operand gapp_ls_missing_parent ls .../nope -> ENOENT, exit 2 gapp_rm_missing_parent rm .../nope/x.gdoc.json -> names the operand gdrive_cat_dir_is_gnu cat /data/sub -> Is a directory clear_cache forces the cold index, which is the branch entry_or_warm now owns. Both wordings match GNU pinned in debian:stable-slim (`cat: <path>: Is a directory`, exit 1). Two things these cases do NOT prove, so nobody reads more into them: - rm builds its own diagnostic from p.rawPath, so it reports the operand whatever the backend error carries. The unlink operand-naming fix is pinned by test_tree_ops.py, not here. - `cat` on a directory is answered by the read generic off stat, so the eisdir consolidation is pinned by the gdrive/dropbox unit tests (which assert virtualPath, the field errorVirtualPath reads), not by these. They sit at seq 930030+ deliberately: at 520100 their clear_cache wiped the index that gdocs_prov_cat (930023) expects warm, and that case started failing on hits=1 vs hits=0. Verified: gapps + gdrive, 2193 passed / 0 failed, on the typescript-node host and again on the python host. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * style: formatters over the warm-helper sweep, + the py eisdir message pin isort regroups the hand-inserted `partial` / `entry_or_warm` imports and prettier rewraps one dropbox thunk; no behavior change. Also completes the eisdir work on the Python side: the gdrive read test asserted only the exception type, so it would not have caught the "EISDIR: " prefix the deleted TS copies were adding. It now asserts the message is the bare operand, which is the string the TS twin pins through virtualPath. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(gate): lower the layout-parity baseline to 301 The ratchet fails on a drop as well as a rise, so a closed divergence has to be locked in. Net -1 from three real movements: -1 core/google: python-only ['tree_ops'] tree_ops.ts now exists -1 workspace/executor/builtins: python-only ['shared'] shared.ts now exists +1 workspace/executor: typescript-only [... 'interpreter'] The +1 is the deliberate layering call from this PR: the shared interpreter handler lives under workspace/ rather than beside Python's twin in commands/builtin/general/, because no commands/ module imports workspace/ today and this should not be the first. Recording it here so the net -1 does not hide that one improvement paid for one new divergence. cache/index/warm does not appear: it landed on both sides, so it is neutral. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(spec): js/node end option parsing at the first operand, like python `node - -e x` ran `x` instead of the piped program, and `node s.js -m` flipped the interpreter into module mode instead of handing s.js its own -m. Both real interpreters disagree, pinned locally: $ echo 'console.log(process.argv.slice(1))' | node - -e 'nope' [ '-', '-e', 'nope' ] $ echo 'import sys; print(sys.argv)' | python3 - -c 'nope' ['-', '-c', 'nope'] (node 22.8.0, CPython 3.13) `Operand.remainder` (argparse nargs=REMAINDER) already exists and both parsers already honor it in one line — the first operand ends option parsing, so every word after it is an operand verbatim. `python`/`python3` have carried it all along; js/node simply never got it, in EITHER language, so this is a symmetric py+ts spec fix rather than a TS-only one. Verified by calling parse_command directly: js/node/python/python3 now agree on `- -e PROG` and on `s.js -m --module`, while a flag BEFORE the first operand is still the interpreter's (`js -m -e CODE a`). One consequence beyond the reported case: remainder also turns off lenient_dash_operands, so `js -Z foo` now reports an invalid option instead of silently treating `-Z` as the script path (which produced "js: /-Z: No such file"). Real node agrees — `node -Z foo` prints "node: bad option: -Z". Nothing in integ or the suites relied on the old leniency. Reported by codex on #792. Its premise was wrong in one respect — it said to mark the rest operand "as the Python specs do", but Python's js/node spec is Operand(type="str") with no remainder, so Python had the same bug. Fixing TS alone would have created a divergence. Tests: 4 parametrized parser cases per language over the real js/node/ python/python3 specs, plus a quickjs end-to-end case. All 4 py and all 5 TS fail without the spec change (python/python3 pass either way, which is what makes them the control). integ/runtime/quickjs.json `stdin_operand` gains the -e and -m steps. Verified: pytest 14159 passed / 0 failed; pnpm -r typecheck clean; core suite 8129 passed (the one python_repl timeout is the known Pyodide-boot-under-load flake, 10/10 alone); runtime/run.ts quickjs 11 passed / 0 failed on the typescript host; spec drift, spec parity (93 match), layout parity (301 = baseline) and width table all clean. The python host's quickjs suite needs the MIRAGE_QUICKJS_HOME WASI blob that CI downloads, so that leg is covered by test_integ.yml rather than locally. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(quickjs): end qjs's own option parsing before the program's argv The python host's quickjs runtime appends the program's arguments straight onto the qjs command line: argv += ["-e", args.code, *named, *args.args] Unlike CPython's -c, qjs's -e does NOT end option parsing, so any program argument that spells a qjs switch was read as one. Pinned against the build CI uses (quickjs-ng v0.15.1 qjs.c): the option loop runs while `*argv[optind] == '-'`, breaks only on a bare `--`, and scriptArgs start at `argv + optind`. That is what made integ-runtime red on the previous commit, and only on the python host: node - -e 'console.log("flag")' -> flag (second -e wins) node - -m -> (empty) (module mode flipped) Both are now `--`-separated, so they land in scriptArgs where the parser already put them. qjs consumes the `--` itself, and the -e branch still wins over any filename, so a named program keeps scriptArgs[0]. Surfaced by the remainder fix in the previous commit rather than caused by it: before that, mirage's own parser swallowed those words, so they never reached the runtime. Any program argument spelling a qjs switch had this hole -- `node script.js -m` included. TypeScript needs no twin. Its quickjs runtime is in-process and assigns scriptArgs as a global (`quickjs.ts:387`), never building a qjs command line, which is why the TS host passed the same integ steps. Checked the other python runtimes too: wasi.py and local.py both go through CPython `-c`, which DOES end option parsing (pinned: `python3 -c prog -e FLAG -m` puts `-e FLAG -m` in sys.argv), and monty passes argv as a data global. quickjs was the only one. Verified with the real engine, after downloading the pinned qjs-wasi.wasm: `runtime/run.py quickjs` on the python host is 11 passed / 0 failed with this change and reproduces CI's exact two-line failure without it; the whole python runtime battery is 99 passed / 0 failed (bridge/docker/wasi skip on unmet env). pytest 14192 passed / 0 failed -- 33 more than the previous run, because the @live quickjs tests now execute instead of skipping. The three new unit tests pin the argv shape without needing the build, and all three fail without this change. 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>
36 lines
763 B
JSON
36 lines
763 B
JSON
{
|
|
"_meta": {
|
|
"by_resource": {
|
|
"": {
|
|
"filetypes": [],
|
|
"has_aggregate": false,
|
|
"has_provision": false,
|
|
"has_write": false
|
|
}
|
|
},
|
|
"filetypes": [],
|
|
"has_aggregate": false,
|
|
"has_provision": false,
|
|
"has_write": false,
|
|
"resources": []
|
|
},
|
|
"description": "Run JavaScript on a sandboxed quickjs engine.",
|
|
"options": [
|
|
{
|
|
"description": "Evaluate the next argument as a script.",
|
|
"short": "-e",
|
|
"type": "str"
|
|
},
|
|
{
|
|
"description": "Run as an ES module (top-level import/export/await); .mjs files select this automatically.",
|
|
"long": "--module",
|
|
"short": "-m",
|
|
"type": "bool"
|
|
}
|
|
],
|
|
"rest": {
|
|
"remainder": true,
|
|
"type": "str"
|
|
}
|
|
}
|