mirror of
https://github.com/morluto/rea.git
synced 2026-11-13 18:49:25 -10:00
Compare commits
73
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
756a588477 | ||
|
|
e1d229cd6b | ||
|
|
01fcf8e3d7 | ||
|
|
0a3368a6a9 | ||
|
|
3986bf9f8e | ||
|
|
bddbd80afb | ||
|
|
f1e250ee76 | ||
|
|
71712f27f7 | ||
|
|
fcc0a8c454 | ||
|
|
243e4015cb | ||
|
|
4a4e959513 | ||
|
|
4e1e9b62a7 | ||
|
|
54677ec1d6 | ||
|
|
1abd327913 | ||
|
|
186b2c352c | ||
|
|
827b31c128 | ||
|
|
ee339e5db5 | ||
|
|
6ce708cfde | ||
|
|
884b683a94 | ||
|
|
8930dd8c31 | ||
|
|
5815178a8d | ||
|
|
87743d67f5 | ||
|
|
d3efeb0c36 | ||
|
|
46728ca41d | ||
|
|
62798fa8b0 | ||
|
|
5bad48e122 | ||
|
|
cc7d73d23a | ||
|
|
da91bbd4eb | ||
|
|
3403338ea0 | ||
|
|
4566e483ab | ||
|
|
bf63760cb4 | ||
|
|
3b006841e2 | ||
|
|
3cb75d46bf | ||
|
|
b67c1e8ce6 | ||
|
|
cdc4541ef7 | ||
|
|
4b26f38d6f | ||
|
|
e841fb16e4 | ||
|
|
26b70c8ea4 | ||
|
|
5d046b9ad7 | ||
|
|
c4cec6373d | ||
|
|
02a33a8f58 | ||
|
|
ea101dbe30 | ||
|
|
fd3e613d23 | ||
|
|
adf8bdf833 | ||
|
|
cd96c5e9eb | ||
|
|
617e4cbfb3 | ||
|
|
bbe2bcc979 | ||
|
|
5cddc69c51 | ||
|
|
b2c4635a36 | ||
|
|
15a2de737c | ||
|
|
c2ca3819e5 | ||
|
|
54906fdb1f | ||
|
|
0084810ef7 | ||
|
|
feca30c72c | ||
|
|
913b029c96 | ||
|
|
bfecbb1d48 | ||
|
|
070cfce443 | ||
|
|
0c01517cc0 | ||
|
|
7c9d65fe97 | ||
|
|
9c86465d47 | ||
|
|
c836f57b7d | ||
|
|
6878a84796 | ||
|
|
937d10df1c | ||
|
|
c19d319929 | ||
|
|
966bde3780 | ||
|
|
4d15b7f509 | ||
|
|
50d4f7bea4 | ||
|
|
594d73985c | ||
|
|
418e3f5e91 | ||
|
|
0f5ac9d5f3 | ||
|
|
7559e06740 | ||
|
|
9d32836d37 | ||
|
|
02ebd3e02d |
No files matched your search
@@ -755,7 +755,9 @@ jobs:
|
||||
needs: [changes, static, build]
|
||||
if: needs.changes.outputs.apple == 'true'
|
||||
runs-on: macos-15
|
||||
timeout-minutes: 15
|
||||
# Passing runs take 10-15 minutes on macOS runners of varying speed, so a
|
||||
# 15-minute budget cancelled about a third of runs. Keep a hang bounded.
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
|
||||
+24
-1
@@ -63,6 +63,18 @@
|
||||
"rea/no-shared-module-mock": "error"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["src/domain/**/*.ts"],
|
||||
"rules": { "rea/no-locale-compare": "error" }
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"src/domain/**/*.test.ts",
|
||||
"src/domain/**/*.spec.ts",
|
||||
"src/domain/**/*.fixture.ts"
|
||||
],
|
||||
"rules": { "rea/no-locale-compare": "off" }
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"scripts/electron-active-hook*.cjs",
|
||||
@@ -76,12 +88,19 @@
|
||||
{
|
||||
"files": ["src/**/*.ts"],
|
||||
"rules": {
|
||||
"rea/no-babel-visitor-keys": "error",
|
||||
"typescript/no-misused-promises": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["tests/**/*.ts", "src/**/*.test.ts", "src/**/*.fixture.ts"],
|
||||
"files": [
|
||||
"tests/**/*.ts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.fixture.ts"
|
||||
],
|
||||
"rules": {
|
||||
"rea/no-babel-visitor-keys": "off",
|
||||
"max-lines": "off",
|
||||
"max-lines-per-function": [
|
||||
"error",
|
||||
@@ -94,6 +113,10 @@
|
||||
"typescript/no-misused-promises": "warn"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["src/domain/javascript/javascriptSemanticTraversal.ts"],
|
||||
"rules": { "rea/no-babel-visitor-keys": "off" }
|
||||
},
|
||||
{
|
||||
"files": ["src/application/**/*.test.ts"],
|
||||
"rules": {
|
||||
|
||||
@@ -464,7 +464,9 @@ class ReaJadxBridge {
|
||||
if (element.getTagName().equals("uses-sdk")) {
|
||||
minSdk = element.getAttributeNS(android, "minSdkVersion");
|
||||
targetSdk = element.getAttributeNS(android, "targetSdkVersion");
|
||||
} else if (element.getTagName().equals("uses-permission")) {
|
||||
} else if (element.getTagName().equals("uses-permission")
|
||||
|| element.getTagName().equals("uses-permission-sdk-23")
|
||||
|| element.getTagName().equals("uses-permission-sdk-m")) {
|
||||
String permission = element.getAttributeNS(android, "name");
|
||||
if (!permission.isEmpty()) permissions.add(permission);
|
||||
}
|
||||
|
||||
@@ -958,15 +958,6 @@ class _BridgeServer(threading.Thread):
|
||||
proof.update(fragment.encode("utf-8"))
|
||||
return proof.hexdigest()
|
||||
|
||||
def _encode_authenticated_response(
|
||||
self, value: dict[str, Any], client_nonce: str, server_nonce: str
|
||||
) -> bytes | None:
|
||||
"""Return one exact frame. The socket path writes the same pieces."""
|
||||
proof = self._authenticated_proof(value, client_nonce, server_nonce)
|
||||
if proof is None:
|
||||
return None
|
||||
return b"".join(self._iter_authenticated_frame(value, proof))
|
||||
|
||||
def _iter_authenticated_frame(self, value: dict[str, Any], proof: str):
|
||||
yield _AUTHENTICATED_PREFIX
|
||||
for fragment in _response_fragments(value):
|
||||
|
||||
@@ -154,13 +154,6 @@ class HopperApiFacade:
|
||||
)
|
||||
|
||||
|
||||
def _configure_hopper_api(document_provider):
|
||||
"""Inject Hopper's document provider or a standalone test facade."""
|
||||
global _hopper_api, _rea_session_document
|
||||
_hopper_api = HopperApiFacade(document_provider)
|
||||
_rea_session_document = None
|
||||
|
||||
|
||||
def _api():
|
||||
"""Resolve Hopper globals lazily so importing this module needs no Hopper."""
|
||||
global _hopper_api
|
||||
|
||||
+21
-27
@@ -238,7 +238,9 @@ broad changes or diagnosing CI, rather than after every edit. Package/install
|
||||
changes additionally need package verification; provider changes need actual
|
||||
provider evidence.
|
||||
|
||||
Vitest projects use up to two workers, bounded by host parallelism.
|
||||
Vitest projects use up to two workers, bounded by host parallelism. GitHub
|
||||
Actions jobs own their runner and use up to four; see
|
||||
[test deadlines](#developer-commands) for how deadlines scale there.
|
||||
`process-boundary` runs later with serial files because process-tree sampling
|
||||
shares host resources. Acceptance and process-global cases use isolated forks
|
||||
without serial scheduling. Pure domain/service, composition, conformance,
|
||||
@@ -283,7 +285,7 @@ coverage. Ordinary CI also exhausted the former 384 MiB budget after earlier
|
||||
files loaded the shared source/contract graph, while later command registration
|
||||
and schema/help rendering still needed transient allocation space. This is a
|
||||
measured capacity control, not a suite-wide speedup claim. The worker count
|
||||
remains bounded at two, and spawned REA processes retain their own heap settings.
|
||||
remains bounded, and spawned REA processes retain their own heap settings.
|
||||
Coverage enablement still controls instrumentation, test deadlines and coverage
|
||||
thresholds; explicitly disabling it or selecting only a provider does not enable
|
||||
instrumentation.
|
||||
@@ -299,6 +301,18 @@ work from fixture generation, schema compilation, subprocess startup and cleanup
|
||||
Choose elapsed, CPU and process-family memory limits before the run; a worker
|
||||
count or test timeout alone does not enforce those limits.
|
||||
|
||||
A test deadline turns a hang into a named failure; it is not a performance
|
||||
assertion. `vitest.config.ts` applies the deadlines in
|
||||
`tests/support/testDeadlines.ts` per project: 30 seconds for source projects
|
||||
and 120 seconds for built projects, with hooks sharing their project's
|
||||
deadline. Coverage doubles every deadline, and so does hosted CI, where
|
||||
CPU-bound cases share every core with other workers and their subprocesses.
|
||||
Do not pass a per-test timeout to fit a slow host. Tag a bounded workload that needs longer with
|
||||
`{ tags: ["long-running"] }`, or a whole file with
|
||||
`// @module-tag long-running`. Assert elapsed time explicitly when speed is the
|
||||
behavior under test. Derive an explicit deadline only when a test must outlive
|
||||
an operation deadline whose diagnostics it checks.
|
||||
|
||||
Keep capacity fixtures intact when diagnosing a test failure. For byte equality
|
||||
of large buffers, use `Buffer.equals` or `Buffer.compare`; a generic structural
|
||||
diff can allocate far more than the production operation.
|
||||
@@ -1077,32 +1091,12 @@ existing four-minute timing collection plus the bounded diagnostic stages.
|
||||
|
||||
Fast built shards target under ten minutes of uncontended shard-job execution.
|
||||
That figure is the `test-shard` job. The job waits for classification and the
|
||||
build, and the portable-test merge runs after the slowest shard. On 2026-10-20,
|
||||
run `39451548945` measured those surrounding jobs with runner queue excluded:
|
||||
classification 15 seconds, build 49 seconds, and the portable-test merge 29
|
||||
seconds. Shard 1/5 of that run spent 22 seconds before built Vitest and 3
|
||||
seconds after it, including `npm ci` at 9 seconds. `node_modules` is about 674
|
||||
MiB, so publishing it as a shared artifact would transfer more data than that
|
||||
install saves. The npm download cache stays the setup cache.
|
||||
|
||||
File timings from that run put the built suite at 7944 seconds of module time.
|
||||
The sequencer keeps each file's full duration on one worker. Extra workers
|
||||
overlap other files in the same sequence group, and `process-boundary` runs
|
||||
after that group. Scheduling the unsplit inventory onto five shards peaks at
|
||||
820 seconds of tests. That run's slowest built shard job lasted 925 seconds
|
||||
(shard 4/5), and shard 1/5 spent 874 seconds in built Vitest, so hosted job
|
||||
time sits above the file schedule. Six shards of the same inventory peak at
|
||||
684 seconds of tests. Eight shards, after the 578-second loader file is split
|
||||
by case count (54 cases stay in that file and 46 move into each of three
|
||||
files), peak at 513 seconds of tests. With the measured 25-second shard setup,
|
||||
that shard job is about 538 seconds. Adding the measured classification, build,
|
||||
and merge makes an uncontended portable critical path of about 631 seconds,
|
||||
still before the gap between this file schedule and hosted Vitest. Runner queue
|
||||
is separate and grows when more shard jobs compete for runners.
|
||||
build, and the portable-test merge runs after the slowest shard. Runner queue is
|
||||
separate and grows when more shard jobs compete for runners. The npm download
|
||||
cache stays the setup cache rather than a shared `node_modules` artifact.
|
||||
`process-boundary` stays serial because process-tree sampling shares the host.
|
||||
Raising the worker cap from two to four on this four-core host cut a six-file
|
||||
acceptance sample from 96–101 seconds to 71 seconds and did not remove the
|
||||
single-file floor, so the cap stays two. Full qualification keeps six covered
|
||||
Hosted CI jobs use four workers; other hosts stay at two
|
||||
because they are shared with other checks. Full qualification keeps six covered
|
||||
shards; its longer instrumentation is outside this fast budget.
|
||||
|
||||
The separate `CI timings` workflow batches recent completed CI and Release runs
|
||||
|
||||
@@ -215,7 +215,10 @@ Resolution follows dyld:
|
||||
image that loaded it, up to the process root.
|
||||
- Dependencies load dependents-first in load-command order, and each image
|
||||
loads once per process. A later @rpath request whose install name matches an
|
||||
already loaded image reuses it.
|
||||
already loaded image reuses it. Loading an image and expanding its dependencies
|
||||
are separate: an already loaded sibling can first be expanded through another
|
||||
dependency, inheriting that traversal's rpath stack. Cycles do not expand an
|
||||
image twice, and lazy edges do not establish a launch-time traversal chain.
|
||||
- Symlinks such as `Versions/Current` are followed inside the analyzed root.
|
||||
An escape through `..` or an absolute link is `escapes-target`.
|
||||
- Dyld path components are POSIX names. On Windows hosts, link text uses `\`
|
||||
@@ -252,8 +255,12 @@ one of:
|
||||
|
||||
A `conditional` image is still traced, because it is an in-root candidate
|
||||
that may load. Every edge below it has `loader_conditional: true`, and a
|
||||
request that reuses it by install name is `conditional` too. An in-root dylib
|
||||
with no platform load command remains unknown unless the root is an executable
|
||||
request that reuses it by install name is `conditional` too. A definitely loaded
|
||||
image can also first expand through a conditional chain: its dependency edges
|
||||
then have `loader_conditional: true`, while later reuse of the image retains its
|
||||
definite first-load result.
|
||||
|
||||
An in-root dylib with no platform load command remains unknown unless the root is an executable
|
||||
with observed macOS platform and the candidate's CPU type is x86_64, i386, or arm64. In that macOS context,
|
||||
[Apple's dyld](https://github.com/apple-oss-distributions/dyld/blob/fd8d0c4d52320ebf64db34f3cb280310d905c5ae/mach_o/UnsafeHeader.cpp#L1137-L1145) supplies an implicit macOS platform to legacy dylibs on those CPU
|
||||
types. The candidate records `platform_basis: "macos-dyld-implicit"`; the
|
||||
|
||||
@@ -174,7 +174,9 @@ Official references: [mitmproxy protocol support](https://github.com/mitmproxy/m
|
||||
|
||||
## Verifier regressions
|
||||
|
||||
After building REA, run these inside the same isolated guest:
|
||||
The built process-boundary test suite runs the Node verifier regressions on
|
||||
POSIX hosts. To run them directly after building REA, and to run the Python
|
||||
laboratory regressions with its pinned dependencies, use the isolated guest:
|
||||
|
||||
```sh
|
||||
node --test scripts/lib/http3-verifier-lifecycle.test.mjs scripts/lib/http3-capture-assertions.test.mjs
|
||||
|
||||
@@ -222,7 +222,7 @@ cannot answer, such as an address without one exact file offset, omits this
|
||||
hint; follow its `remediation.action`. `recovery.kind=inspect_timeout_state` names
|
||||
the operation and the REA server/provider deadline. Inspect partial Evidence
|
||||
and reported cleanup state, then follow the returned remediation. Change a
|
||||
request field or REA_* setting only when the returned remediation explicitly
|
||||
request field or REA\_\* setting only when the returned remediation explicitly
|
||||
recommends that change. Restart REA when that text says the value is read at
|
||||
startup. A mention that a control does not raise the deadline is not a
|
||||
recommendation to change it. When it names no setting, reduce supported work;
|
||||
@@ -1065,6 +1065,17 @@ from an older request does not override a newer outcome. Request deadlines and
|
||||
cancellations neither mark nor clear availability. A new provider client starts
|
||||
without these outcomes.
|
||||
|
||||
A `provider_missing` group may also list `alternatives`: advertised tools the
|
||||
current provider makes available for part of that operation's question. When
|
||||
the provider does not declare `next_address` or `prev_address`, they list
|
||||
whichever of `inspect_native_instruction`, `read_function_instructions` and
|
||||
`procedure_assembly` qualify. These answer instruction-level questions only:
|
||||
none navigates arbitrary analyzed objects such as data or addresses outside a
|
||||
procedure, and neither listing order nor address plus length proves executed
|
||||
control flow. Calling the missing operation returns `capability_unavailable`
|
||||
with the same tools in `details.alternatives` and a remediation stating each
|
||||
one's scope. Without a qualifying alternative, both keep their generic advice.
|
||||
|
||||
Standalone command operations (such as `inspect_with_objdump` and
|
||||
`execute_rizin_command`) similarly reflect observed executable launch failures:
|
||||
an unprobed executable is reported available; once a launch failure is observed,
|
||||
@@ -1911,13 +1922,10 @@ support for omitted historical values.
|
||||
known and differ. If only one value is known, storage comparison is unknown;
|
||||
when both are omitted, storage is not compared.
|
||||
|
||||
<!-- BEGIN:compare-artifacts:compared-format-metadata-kinds -->
|
||||
|
||||
`compare_artifacts` also compares the declarations in the `format_metadata`
|
||||
of UnityFS members (`unityfs-entry`), PyInstaller entries
|
||||
`compare_artifacts` also compares the declarations in the `format_metadata` of <!-- BEGIN:compare-artifacts:compared-format-metadata-kinds -->UnityFS members (`unityfs-entry`), PyInstaller entries
|
||||
(`pyinstaller-entry`), Node SEA members (`node-sea-entry`),
|
||||
ESP image segments (`esp-image-segment`), DfuSe elements (`dfuse-element`),
|
||||
ESP flash partitions (`esp-flash-partition`) and FIT images (`fit-image`);
|
||||
ESP flash partitions (`esp-flash-partition`) and FIT images (`fit-image`)<!-- END:compare-artifacts:compared-format-metadata-kinds -->;
|
||||
each format's guide names the compared fields. A changed declaration is a
|
||||
metadata change, including when content is unavailable. A declaration that
|
||||
only one side records, or that the other side records as another kind, is
|
||||
@@ -1928,8 +1936,6 @@ container or from caller options that identify the target, or repeat
|
||||
occurrence fields, except Godot PCK source paths, which
|
||||
[Godot PCK archives](../godot-pck.md) explains.
|
||||
|
||||
<!-- END:compare-artifacts:compared-format-metadata-kinds -->
|
||||
|
||||
Every occurrence records `entry_kind`, `executable` and `encrypted`. A
|
||||
different entry kind or `encrypted` value is a `metadata` difference, and a
|
||||
different `executable` value is an `executable` difference. Occurrence and node
|
||||
|
||||
@@ -96,6 +96,10 @@ Persistent command Evidence retains the caller-selected and resolved startup pat
|
||||
|
||||
Session status retains up to 64 recent protocol frames. Each history entry keeps a complete-character UTF-8 prefix of at most 16 KiB, plus a truncation marker when needed; `recent_output_truncated` reports omitted history. This history budget also accounts for replacement characters from invalid UTF-8. It does not shorten command Evidence, whose output budget defaults to 16 MiB. Rizin debug startup waits 30000 ms with no runtime override. Each command frame defaults to 120000 ms; its deadline, command-output budget, session capacity and pending-command queue use the [shared debugger launch settings](#debugger-launch-settings). The 16777216-byte frame cap and 4096-frame command cap remain fixed. Evidence names the cap that stopped the command. The one-shot `command_timeout_ms` setting does not change a debug session.
|
||||
|
||||
### Stopped debugger sessions
|
||||
|
||||
A Rizin or GDB session stops accepting commands when REA stops it, for example after an output or frame limit, a command deadline, cancellation or a protocol failure, or when the debugger process exits. REA keeps the stopped session and the first reason it observed until `close_rizin_debug_session` or `close_gdb_session` releases it, including after a retried cleanup succeeds. `rizin_debug_session_status` and `gdb_session_status` then report `state: "closed"` with that reason in `closed_reason`. A later `rizin_debug_command` or `gdb_console` on the session fails with `capability_unavailable`; `details.reason` carries the same reason, and the remediation is to close the session and start a new one. A `session_id` that this server never returned, or that close already released, fails with `invalid_request` instead.
|
||||
|
||||
### Debugger launch settings
|
||||
|
||||
GDB and Rizin share these settings in the environment that launches REA:
|
||||
@@ -136,6 +140,8 @@ rea debug-with-rizin --inferior-tty /dev/pts/7 ./program dr
|
||||
rea execute-rizin-command ./program iI
|
||||
```
|
||||
|
||||
Rizin exits with status 0 even when its shell refuses a statement, so the exit code does not show whether the command ran. Rizin 0.6 through 0.9.1 log one of four stderr lines for such a refusal: `ERROR: Command '…' does not exist.`, ``ERROR: Wrong number of arguments passed to `…`…``, `ERROR: core: Error while executing command: …` (an invalid statement), or `ERROR: core: Error while parsing command: …`. Rizin then skips the refused statement and every later one in the command. When the completed process's stderr contains one of these lines, REA reports `completion_status: "rejected"` and adds a limitation naming the refusal. stdout then holds only the output of earlier statements, so empty stdout is not an empty result. Other `ERROR:` lines, such as `ERROR: Cannot find function at …`, come from commands that ran; REA keeps them in stderr and reports `complete`. A command that lowers `log.level` or redirects stderr hides refusals from REA.
|
||||
|
||||
Captured stdout and stderr use the same `max_output_bytes` (CLI: `--max-output-bytes`) admission and recovery described above, default 16 MiB. One-shot commands independently use `command_timeout_ms`. Raising output admission cannot recover an earlier capture; command effects and cleanup must be checked before another invocation.
|
||||
|
||||
### Decoder and map inputs
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
4. Verifier: `appTaskPython.ts` renders `READER_RUNNER`, `READERS`,
|
||||
`_reader_views` and `evaluate_with_readers` only for a task with readers,
|
||||
so a task without renders as before.
|
||||
5. Application: `readTaskReaderViews` and `readerViewsOfBytes` in
|
||||
`DesktopTaskReaders.ts`; `runReaderHere` in `src/desktop/TaskReaderProcess.ts`;
|
||||
5. Application: `readerViewsOfBytes` in `DesktopTaskReaders.ts`;
|
||||
`runReaderHere` in `src/desktop/TaskReaderProcess.ts`;
|
||||
the witness runs readers through `AgentEnvComputer.exec`; the export runs
|
||||
them on `shippedSeedBytes` and the end's retained content.
|
||||
6. Docs: the tasks reference, the witness page, the build skill's recipes for
|
||||
|
||||
+8
-22
@@ -842,28 +842,14 @@ branches are not producers. A tier with no qualifying artifact leaves history
|
||||
unset, and Vitest uses its own assignment. A later schedule that reuses
|
||||
qualification without uploading timings leaves the previous artifact in place.
|
||||
|
||||
On 2026-10-21 the newest full history was run
|
||||
`39545047294`, recorded before compiled-CLI session reuse. Fast run
|
||||
`39552964873` packed its eight built shards from that history, and those shard
|
||||
jobs executed for 319–591 seconds. Replaying that run's file durations through
|
||||
the sequencer's two-worker schedule balances every shard at about 404 seconds
|
||||
of file time. The same replay with the stale history weights leaves the slowest
|
||||
shard near 547 seconds and the fastest near 293. That spread is file-schedule
|
||||
time, not runner queue, which is why each tier keeps its own history.
|
||||
CI assigns discovered files by historical duration, with a deterministic
|
||||
fallback weight for new files, rather than dividing only by file count.
|
||||
Each file keeps its full duration on one worker. Extra workers overlap other
|
||||
files in the same sequence group, and `process-boundary` runs after that
|
||||
group. History published before the competing-loader split still names only
|
||||
`javascriptLiteralPercentPaths.test.ts`. Until an artifact records
|
||||
`javascriptLiteralLoaderOwnership.test.ts`, the sequencer assigns that
|
||||
recorded duration by the pre-split case counts: 54/192 stays with the
|
||||
percent-path file and 138/192 goes to the consolidated loader file. History
|
||||
that names the retired Early, Middle, and Late shards sums those durations
|
||||
onto the consolidated file instead, and scales a partial set as three equal
|
||||
shares. That weight is the old dual-surface cost. The demoted file runs every
|
||||
loader once through the shared stdio MCP session, so the first schedule
|
||||
overestimates it. Every discovered file still belongs to exactly one shard;
|
||||
Each tier keeps its own history because stale weights from another tier, or
|
||||
from before a large test change, unbalance the shards.
|
||||
CI assigns discovered files by historical duration rather than dividing only
|
||||
by file count. A file with no recorded duration, such as a newly added or
|
||||
renamed test, is weighted at the median recorded duration until a published
|
||||
artifact records it. Each file keeps its full duration on one worker. Extra
|
||||
workers overlap other files in the same sequence group, and `process-boundary`
|
||||
runs after that group. Every discovered file still belongs to exactly one shard;
|
||||
worker limits and project ordering remain unchanged. Fast qualification keeps
|
||||
eight built shards without coverage. The merge job rejects successful reports
|
||||
with missing, unexpected, or duplicate portable files. Coverage instrumentation
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"entry": [
|
||||
"src/main.ts",
|
||||
"src/cli.ts",
|
||||
"src/cliOneShot.ts",
|
||||
"src/cliOutput.ts",
|
||||
"src/cliBuiltinArguments.ts",
|
||||
"src/cli/cliResultOutput.ts",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"entry": [
|
||||
"src/main.ts!",
|
||||
"src/cli.ts!",
|
||||
"src/cliOneShot.ts!",
|
||||
"src/cliOutput.ts!",
|
||||
"src/cliBuiltinArguments.ts!",
|
||||
"src/cli/cliResultOutput.ts!",
|
||||
|
||||
@@ -5,7 +5,8 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
|
||||
/**
|
||||
* Reduce filesystem resolution during CLI and MCP startup. Keep dependencies
|
||||
* external, dynamic imports lazy, and original compiled worker entrypoints.
|
||||
* external and original compiled worker entrypoints. Dynamic imports can split
|
||||
* into lazy chunks unless the same module is also statically reachable.
|
||||
*/
|
||||
export const bundleRuntime = async (root) => {
|
||||
const compiledRoot = resolve(root, "dist");
|
||||
@@ -23,7 +24,7 @@ export const bundleRuntime = async (root) => {
|
||||
"cli-runtime",
|
||||
{
|
||||
cli: "cli.js",
|
||||
cliOutput: "cliOutput.js",
|
||||
cliOneShot: "cliOneShot.js",
|
||||
cliBuiltinArguments: "cliBuiltinArguments.js",
|
||||
cliResultOutput: "cli/cliResultOutput.js",
|
||||
mcpDoctor: "mcpDoctor.js",
|
||||
|
||||
@@ -14,7 +14,7 @@ const directDependencies = (rootPackage) => ({
|
||||
const installedPackageKey = (name) => `node_modules/${name}`;
|
||||
|
||||
/** Compare root direct dependencies with npm's hidden installed lockfile. */
|
||||
export const dependencyInstallProblems = (rootLock, installedLock) => {
|
||||
const dependencyInstallProblems = (rootLock, installedLock) => {
|
||||
const rootPackage = rootLock.packages?.[""];
|
||||
if (rootPackage === undefined)
|
||||
return ["package-lock.json does not contain a root package entry"];
|
||||
|
||||
@@ -26,8 +26,6 @@ export type CacheRequest = (
|
||||
endpoint: string,
|
||||
) => Promise<unknown>;
|
||||
|
||||
export function assertRetentionPrefix(prefix: string): void;
|
||||
|
||||
export function selectPrefixDeletions(
|
||||
caches: readonly unknown[],
|
||||
family?: RetentionFamily,
|
||||
|
||||
@@ -22,7 +22,7 @@ const PREFIX_PATTERNS = [
|
||||
];
|
||||
|
||||
/** Reject prefixes that could match npm downloads or another platform. */
|
||||
export function assertRetentionPrefix(prefix) {
|
||||
function assertRetentionPrefix(prefix) {
|
||||
if (
|
||||
typeof prefix !== "string" ||
|
||||
!PREFIX_PATTERNS.some((pattern) => pattern.test(prefix))
|
||||
|
||||
@@ -99,9 +99,3 @@ export function createGitHubApi(options?: {
|
||||
budget: { requests: number; startedAt: number; limitMs: number },
|
||||
) => Promise<unknown>;
|
||||
}): FailureReportApi;
|
||||
|
||||
export function writeFailureReport(
|
||||
report: CiFailureReport,
|
||||
outputDirectory?: string,
|
||||
summaryPath?: string,
|
||||
): Promise<{ jsonPath: string; markdownPath: string }>;
|
||||
@@ -467,7 +467,7 @@ function errorCode(error) {
|
||||
return error instanceof Error ? error.message.slice(0, 160) : "unknown_error";
|
||||
}
|
||||
|
||||
export async function writeFailureReport(
|
||||
async function writeFailureReport(
|
||||
report,
|
||||
outputDirectory = OUTPUT_DIRECTORY,
|
||||
summaryPath,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
@@ -91,5 +91,4 @@ export function writePrCiHealthReport(
|
||||
outputDirectory?: string,
|
||||
): Promise<{ jsonPath: string; markdownPath: string }>;
|
||||
|
||||
export function renderMarkdownSummary(report: PrCiHealthReport): string;
|
||||
import type { GitHubApiRequest } from "./github-api.mjs";
|
||||
@@ -656,7 +656,7 @@ export async function writePrCiHealthReport(
|
||||
return { jsonPath, markdownPath };
|
||||
}
|
||||
|
||||
export function renderMarkdownSummary(report) {
|
||||
function renderMarkdownSummary(report) {
|
||||
const rows = report.pullRequests.map(
|
||||
(pull) =>
|
||||
`| #${String(pull.number ?? "?")} | ${escapeCell(pull.head.repository ?? "unknown")} | ${escapeCell(pull.head.ref ?? "unknown")} | \`${pull.head.sha ?? "unknown"}\` | ${pull.ci.category} | ${escapeCell(pull.ci.reason)} |`,
|
||||
|
||||
@@ -28,6 +28,3 @@ export interface ProductionReachabilityReport {
|
||||
export function parseProductionReachabilityReport(
|
||||
text: string,
|
||||
): ProductionReachabilityReport;
|
||||
export function recordProductionReachabilityReport(
|
||||
root?: string,
|
||||
): Promise<ProductionReachabilityReport>;
|
||||
@@ -34,9 +34,7 @@ export const parseProductionReachabilityReport = (text) =>
|
||||
reportSchema.parse(JSON.parse(text));
|
||||
|
||||
/** Run production reachability without turning candidates into failures. */
|
||||
export const recordProductionReachabilityReport = async (
|
||||
root = process.cwd(),
|
||||
) => {
|
||||
const recordProductionReachabilityReport = async (root = process.cwd()) => {
|
||||
const reportDirectory = join(root, ".cache", "static-checks");
|
||||
const reportPath = join(reportDirectory, "production-reachability.json");
|
||||
|
||||
|
||||
@@ -19,6 +19,11 @@ if (
|
||||
const child = spawn(
|
||||
process.execPath,
|
||||
[
|
||||
// Reuse the development runner's POSIX process-group owner. Windows
|
||||
// source tests must remain usable without the built native Job Object.
|
||||
...(process.platform === "win32"
|
||||
? []
|
||||
: [resolve("scripts/run-exclusive.mjs"), "--unlocked", process.execPath]),
|
||||
resolve("node_modules/vitest/vitest.mjs"),
|
||||
"run",
|
||||
...projects.flatMap(({ name }) => ["--project", name]),
|
||||
|
||||
@@ -6,7 +6,7 @@ import { promisify } from "node:util";
|
||||
const exec = promisify(execFile);
|
||||
|
||||
/** Commands the macOS bundle fixture needs from Command Line Tools. */
|
||||
export const MACOS_BUNDLE_FIXTURE_COMMANDS = [
|
||||
const MACOS_BUNDLE_FIXTURE_COMMANDS = [
|
||||
["/usr/bin/xcrun", ["--find", "clang"]],
|
||||
["/usr/bin/xcrun", ["--find", "codesign"]],
|
||||
["/usr/bin/ditto", ["-h"]],
|
||||
|
||||
@@ -2,6 +2,17 @@ import type {
|
||||
Client,
|
||||
CallToolRequestOptions,
|
||||
} from "@modelcontextprotocol/client";
|
||||
import type { VerifierCommands } from "./verify-commands.mjs";
|
||||
|
||||
/** Keep the opened target and its owned stdio server alive through verification. */
|
||||
export function withArtifactMcp(
|
||||
target: string,
|
||||
verify: (client: Client) => Promise<void>,
|
||||
options?: {
|
||||
readonly commands?: VerifierCommands;
|
||||
readonly signal?: AbortSignal;
|
||||
},
|
||||
): Promise<void>;
|
||||
|
||||
/** Call with the server's advertised schema; callers cannot replace it. */
|
||||
export function artifactMcpCall(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export function sourceMapSummary(sourceMaps) {
|
||||
function sourceMapSummary(sourceMaps) {
|
||||
return {
|
||||
status: sourceMaps.status,
|
||||
requested: sourceMaps.requested,
|
||||
|
||||
@@ -1,11 +1,3 @@
|
||||
export const CURSOR_PLUGIN_MANIFEST: ".cursor-plugin/plugin.json";
|
||||
export const CURSOR_PLUGIN_MCP: "mcp.json";
|
||||
export const AUTHORED_SKILL_DIRECTORY: ".agents/skills/reverse-engineer-anything";
|
||||
export const CURSOR_PLUGIN_SKILLS: ".cursor-plugin/skills";
|
||||
export const CURSOR_PLUGIN_SKILL: "reverse-engineer-anything";
|
||||
export const CURSOR_PLUGIN_LOGO: "assets/rea-plugin.png";
|
||||
export const CURSOR_PLUGIN_REPOSITORY: "https://github.com/morluto/rea";
|
||||
|
||||
export interface CursorPluginBundleIssue {
|
||||
readonly field: string;
|
||||
}
|
||||
@@ -56,11 +48,6 @@ export interface CursorPluginInspection {
|
||||
readonly issues: readonly CursorPluginBundleIssue[];
|
||||
}
|
||||
|
||||
export function cursorPluginSkillPaths(
|
||||
directory: string,
|
||||
prefix?: string,
|
||||
): Promise<readonly string[] | null>;
|
||||
|
||||
export function cursorPluginBundleIssues(
|
||||
input: CursorPluginBundleInput,
|
||||
): readonly CursorPluginBundleIssue[];
|
||||
|
||||
@@ -7,14 +7,13 @@ import { ensureGeneratedFile } from "./generated-file.mjs";
|
||||
// publication (name, release version, repository, skill, and icon). The
|
||||
// Marketplace checkout has no dist/, so the server is the pinned npm command
|
||||
// instead of the bundled launcher.
|
||||
export const CURSOR_PLUGIN_MANIFEST = ".cursor-plugin/plugin.json";
|
||||
export const CURSOR_PLUGIN_MCP = "mcp.json";
|
||||
export const AUTHORED_SKILL_DIRECTORY =
|
||||
".agents/skills/reverse-engineer-anything";
|
||||
export const CURSOR_PLUGIN_SKILLS = ".cursor-plugin/skills";
|
||||
export const CURSOR_PLUGIN_SKILL = "reverse-engineer-anything";
|
||||
export const CURSOR_PLUGIN_LOGO = "assets/rea-plugin.png";
|
||||
export const CURSOR_PLUGIN_REPOSITORY = "https://github.com/morluto/rea";
|
||||
const CURSOR_PLUGIN_MANIFEST = ".cursor-plugin/plugin.json";
|
||||
const CURSOR_PLUGIN_MCP = "mcp.json";
|
||||
const AUTHORED_SKILL_DIRECTORY = ".agents/skills/reverse-engineer-anything";
|
||||
const CURSOR_PLUGIN_SKILLS = ".cursor-plugin/skills";
|
||||
const CURSOR_PLUGIN_SKILL = "reverse-engineer-anything";
|
||||
const CURSOR_PLUGIN_LOGO = "assets/rea-plugin.png";
|
||||
const CURSOR_PLUGIN_REPOSITORY = "https://github.com/morluto/rea";
|
||||
const GENERATE_COMMAND = "node scripts/generate-cursor-plugin-skill.mjs";
|
||||
const INSTALL_LINK_MARKERS = ["install-mcp", "deeplink/mcp", "mcp-install"];
|
||||
|
||||
@@ -27,7 +26,7 @@ const failure = (code, detail) => {
|
||||
const cursorServerArgs = (version) => ["-y", `rea-agents@${version}`, "mcp"];
|
||||
|
||||
/** Relative file inventory for one skill directory, in stable order. */
|
||||
export const cursorPluginSkillPaths = async (directory, prefix = "") => {
|
||||
const cursorPluginSkillPaths = async (directory, prefix = "") => {
|
||||
const paths = [];
|
||||
let entries;
|
||||
try {
|
||||
|
||||
@@ -15,6 +15,7 @@ export function parseDevelopmentTestRequest(
|
||||
export function developmentTestPlan(
|
||||
request: DevelopmentTestRequest,
|
||||
baseCommit?: string,
|
||||
resolvedPaths?: readonly string[],
|
||||
): {
|
||||
readonly needsBuild: boolean;
|
||||
readonly artifactTasks: readonly string[];
|
||||
|
||||
@@ -58,7 +58,11 @@ const artifactConsumers = {
|
||||
};
|
||||
|
||||
/** Plan source feedback or explicit tests with their runtime/artifact prerequisites. */
|
||||
export const developmentTestPlan = (request, baseCommit) => {
|
||||
export const developmentTestPlan = (
|
||||
request,
|
||||
baseCommit,
|
||||
resolvedPaths = request.paths,
|
||||
) => {
|
||||
const explicit = request.paths.length > 0;
|
||||
if (
|
||||
request.mode === "changed" &&
|
||||
@@ -70,12 +74,12 @@ export const developmentTestPlan = (request, baseCommit) => {
|
||||
? []
|
||||
: SOURCE_TEST_PROJECTS.map(({ name }) => name);
|
||||
return {
|
||||
needsBuild: request.paths.some(
|
||||
needsBuild: resolvedPaths.some(
|
||||
(path) => projectForTest(path).group === "built",
|
||||
),
|
||||
artifactTasks: Object.entries(artifactConsumers).flatMap(
|
||||
([task, consumers]) =>
|
||||
request.paths.some((path) => consumers.includes(path)) ? [task] : [],
|
||||
resolvedPaths.some((path) => consumers.includes(path)) ? [task] : [],
|
||||
),
|
||||
vitestArguments: [
|
||||
"run",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
export const WINDOWS_GHIDRA_BYTE_SEARCH_FIXTURE = Object.freeze({
|
||||
const WINDOWS_GHIDRA_BYTE_SEARCH_FIXTURE = Object.freeze({
|
||||
pattern: "a0 ?? c0",
|
||||
mask: "f0 ff f0",
|
||||
startFileOffset: 0x380,
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { ArtifactResourceScope } from "../../src/artifacts/ArtifactResourceScope.js";
|
||||
|
||||
export class HopperDeadlineCleanupFailure extends AggregateError {
|
||||
readonly _tag: "HopperDeadlineCleanupFailure";
|
||||
readonly cleanupOwner: HopperDeadlineCleanup;
|
||||
constructor(owner: HopperDeadlineCleanup, failures: unknown[]);
|
||||
}
|
||||
|
||||
export class HopperDeadlineCleanup {
|
||||
readonly workspace: string;
|
||||
constructor(workspace: string, profile: ArtifactResourceScope);
|
||||
setClient(client: {
|
||||
close(): Promise<{ ok: true } | { ok: false; error: unknown }>;
|
||||
}): void;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { rm } from "node:fs/promises";
|
||||
|
||||
export class HopperDeadlineCleanupFailure extends AggregateError {
|
||||
constructor(owner, failures) {
|
||||
super(
|
||||
failures,
|
||||
`Hopper cleanup incomplete; workspace may remain at ${owner.workspace}`,
|
||||
);
|
||||
this._tag = "HopperDeadlineCleanupFailure";
|
||||
this.cleanupOwner = owner;
|
||||
}
|
||||
}
|
||||
|
||||
/** Own the client, profile scope, and workspace until all cleanup completes. */
|
||||
export class HopperDeadlineCleanup {
|
||||
#client;
|
||||
#profile;
|
||||
#pending;
|
||||
#removed = false;
|
||||
|
||||
constructor(workspace, profile) {
|
||||
this.workspace = workspace;
|
||||
this.#profile = profile;
|
||||
}
|
||||
|
||||
setClient(client) {
|
||||
this.#client = client;
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.#pending !== undefined) return this.#pending;
|
||||
const attempt = this.#close();
|
||||
const pending = attempt.finally(() => {
|
||||
if (this.#pending === pending) this.#pending = undefined;
|
||||
});
|
||||
this.#pending = pending;
|
||||
return pending;
|
||||
}
|
||||
|
||||
async #close() {
|
||||
if (this.#removed) return;
|
||||
const failures = [];
|
||||
if (this.#client !== undefined) {
|
||||
try {
|
||||
const result = await this.#client.close();
|
||||
if (!result.ok) throw result.error;
|
||||
this.#client = undefined;
|
||||
} catch (cause) {
|
||||
failures.push(cause);
|
||||
}
|
||||
}
|
||||
if (this.#profile !== undefined) {
|
||||
try {
|
||||
await this.#profile.close();
|
||||
this.#profile = undefined;
|
||||
} catch (cause) {
|
||||
failures.push(cause);
|
||||
}
|
||||
}
|
||||
if (failures.length === 0) {
|
||||
try {
|
||||
await rm(this.workspace, { recursive: true, force: true });
|
||||
this.#removed = true;
|
||||
return;
|
||||
} catch (cause) {
|
||||
failures.push(cause);
|
||||
}
|
||||
}
|
||||
throw new HopperDeadlineCleanupFailure(this, failures);
|
||||
}
|
||||
}
|
||||
@@ -64,7 +64,7 @@ function assertHeaders(record, side, headers) {
|
||||
}
|
||||
}
|
||||
|
||||
export function assertEndpointHeaders(actual, expected) {
|
||||
function assertEndpointHeaders(actual, expected) {
|
||||
// The proxy rebuilds pseudo fields from its HTTP state. Compare their values
|
||||
// by name while retaining the exact order and duplicates of regular headers.
|
||||
const partition = (headers) => {
|
||||
|
||||
@@ -1,28 +1,10 @@
|
||||
import { analyzeJavaScriptApplication } from "../../dist/application/javascript/JavaScriptApplicationService.js";
|
||||
import { ArtifactResourceScope } from "../../dist/artifacts/ArtifactResourceScope.js";
|
||||
import { withVerifierArtifactScope } from "./verifier-artifact-scope.mjs";
|
||||
|
||||
/** Analyze one JavaScript application and close its caller-owned resources. */
|
||||
export const analyzeJavaScriptApplicationWithOwnedScope = async (input) => {
|
||||
const scope = new ArtifactResourceScope();
|
||||
let outcome;
|
||||
try {
|
||||
export const analyzeJavaScriptApplicationWithOwnedScope = (input) =>
|
||||
withVerifierArtifactScope(async (scope) => {
|
||||
const result = await analyzeJavaScriptApplication(input, scope);
|
||||
outcome = result.ok
|
||||
? { kind: "completed", value: result.value }
|
||||
: { kind: "failed", cause: result.error };
|
||||
} catch (cause) {
|
||||
outcome = { kind: "failed", cause };
|
||||
}
|
||||
try {
|
||||
await scope.close();
|
||||
} catch (cleanupCause) {
|
||||
if (outcome.kind === "failed")
|
||||
throw new AggregateError(
|
||||
[outcome.cause, cleanupCause],
|
||||
"JavaScript analysis and artifact cleanup both failed",
|
||||
);
|
||||
throw cleanupCause;
|
||||
}
|
||||
if (outcome.kind === "failed") throw outcome.cause;
|
||||
return outcome.value;
|
||||
};
|
||||
if (!result.ok) throw result.error;
|
||||
return result.value;
|
||||
});
|
||||
@@ -78,7 +78,7 @@ export const expandMcpSessionStatus = (status) => ({
|
||||
});
|
||||
|
||||
/** Expand provider capability groups while retaining candidate identity/state. */
|
||||
export const expandProviderCandidates = (candidates) =>
|
||||
const expandProviderCandidates = (candidates) =>
|
||||
candidates.map((candidate) => ({
|
||||
...candidate,
|
||||
capabilities: expandProviderCapabilities(
|
||||
|
||||
@@ -1,176 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { test } from "node:test";
|
||||
|
||||
import { inspectModuleBoundaries } from "./module-boundaries.mjs";
|
||||
|
||||
const fixture = async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "rea-module-boundaries-"));
|
||||
await mkdir(join(root, "src/domain"), { recursive: true });
|
||||
await mkdir(join(root, "src/composition"), { recursive: true });
|
||||
await writeFile(
|
||||
join(root, "src/composition/worker.ts"),
|
||||
"export default 1;\n",
|
||||
);
|
||||
await writeFile(
|
||||
join(root, "package.json"),
|
||||
JSON.stringify({ imports: { "#worker": "./dist/composition/worker.js" } }),
|
||||
);
|
||||
return root;
|
||||
};
|
||||
|
||||
await test("reports a pure-layer composition import written relatively", async () => {
|
||||
const root = await fixture();
|
||||
try {
|
||||
const violations = inspectModuleBoundaries(
|
||||
"src/domain/probe.ts",
|
||||
'import worker from "../composition/worker.js";\nexport default worker;\n',
|
||||
root,
|
||||
);
|
||||
assert.deepEqual(
|
||||
violations.map(({ boundary, target }) => ({ boundary, target })),
|
||||
[{ boundary: "pure-layer", target: "src/composition/worker.js" }],
|
||||
);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
await test("reports a pure-layer composition import written through the package map", async () => {
|
||||
const root = await fixture();
|
||||
try {
|
||||
const violations = inspectModuleBoundaries(
|
||||
"src/domain/probe.ts",
|
||||
'import worker from "#worker";\nexport default worker;\n',
|
||||
root,
|
||||
);
|
||||
assert.deepEqual(
|
||||
violations.map(({ boundary, target }) => ({ boundary, target })),
|
||||
[{ boundary: "pure-layer", target: "src/composition/worker.ts" }],
|
||||
);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
for (const [name, specifier, imports] of [
|
||||
[
|
||||
"conditional",
|
||||
"#worker",
|
||||
{
|
||||
"#worker": {
|
||||
import: "./dist/composition/worker.js",
|
||||
default: "./dist/composition/other.js",
|
||||
},
|
||||
},
|
||||
],
|
||||
["array", "#worker", { "#worker": ["./dist/composition/worker.js"] }],
|
||||
["pattern", "#workers/worker", { "#workers/*": "./dist/composition/*.js" }],
|
||||
]) {
|
||||
await test(`refuses an unsupported valid ${name} package map rather than skipping its edge`, async () => {
|
||||
const root = await fixture();
|
||||
try {
|
||||
await writeFile(join(root, "package.json"), JSON.stringify({ imports }));
|
||||
const file = "src/domain/probe.ts";
|
||||
await writeFile(
|
||||
join(root, file),
|
||||
`import ${JSON.stringify(specifier)};\n`,
|
||||
);
|
||||
// Resolve only: no worker module or authored target source is executed.
|
||||
const resolved = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
"--experimental-import-meta-resolve",
|
||||
"--input-type=module",
|
||||
"-e",
|
||||
"console.log(import.meta.resolve(process.argv[1], process.argv[2]))",
|
||||
specifier,
|
||||
pathToFileURL(join(root, file)).href,
|
||||
],
|
||||
{ encoding: "utf8" },
|
||||
);
|
||||
if (resolved.error !== undefined) throw resolved.error;
|
||||
assert.equal(resolved.status, 0, resolved.stderr);
|
||||
assert.equal(
|
||||
resolved.stdout.trim(),
|
||||
pathToFileURL(join(root, "dist/composition/worker.js")).href,
|
||||
);
|
||||
const checked = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
fileURLToPath(
|
||||
new URL("../verify-module-boundaries.mjs", import.meta.url),
|
||||
),
|
||||
file,
|
||||
],
|
||||
{ cwd: root, encoding: "utf8" },
|
||||
);
|
||||
if (checked.error !== undefined) throw checked.error;
|
||||
assert.equal(checked.status, 1, checked.stdout);
|
||||
assert.match(checked.stderr, /Could not inspect src\/domain\/probe.ts/u);
|
||||
assert.match(checked.stderr, /Cannot resolve package import/u);
|
||||
assert.match(checked.stderr, /relative source import/u);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (const target of [
|
||||
"./dist/composition/worker.js",
|
||||
"./dist/%63omposition/worker.js",
|
||||
"./%64ist/composition/worker.js",
|
||||
]) {
|
||||
await test(`checks the URL-normalized source owner of ${target}`, async () => {
|
||||
const root = await fixture();
|
||||
try {
|
||||
await writeFile(
|
||||
join(root, "package.json"),
|
||||
JSON.stringify({ imports: { "#worker": target } }),
|
||||
);
|
||||
await mkdir(join(root, "src/application"), { recursive: true });
|
||||
const file = "src/application/probe.ts";
|
||||
await writeFile(join(root, file), 'import "#worker";\n');
|
||||
const resolved = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
"--experimental-import-meta-resolve",
|
||||
"--input-type=module",
|
||||
"-e",
|
||||
"console.log(import.meta.resolve(process.argv[1], process.argv[2]))",
|
||||
"#worker",
|
||||
pathToFileURL(join(root, file)).href,
|
||||
],
|
||||
{ encoding: "utf8" },
|
||||
);
|
||||
if (resolved.error !== undefined) throw resolved.error;
|
||||
assert.equal(resolved.status, 0, resolved.stderr);
|
||||
assert.equal(
|
||||
fileURLToPath(resolved.stdout.trim()),
|
||||
join(root, "dist/composition/worker.js"),
|
||||
);
|
||||
const checked = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
fileURLToPath(
|
||||
new URL("../verify-module-boundaries.mjs", import.meta.url),
|
||||
),
|
||||
file,
|
||||
],
|
||||
{ cwd: root, encoding: "utf8" },
|
||||
);
|
||||
if (checked.error !== undefined) throw checked.error;
|
||||
assert.equal(checked.status, 1, checked.stdout);
|
||||
assert.match(
|
||||
checked.stderr,
|
||||
/application-composition: #worker resolves to src\/composition\/worker.ts/u,
|
||||
);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,8 +1,5 @@
|
||||
export { runOwnedVerifierTerminal } from "./verifier-terminal-owner.mjs";
|
||||
export type {
|
||||
VerifierTerminalOptions,
|
||||
VerifierTerminalDependencies,
|
||||
VerifierTerminalResult,
|
||||
VerifierTerminalFailure,
|
||||
VerifierTerminalCleanupFailure,
|
||||
} from "./verifier-terminal-owner.mjs";
|
||||
@@ -10,7 +10,6 @@ import { access, readFile, readdir, stat } from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
import { setTimeout as delay } from "node:timers/promises";
|
||||
import { promisify } from "node:util";
|
||||
import { GHIDRA_ANNOTATIONS_RETAINED } from "../../dist/ghidra/GhidraProviderClient.js";
|
||||
import Ajv from "ajv/dist/2020.js";
|
||||
import addFormats from "ajv-formats";
|
||||
import { OFFICIAL_TOOL_CONTRACTS } from "../../dist/contracts/officialToolContracts.js";
|
||||
@@ -571,7 +570,11 @@ export async function verifyGhidraBoundaries(
|
||||
// The producer reason describes the failure; continuing-session guidance
|
||||
// is projected separately and the following live calls verify it.
|
||||
assert.match(regexError.details.reason, /stack while matching pattern/u);
|
||||
assert.ok(regexError.message.includes(GHIDRA_ANNOTATIONS_RETAINED));
|
||||
assert.ok(
|
||||
regexError.message.includes(
|
||||
"The active analysis session and annotations remain available.",
|
||||
),
|
||||
);
|
||||
rejectedCalls++;
|
||||
const nestedPattern = "(".repeat(12000) + "a" + ")".repeat(12000);
|
||||
const compileFailure = await client.callTool(
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
/** Observe concrete Hopper runtime resources for a before/after cleanup check. */
|
||||
/** Observe possible Hopper resources, including entries with unresolved ownership. */
|
||||
export function snapshotHopperRuntime(
|
||||
parent: string,
|
||||
targetLeaseDirectory: string,
|
||||
ownedProcessIds?: ReadonlySet<number>,
|
||||
): Promise<Set<string>>;
|
||||
expectedRunId?: string,
|
||||
): Promise<{
|
||||
readonly paths: Set<string>;
|
||||
readonly confirmedSessions: Set<string>;
|
||||
readonly unresolvedSessions: Set<string>;
|
||||
}>;
|
||||
@@ -1,35 +1,38 @@
|
||||
import { lstat, readFile, readdir } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
/** Observe session paths and target leases without treating their shared parent as a session. */
|
||||
/** Observe possible sessions and target leases without discarding uncertainty. */
|
||||
export async function snapshotHopperRuntime(
|
||||
parent,
|
||||
targetLeaseDirectory,
|
||||
ownedProcessIds,
|
||||
expectedRunId,
|
||||
) {
|
||||
const resources = new Set();
|
||||
const paths = new Set();
|
||||
const confirmedSessions = new Set();
|
||||
const unresolvedSessions = new Set();
|
||||
for (const entry of await readdir(parent, { withFileTypes: true })) {
|
||||
const path = join(parent, entry.name);
|
||||
if (!entry.name.startsWith("rea-") || path === targetLeaseDirectory)
|
||||
continue;
|
||||
if (
|
||||
ownedProcessIds !== undefined &&
|
||||
(!entry.isDirectory() ||
|
||||
!(await belongsToOwnedProcess(path, ownedProcessIds)))
|
||||
)
|
||||
continue;
|
||||
resources.add(path);
|
||||
const confirmed =
|
||||
typeof expectedRunId === "string" &&
|
||||
expectedRunId.length > 0 &&
|
||||
entry.isDirectory() &&
|
||||
(await hasRunIdentity(path, expectedRunId));
|
||||
(confirmed ? confirmedSessions : unresolvedSessions).add(path);
|
||||
paths.add(path);
|
||||
}
|
||||
const leaseRoot = await lstat(targetLeaseDirectory).catch(missingIsAbsent);
|
||||
if (leaseRoot === undefined) return resources;
|
||||
if (!leaseRoot.isDirectory() || leaseRoot.isSymbolicLink()) {
|
||||
resources.add(targetLeaseDirectory);
|
||||
return resources;
|
||||
if (leaseRoot !== undefined) {
|
||||
if (!leaseRoot.isDirectory() || leaseRoot.isSymbolicLink())
|
||||
paths.add(targetLeaseDirectory);
|
||||
else {
|
||||
const leases = await readdir(targetLeaseDirectory).catch(missingIsAbsent);
|
||||
for (const name of leases ?? [])
|
||||
paths.add(join(targetLeaseDirectory, name));
|
||||
}
|
||||
}
|
||||
const leases = await readdir(targetLeaseDirectory).catch(missingIsAbsent);
|
||||
for (const name of leases ?? [])
|
||||
resources.add(join(targetLeaseDirectory, name));
|
||||
return resources;
|
||||
return { paths, confirmedSessions, unresolvedSessions };
|
||||
}
|
||||
|
||||
const missingIsAbsent = (cause) => {
|
||||
@@ -37,16 +40,21 @@ const missingIsAbsent = (cause) => {
|
||||
throw cause;
|
||||
};
|
||||
|
||||
const belongsToOwnedProcess = async (path, ownedProcessIds) => {
|
||||
const hasRunIdentity = async (path, expectedRunId) => {
|
||||
const encoded = await readFile(join(path, "ownership.json"), "utf8").catch(
|
||||
missingIsAbsent,
|
||||
);
|
||||
if (encoded === undefined) return false;
|
||||
const ownership = JSON.parse(encoded);
|
||||
let ownership;
|
||||
try {
|
||||
ownership = JSON.parse(encoded);
|
||||
} catch (cause) {
|
||||
if (cause instanceof SyntaxError) return false;
|
||||
throw cause;
|
||||
}
|
||||
return (
|
||||
ownership !== null &&
|
||||
typeof ownership === "object" &&
|
||||
Number.isSafeInteger(ownership.parent_pid) &&
|
||||
ownedProcessIds.has(ownership.parent_pid)
|
||||
ownership.run_id === expectedRunId
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
export interface FixtureProcedure {
|
||||
interface FixtureProcedure {
|
||||
readonly address: string;
|
||||
readonly name: string;
|
||||
}
|
||||
@@ -26,13 +26,6 @@ export function requireCurrentDocument(
|
||||
normalizedResult: (value: unknown, operation: string) => unknown,
|
||||
): Promise<string>;
|
||||
|
||||
export function analyzeFixtureProcedure(
|
||||
client: ToolClient,
|
||||
options: unknown,
|
||||
procedure: FixtureProcedure,
|
||||
normalizedResult: (value: unknown, operation: string) => unknown,
|
||||
): Promise<unknown>;
|
||||
|
||||
export function verifyRealHopperFixture(options: {
|
||||
readonly client: ToolClient;
|
||||
readonly options: unknown;
|
||||
|
||||
@@ -310,7 +310,7 @@ export async function resolveFixtureProcedure(
|
||||
}
|
||||
|
||||
/** Request the assembly evidence required by the fixture oracle. */
|
||||
export const analyzeFixtureProcedure = async (
|
||||
const analyzeFixtureProcedure = async (
|
||||
client,
|
||||
options,
|
||||
procedure,
|
||||
|
||||
@@ -6,6 +6,3 @@ export function unsupportedRuntimeMessage(
|
||||
range: string,
|
||||
version: string,
|
||||
): string | undefined;
|
||||
|
||||
/** Human-readable release families for an `engines.node` range. */
|
||||
export function supportedReleaseProse(range: string): string;
|
||||
@@ -21,7 +21,7 @@ export const unsupportedRuntimeMessage = (range, version) => {
|
||||
};
|
||||
|
||||
/** Human-readable release families for an `engines.node` range. */
|
||||
export const supportedReleaseProse = (range) =>
|
||||
const supportedReleaseProse = (range) =>
|
||||
`Node.js ${range
|
||||
.split("||")
|
||||
.map((clause) => {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { ArtifactResourceScope } from "../../src/artifacts/ArtifactResourceScope.js";
|
||||
|
||||
export class VerifierArtifactCleanupFailure extends AggregateError {
|
||||
readonly _tag: "VerifierArtifactCleanupFailure";
|
||||
readonly cleanupOwner: ArtifactResourceScope;
|
||||
constructor(
|
||||
scope: ArtifactResourceScope,
|
||||
failures: unknown[],
|
||||
cause: unknown,
|
||||
);
|
||||
}
|
||||
|
||||
export function withVerifierArtifactScope<T>(
|
||||
operation: (scope: ArtifactResourceScope) => Promise<T>,
|
||||
): Promise<T>;
|
||||
@@ -0,0 +1,32 @@
|
||||
import { ArtifactResourceScope } from "../../dist/artifacts/ArtifactResourceScope.js";
|
||||
|
||||
/** A failed close retains the scope that can retry all of its owners. */
|
||||
export class VerifierArtifactCleanupFailure extends AggregateError {
|
||||
constructor(scope, failures, cause) {
|
||||
super(failures, "Verifier cleanup failed", { cause });
|
||||
this._tag = "VerifierArtifactCleanupFailure";
|
||||
this.cleanupOwner = scope;
|
||||
}
|
||||
}
|
||||
|
||||
/** Run one verifier operation and retain its exact scope if close fails. */
|
||||
export async function withVerifierArtifactScope(operation) {
|
||||
const scope = new ArtifactResourceScope();
|
||||
let outcome;
|
||||
try {
|
||||
outcome = { ok: true, value: await operation(scope) };
|
||||
} catch (cause) {
|
||||
outcome = { ok: false, cause };
|
||||
}
|
||||
try {
|
||||
await scope.close();
|
||||
} catch (cleanupCause) {
|
||||
throw new VerifierArtifactCleanupFailure(
|
||||
scope,
|
||||
outcome.ok ? [cleanupCause] : [outcome.cause, cleanupCause],
|
||||
outcome.ok ? cleanupCause : outcome.cause,
|
||||
);
|
||||
}
|
||||
if (!outcome.ok) throw outcome.cause;
|
||||
return outcome.value;
|
||||
}
|
||||
@@ -2,10 +2,7 @@ import { randomUUID } from "node:crypto";
|
||||
|
||||
import type { RetainedResourceScope } from "../../src/application/AnalysisProvider.js";
|
||||
import { AnalysisCleanupLease } from "../../src/artifacts/AnalysisCleanupLease.js";
|
||||
import {
|
||||
ArtifactResourceScope,
|
||||
type ArtifactResourceOwner,
|
||||
} from "../../src/artifacts/ArtifactResourceScope.js";
|
||||
import { ArtifactResourceScope } from "../../src/artifacts/ArtifactResourceScope.js";
|
||||
import {
|
||||
prepareOwnedCommand,
|
||||
runOwnedCommand,
|
||||
@@ -61,6 +58,7 @@ export interface VerifierCommands {
|
||||
arguments_: readonly string[],
|
||||
options?: VerifierCommandOptions,
|
||||
): Promise<VerifierCommandOutput>;
|
||||
/** Retain a live resource until close, independently of ordinary command execution. */
|
||||
adopt(owner: VerifierCleanupOwner, resource: string): void;
|
||||
readonly cleanupResources: readonly string[];
|
||||
close(): Promise<void>;
|
||||
@@ -152,13 +150,17 @@ const commandLimits = (options: VerifierCommandOptions) => {
|
||||
};
|
||||
};
|
||||
|
||||
type AdoptedVerifierOwner = {
|
||||
readonly kind: "lease";
|
||||
readonly resource: string;
|
||||
readonly lease: AnalysisCleanupLease;
|
||||
};
|
||||
|
||||
class VerifierCommandOwner implements VerifierCommands {
|
||||
readonly #resources = new ArtifactResourceScope();
|
||||
readonly #controller = new AbortController();
|
||||
readonly #adopted = new WeakMap<
|
||||
VerifierCleanupOwner,
|
||||
ArtifactResourceOwner
|
||||
>();
|
||||
readonly #adopted = new WeakMap<VerifierCleanupOwner, AdoptedVerifierOwner>();
|
||||
readonly #liveAdopted = new Set<AdoptedVerifierOwner>();
|
||||
readonly #resourceOwner: RetainedResourceScope;
|
||||
#closed = false;
|
||||
#released = false;
|
||||
@@ -170,13 +172,21 @@ class VerifierCommandOwner implements VerifierCommands {
|
||||
) => Promise<SpawnedOwnedProviderProcess>,
|
||||
) {
|
||||
this.#resourceOwner = {
|
||||
retain: (owner, resource) => this.adopt(owner, resource),
|
||||
retain: (owner, resource) =>
|
||||
this.#resources.retain(this.#adoptedOwner(owner, resource)),
|
||||
run: (work) => this.#resources.run(work),
|
||||
};
|
||||
}
|
||||
|
||||
get cleanupResources(): readonly string[] {
|
||||
return this.#resources.cleanupResources;
|
||||
return [
|
||||
...new Set([
|
||||
...this.#resources.cleanupResources,
|
||||
...[...this.#liveAdopted].flatMap(({ lease, resource }) =>
|
||||
lease.released ? [] : (lease.cleanupResources ?? [resource]),
|
||||
),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
exec(
|
||||
@@ -233,6 +243,15 @@ class VerifierCommandOwner implements VerifierCommands {
|
||||
}
|
||||
|
||||
adopt(owner: VerifierCleanupOwner, resource: string): void {
|
||||
const retained = this.#adoptedOwner(owner, resource);
|
||||
if (this.#closed) this.#resources.retain(retained);
|
||||
else this.#liveAdopted.add(retained);
|
||||
}
|
||||
|
||||
#adoptedOwner(
|
||||
owner: VerifierCleanupOwner,
|
||||
resource: string,
|
||||
): AdoptedVerifierOwner {
|
||||
if (this.#released)
|
||||
throw new Error("Verifier command resource release has completed");
|
||||
let retained = this.#adopted.get(owner);
|
||||
@@ -244,15 +263,18 @@ class VerifierCommandOwner implements VerifierCommands {
|
||||
};
|
||||
this.#adopted.set(owner, retained);
|
||||
}
|
||||
// An admitted command can transfer a late preparation/cleanup capability
|
||||
// while close is joining it. Acquisition must precede this scope's release.
|
||||
this.#resources.retain(retained);
|
||||
return retained;
|
||||
}
|
||||
|
||||
close(): Promise<void> {
|
||||
this.#closed = true;
|
||||
this.#controller.abort(new Error("Verifier command owner closed"));
|
||||
if (this.#closing !== undefined) return this.#closing;
|
||||
// Live adoption owns a lifetime, not a failed cleanup. Only closure moves
|
||||
// these leases into the retry scope; command failures enter it immediately.
|
||||
// Late transfers during admitted work still join that same close below.
|
||||
for (const owner of this.#liveAdopted) this.#resources.retain(owner);
|
||||
this.#liveAdopted.clear();
|
||||
let attempt: Promise<void>;
|
||||
attempt = this.#resources
|
||||
.close()
|
||||
|
||||
@@ -40,7 +40,7 @@ export interface VerifierTerminalOptions {
|
||||
readonly signal?: AbortSignal;
|
||||
}
|
||||
|
||||
export interface VerifierTerminalDependencies {
|
||||
interface VerifierTerminalDependencies {
|
||||
readonly loadPty?: () => Promise<typeof import("@lydell/node-pty")>;
|
||||
readonly cleanupProcessGroup?: typeof cleanupOwnedProcessGroup;
|
||||
}
|
||||
@@ -59,7 +59,7 @@ type TerminalFailureReason =
|
||||
| "unsupported_platform";
|
||||
|
||||
/** Expected terminal failures retain the decoded output observed at failure. */
|
||||
export class VerifierTerminalFailure extends Error {
|
||||
class VerifierTerminalFailure extends Error {
|
||||
readonly _tag = "VerifierTerminalFailure";
|
||||
|
||||
constructor(
|
||||
@@ -74,7 +74,7 @@ export class VerifierTerminalFailure extends Error {
|
||||
}
|
||||
|
||||
/** The adopted owner remains the exact authority for retrying this release. */
|
||||
export class VerifierTerminalCleanupFailure extends Error {
|
||||
class VerifierTerminalCleanupFailure extends Error {
|
||||
readonly _tag = "VerifierTerminalCleanupFailure";
|
||||
readonly reason = "cleanup_incomplete";
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ import noEvidenceJsonPreadmission from "./rules/no-evidence-json-preadmission.mj
|
||||
import noFixtureRootCleanup from "./rules/no-fixture-root-cleanup.mjs";
|
||||
import noPositionalToolContract from "./rules/no-positional-tool-contract.mjs";
|
||||
import noSharedModuleMock from "./rules/no-shared-module-mock.mjs";
|
||||
import noLocaleCompare from "./rules/no-locale-compare.mjs";
|
||||
import noBabelVisitorKeys from "./rules/no-babel-visitor-keys.mjs";
|
||||
|
||||
export default {
|
||||
meta: { name: "rea" },
|
||||
@@ -15,5 +17,7 @@ export default {
|
||||
"no-fixture-root-cleanup": noFixtureRootCleanup,
|
||||
"no-positional-tool-contract": noPositionalToolContract,
|
||||
"no-shared-module-mock": noSharedModuleMock,
|
||||
"no-locale-compare": noLocaleCompare,
|
||||
"no-babel-visitor-keys": noBabelVisitorKeys,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
import type { RuleTester } from "oxlint/plugins-dev";
|
||||
|
||||
declare const rule: Parameters<RuleTester["run"]>[1];
|
||||
export default rule;
|
||||
@@ -0,0 +1,73 @@
|
||||
import { findBinding } from "../bindings.mjs";
|
||||
|
||||
const nameOf = (node) =>
|
||||
node.type === "Identifier"
|
||||
? node.name
|
||||
: node.type === "Literal"
|
||||
? node.value
|
||||
: node.type === "TemplateLiteral" && node.expressions.length === 0
|
||||
? node.quasis[0]?.value.cooked
|
||||
: undefined;
|
||||
|
||||
export default {
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: { description: "Keep Babel child expansion in its shared owner." },
|
||||
schema: [],
|
||||
messages: {
|
||||
owner:
|
||||
"Use childNodes or traverseJavaScriptAst instead of Babel VISITOR_KEYS.",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const source = context.sourceCode;
|
||||
const isBabelNamespace = (node) => {
|
||||
if (node?.type !== "Identifier") return false;
|
||||
const definition = findBinding(source, node)?.defs[0];
|
||||
if (
|
||||
definition?.type !== "ImportBinding" ||
|
||||
definition.node.type !== "ImportNamespaceSpecifier"
|
||||
)
|
||||
return false;
|
||||
const declaration = definition.parent ?? definition.node.parent;
|
||||
return declaration?.source?.value === "@babel/types";
|
||||
};
|
||||
return {
|
||||
ImportSpecifier(node) {
|
||||
if (
|
||||
node.parent.source.value === "@babel/types" &&
|
||||
(node.imported.name ?? node.imported.value) === "VISITOR_KEYS"
|
||||
)
|
||||
context.report({ node, messageId: "owner" });
|
||||
},
|
||||
ExportNamedDeclaration(node) {
|
||||
if (node.source?.value !== "@babel/types") return;
|
||||
for (const specifier of node.specifiers)
|
||||
if (
|
||||
specifier.type === "ExportSpecifier" &&
|
||||
nameOf(specifier.local) === "VISITOR_KEYS"
|
||||
)
|
||||
context.report({ node: specifier, messageId: "owner" });
|
||||
},
|
||||
MemberExpression(node) {
|
||||
if (
|
||||
(!node.computed || node.property.type !== "Identifier") &&
|
||||
nameOf(node.property) === "VISITOR_KEYS" &&
|
||||
isBabelNamespace(node.object)
|
||||
)
|
||||
context.report({ node, messageId: "owner" });
|
||||
},
|
||||
VariableDeclarator(node) {
|
||||
if (node.id.type !== "ObjectPattern" || !isBabelNamespace(node.init))
|
||||
return;
|
||||
for (const property of node.id.properties)
|
||||
if (
|
||||
property.type === "Property" &&
|
||||
(!property.computed || property.key.type !== "Identifier") &&
|
||||
nameOf(property.key) === "VISITOR_KEYS"
|
||||
)
|
||||
context.report({ node: property, messageId: "owner" });
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
import type { RuleTester } from "oxlint/plugins-dev";
|
||||
|
||||
declare const rule: Parameters<RuleTester["run"]>[1];
|
||||
export default rule;
|
||||
@@ -0,0 +1,34 @@
|
||||
export default {
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description: "Keep domain ordering independent of the host locale.",
|
||||
},
|
||||
schema: [],
|
||||
messages: {
|
||||
locale:
|
||||
"Use compareUnicodeCodePoints instead of localeCompare for domain ordering.",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
CallExpression(node) {
|
||||
const member = node.callee;
|
||||
if (member.type !== "MemberExpression") return;
|
||||
if (
|
||||
(!member.computed &&
|
||||
member.property.type === "Identifier" &&
|
||||
member.property.name === "localeCompare") ||
|
||||
(member.computed &&
|
||||
member.property.type === "Literal" &&
|
||||
member.property.value === "localeCompare") ||
|
||||
(member.computed &&
|
||||
member.property.type === "TemplateLiteral" &&
|
||||
member.property.expressions.length === 0 &&
|
||||
member.property.quasis[0]?.value.cooked === "localeCompare")
|
||||
)
|
||||
context.report({ node, messageId: "locale" });
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
+1
-1
@@ -54,7 +54,7 @@ if (await pathExists(join(root, "src"))) {
|
||||
"dist/main.js",
|
||||
"dist/mcp/main.js",
|
||||
"dist/cli-runtime/cli.js",
|
||||
"dist/cli-runtime/cliOutput.js",
|
||||
"dist/cli-runtime/cliOneShot.js",
|
||||
"dist/cli-runtime/cliBuiltinArguments.js",
|
||||
"dist/cli-runtime/cliResultOutput.js",
|
||||
"dist/cli-runtime/mcpDoctor.js",
|
||||
|
||||
+54
-142
@@ -64,7 +64,7 @@ const runtimeFiles = [
|
||||
? ["dist/mcp/main.js", "dist/cli-runtime/mcpDoctor.js"]
|
||||
: [
|
||||
"dist/cli-runtime/cli.js",
|
||||
"dist/cli-runtime/cliOutput.js",
|
||||
"dist/cli-runtime/cliOneShot.js",
|
||||
"dist/cli-runtime/cliResultOutput.js",
|
||||
]),
|
||||
];
|
||||
@@ -104,154 +104,66 @@ if (!(await compiledRuntimeExists(runtimeFiles))) {
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const {
|
||||
cliConfigurationError,
|
||||
renderCliOutputArgumentError,
|
||||
renderEmptyFilteredCliOutput,
|
||||
renderVersionReport,
|
||||
parseCliOutputArguments,
|
||||
} = await import("../dist/cli-runtime/cliOutput.js");
|
||||
const { runOneShotCli } = await import("../dist/cli-runtime/cliOneShot.js");
|
||||
guardClosedStdout();
|
||||
const cliArgs = isShellCompletion
|
||||
? args
|
||||
: builtinArguments.normalizeCliArguments(args);
|
||||
// Completion words are transport data, including partial output flags.
|
||||
const outputOptions = parseCliOutputArguments(
|
||||
isShellCompletion ? [] : cliArgs,
|
||||
);
|
||||
const outputArguments = outputOptions.validation;
|
||||
// `--version` with an explicit format reports the runtime as a document;
|
||||
// plain `--version` stays Incur's bare version string.
|
||||
const versionOutput = renderVersionReport(
|
||||
outputOptions,
|
||||
{
|
||||
packageName: packageJson.name,
|
||||
packageVersion: packageJson.version,
|
||||
versions: process.versions,
|
||||
platform: process.platform,
|
||||
architecture: process.arch,
|
||||
},
|
||||
process.stdout.isTTY === true,
|
||||
);
|
||||
const configurationError = cliConfigurationError(process.env);
|
||||
let dispatchFailure;
|
||||
let closeCliResources;
|
||||
// Incur can exit from the callback below before this function returns.
|
||||
// That callback owns the status, including a closed-pipe exit of 141.
|
||||
let exiting = false;
|
||||
try {
|
||||
// Doctor is the readiness report for this failure. Other commands still stop
|
||||
// here so they do not run with an unusable log level.
|
||||
if (
|
||||
configurationError !== undefined &&
|
||||
(isShellCompletion ||
|
||||
builtinArguments.splitIncurBuiltinArguments(cliArgs).command[0] !==
|
||||
"doctor")
|
||||
) {
|
||||
process.stderr.write(`${configurationError}\n`);
|
||||
process.exitCode = 1;
|
||||
} else if (!outputArguments.ok) {
|
||||
writeStdout(renderCliOutputArgumentError(outputArguments));
|
||||
process.exitCode = 1;
|
||||
} else if (versionOutput !== undefined) {
|
||||
writeStdout(versionOutput);
|
||||
} else if (
|
||||
!isShellCompletion &&
|
||||
builtinArguments.isProjectScopedAmpRegistration(cliArgs)
|
||||
) {
|
||||
// Amp has global settings only. Share the parsed scope fact with the
|
||||
// registration workflow and refuse before loading the command runtime.
|
||||
//
|
||||
// A caller that asked for a structured document gets the refusal in
|
||||
// that document, through the same pre-execution error renderer the
|
||||
// output-argument branch uses; anything else keeps the plain message.
|
||||
const refusalMessage =
|
||||
"Amp does not support project-scoped registration. Run without --no-global to register Amp globally, or select a client with project settings. No client settings were changed.";
|
||||
const refusalFormat = outputOptions.format;
|
||||
if (
|
||||
refusalFormat === "json" ||
|
||||
refusalFormat === "jsonl" ||
|
||||
refusalFormat === "yaml"
|
||||
) {
|
||||
writeStdout(
|
||||
renderCliOutputArgumentError({
|
||||
ok: false,
|
||||
format: refusalFormat,
|
||||
code: "AMP_PROJECT_SCOPE_UNSUPPORTED",
|
||||
message: refusalMessage,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
process.stderr.write(`${refusalMessage}\n`);
|
||||
}
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
const cliRuntime = await import("../dist/cli-runtime/cli.js");
|
||||
closeCliResources = cliRuntime.closeCliResources;
|
||||
const { createCli } = cliRuntime;
|
||||
const { createCliResultOutput } =
|
||||
await import("../dist/cli-runtime/cliResultOutput.js");
|
||||
let wroteOutput = false;
|
||||
const resultOutput = createCliResultOutput(outputOptions, process.stdout);
|
||||
try {
|
||||
await createCli(process.env, resultOutput).serve(cliArgs, {
|
||||
stdout: (output) => {
|
||||
if (resultOutput?.handled) {
|
||||
if (resultOutput.failed && !stdoutClosed)
|
||||
process.stderr.write(output);
|
||||
return;
|
||||
}
|
||||
if (output.length > 0) wroteOutput = true;
|
||||
if (resultOutput?.captureSdkOutput(output)) return;
|
||||
writeStdout(output);
|
||||
},
|
||||
// A command failure caused by the closed pipe keeps the pipe's status.
|
||||
// Worker shutdown is shared with the dispatch finally below. A failed
|
||||
// shutdown is reported here because this callback is what exits.
|
||||
exit: (code) => {
|
||||
exiting = true;
|
||||
void (async () => {
|
||||
let status = typeof code === "number" ? code : 0;
|
||||
try {
|
||||
await closeCliResources();
|
||||
} catch (cleanup) {
|
||||
process.stderr.write(
|
||||
`Failed to close CLI resources: ${cleanupFailureText(cleanup)}\n`,
|
||||
);
|
||||
if (status === 0) status = 1;
|
||||
}
|
||||
try {
|
||||
await resultOutput?.finishSdkOutput(status);
|
||||
} catch (cause) {
|
||||
if (!stdoutClosed)
|
||||
process.stderr.write(`${cleanupFailureText(cause)}\n`);
|
||||
if (status === 0) status = 1;
|
||||
}
|
||||
process.exit(stdoutClosed ? SIGPIPE_EXIT_STATUS : status);
|
||||
})();
|
||||
},
|
||||
});
|
||||
} catch (cause) {
|
||||
if (wroteOutput) throw cause;
|
||||
const recovered = await resultOutput
|
||||
?.recoverFormattingFailure(cause)
|
||||
.catch((writeFailure) => {
|
||||
// Recovery writes outside Incur's command boundary. A reader that
|
||||
// declined this document keeps the same quiet SIGPIPE outcome.
|
||||
if (!stdoutClosed) throw writeFailure;
|
||||
return true;
|
||||
});
|
||||
if (!recovered) throw cause;
|
||||
process.exitCode = stdoutClosed ? SIGPIPE_EXIT_STATUS : 1;
|
||||
}
|
||||
if (!wroteOutput && !resultOutput?.handled)
|
||||
writeStdout(renderEmptyFilteredCliOutput(outputOptions) ?? "");
|
||||
// An SDK failure exits through the callback, which owns its pending write.
|
||||
if (!exiting)
|
||||
await resultOutput?.finishSdkOutput(
|
||||
typeof process.exitCode === "number" ? process.exitCode : 0,
|
||||
);
|
||||
}
|
||||
await runOneShotCli(args, {
|
||||
environment: process.env,
|
||||
runtime: {
|
||||
packageName: packageJson.name,
|
||||
packageVersion: packageJson.version,
|
||||
versions: process.versions,
|
||||
platform: process.platform,
|
||||
architecture: process.arch,
|
||||
},
|
||||
stdoutIsTerminal: process.stdout.isTTY === true,
|
||||
resultDestination: process.stdout,
|
||||
closedStdoutStatus: SIGPIPE_EXIT_STATUS,
|
||||
writeStdout,
|
||||
writeStderr: (text) => {
|
||||
process.stderr.write(text);
|
||||
},
|
||||
stdoutClosed: () => stdoutClosed,
|
||||
exitCode: () =>
|
||||
typeof process.exitCode === "number" ? process.exitCode : 0,
|
||||
setExitCode: (code) => {
|
||||
process.exitCode = code;
|
||||
},
|
||||
ownCliResources: (close) => {
|
||||
closeCliResources = close;
|
||||
},
|
||||
exitRequested: () => exiting,
|
||||
// A command failure caused by the closed pipe keeps the pipe's status.
|
||||
// Worker shutdown is shared with the dispatch finally below. A failed
|
||||
// shutdown is reported here because this callback is what exits.
|
||||
exit: (code, finishOutput) => {
|
||||
exiting = true;
|
||||
void (async () => {
|
||||
let status = code;
|
||||
try {
|
||||
await closeCliResources();
|
||||
} catch (cleanup) {
|
||||
process.stderr.write(
|
||||
`Failed to close CLI resources: ${cleanupFailureText(cleanup)}\n`,
|
||||
);
|
||||
if (status === 0) status = 1;
|
||||
}
|
||||
try {
|
||||
await finishOutput(status);
|
||||
} catch (cause) {
|
||||
if (!stdoutClosed)
|
||||
process.stderr.write(`${cleanupFailureText(cause)}\n`);
|
||||
if (status === 0) status = 1;
|
||||
}
|
||||
process.exit(stdoutClosed ? SIGPIPE_EXIT_STATUS : status);
|
||||
})();
|
||||
},
|
||||
});
|
||||
} catch (cause) {
|
||||
if (cause?.code === "EPIPE") {
|
||||
stdoutClosed = true;
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { stat } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
import { realpath, stat } from "node:fs/promises";
|
||||
import { relative, resolve, sep } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import {
|
||||
developmentTestPlan,
|
||||
parseDevelopmentTestRequest,
|
||||
} from "./lib/development-tests.mjs";
|
||||
import { projectForTest } from "./lib/test-projects.mjs";
|
||||
|
||||
let child;
|
||||
let forceTimer;
|
||||
@@ -29,9 +30,22 @@ for (const signal of Object.keys(signalExitCodes))
|
||||
try {
|
||||
const [mode, ...arguments_] = process.argv.slice(2);
|
||||
const request = parseDevelopmentTestRequest(mode, arguments_);
|
||||
const resolvedPaths = [];
|
||||
const repository = await realpath(".");
|
||||
for (const path of request.paths) {
|
||||
if (!(await stat(path)).isFile())
|
||||
throw new Error(`Test path is not a file: ${path}`);
|
||||
// Ownership follows the filesystem's canonical spelling. Execution keeps
|
||||
// the caller path so exact discovery still rejects excluded symlink aliases.
|
||||
const resolvedPath = relative(repository, await realpath(path))
|
||||
.split(sep)
|
||||
.join("/");
|
||||
if (
|
||||
request.mode !== "focused" &&
|
||||
projectForTest(resolvedPath).group !== "source"
|
||||
)
|
||||
throw new Error(`Use test:focused for built test projects: ${path}`);
|
||||
resolvedPaths.push(resolvedPath);
|
||||
}
|
||||
let baseCommit;
|
||||
if (request.mode === "changed") {
|
||||
@@ -42,7 +56,7 @@ try {
|
||||
);
|
||||
baseCommit = base.output.trim();
|
||||
}
|
||||
const plan = developmentTestPlan(request, baseCommit);
|
||||
const plan = developmentTestPlan(request, baseCommit, resolvedPaths);
|
||||
if (request.dryRun) {
|
||||
console.log(JSON.stringify({ ...request, baseCommit, ...plan }, null, 2));
|
||||
} else {
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
#!/usr/bin/env node
|
||||
import assert from "node:assert/strict";
|
||||
import { isUtf8 } from "node:buffer";
|
||||
import { execFile } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { lstat, mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { isAbsolute, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
import { parseEvidence } from "../dist/domain/evidence.js";
|
||||
import { goBinarySchema } from "../dist/domain/go/goBinary.js";
|
||||
import { PrivateRuntimeRoot } from "../dist/process/PrivateRuntimeRoot.js";
|
||||
import { AnalysisCleanupLease } from "../dist/artifacts/AnalysisCleanupLease.js";
|
||||
import { artifactMcpCall } from "./lib/artifact-e2e.mjs";
|
||||
import { requireMcpOutput } from "./lib/mcp-verifier-results.mjs";
|
||||
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
|
||||
import { createVerifierCommands } from "./lib/verify-commands.mjs";
|
||||
import { createOwnedMcpStdioTransport } from "./lib/owned-mcp-stdio.mjs";
|
||||
import { closeVerifierResources } from "./lib/verifier-cleanup.mjs";
|
||||
|
||||
const execute = promisify(execFile);
|
||||
const commands = await createVerifierCommands();
|
||||
const compiler = process.env.REA_VERIFY_GO_COMMAND ?? "go";
|
||||
if (process.env.REA_VERIFY_GO_COMMAND !== undefined && !isAbsolute(compiler))
|
||||
throw new Error(
|
||||
@@ -51,13 +52,16 @@ const goEnvironment = {
|
||||
// Go's own verifier seam isolates telemetry; no global configuration is changed.
|
||||
TEST_TELEMETRY_DIR: join(root.path, "telemetry"),
|
||||
};
|
||||
const client = new Client({ name: "go-binary-verifier", version: "1" });
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [entrypoint, "mcp"],
|
||||
env: environment,
|
||||
stderr: "pipe",
|
||||
});
|
||||
let client;
|
||||
let transport;
|
||||
const cleanup = new AnalysisCleanupLease(async () => {
|
||||
await closeVerifierResources([
|
||||
() => client?.close(),
|
||||
() => transport?.close(),
|
||||
() => commands.close(),
|
||||
]);
|
||||
await root.close();
|
||||
}, [root.path]);
|
||||
const targets = [
|
||||
["linux", "amd64", "elf", 64, "little"],
|
||||
["linux", "386", "elf", 32, "little"],
|
||||
@@ -73,10 +77,19 @@ const verified = [];
|
||||
const omittedCoverage = [];
|
||||
const failures = [];
|
||||
try {
|
||||
client = new Client({ name: "go-binary-verifier", version: "1" });
|
||||
transport = await createOwnedMcpStdioTransport({
|
||||
command: process.execPath,
|
||||
arguments: [entrypoint, "mcp"],
|
||||
hostEnvironment: environment,
|
||||
cwd: process.cwd(),
|
||||
expectedCommand: null,
|
||||
sweepTokenOwnedProcesses: true,
|
||||
});
|
||||
await prepareFixture();
|
||||
try {
|
||||
compilerVersion = (
|
||||
await execute(compiler, ["version"], {
|
||||
await commands.exec(compiler, ["version"], {
|
||||
cwd: root.path,
|
||||
env: goEnvironment,
|
||||
timeout: 10_000,
|
||||
@@ -185,7 +198,7 @@ try {
|
||||
const path = join(root.path, "linux-amd64-byte-strings");
|
||||
const expected = JSON.parse(
|
||||
(
|
||||
await execute(oracle, ["--build-byte-fixture", compiler, path], {
|
||||
await commands.exec(oracle, ["--build-byte-fixture", compiler, path], {
|
||||
cwd: sourceRoot,
|
||||
env: { ...goEnvironment, GOOS: "linux", GOARCH: "amd64" },
|
||||
timeout: 120_000,
|
||||
@@ -198,21 +211,16 @@ try {
|
||||
} catch (cause) {
|
||||
failures.push(cause);
|
||||
} finally {
|
||||
for (const close of [
|
||||
() => client.close(),
|
||||
() => transport.close(),
|
||||
() => root.close(),
|
||||
]) {
|
||||
try {
|
||||
await close();
|
||||
} catch (cause) {
|
||||
failures.push(cause);
|
||||
}
|
||||
try {
|
||||
await cleanup.release();
|
||||
} catch (cause) {
|
||||
failures.push(cause);
|
||||
}
|
||||
}
|
||||
const verifier = await completeVerifierRun(run);
|
||||
try {
|
||||
await assert.rejects(lstat(root.path), { code: "ENOENT" });
|
||||
if (cleanup.released)
|
||||
await assert.rejects(lstat(root.path), { code: "ENOENT" });
|
||||
if (verifier.process_lineage.status === "verified")
|
||||
assert.deepEqual(verifier.process_lineage.descendants, []);
|
||||
} catch (cause) {
|
||||
@@ -226,15 +234,21 @@ if (failures.length > 0) {
|
||||
compiler: compilerVersion,
|
||||
verified,
|
||||
omitted_coverage: omittedCoverage,
|
||||
retained_root: cleanup.released ? null : root.path,
|
||||
verifier,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
throw new AggregateError(
|
||||
failures,
|
||||
"Real Go metadata verification failed; cleanup failures are retained.",
|
||||
throw Object.assign(
|
||||
new AggregateError(
|
||||
failures,
|
||||
cleanup.released
|
||||
? "Real Go metadata verification failed; cleanup failures are retained."
|
||||
: `Real Go metadata verification failed; cleanup is incomplete at ${root.path}.`,
|
||||
),
|
||||
{ cleanupLeases: cleanup.released ? [] : [cleanup] },
|
||||
);
|
||||
}
|
||||
console.log(
|
||||
@@ -256,7 +270,7 @@ console.log(
|
||||
);
|
||||
|
||||
async function go(args, overrides = {}) {
|
||||
return execute(compiler, args, {
|
||||
return commands.exec(compiler, args, {
|
||||
cwd: sourceRoot,
|
||||
env: { ...goEnvironment, ...overrides },
|
||||
timeout: 120_000,
|
||||
@@ -356,7 +370,7 @@ async function inspect(mode, path, includeStringLiterals = false) {
|
||||
...(includeStringLiterals ? ["--include-string-literals"] : []),
|
||||
"--json",
|
||||
];
|
||||
const { stdout } = await execute(process.execPath, args, {
|
||||
const { stdout } = await commands.exec(process.execPath, args, {
|
||||
env: environment,
|
||||
timeout: 30_000,
|
||||
maxBuffer: 16 * 1024 * 1024,
|
||||
@@ -424,7 +438,7 @@ async function verify(
|
||||
byteExpected ??
|
||||
JSON.parse(
|
||||
(
|
||||
await execute(oracle, [oraclePath], {
|
||||
await commands.exec(oracle, [oraclePath], {
|
||||
timeout: 10_000,
|
||||
maxBuffer: 1024 * 1024,
|
||||
})
|
||||
|
||||
@@ -1,13 +1,3 @@
|
||||
export interface PackageHopperEnvironment {
|
||||
readonly HOPPER_LAUNCHER_PATH: string;
|
||||
readonly HOPPER_LOADER_ARGS_JSON: string;
|
||||
}
|
||||
|
||||
export function packageHopperEnvironment(
|
||||
root: string,
|
||||
platform?: string,
|
||||
): PackageHopperEnvironment;
|
||||
|
||||
export function verifyPackageEnvironment(input: {
|
||||
readonly root: string;
|
||||
readonly workspace: string;
|
||||
|
||||
@@ -117,7 +117,7 @@ export async function verifyPackageEnvironment({
|
||||
};
|
||||
}
|
||||
|
||||
export const packageHopperEnvironment = (root, platform = process.platform) => {
|
||||
const packageHopperEnvironment = (root, platform = process.platform) => {
|
||||
const fakeLauncher = join(root, "tests/fixtures/fakeLauncher.mjs");
|
||||
if (platform !== "linux")
|
||||
return {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { deepStrictEqual } from "node:assert";
|
||||
import {
|
||||
lstat,
|
||||
mkdir,
|
||||
@@ -13,7 +12,6 @@ import { dirname, isAbsolute, join } from "node:path";
|
||||
import { parse as parseToml } from "smol-toml";
|
||||
|
||||
import { TOOL_CONTRACTS } from "../dist/contracts/toolContracts.js";
|
||||
import { PRODUCT_IDENTITY } from "../dist/identity.js";
|
||||
import { pathExists } from "./lib/verify-package-core.mjs";
|
||||
import { parse as parseJsonc } from "jsonc-parser";
|
||||
import { skillReferenceIssues } from "./lib/docs-facts.mjs";
|
||||
|
||||
@@ -99,8 +99,7 @@ async function verifyPublishedUpdate({
|
||||
environment,
|
||||
);
|
||||
|
||||
await commands.exec(
|
||||
"npm",
|
||||
await commands.execNpm(
|
||||
[
|
||||
"install",
|
||||
"--global",
|
||||
@@ -118,7 +117,7 @@ async function verifyPublishedUpdate({
|
||||
);
|
||||
|
||||
const npmRoot = (
|
||||
await commands.exec("npm", ["root", "--global"], {
|
||||
await commands.execNpm(["root", "--global"], {
|
||||
cwd: canaryRoot,
|
||||
env: environment,
|
||||
timeout: 30000,
|
||||
@@ -186,8 +185,7 @@ if (result.status === "failed") process.exitCode = 1;
|
||||
}
|
||||
|
||||
async function findPreviousPublishedVersion(targetVersion, cwd, environment) {
|
||||
const { stdout } = await commands.exec(
|
||||
"npm",
|
||||
const { stdout } = await commands.execNpm(
|
||||
["view", "rea-agents", "versions", "--json"],
|
||||
{ cwd, env: environment, timeout: 30000, maxBuffer: 16 * 1024 * 1024 },
|
||||
);
|
||||
|
||||
@@ -414,7 +414,7 @@ export function matchesSymbol(value, expected) {
|
||||
return tail === expected || tail === `_${expected}`;
|
||||
}
|
||||
|
||||
export function symbolTail(value) {
|
||||
function symbolTail(value) {
|
||||
if (typeof value !== "string") return undefined;
|
||||
return value.split("::").at(-1);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFile, mkdtemp, mkdir, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
@@ -10,10 +9,9 @@ import { promisify } from "node:util";
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js";
|
||||
import { ArtifactResourceScope } from "../dist/artifacts/ArtifactResourceScope.js";
|
||||
import { withVerifierArtifactScope } from "./lib/verifier-artifact-scope.mjs";
|
||||
import { readGhidraLanguageCatalog } from "../dist/ghidra/GhidraLanguageCatalog.js";
|
||||
import { parseEvidence } from "../dist/domain/evidence.js";
|
||||
import { parseAnalysisSnapshot } from "../dist/domain/analysisSnapshot.js";
|
||||
import { functionDossierSchema } from "../dist/domain/native/functionDossier.js";
|
||||
import { inspectVerifierGhidraInstallation } from "./lib/ghidra-installation-inspection.mjs";
|
||||
import { buildGameCubeDolFixture } from "../tests/conformance/ghidra/dol-fixture.mjs";
|
||||
@@ -84,20 +82,15 @@ for (const setting of ["GHIDRA_HEADLESS_MAXMEM", "GHIDRA_MAXMEM"]) {
|
||||
* installation catalog reader it uses. User extensions are not in that catalog.
|
||||
*/
|
||||
async function languageInstalled(languageId) {
|
||||
const scope = new ArtifactResourceScope();
|
||||
try {
|
||||
return withVerifierArtifactScope(async (scope) => {
|
||||
const catalog = await readGhidraLanguageCatalog(
|
||||
installation.installDir,
|
||||
scope,
|
||||
);
|
||||
return catalog.some((definition) => definition.id === languageId);
|
||||
} finally {
|
||||
await scope.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
||||
|
||||
/** One MCP session plus CLI check. `languageOverride` is set only for the Gekko pass. */
|
||||
async function runPass({ fixture, languageOverride, label, layout }) {
|
||||
const env = { ...baseEnv };
|
||||
|
||||
@@ -11,6 +11,10 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
import { artifactMcpCall } from "./lib/artifact-e2e.mjs";
|
||||
import { snapshotHopperRuntime } from "./lib/real-hopper-cleanup.mjs";
|
||||
import {
|
||||
closeVerifierResources,
|
||||
withVerifierCleanup,
|
||||
} from "./lib/verifier-cleanup.mjs";
|
||||
import { HOPPER_TARGET_LEASE_DIRECTORY } from "../dist/process/HopperLease.js";
|
||||
|
||||
assert.equal(process.platform, "darwin", "requires macOS");
|
||||
@@ -48,11 +52,9 @@ const digest = async (path) =>
|
||||
createHash("sha256")
|
||||
.update(await readFile(path))
|
||||
.digest("hex");
|
||||
const sessionsBefore = await snapshotHopperRuntime(
|
||||
"/tmp",
|
||||
HOPPER_TARGET_LEASE_DIRECTORY,
|
||||
);
|
||||
const ownedProcessIds = new Set();
|
||||
const sessionsBefore = (
|
||||
await snapshotHopperRuntime("/tmp", HOPPER_TARGET_LEASE_DIRECTORY)
|
||||
).paths;
|
||||
const client = new Client({ name: "hopper-callers-verifier", version: "1" });
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
@@ -93,7 +95,7 @@ const matchingRef = (ref, site, from, to) =>
|
||||
ref.source_procedure?.address === from &&
|
||||
ref.target_procedure?.address === to &&
|
||||
ref.call?.classification === "direct";
|
||||
try {
|
||||
await withVerifierCleanup(async () => {
|
||||
await run(clang, [
|
||||
"-isysroot",
|
||||
sdk,
|
||||
@@ -129,7 +131,6 @@ try {
|
||||
await client.connect(transport);
|
||||
if (transport.pid === null)
|
||||
throw new Error("MCP process identity unavailable");
|
||||
ownedProcessIds.add(transport.pid);
|
||||
let notifications = 0;
|
||||
const original = transport.onmessage;
|
||||
transport.onmessage = (message) => {
|
||||
@@ -287,23 +288,36 @@ try {
|
||||
sha256: beforeLarge,
|
||||
}),
|
||||
);
|
||||
} finally {
|
||||
await client.close();
|
||||
await transport.close();
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
const leaked = [
|
||||
...(await snapshotHopperRuntime(
|
||||
"/tmp",
|
||||
HOPPER_TARGET_LEASE_DIRECTORY,
|
||||
ownedProcessIds,
|
||||
)),
|
||||
].filter((path) => !sessionsBefore.has(path));
|
||||
if (leaked.length === 0)
|
||||
}, [
|
||||
async () => {
|
||||
try {
|
||||
await closeVerifierResources([
|
||||
() => client.close(),
|
||||
() => transport.close(),
|
||||
async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
const leaked = [
|
||||
...(
|
||||
await snapshotHopperRuntime("/tmp", HOPPER_TARGET_LEASE_DIRECTORY)
|
||||
).paths,
|
||||
].filter((path) => !sessionsBefore.has(path));
|
||||
assert.deepEqual(
|
||||
leaked,
|
||||
[],
|
||||
`Hopper resources remain: ${leaked.join(", ")}`,
|
||||
);
|
||||
},
|
||||
]);
|
||||
} catch (cause) {
|
||||
throw new Error(
|
||||
`Hopper cleanup failed; fixture retained at ${directory}`,
|
||||
{
|
||||
cause,
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
if (stderr) process.stderr.write(stderr);
|
||||
}
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
if (stderr) process.stderr.write(stderr);
|
||||
assert.deepEqual(
|
||||
leaked,
|
||||
[],
|
||||
`Hopper resources remain: ${leaked.join(", ")}; fixture retained at ${directory}`,
|
||||
);
|
||||
}
|
||||
},
|
||||
]);
|
||||
@@ -1,10 +1,11 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { copyFile, mkdtemp, realpath, rm } from "node:fs/promises";
|
||||
import { copyFile, mkdtemp, realpath } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { firstProcedureAddress } from "./lib/real-hopper-assertions.mjs";
|
||||
import { HopperDeadlineCleanup } from "./lib/hopper-deadline-cleanup.mjs";
|
||||
import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js";
|
||||
import { ArtifactResourceScope } from "../dist/artifacts/ArtifactResourceScope.js";
|
||||
import { AnalysisCleanupLease } from "../dist/artifacts/AnalysisCleanupLease.js";
|
||||
@@ -29,6 +30,7 @@ const root = await realpath(
|
||||
);
|
||||
const target = join(root, "deadline-fixture");
|
||||
const profileResources = new ArtifactResourceScope();
|
||||
const cleanupOwner = new HopperDeadlineCleanup(root, profileResources);
|
||||
const profileResourceScope = {
|
||||
retain: (owner, resource) =>
|
||||
profileResources.retain({
|
||||
@@ -40,6 +42,7 @@ const profileResourceScope = {
|
||||
};
|
||||
let client;
|
||||
let primaryFailure;
|
||||
let operationFailed = false;
|
||||
try {
|
||||
await copyFile(fixtures.primary.path, target);
|
||||
const parsed = await parseBinaryTarget(target);
|
||||
@@ -148,31 +151,19 @@ try {
|
||||
),
|
||||
);
|
||||
} catch (cause) {
|
||||
operationFailed = true;
|
||||
primaryFailure = cause;
|
||||
throw cause;
|
||||
} finally {
|
||||
let cleanupFailure;
|
||||
if (client !== undefined) {
|
||||
const closed = await client.close();
|
||||
if (!closed.ok) cleanupFailure = closed.error;
|
||||
}
|
||||
try {
|
||||
await profileResources.close();
|
||||
} catch (cause) {
|
||||
cleanupFailure =
|
||||
cleanupFailure === undefined
|
||||
? cause
|
||||
: new AggregateError(
|
||||
[cleanupFailure, cause],
|
||||
"Hopper profile and client cleanup failed",
|
||||
);
|
||||
}
|
||||
await rm(root, { recursive: true, force: true });
|
||||
if (cleanupFailure !== undefined)
|
||||
throw primaryFailure === undefined
|
||||
? cleanupFailure
|
||||
: new AggregateError(
|
||||
if (client !== undefined) cleanupOwner.setClient(client);
|
||||
await cleanupOwner.close();
|
||||
} catch (cleanupFailure) {
|
||||
throw operationFailed
|
||||
? new AggregateError(
|
||||
[primaryFailure, cleanupFailure],
|
||||
"Hopper verification and cleanup failed",
|
||||
);
|
||||
)
|
||||
: cleanupFailure;
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,10 @@ import {
|
||||
requireMcpOperationResult,
|
||||
} from "./lib/mcp-verifier-results.mjs";
|
||||
import { snapshotHopperRuntime } from "./lib/real-hopper-cleanup.mjs";
|
||||
import {
|
||||
closeVerifierResources,
|
||||
withVerifierCleanup,
|
||||
} from "./lib/verifier-cleanup.mjs";
|
||||
import { HOPPER_TARGET_LEASE_DIRECTORY } from "../dist/process/HopperLease.js";
|
||||
import { parseConfig } from "../dist/config/parseConfig.js";
|
||||
import {
|
||||
@@ -70,14 +74,12 @@ const intel = join(directory, "c-x86_64");
|
||||
const fat = join(directory, "c-fat");
|
||||
const fat64 = join(directory, "c-fat64");
|
||||
const singleFat = join(directory, "c-single-fat");
|
||||
const owned = new Set();
|
||||
const extraFat64 = [];
|
||||
const invalidFat64 = [];
|
||||
let preparedImagePath;
|
||||
const before = await snapshotHopperRuntime(
|
||||
"/tmp",
|
||||
HOPPER_TARGET_LEASE_DIRECTORY,
|
||||
);
|
||||
const before = (
|
||||
await snapshotHopperRuntime("/tmp", HOPPER_TARGET_LEASE_DIRECTORY)
|
||||
).paths;
|
||||
const client = new Client({ name: "hopper-fat-verifier", version: "1" });
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
@@ -94,7 +96,7 @@ const ajv = new Ajv({ strict: false });
|
||||
addFormats(ajv);
|
||||
const observations = [];
|
||||
const sourceBinding = [];
|
||||
try {
|
||||
await withVerifierCleanup(async () => {
|
||||
await run(toolPaths.clang, [
|
||||
"-isysroot",
|
||||
sdk,
|
||||
@@ -183,9 +185,17 @@ try {
|
||||
"overlap",
|
||||
/slices overlap/u,
|
||||
(bytes) => {
|
||||
const cpu = process.arch === "arm64" ? 0x100000c : 0x1000007;
|
||||
const base = bytes.readUInt32BE(8) === cpu ? 8 : 40;
|
||||
bytes.copy(bytes, base === 8 ? 40 : 8, base, base + 32);
|
||||
const firstOffset = bytes.readBigUInt64BE(16);
|
||||
const secondOffset = bytes.readBigUInt64BE(48);
|
||||
const earlier = firstOffset < secondOffset ? 8 : 40;
|
||||
const laterOffset =
|
||||
firstOffset < secondOffset ? secondOffset : firstOffset;
|
||||
const earlierOffset =
|
||||
firstOffset < secondOffset ? firstOffset : secondOffset;
|
||||
bytes.writeBigUInt64BE(
|
||||
laterOffset - earlierOffset + 16n,
|
||||
earlier + 16,
|
||||
);
|
||||
},
|
||||
],
|
||||
]) {
|
||||
@@ -211,11 +221,14 @@ try {
|
||||
}
|
||||
const path = join(directory, "c-fat64-ambiguous");
|
||||
await writeFile(path, ambiguous);
|
||||
invalidFat64.push({ path, reason: /selection.*ambiguous/u });
|
||||
invalidFat64.push({
|
||||
path,
|
||||
reason:
|
||||
/FAT binary declares several (?:arm64|x86_64) slices; no slice was selected/u,
|
||||
});
|
||||
}
|
||||
await client.connect(transport);
|
||||
assert.ok(transport.pid !== null);
|
||||
owned.add(transport.pid);
|
||||
const catalog = await client.listTools();
|
||||
const call = async (name, args = {}) => {
|
||||
const reply = await client.callTool(
|
||||
@@ -359,14 +372,27 @@ try {
|
||||
if (verifyFat64) {
|
||||
await call("open_binary", { path: fat64 });
|
||||
await call("list_procedures");
|
||||
const resources = [
|
||||
...(await snapshotHopperRuntime(
|
||||
"/tmp",
|
||||
HOPPER_TARGET_LEASE_DIRECTORY,
|
||||
owned,
|
||||
)),
|
||||
].filter((path) => !before.has(path));
|
||||
for (const root of resources) {
|
||||
const session = await call("binary_session");
|
||||
const runId = session.analysis_run?.run_id;
|
||||
assert.ok(
|
||||
typeof runId === "string" && runId.length > 0,
|
||||
"Missing analysis run identity",
|
||||
);
|
||||
const resources = await snapshotHopperRuntime(
|
||||
"/tmp",
|
||||
HOPPER_TARGET_LEASE_DIRECTORY,
|
||||
runId,
|
||||
);
|
||||
const unresolvedNew = [...resources.unresolvedSessions].filter(
|
||||
(path) => !before.has(path),
|
||||
);
|
||||
assert.deepEqual(
|
||||
unresolvedNew,
|
||||
[],
|
||||
"Cannot identify a prepared image while new Hopper resource ownership is unresolved",
|
||||
);
|
||||
for (const root of resources.confirmedSessions) {
|
||||
if (before.has(root)) continue;
|
||||
const image = join(root, "image.macho");
|
||||
if (
|
||||
await access(image).then(
|
||||
@@ -403,27 +429,43 @@ try {
|
||||
},
|
||||
);
|
||||
assert.ok(pending.child.pid !== undefined);
|
||||
owned.add(pending.child.pid);
|
||||
const cli = JSON.parse((await pending).stdout);
|
||||
assert.deepEqual(cli.normalized_result, expected.mapping);
|
||||
}
|
||||
} finally {
|
||||
await client.close();
|
||||
await transport.close();
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
}
|
||||
const retained = [
|
||||
...(await snapshotHopperRuntime(
|
||||
"/tmp",
|
||||
HOPPER_TARGET_LEASE_DIRECTORY,
|
||||
owned,
|
||||
)),
|
||||
].filter((path) => !before.has(path));
|
||||
assert.deepEqual(
|
||||
retained,
|
||||
[],
|
||||
"FAT verification retained owned Hopper sessions or leases",
|
||||
);
|
||||
}, [
|
||||
async () => {
|
||||
try {
|
||||
await closeVerifierResources([
|
||||
() => client.close(),
|
||||
() => transport.close(),
|
||||
]);
|
||||
} catch (cause) {
|
||||
throw new Error(
|
||||
`Hopper cleanup failed; fixture retained at ${directory}`,
|
||||
{
|
||||
cause,
|
||||
},
|
||||
);
|
||||
}
|
||||
try {
|
||||
const retained = [
|
||||
...(await snapshotHopperRuntime("/tmp", HOPPER_TARGET_LEASE_DIRECTORY))
|
||||
.paths,
|
||||
].filter((path) => !before.has(path));
|
||||
assert.deepEqual(
|
||||
retained,
|
||||
[],
|
||||
"Hopper resources remain or are unresolved",
|
||||
);
|
||||
} catch (cause) {
|
||||
throw new Error(
|
||||
`Hopper resource verification failed; fixture retained at ${directory}`,
|
||||
{ cause },
|
||||
);
|
||||
}
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
},
|
||||
]);
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
|
||||
@@ -62,10 +62,9 @@ const parseServerArgs = (encoded) => {
|
||||
};
|
||||
|
||||
const runtimeParent = process.platform === "darwin" ? "/tmp" : tmpdir();
|
||||
const sessionsBefore = await snapshotHopperRuntime(
|
||||
runtimeParent,
|
||||
HOPPER_TARGET_LEASE_DIRECTORY,
|
||||
);
|
||||
const sessionsBefore = (
|
||||
await snapshotHopperRuntime(runtimeParent, HOPPER_TARGET_LEASE_DIRECTORY)
|
||||
).paths;
|
||||
const ownedProcessIds = new Set();
|
||||
const textValue = mcpTextValue;
|
||||
|
||||
@@ -557,11 +556,8 @@ try {
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
const sessionsAfter = [
|
||||
...(await snapshotHopperRuntime(
|
||||
runtimeParent,
|
||||
HOPPER_TARGET_LEASE_DIRECTORY,
|
||||
ownedProcessIds,
|
||||
)),
|
||||
...(await snapshotHopperRuntime(runtimeParent, HOPPER_TARGET_LEASE_DIRECTORY))
|
||||
.paths,
|
||||
]
|
||||
.filter((path) => !sessionsBefore.has(path))
|
||||
.sort();
|
||||
@@ -573,7 +569,7 @@ if (sessionsAfter.length > 0) {
|
||||
retainedRuntimePaths: sessionsAfter,
|
||||
});
|
||||
throw new Error(
|
||||
`The MCP runtime retained Hopper resources: ${sessionsAfter.join(", ")}`,
|
||||
`Hopper resources remain or have unresolved ownership: ${sessionsAfter.join(", ")}`,
|
||||
);
|
||||
}
|
||||
if (summary === undefined)
|
||||
|
||||
@@ -40,7 +40,7 @@ try {
|
||||
const expected = [];
|
||||
for (const path of files.stdout
|
||||
.split("\0")
|
||||
.filter((path) => path.endsWith(".test.ts"))) {
|
||||
.filter((path) => /\.(?:test|spec)\.[cm]?[jt]sx?$/u.test(path))) {
|
||||
try {
|
||||
if ((await stat(join(root, path))).isFile()) expected.push(path);
|
||||
} catch (cause) {
|
||||
@@ -65,4 +65,6 @@ try {
|
||||
await rm(temporary, { recursive: true, force: true });
|
||||
}
|
||||
if (discovered !== undefined)
|
||||
console.log(`Discovered all ${discovered} test files exactly once.`);
|
||||
console.log(
|
||||
`Discovered all ${discovered} test and spec files in src and tests exactly once.`,
|
||||
);
|
||||
@@ -151,7 +151,7 @@ export async function verifyAndroidFailureRecovery({
|
||||
failure_code: "runtime_unavailable",
|
||||
},
|
||||
availabilityReason: "provider_unavailable",
|
||||
availabilityRemediation: /selected Java runtime/u,
|
||||
availabilityRemediation: /selected Java launcher/u,
|
||||
message: /selected Java runtime/u,
|
||||
remediation: /selected launcher/u,
|
||||
});
|
||||
@@ -194,12 +194,7 @@ export async function verifyAndroidFailureRecovery({
|
||||
repository,
|
||||
available: false,
|
||||
unavailableReason: scenario.availabilityReason ?? "provider_missing",
|
||||
remediation:
|
||||
scenario.availabilityRemediation ??
|
||||
new RegExp(
|
||||
scenario.observation.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"),
|
||||
"u",
|
||||
),
|
||||
remediation: scenario.availabilityRemediation ?? scenario.remediation,
|
||||
});
|
||||
console.log(`PASS CLI applicable recovery: ${scenario.label}`);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
import { parseBinaryTarget } from "../../../dist/application/BinaryTargetResolver.js";
|
||||
import { ArtifactResourceScope } from "../../../dist/artifacts/ArtifactResourceScope.js";
|
||||
import { withVerifierArtifactScope } from "../../lib/verifier-artifact-scope.mjs";
|
||||
import { inspectAppleDispatchMetadata } from "../../../dist/native/AppleDispatchMetadata.js";
|
||||
import {
|
||||
artifactCliEvidence,
|
||||
@@ -57,8 +57,7 @@ const fixupCoverage = (metadata) =>
|
||||
const inspect = async (path) => {
|
||||
const target = await parseBinaryTarget(path);
|
||||
if (!target.ok) throw target.error;
|
||||
const resources = new ArtifactResourceScope();
|
||||
try {
|
||||
return withVerifierArtifactScope(async (resources) => {
|
||||
return {
|
||||
target: target.value,
|
||||
metadata: (
|
||||
@@ -67,10 +66,7 @@ const inspect = async (path) => {
|
||||
)
|
||||
).result,
|
||||
};
|
||||
} finally {
|
||||
// Retry any descriptor whose close failed, surfacing unresolved cleanup.
|
||||
await resources.close();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const checkObjc = (metadata, mode) => {
|
||||
|
||||
@@ -5,17 +5,12 @@ import { join, relative, sep } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
// Architecture guards for the strict-identity / best-effort-inference split.
|
||||
// - Evidence ordering must be locale-independent (ICU varies by host), so
|
||||
// src/domain orders with compareUnicodeCodePoints, never localeCompare.
|
||||
// - Composite dedup keys must not NUL-join variable-length or free-form
|
||||
// MIDDLE fields: ["a\0b"] collapses with ["a", "b"] and drops a finding.
|
||||
// compositeKey (JSON tuple) is the single owner for composite keys.
|
||||
// The text check below only flags one spelling; it is not a complete audit.
|
||||
// - Transparent wrappers unwrap in exactly one place
|
||||
// (javascriptAstValues.unwrapJavaScriptExpression); direct isTS* unwrapping
|
||||
// elsewhere drifts (the `satisfies` class of bug).
|
||||
// - AST walks go through traverseJavaScriptAst/childNodes: bespoke
|
||||
// Object.values walkers diverge on loc/comment fields and overflow the
|
||||
// stack on deep generated chains.
|
||||
|
||||
const root = fileURLToPath(new URL("../../", import.meta.url));
|
||||
|
||||
@@ -27,6 +22,7 @@ const listFiles = async (dir) => {
|
||||
else if (
|
||||
entry.name.endsWith(".ts") &&
|
||||
!entry.name.endsWith(".test.ts") &&
|
||||
!entry.name.endsWith(".spec.ts") &&
|
||||
!entry.name.endsWith(".fixture.ts")
|
||||
)
|
||||
output.push(path);
|
||||
@@ -48,8 +44,6 @@ const files = await Promise.all(
|
||||
content: stripComments(await readFile(path, "utf8")),
|
||||
})),
|
||||
);
|
||||
const inDir = (dir) => files.filter((file) => file.path.startsWith(dir));
|
||||
|
||||
const failures = [];
|
||||
const check = (label, selected, pattern, allow = []) => {
|
||||
const offenders = selected.filter(
|
||||
@@ -68,21 +62,7 @@ check(
|
||||
/providers\/ProviderInspectionScope\.js/,
|
||||
);
|
||||
|
||||
check(
|
||||
"src/domain uses localeCompare (use compareUnicodeCodePoints)",
|
||||
inDir("src/domain/"),
|
||||
/\.localeCompare\(/,
|
||||
);
|
||||
|
||||
check(
|
||||
"NUL-joined list key (use compositeKey)",
|
||||
files,
|
||||
/\.join\(["']\\0["']\)/,
|
||||
[
|
||||
// Sorted pair of constrained trace IDs; documented in plan.
|
||||
"src/domain/process/processTraceSpecification.ts",
|
||||
],
|
||||
);
|
||||
check("NUL-joined list key (use compositeKey)", files, /\.join\(["']\\0["']\)/);
|
||||
|
||||
check(
|
||||
"direct transparent-wrapper unwrap outside javascriptAstValues (use unwrapJavaScriptExpression)",
|
||||
@@ -91,13 +71,6 @@ check(
|
||||
["src/domain/javascript/javascriptAstValues.ts"],
|
||||
);
|
||||
|
||||
check(
|
||||
"bespoke AST child walker (use traverseJavaScriptAst/childNodes)",
|
||||
files,
|
||||
/VISITOR_KEYS/,
|
||||
["src/domain/javascript/javascriptSemanticTraversal.ts"],
|
||||
);
|
||||
|
||||
// A producer that pins both the soft and hard ceiling of every resource takes
|
||||
// the vector from one owner. prlimitCeilingArguments is the command-less prefix
|
||||
// of prlimitArgumentVector; both live in src/process/LinuxResourceLimits.ts.
|
||||
@@ -163,7 +136,6 @@ check(
|
||||
"src/application/GuidanceAccess.ts",
|
||||
"src/application/javascript/ElectronBoundaryAnalysis.ts",
|
||||
"src/application/javascript/ElectronBoundaryGraphContext.ts",
|
||||
"src/application/javascript/JavaScriptArtifactGraphContext.ts",
|
||||
"src/browser/CdpCaptureCompleteness.ts",
|
||||
"src/browser/CdpCaptureEventHandlers.ts",
|
||||
"src/browser/CdpPageCapture.ts",
|
||||
@@ -179,7 +151,6 @@ check(
|
||||
"src/domain/javascript/javascriptNativeHandoff.ts",
|
||||
"src/domain/javascript/javascriptRuntimeReconciliationMatching.ts",
|
||||
"src/domain/javascript/javascriptRuntimeReconciliationSchemas.ts",
|
||||
"src/domain/javascript/javascriptSemanticQuery.ts",
|
||||
"src/domain/javascript/javascriptStaticAnalysisBundler.ts",
|
||||
"src/domain/javascript/javascriptStaticAnalysisCalls.ts",
|
||||
"src/domain/javascript/javascriptStaticAnalysisFindings.ts",
|
||||
|
||||
@@ -5,7 +5,6 @@ import { parseArgs } from "node:util";
|
||||
|
||||
import { readStableArtifact } from "../../../dist/artifacts/readStableArtifact.js";
|
||||
import { ArtifactReaderFailure } from "../../../dist/artifacts/ArtifactReader.js";
|
||||
import { ArtifactResourceScope } from "../../../dist/artifacts/ArtifactResourceScope.js";
|
||||
import {
|
||||
ArtifactFileCleanupFailure,
|
||||
artifactFileCompletion,
|
||||
@@ -14,6 +13,7 @@ import {
|
||||
|
||||
import { reconcileJavaScriptRuntimeEvidence } from "../../../dist/application/javascript/JavaScriptRuntimeReconciliationService.js";
|
||||
import { analyzeJavaScriptApplicationWithOwnedScope } from "../../lib/javascript-application-analysis.mjs";
|
||||
import { withVerifierArtifactScope } from "../../lib/verifier-artifact-scope.mjs";
|
||||
import {
|
||||
completeVerifierRun,
|
||||
createVerifierRun,
|
||||
@@ -106,33 +106,23 @@ process.stdout.write(
|
||||
);
|
||||
|
||||
async function readEvidenceFile(path) {
|
||||
const scope = new ArtifactResourceScope();
|
||||
let outcome;
|
||||
try {
|
||||
outcome = {
|
||||
ok: true,
|
||||
value: await readStableArtifact(
|
||||
await realpath(path),
|
||||
64 * 1024 * 1024,
|
||||
undefined,
|
||||
undefined,
|
||||
scope,
|
||||
value: await withVerifierArtifactScope(async (scope) =>
|
||||
readStableArtifact(
|
||||
await realpath(path),
|
||||
64 * 1024 * 1024,
|
||||
undefined,
|
||||
undefined,
|
||||
scope,
|
||||
),
|
||||
),
|
||||
};
|
||||
} catch (cause) {
|
||||
outcome = { ok: false, cause };
|
||||
}
|
||||
try {
|
||||
await scope.close();
|
||||
} catch (cleanupCause) {
|
||||
if (!outcome.ok)
|
||||
throw new AggregateError(
|
||||
[outcome.cause, cleanupCause],
|
||||
"Verifier cleanup failed",
|
||||
{ cause: outcome.cause },
|
||||
);
|
||||
throw cleanupCause;
|
||||
}
|
||||
if (!outcome.ok && outcome.cause instanceof ArtifactFileCleanupFailure) {
|
||||
const completion = artifactFileCompletion(outcome.cause);
|
||||
outcome =
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { basename, dirname, isAbsolute, resolve } from "node:path";
|
||||
import { ArtifactResourceScope } from "../../../dist/artifacts/ArtifactResourceScope.js";
|
||||
import {
|
||||
readManagedSnapshot,
|
||||
verifyManagedSourceIdentity,
|
||||
} from "../../../dist/dotnet/ManagedSnapshot.js";
|
||||
import { withVerifierCleanup } from "../../lib/verifier-cleanup.mjs";
|
||||
import { withVerifierArtifactScope } from "../../lib/verifier-artifact-scope.mjs";
|
||||
|
||||
import { parseBinaryTarget } from "../../../dist/application/BinaryTargetResolver.js";
|
||||
import { inspectManagedArtifactBytes } from "../../../dist/dotnet/ManagedArtifactInspector.js";
|
||||
@@ -24,13 +23,10 @@ import {
|
||||
const READ_FILE_MAX_BYTES = 0x7fff_ffff;
|
||||
|
||||
export async function verifyManagedAppManifest(rawManifest, manifestPath) {
|
||||
const resources = new ArtifactResourceScope();
|
||||
return withVerifierCleanup(
|
||||
() =>
|
||||
resources.run(() =>
|
||||
verifyManifestSnapshot(rawManifest, manifestPath, resources),
|
||||
),
|
||||
[() => resources.close()],
|
||||
return withVerifierArtifactScope((resources) =>
|
||||
resources.run(() =>
|
||||
verifyManifestSnapshot(rawManifest, manifestPath, resources),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
/** Relative path of the registry. Its text is a declaration, not a producer. */
|
||||
export const NUMERIC_LIMIT_DECLARATION: "src/domain/numericLimitRegistry.ts";
|
||||
|
||||
/** Limits that must become analysis settings and cannot be registry exceptions. */
|
||||
export const REQUIRED_RUNTIME_LIMITS: readonly [
|
||||
"REA_NTFS_MFT_MAX_FILE_BYTES",
|
||||
"REA_REGF_HIVE_MAX_FILE_BYTES",
|
||||
"REA_WEB_BUNDLE_ENDPOINT_LITERAL_CANDIDATES",
|
||||
];
|
||||
|
||||
/** True for a numeric `REA_` limit, including the documented endpoint-candidate count. */
|
||||
export function isNumericLimit(name: string): boolean;
|
||||
|
||||
@@ -55,6 +48,8 @@ export function discoverNumericLimitUses(
|
||||
readonly unresolved: readonly {
|
||||
readonly path: string;
|
||||
readonly offset: number;
|
||||
readonly line: number;
|
||||
readonly column: number;
|
||||
readonly reason: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
@@ -25,6 +25,7 @@ const listFiles = async (dir) => {
|
||||
} else if (
|
||||
entry.name.endsWith(".ts") &&
|
||||
!entry.name.endsWith(".test.ts") &&
|
||||
!entry.name.endsWith(".spec.ts") &&
|
||||
!entry.name.endsWith(".fixture.ts")
|
||||
)
|
||||
output.push(path);
|
||||
@@ -39,7 +40,7 @@ export const NUMERIC_LIMIT_DECLARATION = "src/domain/numericLimitRegistry.ts";
|
||||
* These limits are moving onto runtime analysis settings on their own PRs.
|
||||
* A producer or a registry entry cannot satisfy that requirement.
|
||||
*/
|
||||
export const REQUIRED_RUNTIME_LIMITS = [
|
||||
const REQUIRED_RUNTIME_LIMITS = [
|
||||
"REA_NTFS_MFT_MAX_FILE_BYTES",
|
||||
"REA_REGF_HIVE_MAX_FILE_BYTES",
|
||||
"REA_WEB_BUNDLE_ENDPOINT_LITERAL_CANDIDATES",
|
||||
@@ -94,29 +95,36 @@ const stringBindings = (sourceFile, symbols, unresolved) => {
|
||||
ts.forEachChild(node, collect);
|
||||
};
|
||||
collect(sourceFile);
|
||||
const potential = (node) => {
|
||||
const potential = (node, matchesCandidate) => {
|
||||
const expression = unwrapExpression(node);
|
||||
if (exactLimit(literalText(expression)) !== undefined) return true;
|
||||
if (ts.isIdentifier(expression)) return candidates.has(expression.text);
|
||||
if (ts.isIdentifier(expression)) return matchesCandidate(expression);
|
||||
if (ts.isPropertyAccessExpression(expression))
|
||||
return (
|
||||
exactLimit(expression.name.text) !== undefined ||
|
||||
potential(expression.expression)
|
||||
potential(expression.expression, matchesCandidate)
|
||||
);
|
||||
if (ts.isElementAccessExpression(expression))
|
||||
return (
|
||||
expression.argumentExpression !== undefined &&
|
||||
potential(expression.argumentExpression)
|
||||
potential(expression.argumentExpression, matchesCandidate)
|
||||
);
|
||||
if (ts.isObjectLiteralExpression(expression))
|
||||
return expression.properties.some(
|
||||
(property) =>
|
||||
ts.isPropertyAssignment(property) && potential(property.initializer),
|
||||
ts.isPropertyAssignment(property) &&
|
||||
potential(property.initializer, matchesCandidate),
|
||||
);
|
||||
if (ts.isConditionalExpression(expression))
|
||||
return potential(expression.whenTrue) || potential(expression.whenFalse);
|
||||
return (
|
||||
potential(expression.whenTrue, matchesCandidate) ||
|
||||
potential(expression.whenFalse, matchesCandidate)
|
||||
);
|
||||
if (ts.isBinaryExpression(expression))
|
||||
return potential(expression.left) || potential(expression.right);
|
||||
return (
|
||||
potential(expression.left, matchesCandidate) ||
|
||||
potential(expression.right, matchesCandidate)
|
||||
);
|
||||
return false;
|
||||
};
|
||||
let changed = true;
|
||||
@@ -125,13 +133,34 @@ const stringBindings = (sourceFile, symbols, unresolved) => {
|
||||
for (const declaration of declarations) {
|
||||
if (
|
||||
!candidates.has(declaration.name.text) &&
|
||||
potential(declaration.initializer)
|
||||
potential(declaration.initializer, (identifier) =>
|
||||
candidates.has(identifier.text),
|
||||
)
|
||||
) {
|
||||
candidates.add(declaration.name.text);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Names narrow the search cheaply, but only the lexical declaration can
|
||||
// establish that a particular use is an alias of a limit.
|
||||
const lexicalCandidates = new Set();
|
||||
changed = true;
|
||||
while (changed) {
|
||||
changed = false;
|
||||
for (const declaration of declarations) {
|
||||
if (
|
||||
candidates.has(declaration.name.text) &&
|
||||
!lexicalCandidates.has(declaration) &&
|
||||
potential(declaration.initializer, (identifier) =>
|
||||
lexicalCandidates.has(symbols(identifier)?.valueDeclaration),
|
||||
)
|
||||
) {
|
||||
lexicalCandidates.add(declaration);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
const unknown = (node, reason) => {
|
||||
const path = sourceFile.fileName;
|
||||
const offset = node.getStart();
|
||||
@@ -142,8 +171,16 @@ const stringBindings = (sourceFile, symbols, unresolved) => {
|
||||
item.offset === offset &&
|
||||
item.reason === reason,
|
||||
)
|
||||
)
|
||||
unresolved.push({ path, offset, reason });
|
||||
) {
|
||||
const location = sourceFile.getLineAndCharacterOfPosition(offset);
|
||||
unresolved.push({
|
||||
path,
|
||||
offset,
|
||||
line: location.line + 1,
|
||||
column: location.character + 1,
|
||||
reason,
|
||||
});
|
||||
}
|
||||
};
|
||||
const initializer = (identifier) => {
|
||||
if (!candidates.has(identifier.text)) return undefined;
|
||||
@@ -152,11 +189,12 @@ const stringBindings = (sourceFile, symbols, unresolved) => {
|
||||
declaration === undefined ||
|
||||
!ts.isVariableDeclaration(declaration) ||
|
||||
!ts.isVariableDeclarationList(declaration.parent) ||
|
||||
declaration.initializer === undefined
|
||||
declaration.initializer === undefined ||
|
||||
!lexicalCandidates.has(declaration)
|
||||
)
|
||||
return undefined;
|
||||
if ((declaration.parent.flags & ts.NodeFlags.Const) === 0) {
|
||||
unknown(identifier, "mutable setting-key or value alias");
|
||||
unknown(declaration.name, "mutable setting-key or value alias");
|
||||
return undefined;
|
||||
}
|
||||
if (declaration.pos >= identifier.pos) {
|
||||
@@ -890,7 +928,7 @@ if (direct) {
|
||||
const entries = parseNumericLimitRegistry(registry);
|
||||
const listed = new Map();
|
||||
for (const entry of entries) {
|
||||
const { name, policy, read, reason } = entry;
|
||||
const { name, read, reason } = entry;
|
||||
if (listed.has(name)) failures.push(`${name} is listed twice`);
|
||||
listed.set(name, entry);
|
||||
if (reason.trim().length < 40)
|
||||
@@ -920,7 +958,7 @@ if (direct) {
|
||||
|
||||
for (const issue of discovered.unresolved)
|
||||
console.log(
|
||||
`${issue.path}:${issue.offset}: numeric limit candidate unresolved (${issue.reason})`,
|
||||
`${issue.path}:${issue.line}:${issue.column}: numeric limit candidate unresolved (${issue.reason})`,
|
||||
);
|
||||
|
||||
for (const [name, file] of [...discovered].sort(([left], [right]) =>
|
||||
|
||||
@@ -20,6 +20,7 @@ it.each([
|
||||
source:
|
||||
'process.stdout.write("partial-雪"); process.stderr.write("waiting\\n"); setInterval(() => {}, 1000)',
|
||||
code: "provider_timeout",
|
||||
timeoutMs: 500,
|
||||
stdout: "partial-雪",
|
||||
stderr: "waiting\n",
|
||||
},
|
||||
@@ -28,6 +29,7 @@ it.each([
|
||||
source:
|
||||
'process.stdout.write("partial-雪"); process.stderr.write("adb: error: device offline\\n"); process.exitCode = 1',
|
||||
code: "unsupported_target",
|
||||
expectedPath: "caller-device",
|
||||
stdout: "partial-雪",
|
||||
stderr: "adb: error: device offline\n",
|
||||
},
|
||||
@@ -36,6 +38,7 @@ it.each([
|
||||
source:
|
||||
'process.stdout.write("partial-雪"); process.stderr.write("adb: error: cannot stat \'/host/token file\': Permission denied\\n"); process.exitCode = 1',
|
||||
code: "unsupported_target",
|
||||
expectedPath: "/host/token file",
|
||||
stdout: "partial-雪",
|
||||
stderr: "adb: error: cannot stat '/host/token file': Permission denied\n",
|
||||
},
|
||||
@@ -54,7 +57,7 @@ it.each([
|
||||
},
|
||||
["-e", testCase.source],
|
||||
{
|
||||
timeoutMs: testCase.name === "timeout" ? 500 : 5_000,
|
||||
timeoutMs: testCase.timeoutMs ?? 5_000,
|
||||
maxOutputBytes: testCase.maxOutputBytes ?? 1024,
|
||||
},
|
||||
undefined,
|
||||
@@ -75,12 +78,34 @@ it.each([
|
||||
},
|
||||
});
|
||||
if (testCase.code === "unsupported_target")
|
||||
expect(projected.details?.path).toBe(
|
||||
testCase.name === "local path refusal"
|
||||
? "/host/token file"
|
||||
: "caller-device",
|
||||
);
|
||||
expect(projected.details?.path).toBe(testCase.expectedPath);
|
||||
if (testCase.code === "provider_timeout") {
|
||||
expect(projected.details?.timeout_ms).toBe(testCase.timeoutMs);
|
||||
expect(projected.details?.limit).toBeUndefined();
|
||||
}
|
||||
expect(JSON.stringify(projected)).not.toContain(
|
||||
"ambient-value-must-not-be-persisted",
|
||||
);
|
||||
});
|
||||
|
||||
it.each([true, false])(
|
||||
"projects a truncated adb capture as retryable only for volatile output (%s)",
|
||||
async (volatileOutput) => {
|
||||
const result = await runAdbCapture(
|
||||
volatileOutput ? "read_adb_logcat" : "install_adb_package",
|
||||
{ device: "device" },
|
||||
{ binary: process.execPath, pathSource: "environment", environment: {} },
|
||||
["-e", 'process.stdout.write("X".repeat(4096))'],
|
||||
{ timeoutMs: 5_000, maxOutputBytes: 1024, volatileOutput },
|
||||
undefined,
|
||||
);
|
||||
if (result.ok) throw new Error("Expected a capture allowance refusal");
|
||||
const projected = projectAnalysisError(result.error);
|
||||
expect(projected.retryable).toBe(volatileOutput);
|
||||
expect(projected).toMatchObject({
|
||||
code: "resource_constraint",
|
||||
details: { reported_limits: { max_output_bytes: 1024 } },
|
||||
});
|
||||
expect(projected.details?.limit).toBeUndefined();
|
||||
},
|
||||
);
|
||||
@@ -1,5 +1,4 @@
|
||||
import { expect, it, vi } from "vitest";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import {
|
||||
ExecFileOutputCancelledError,
|
||||
execFileBytes,
|
||||
@@ -212,65 +211,6 @@ it("does not infer cancellation from a foreign ABORT_ERR code", async () => {
|
||||
});
|
||||
});
|
||||
|
||||
it.each([true, false])(
|
||||
"projects a truncated adb capture as retryable only for volatile output (%s)",
|
||||
async (volatileOutput) => {
|
||||
vi.mocked(execFileBytes).mockRejectedValueOnce(
|
||||
Object.assign(new Error("too much"), {
|
||||
code: 1,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
signal: null,
|
||||
killed: false,
|
||||
outputTruncated: true,
|
||||
}),
|
||||
);
|
||||
const result = await runAdbCapture(
|
||||
volatileOutput ? "read_adb_logcat" : "install_adb_package",
|
||||
{ device: "device" },
|
||||
{ binary: "/host/adb", pathSource: "environment", environment: {} },
|
||||
volatileOutput ? ["logcat", "-d"] : ["install", "app.apk"],
|
||||
{ timeoutMs: 100, maxOutputBytes: 1024, volatileOutput },
|
||||
undefined,
|
||||
);
|
||||
if (result.ok) throw new Error("Expected a capture allowance refusal");
|
||||
const projected = projectAnalysisError(result.error);
|
||||
expect(projected.retryable).toBe(volatileOutput);
|
||||
expect(projected).toMatchObject({
|
||||
code: "resource_constraint",
|
||||
details: { reported_limits: { max_output_bytes: 1024 } },
|
||||
});
|
||||
expect(projected.details?.limit).toBeUndefined();
|
||||
},
|
||||
);
|
||||
|
||||
it("reports the effective fixed adb deadline", async () => {
|
||||
vi.mocked(execFileBytes).mockRejectedValueOnce(
|
||||
Object.assign(new Error("deadline"), {
|
||||
code: "ETIMEDOUT",
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
signal: null,
|
||||
killed: true,
|
||||
}),
|
||||
);
|
||||
const result = await runAdbCapture(
|
||||
"pull_adb_file",
|
||||
{ device: "device" },
|
||||
{ binary: "/host/adb", pathSource: "environment", environment: {} },
|
||||
["pull", "/remote"],
|
||||
{ timeoutMs: 600_000, maxOutputBytes: 1024 },
|
||||
undefined,
|
||||
);
|
||||
if (result.ok) throw new Error("Expected an adb deadline");
|
||||
const projected = projectAnalysisError(result.error);
|
||||
expect(projected).toMatchObject({
|
||||
code: "provider_timeout",
|
||||
details: { timeout_ms: 600_000 },
|
||||
});
|
||||
expect(projected.details?.limit).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps a physical failure even when the later abort reason is that same error", async () => {
|
||||
const controller = new AbortController();
|
||||
const failure = new Error("physical command failure");
|
||||
|
||||
@@ -191,7 +191,7 @@ fi`,
|
||||
await Promise.allSettled([inspection, closing]);
|
||||
await provider.close();
|
||||
}
|
||||
}, 15_000);
|
||||
});
|
||||
|
||||
it("fails closed when adb exits successfully without a recognized version", async () => {
|
||||
const stdout = `unexpected adb output ${"x".repeat(700)}\n`;
|
||||
|
||||
@@ -84,6 +84,7 @@ import {
|
||||
import { RegularFileOperationCancelled } from "../filesystem/RegularFile.js";
|
||||
import { OwnedFileHandle } from "../filesystem/OwnedFileHandle.js";
|
||||
import { createEvidence } from "../domain/evidence.js";
|
||||
import { createAnalysisExecutionEvidence } from "../domain/analysisObservation.js";
|
||||
import {
|
||||
PrivateRuntimeRoot,
|
||||
type PrivateRuntimeRootOptions,
|
||||
@@ -993,15 +994,12 @@ export class AdbProvider {
|
||||
const partialObservation =
|
||||
execution === undefined
|
||||
? undefined
|
||||
: createEvidence(execution.subject ?? undefined, execution.provider, {
|
||||
: createAnalysisExecutionEvidence(
|
||||
operation,
|
||||
parameters,
|
||||
result: execution.result,
|
||||
rawResult: execution.rawResult,
|
||||
limitations: execution.limitations,
|
||||
locations: execution.locations,
|
||||
confidence: "observed",
|
||||
});
|
||||
execution,
|
||||
"observed",
|
||||
);
|
||||
return new ProviderCleanupError(
|
||||
ADB_PROVIDER_IDENTITY.id,
|
||||
resources,
|
||||
|
||||
@@ -47,19 +47,32 @@ it("reports the fixed adb version-probe deadline", async () => {
|
||||
});
|
||||
|
||||
it("reports the fixed adb version-output cap", async () => {
|
||||
vi.mocked(execFileOutput).mockRejectedValueOnce(
|
||||
Object.assign(new Error("too much"), {
|
||||
code: "ERR_CHILD_PROCESS_STDIO_MAXBUFFER",
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
signal: null,
|
||||
killed: false,
|
||||
outputTruncated: true,
|
||||
}),
|
||||
);
|
||||
const failure = await inspectAdbClient(selection).catch(
|
||||
(cause: unknown) => cause,
|
||||
);
|
||||
let selected: string = selection.binary;
|
||||
if (process.platform === "win32") {
|
||||
vi.mocked(execFileOutput).mockRejectedValueOnce(
|
||||
Object.assign(new Error("too much"), {
|
||||
code: "ERR_CHILD_PROCESS_STDIO_MAXBUFFER",
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
signal: null,
|
||||
killed: false,
|
||||
outputTruncated: true,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
const root = await mkdtemp(join(tmpdir(), "rea-adb-version-cap-"));
|
||||
onTestFinished(() => rm(root, { recursive: true, force: true }));
|
||||
selected = join(root, "adb");
|
||||
await writeFile(
|
||||
selected,
|
||||
`#!${process.execPath}\nprocess.stdout.write("x".repeat(100_000));\n`,
|
||||
{ mode: 0o755 },
|
||||
);
|
||||
}
|
||||
const failure = await inspectAdbClient({
|
||||
...selection,
|
||||
binary: selected,
|
||||
}).catch((cause: unknown) => cause);
|
||||
const projected = projectAnalysisError(
|
||||
failure as Parameters<typeof projectAnalysisError>[0],
|
||||
);
|
||||
@@ -74,25 +87,28 @@ it("reports the fixed adb version-output cap", async () => {
|
||||
expect(projected.details?.limit).toBeUndefined();
|
||||
});
|
||||
|
||||
it("distinguishes execute denial from missing or unreadable version output", async () => {
|
||||
vi.mocked(execFileOutput).mockRejectedValueOnce(
|
||||
Object.assign(new Error("denied"), {
|
||||
code: "EACCES",
|
||||
spawnFailed: true,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
signal: null,
|
||||
killed: false,
|
||||
}),
|
||||
);
|
||||
await expect(inspectAdbClient(selection)).rejects.toMatchObject({
|
||||
code: "binary_not_executable",
|
||||
diagnostics: {
|
||||
selected_binary: selection.binary,
|
||||
filesystem_error: "EACCES",
|
||||
},
|
||||
});
|
||||
});
|
||||
it.skipIf(process.platform !== "win32")(
|
||||
"classifies an EACCES execute refusal on Windows",
|
||||
async () => {
|
||||
vi.mocked(execFileOutput).mockRejectedValueOnce(
|
||||
Object.assign(new Error("denied"), {
|
||||
code: "EACCES",
|
||||
spawnFailed: true,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
signal: null,
|
||||
killed: false,
|
||||
}),
|
||||
);
|
||||
await expect(inspectAdbClient(selection)).rejects.toMatchObject({
|
||||
code: "binary_not_executable",
|
||||
diagnostics: {
|
||||
selected_binary: selection.binary,
|
||||
filesystem_error: "EACCES",
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("names an EPERM execute refusal separately from missing permission", async () => {
|
||||
vi.mocked(execFileOutput).mockRejectedValueOnce(
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { JadxToolPort } from "./JadxAnalysis.js";
|
||||
import type { AndroidNativeDeclarations } from "../domain/android/androidNativeDeclarations.js";
|
||||
import { jadxNativeDeclarationsSchema } from "./JadxProtocol.js";
|
||||
import {
|
||||
androidNativeDeclarationsSchema,
|
||||
type AndroidNativeDeclarations,
|
||||
} from "../domain/android/androidNativeDeclarations.js";
|
||||
import { AnalysisOutputError } from "../domain/analysisErrorCore.js";
|
||||
|
||||
/** Validate the native-only batch at its producing boundary, preserving raw facts. */
|
||||
@@ -9,7 +11,7 @@ export const readAndroidNativeInventory = async (
|
||||
snapshot: string,
|
||||
): Promise<AndroidNativeDeclarations> => {
|
||||
const raw = await tools.json("rea_native_declarations", { path: snapshot });
|
||||
const parsed = jadxNativeDeclarationsSchema.safeParse(raw);
|
||||
const parsed = androidNativeDeclarationsSchema.safeParse(raw);
|
||||
if (!parsed.success)
|
||||
throw new AnalysisOutputError(
|
||||
"inspect_android_jni_bindings",
|
||||
|
||||
@@ -58,7 +58,7 @@ export const androidSnapshotCopy = (
|
||||
: undefined;
|
||||
|
||||
/** Adopt a snapshot descriptor whose close rejected, keyed by its exact path. */
|
||||
export type AndroidSnapshotCloseRetention = (
|
||||
type AndroidSnapshotCloseRetention = (
|
||||
path: string,
|
||||
owner: OwnedFileHandle,
|
||||
) => void;
|
||||
@@ -69,8 +69,8 @@ export const snapshotAndroidTarget = async (
|
||||
sha256: string,
|
||||
root: string,
|
||||
operation: AndroidOperation,
|
||||
signal?: AbortSignal,
|
||||
retainFailedClose?: AndroidSnapshotCloseRetention,
|
||||
signal: AbortSignal | undefined,
|
||||
retainFailedClose: AndroidSnapshotCloseRetention,
|
||||
): Promise<string> => {
|
||||
const snapshot = join(root, "target.apk");
|
||||
try {
|
||||
@@ -105,8 +105,8 @@ export const snapshotAndroidEngine = async (
|
||||
sha256: string,
|
||||
root: string,
|
||||
operation: AndroidOperation,
|
||||
signal?: AbortSignal,
|
||||
retainFailedClose?: AndroidSnapshotCloseRetention,
|
||||
signal: AbortSignal | undefined,
|
||||
retainFailedClose: AndroidSnapshotCloseRetention,
|
||||
): Promise<{ path: string; sha256: string }> => {
|
||||
const path = join(root, "engine.jar");
|
||||
try {
|
||||
@@ -159,17 +159,14 @@ const copyAndroidSnapshot = async (
|
||||
snapshotPath: string,
|
||||
expectedSha256: string,
|
||||
signal: AbortSignal | undefined,
|
||||
retainFailedClose: AndroidSnapshotCloseRetention | undefined,
|
||||
retainFailedClose: AndroidSnapshotCloseRetention,
|
||||
): Promise<AndroidSnapshotCopy> => {
|
||||
assertRegularFileActive(signal);
|
||||
let removeSnapshot = false;
|
||||
const retain = (path: string) =>
|
||||
retainFailedClose === undefined
|
||||
? {}
|
||||
: {
|
||||
retainFailedClose: (owner: OwnedFileHandle) =>
|
||||
retainFailedClose(path, owner),
|
||||
};
|
||||
const retain = (path: string) => ({
|
||||
retainFailedClose: (owner: OwnedFileHandle) =>
|
||||
retainFailedClose(path, owner),
|
||||
});
|
||||
try {
|
||||
// Close each descriptor through its owner. A rejected close keeps the
|
||||
// copy outcome and stays retained until that owner is retried; the
|
||||
@@ -287,7 +284,7 @@ const openSnapshotSource = async (
|
||||
path: string,
|
||||
signal: AbortSignal | undefined,
|
||||
retention: {
|
||||
readonly retainFailedClose?: (owner: OwnedFileHandle) => void;
|
||||
readonly retainFailedClose: (owner: OwnedFileHandle) => void;
|
||||
},
|
||||
): Promise<StableRegularFileDescriptor> => {
|
||||
try {
|
||||
@@ -305,10 +302,9 @@ const openSnapshotSource = async (
|
||||
};
|
||||
|
||||
const retainAdmissionOwner = (
|
||||
retain: ((owner: OwnedFileHandle) => void) | undefined,
|
||||
retain: (owner: OwnedFileHandle) => void,
|
||||
owner: OwnedFileHandle,
|
||||
): RegularFileCleanupFailure["retention"] => {
|
||||
if (retain === undefined) return { kind: "not_requested" };
|
||||
try {
|
||||
retain(owner);
|
||||
return { kind: "callback_returned" };
|
||||
|
||||
@@ -10,11 +10,6 @@ import {
|
||||
dexDescriptorSchema,
|
||||
dexTypeSchema,
|
||||
} from "../domain/android/dexTypes.js";
|
||||
import { androidNativeDeclarationsSchema } from "../domain/android/androidNativeDeclarations.js";
|
||||
|
||||
/** Native-only raw DEX facts authored before decompiler deduplication. */
|
||||
export const jadxNativeDeclarationsSchema = androidNativeDeclarationsSchema;
|
||||
|
||||
const count = z.int().nonnegative();
|
||||
/** Typed allocation failures authored before upstream erases the JVM exception. */
|
||||
const jadxMemoryFailureSchema = z.strictObject({
|
||||
|
||||
@@ -313,10 +313,10 @@ export class JadxProvider implements AndroidAnalysisPort {
|
||||
: AbortSignal.any([signal, this.#shutdown.signal]),
|
||||
});
|
||||
this.#inspections.add(pending);
|
||||
void pending.then(
|
||||
() => this.#inspections.delete(pending),
|
||||
() => this.#inspections.delete(pending),
|
||||
);
|
||||
const remove = (): void => {
|
||||
this.#inspections.delete(pending);
|
||||
};
|
||||
void pending.then(remove, remove);
|
||||
return pending;
|
||||
}
|
||||
|
||||
|
||||
@@ -415,11 +415,11 @@ export class ApkSignatureProvider implements ApkSignaturePort {
|
||||
const { v1, decoded: v1Decoded, duplicates } = v1Read.value;
|
||||
|
||||
const v2 = decodeScheme(block.value, APK_BLOCK_ID.signatureV2, "v2");
|
||||
if (!v2.ok) return err(v2.error);
|
||||
if (!v2.ok) return v2;
|
||||
const v3 = decodeScheme(block.value, APK_BLOCK_ID.signatureV3, "v3");
|
||||
if (!v3.ok) return err(v3.error);
|
||||
if (!v3.ok) return v3;
|
||||
const v31 = decodeScheme(block.value, APK_BLOCK_ID.signatureV31, "v3.1");
|
||||
if (!v31.ok) return err(v31.error);
|
||||
if (!v31.ok) return v31;
|
||||
|
||||
// Account for representation expansion before allocating the report.
|
||||
const projected = projectedOutputBytes(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { fixedAllowanceOptions } from "../domain/fixedAllowanceRemediation.js";
|
||||
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
|
||||
import { stableArtifactInputRecovery } from "../artifacts/StableArtifactFailure.js";
|
||||
import { stableArtifactInputFailure } from "../artifacts/StableArtifactFailure.js";
|
||||
import {
|
||||
artifactFilePrimaryFacts,
|
||||
type ArtifactFilePrimaryFacts,
|
||||
@@ -231,11 +231,7 @@ const artifactReaderFailure = (
|
||||
},
|
||||
);
|
||||
case "path":
|
||||
return new AnalysisInputError(
|
||||
OPERATION,
|
||||
{ cause, ...stableArtifactInputRecovery(cause) },
|
||||
[{ path: ["path"], reason: "invalid_format", message: cause.message }],
|
||||
);
|
||||
return stableArtifactInputFailure(cause, OPERATION);
|
||||
default:
|
||||
return adapterFailure(cause, path);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as commandOwner from "../process/OwnedCommand.js";
|
||||
import { failedInspectionDescriptor } from "../../tests/fixtures/processInspectionCleanup.js";
|
||||
import { awaitAcknowledgement } from "../../tests/fixtures/lifecycleAcknowledgement.js";
|
||||
import {
|
||||
AnalysisError,
|
||||
primaryAnalysisError,
|
||||
@@ -486,6 +487,7 @@ it("cancels a pending filesystem lookup and owns its eventual rejection", async
|
||||
const pendingStat = new Promise((_resolve, reject) => {
|
||||
rejectLookup = reject;
|
||||
});
|
||||
void pendingStat.catch(() => undefined);
|
||||
const observed = new Promise<void>((resolve) => {
|
||||
started = resolve;
|
||||
});
|
||||
@@ -502,14 +504,20 @@ it("cancels a pending filesystem lookup and owns its eventual rejection", async
|
||||
"win32",
|
||||
controller.signal,
|
||||
);
|
||||
await observed;
|
||||
controller.abort();
|
||||
await expect(pending).rejects.toMatchObject({
|
||||
_tag: "AnalysisCancelledError",
|
||||
});
|
||||
rejectLookup(new Error("filesystem completed after cancellation"));
|
||||
await Promise.resolve();
|
||||
expect(lookup.stat).toHaveBeenCalledTimes(1);
|
||||
try {
|
||||
await awaitAcknowledgement(observed, pending, "apktool filesystem lookup");
|
||||
controller.abort();
|
||||
await expect(pending).rejects.toMatchObject({
|
||||
_tag: "AnalysisCancelledError",
|
||||
});
|
||||
rejectLookup(new Error("filesystem completed after cancellation"));
|
||||
await Promise.resolve();
|
||||
expect(lookup.stat).toHaveBeenCalledTimes(1);
|
||||
} finally {
|
||||
controller.abort();
|
||||
rejectLookup(new Error("fixture cleanup"));
|
||||
await pending.catch(() => undefined);
|
||||
}
|
||||
});
|
||||
|
||||
it("bounds the entire pending PATH lookup by the selection deadline", async () => {
|
||||
|
||||
@@ -58,7 +58,6 @@ it.skipIf(process.platform === "win32")(
|
||||
await fixture.cleanup([probe]);
|
||||
}
|
||||
},
|
||||
30_000,
|
||||
);
|
||||
|
||||
let commands = new ApktoolCommandCleanup();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, onTestFinished } from "vitest";
|
||||
import { createDeferred } from "../../tests/fixtures/binarySession.js";
|
||||
import { awaitAcknowledgement } from "../../tests/fixtures/lifecycleAcknowledgement.js";
|
||||
|
||||
import {
|
||||
getNavigationContext,
|
||||
@@ -500,7 +501,16 @@ it("waits for late facet cleanup after cancellation and retains its owner", asyn
|
||||
settled = true;
|
||||
return result;
|
||||
});
|
||||
await started.promise;
|
||||
onTestFinished(async () => {
|
||||
controller.abort();
|
||||
late.resolve(err(failure));
|
||||
try {
|
||||
await pending;
|
||||
} finally {
|
||||
await lease.release();
|
||||
}
|
||||
});
|
||||
await awaitAcknowledgement(started.promise, pending, "facet execution");
|
||||
controller.abort();
|
||||
await Promise.resolve();
|
||||
expect(settled).toBe(false);
|
||||
@@ -530,7 +540,12 @@ it("reports cancellation after a late successful facet that ignored its signal",
|
||||
{ address: "0x401000", document: "fixture" },
|
||||
controller.signal,
|
||||
);
|
||||
await started.promise;
|
||||
onTestFinished(async () => {
|
||||
controller.abort();
|
||||
late.resolve(ok(createAnalysisExecution("finished", provider)));
|
||||
await pending;
|
||||
});
|
||||
await awaitAcknowledgement(started.promise, pending, "facet execution");
|
||||
controller.abort();
|
||||
late.resolve(ok(createAnalysisExecution("finished", provider)));
|
||||
const result = await pending;
|
||||
@@ -586,7 +601,11 @@ it("preserves a provider's cancellation for the same signal", async () => {
|
||||
{ address: "0x401000", document: "fixture" },
|
||||
controller.signal,
|
||||
);
|
||||
await started.promise;
|
||||
onTestFinished(async () => {
|
||||
controller.abort(reason);
|
||||
await pending;
|
||||
});
|
||||
await awaitAcknowledgement(started.promise, pending, "provider execution");
|
||||
controller.abort(reason);
|
||||
const result = await pending;
|
||||
if (result.ok) throw new Error("expected cancellation");
|
||||
|
||||
@@ -53,6 +53,11 @@ export interface ExecutionOptions {
|
||||
* supplied it.
|
||||
*/
|
||||
readonly selectorOrigin?: "provider";
|
||||
/**
|
||||
* Whether the caller's catalog advertises an operation; refusal recovery
|
||||
* names only advertised operations. Absent means every operation is.
|
||||
*/
|
||||
readonly isOperationAdvertised?: (operation: AnalysisOperation) => boolean;
|
||||
}
|
||||
|
||||
/** Closed operations accepted by provider-backed analysis sessions. */
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
resolveWindowsChangeTimeSettle,
|
||||
} from "../config/windowsChangeTimeSettle.js";
|
||||
import { pwntoolsInputBytes } from "../native/pwntools/PwntoolsInputLimit.js";
|
||||
import { resolveBridgeMaxRequestBytes } from "../config/bridgeMaxRequestBytes.js";
|
||||
import { resolveCaptureDecoderHeap } from "../config/captureDecoderHeap.js";
|
||||
import { resolveRegexMatchingDeadline } from "../config/regexMatchingDeadline.js";
|
||||
import { resolveRegexWorkerHeapRequest } from "../config/regexWorkerHeapRequest.js";
|
||||
@@ -366,7 +365,6 @@ it("reads an accepted override before an invalid launch seed", () => {
|
||||
{
|
||||
revision: 1,
|
||||
overrides: {
|
||||
"bridge.max_request_bytes": 8192,
|
||||
"capture.decoder_old_generation_mib": 64,
|
||||
"regex.matching_deadline_ms": 250,
|
||||
"regex.worker_heap_request_mib": 128,
|
||||
@@ -375,9 +373,6 @@ it("reads an accepted override before an invalid launch seed", () => {
|
||||
},
|
||||
},
|
||||
() => ({
|
||||
bridge: resolveBridgeMaxRequestBytes({
|
||||
REA_BRIDGE_MAX_REQUEST_BYTES: "nope",
|
||||
}),
|
||||
capture: resolveCaptureDecoderHeap({
|
||||
REA_CAPTURE_DECODER_OLD_GENERATION_MIB: "0",
|
||||
}),
|
||||
@@ -393,10 +388,6 @@ it("reads an accepted override before an invalid launch seed", () => {
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(resolved.bridge).toMatchObject({
|
||||
maximumBytes: 8192,
|
||||
source: "runtime",
|
||||
});
|
||||
expect(resolved.capture).toMatchObject({
|
||||
oldGenerationMiB: 64,
|
||||
source: "runtime",
|
||||
|
||||
@@ -4,7 +4,7 @@ import { join } from "node:path";
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
|
||||
import { resolveAppBundleExecutable } from "./AppBundleExecutable.js";
|
||||
import { resolveAppleBundleExecutable } from "./AppBundleExecutable.js";
|
||||
|
||||
it.each([
|
||||
["no comment", ""],
|
||||
@@ -22,7 +22,7 @@ it.each([
|
||||
await writeFile(infoPlist, source);
|
||||
await writeFile(executable, "fixture - never executed");
|
||||
|
||||
expect(await resolveAppBundleExecutable(bundle)).toEqual({
|
||||
expect(await resolveAppleBundleExecutable(bundle)).toEqual({
|
||||
ok: true,
|
||||
value: { executable, infoPlist },
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
|
||||
import { binaryPlistObjectTable } from "../../tests/fixtures/binaryPlistObjectTable.js";
|
||||
import { resolveAppBundleExecutable } from "./AppBundleExecutable.js";
|
||||
import { resolveAppleBundleExecutable } from "./AppBundleExecutable.js";
|
||||
|
||||
const binaryPlistExecutableWithThreeByteUid = (): Buffer => {
|
||||
const ascii = (value: string): Buffer => {
|
||||
@@ -53,7 +53,7 @@ it.each([
|
||||
);
|
||||
await writeFile(executable, "inert fixture; never executed");
|
||||
|
||||
const result = await resolveAppBundleExecutable(bundle);
|
||||
const result = await resolveAppleBundleExecutable(bundle);
|
||||
expect(result).toEqual({
|
||||
ok: true,
|
||||
value: { executable, infoPlist: plistPath },
|
||||
@@ -71,7 +71,7 @@ describe("app bundle Info.plist magic", () => {
|
||||
await writeFile(plistPath, binaryPlistExecutableWithThreeByteUid());
|
||||
await writeFile(executable, "inert fixture; never executed");
|
||||
|
||||
expect(await resolveAppBundleExecutable(bundle)).toEqual({
|
||||
expect(await resolveAppleBundleExecutable(bundle)).toEqual({
|
||||
ok: true,
|
||||
value: { executable, infoPlist: plistPath },
|
||||
});
|
||||
@@ -86,17 +86,13 @@ describe("app bundle Info.plist magic", () => {
|
||||
const plist = Buffer.from("bplist00");
|
||||
plist[0] = (plist[0] ?? 0) | 0x80;
|
||||
await writeFile(plistPath, plist);
|
||||
let decoded = false;
|
||||
const result = await resolveAppBundleExecutable(bundle, {
|
||||
readFile: async () => plist,
|
||||
realpath: async (path) => path,
|
||||
decodeBinaryPlist: async () => {
|
||||
decoded = true;
|
||||
return "Fixture";
|
||||
const result = await resolveAppleBundleExecutable(bundle);
|
||||
expect(result).toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
cause: { name: "XmlPlistTextDecodeError", reason: "invalid-bytes" },
|
||||
},
|
||||
});
|
||||
expect(decoded).toBe(false);
|
||||
expect(result.ok).toBe(false);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
@@ -118,7 +114,7 @@ describe("app bundle program directory containment", () => {
|
||||
await writeFile(join(bundle, "Contents", "Info.plist"), plist);
|
||||
let childResolved = false;
|
||||
|
||||
const result = await resolveAppBundleExecutable(bundle, {
|
||||
const result = await resolveAppleBundleExecutable(bundle, {
|
||||
readFile: async () => Buffer.from(plist),
|
||||
realpath: async (path) => {
|
||||
if (path === bundle) return canonicalBundle;
|
||||
|
||||
@@ -88,10 +88,9 @@ export interface ResolvedAppBundleResourceRoot {
|
||||
}
|
||||
|
||||
/** Filesystem operations used by the resolver, injectable for boundary tests. */
|
||||
export interface AppBundleFileSystem {
|
||||
interface AppBundleFileSystem {
|
||||
readonly readFile: (path: string) => Promise<Buffer>;
|
||||
readonly realpath: (path: string) => Promise<string>;
|
||||
readonly decodeBinaryPlist?: (path: string) => Promise<string>;
|
||||
}
|
||||
|
||||
const defaultFileSystem: AppBundleFileSystem = { readFile, realpath };
|
||||
@@ -102,7 +101,7 @@ const defaultFileSystem: AppBundleFileSystem = { readFile, realpath };
|
||||
* a wrapper whose `Wrapper` directory holds the iOS bundle; that bundle is
|
||||
* found by listing `Wrapper`, never by following the `WrappedBundle` link.
|
||||
*/
|
||||
export const resolveAppBundleExecutable = async (
|
||||
const resolveAppBundleExecutable = async (
|
||||
path: string,
|
||||
fileSystem: AppBundleFileSystem = defaultFileSystem,
|
||||
): Promise<Result<ResolvedAppBundle, BinaryTargetError>> => {
|
||||
@@ -396,15 +395,6 @@ const codeDescription = (cause: unknown): string => {
|
||||
: "";
|
||||
};
|
||||
|
||||
const isSystemErrorCode = (cause: unknown): boolean => {
|
||||
const code = errorCode(cause);
|
||||
return (
|
||||
typeof code === "string" &&
|
||||
/^[A-Z][A-Z0-9_]+$/u.test(code) &&
|
||||
!code.startsWith("ERR_")
|
||||
);
|
||||
};
|
||||
|
||||
const isPermissionDenied = (cause: unknown): boolean =>
|
||||
errorCode(cause) === "EACCES" || errorCode(cause) === "EPERM";
|
||||
|
||||
@@ -461,20 +451,12 @@ const resolveLayoutExecutable = async (
|
||||
|
||||
let name: string;
|
||||
try {
|
||||
name =
|
||||
hasBinaryPlistSignature(plist) &&
|
||||
fileSystem.decodeBinaryPlist !== undefined
|
||||
? await fileSystem.decodeBinaryPlist(plistPath)
|
||||
: executableName(decodeInfoPlistValue(plist));
|
||||
name = executableName(decodeInfoPlistValue(plist));
|
||||
} catch (cause: unknown) {
|
||||
return err(
|
||||
new BinaryTargetError(
|
||||
plistPath,
|
||||
isPermissionDenied(cause)
|
||||
? `permission denied decoding ${noun} Info.plist ${plistPath}${codeDescription(cause)}`
|
||||
: isSystemErrorCode(cause)
|
||||
? `could not decode ${noun} Info.plist ${plistPath}${codeDescription(cause)}`
|
||||
: `${noun} Info.plist is malformed, unsupported, or lacks CFBundleExecutable: ${plistPath}${cause instanceof Error ? ` (${cause.message})` : ""}`,
|
||||
`${noun} Info.plist is malformed, unsupported, or lacks CFBundleExecutable: ${plistPath}${cause instanceof Error ? ` (${cause.message})` : ""}`,
|
||||
{
|
||||
cause,
|
||||
// A bundle without code, such as a resource bundle, declares no
|
||||
|
||||
@@ -3,7 +3,7 @@ import { join } from "node:path";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { resolveAppBundleExecutable } from "./AppBundleExecutable.js";
|
||||
import { resolveAppleBundleExecutable } from "./AppBundleExecutable.js";
|
||||
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
|
||||
|
||||
describe("OpenStep Info.plist nesting during app target resolution", () => {
|
||||
@@ -20,7 +20,7 @@ describe("OpenStep Info.plist nesting during app target resolution", () => {
|
||||
`{ CFBundleExecutable = App; payload = ${"(".repeat(depth)}x${")".repeat(depth)}; }`,
|
||||
);
|
||||
|
||||
const result = await resolveAppBundleExecutable(app);
|
||||
const result = await resolveAppleBundleExecutable(app);
|
||||
|
||||
if (depth === 127) {
|
||||
expect(result).toMatchObject({
|
||||
@@ -52,7 +52,7 @@ describe("OpenStep Info.plist nesting during app target resolution", () => {
|
||||
`{ CFBundleExecutable = App; }${"(".repeat(3000)}x${")".repeat(3000)}`,
|
||||
);
|
||||
|
||||
expect(await resolveAppBundleExecutable(app)).toMatchObject({
|
||||
expect(await resolveAppleBundleExecutable(app)).toMatchObject({
|
||||
ok: true,
|
||||
value: { executable: join(programs, "App") },
|
||||
});
|
||||
|
||||
@@ -208,7 +208,7 @@ export const parseBinaryTarget = async (
|
||||
throwIfTargetResolutionCancelled(signal);
|
||||
const resolved = await resolveAppBundle(canonical);
|
||||
throwIfTargetResolutionCancelled(signal);
|
||||
if (!resolved.ok) return err(resolved.error);
|
||||
if (!resolved.ok) return resolved;
|
||||
const { executable: path, infoPlist } = resolved.value;
|
||||
const bundle =
|
||||
infoPlist === undefined ? {} : { bundleInfoPlist: infoPlist };
|
||||
@@ -747,8 +747,8 @@ const throwIfTargetResolutionCancelled = (signal?: AbortSignal): void => {
|
||||
const detectArtifactFormat = async (
|
||||
path: string,
|
||||
source: StableRegularFileDescriptor,
|
||||
signal?: AbortSignal,
|
||||
sourcePath = path,
|
||||
signal: AbortSignal | undefined,
|
||||
sourcePath: string,
|
||||
): Promise<
|
||||
| Exclude<
|
||||
BinaryTarget["format"],
|
||||
|
||||
Loaded 100 of 1555 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user