From e1363ae2ae2936c21df97d2ab0fe410857c860fa Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Tue, 15 Sep 2026 10:44:06 +0800 Subject: [PATCH 01/24] Bump dependencies and increase coverage --- .github/actions/free-disk-space/action.yaml | 22 + .github/actions/install-lavapipe/action.yaml | 20 + .../actions/install-mise-tools/action.yaml | 19 +- .github/workflows/check.yaml | 13 - .github/workflows/codeql.yaml | 6 - .github/workflows/coverage.yaml | 101 +- .github/workflows/k3s.yaml | 3 - .github/workflows/test.yaml | 22 +- .mise/config.coverage.toml | 142 +++ .mise/config.linux.toml | 4 +- .mise/config.macos.toml | 34 +- .mise/config.toml | 85 +- .mise/config.windows.toml | 5 +- .mise/mise.lock | 33 +- .mise/mise.macos.lock | 206 ---- Cargo.lock | 923 +++++++++--------- Cargo.toml | 48 +- config/conftest/policy/cargo/cargo.rego | 59 ++ config/conftest/policy/cargo/cargo_test.rego | 98 ++ .../policy/checksums/checksums_test.rego | 79 ++ .../dockerfile_heredoc.rego | 6 +- .../dockerfile_heredoc_test.rego | 61 ++ config/conftest/policy/mise/mise.rego | 7 +- config/conftest/policy/mise/mise_test.rego | 113 +++ .../no_trailing_backslash_test.rego | 43 + .../policy/policy_tests/policy_tests.rego | 76 ++ .../policy_tests/policy_tests_test.rego | 95 ++ config/jscpd.json | 1 + config/semgrep/no-jscpd-mentions.yaml | 46 + generated/rust-rest/src/lib.rs | 38 +- libs/ws-runner-common/src/lib.rs | 2 +- services/websockify/tests/relay.rs | 18 +- services/ws-pyo3-runner/src/agent.rs | 10 +- services/ws-pyo3-runner/src/lib.rs | 1 - services/ws-pyo3-runner/tests/modules.rs | 82 +- services/ws-test-server/src/math1.rs | 6 +- services/ws-test-server/tests/helpers.rs | 4 +- .../ws-test-server/tests/math1_exchange.rs | 6 +- services/ws-wasi-runner/src/host/ws.rs | 8 +- 39 files changed, 1616 insertions(+), 929 deletions(-) create mode 100644 .github/actions/free-disk-space/action.yaml create mode 100644 .github/actions/install-lavapipe/action.yaml create mode 100644 config/conftest/policy/cargo/cargo_test.rego create mode 100644 config/conftest/policy/checksums/checksums_test.rego create mode 100644 config/conftest/policy/dockerfile_heredoc/dockerfile_heredoc_test.rego create mode 100644 config/conftest/policy/mise/mise_test.rego create mode 100644 config/conftest/policy/no_trailing_backslash/no_trailing_backslash_test.rego create mode 100644 config/conftest/policy/policy_tests/policy_tests.rego create mode 100644 config/conftest/policy/policy_tests/policy_tests_test.rego create mode 100644 config/semgrep/no-jscpd-mentions.yaml diff --git a/.github/actions/free-disk-space/action.yaml b/.github/actions/free-disk-space/action.yaml new file mode 100644 index 00000000..46307d6a --- /dev/null +++ b/.github/actions/free-disk-space/action.yaml @@ -0,0 +1,22 @@ +name: Free disk space +description: >- + Reclaim runner disk before the long build and test steps, on whichever + platform the job is running: jlumbroso/free-disk-space on Linux, this + repo's free-disk-space-windows on Windows. macOS runners need neither. + + Each step carries its own `runner.os` guard, so a caller needs none -- + that is the point of one entry rather than a matched pair at every call + site. A caller wanting the Windows reclaim's opt-in removals (the + visual-studio / windows-kits inputs) calls free-disk-space-windows + directly instead, as docker-windows.yaml does. + +runs: + using: composite + steps: + - name: Free disk space on Linux + if: runner.os == 'Linux' + uses: jlumbroso/free-disk-space@v1.3.1 + + - name: Free disk space on Windows + if: runner.os == 'Windows' + uses: ./.github/actions/free-disk-space-windows diff --git a/.github/actions/install-lavapipe/action.yaml b/.github/actions/install-lavapipe/action.yaml new file mode 100644 index 00000000..ae4ce16a --- /dev/null +++ b/.github/actions/install-lavapipe/action.yaml @@ -0,0 +1,20 @@ +name: Install Mesa Vulkan drivers (lavapipe) +description: >- + Install Mesa's lavapipe software Vulkan driver on a GHA Linux runner, so + the wasi-webgpu tests find a wgpu adapter on an image that has no GPU. + + There is no counterpart on the other platforms: macOS runners reach Metal + through the OS itself, and no Windows lane runs these tests. + + Caller is responsible for the `if: runner.os == 'Linux'` guard, the same + way free-disk-space-windows expects its own -- the apt-get calls below + mean nothing on an image that has no apt. + +runs: + using: composite + steps: + - name: Install mesa-vulkan-drivers + shell: bash --noprofile --norc -euo pipefail {0} + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends mesa-vulkan-drivers diff --git a/.github/actions/install-mise-tools/action.yaml b/.github/actions/install-mise-tools/action.yaml index 6fb3e6b5..5010060b 100644 --- a/.github/actions/install-mise-tools/action.yaml +++ b/.github/actions/install-mise-tools/action.yaml @@ -6,11 +6,19 @@ description: >- HTTP flakes). Shared by check.yaml and test.yaml; the caller must run `actions/checkout` before this so .mise/config*.toml exists for `mise trust`. + Allow well over the usual budget on any lane that includes Windows. Under + mise 2026.7.1 the per-tool msvc `install_env` override no longer reaches + cargo-binstall there, so `cargo:` tools such as action-validator compile + from source instead of fetching their msvc prebuilts; that is why the + callers sit at `timeout-minutes: 60`. Bring it back down once mise fixes it. + inputs: github-token: description: >- GitHub token forwarded as `$GITHUB_TOKEN` to the mise install steps. - required: true + Defaults to the workflow's own token. + required: false + default: ${{ github.token }} install-action-tools: description: >- Comma-separated tools to co-install via taiki-e/install-action in the @@ -31,6 +39,15 @@ inputs: runs: using: composite steps: + # Reclaim runner disk before anything is installed into it. + # Folded in here rather than written at each call site, where every caller had it immediately before this + # action anyway -- six workflows opening with the same checkout / free-disk / install-mise trio is a lot of + # copy-paste for one fixed order. The action dispatches per OS internally (jlumbroso on Linux, this repo's + # Windows reclaim on Windows, nothing on macOS), so it needs no guard here. A caller that wants the Windows + # reclaim's opt-in removals still calls free-disk-space-windows directly, as docker-windows.yaml does. + - name: Free disk space + uses: ./.github/actions/free-disk-space + # No cargo: tool installs on the Windows lane right now -- cargo-binstall fails to execute there (os error 193). # open-o2's opener isn't needed in CI, so skip cargo:open rather than let the install fail. - name: Skip cargo:open on Windows diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index 7c154dbe..3908622a 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -57,22 +57,9 @@ jobs: fetch-depth: 1 persist-credentials: false - - name: Free disk space on Linux - if: runner.os == 'Linux' - uses: jlumbroso/free-disk-space@v1.3.1 - - - name: Free disk space on Windows - if: runner.os == 'Windows' - uses: ./.github/actions/free-disk-space-windows - - # Windows tools source-build under mise 2026.7.1, so this step runs long (bumped from 25). - # The per-tool msvc `install_env` override no longer reaches cargo-binstall there, so cargo tools like - # action-validator compile from source instead of fetching their msvc prebuilts. Revert once mise fixes it. - name: Install mise + tools uses: ./.github/actions/install-mise-tools timeout-minutes: 60 - with: - github-token: ${{ github.token }} - name: Prefetch Rust dependencies run: mise run prefetch:rust diff --git a/.github/workflows/codeql.yaml b/.github/workflows/codeql.yaml index 0539ea01..542ade12 100644 --- a/.github/workflows/codeql.yaml +++ b/.github/workflows/codeql.yaml @@ -71,16 +71,10 @@ jobs: fetch-depth: 1 persist-credentials: false - - name: Free disk space on Linux - if: matrix.language == 'rust' - uses: jlumbroso/free-disk-space@v1.3.1 - - name: Install mise + tools if: matrix.language == 'rust' uses: ./.github/actions/install-mise-tools timeout-minutes: 60 - with: - github-token: ${{ github.token }} - name: Prefetch Rust dependencies if: matrix.language == 'rust' diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml index 8b48249d..6a666839 100644 --- a/.github/workflows/coverage.yaml +++ b/.github/workflows/coverage.yaml @@ -25,8 +25,26 @@ env: jobs: coverage: - runs-on: ubuntu-latest - timeout-minutes: 150 + runs-on: ${{ matrix.os }} + timeout-minutes: ${{ matrix.timeout }} + # Two lanes, with only one of them reporting outward. + # ubuntu-latest stays the canonical lane: it is the only one that can run the wasm/browser half of the + # pipeline, and it alone uploads to Codecov and DeepSource, so the published numbers keep meaning exactly + # what they did before. macos-latest re-runs the native half as a second platform, which is what catches a + # branch reachable only under a different target's cfg. Its report is kept as an artifact rather than + # uploaded: both lanes would otherwise land on the same commit under the same `rust` flag / `--key rust`, + # and the two partial pictures would fight rather than merge. + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + timeout: 150 + # Longer than the Linux lane, despite doing less. + # The macOS runners are slower per-core, and this one still pays the full instrumented rebuild even + # though it skips the wasm/browser steps. + - os: macos-latest + timeout: 180 env: MISE_ENV: dart,dotnet,java,js,kotlin,python,r,rust,zig,coverage ET_TEST_COVERAGE: "true" @@ -42,26 +60,23 @@ jobs: fetch-depth: 1 persist-credentials: false - - name: Free disk space on Linux - uses: jlumbroso/free-disk-space@v1.3.1 - - name: Install Mesa Vulkan drivers (lavapipe) - run: | - sudo apt-get update - sudo apt-get install -y --no-install-recommends mesa-vulkan-drivers + if: runner.os == 'Linux' + uses: ./.github/actions/install-lavapipe - name: Install mise + coverage tools uses: ./.github/actions/install-mise-tools timeout-minutes: 20 with: - github-token: ${{ github.token }} install-action-tools: cargo-llvm-cov,deepsource # Counted here, before the build and coverage steps, because sloc needs only git plus the two counters. # Running it early means the figures land in the job summary even when a later coverage step fails, and a # misclassification (the task's partition-sum check) fails in seconds rather than after the long run. # sloc-crosscheck depends on sloc, so this one invocation emits both the tables and the gocloc deltas. + # Linux only: the count is of the checked-out tree, so the second lane would recompute the same figures. - name: Count source lines (scc, cross-checked with gocloc) + if: runner.os == 'Linux' timeout-minutes: 5 run: | mise run sloc-crosscheck | tee "$RUNNER_TEMP/sloc-report.txt" @@ -73,7 +88,7 @@ jobs: } >>"$GITHUB_STEP_SUMMARY" - name: Upload sloc reports as artifacts - if: ${{ !cancelled() }} + if: ${{ !cancelled() && runner.os == 'Linux' }} uses: actions/upload-artifact@v7 with: name: sloc-reports @@ -96,6 +111,15 @@ jobs: timeout-minutes: 75 run: mise run cargo-llvm-cov + # Deliberately unguarded, unlike the merges and uploads below. + # Those carry `!cancelled()` so a failing test still publishes a report; this is a gate, and a gate has + # nothing to say about a run whose tests did not pass. Leaving it on the default "skip if an earlier step + # failed" means a red test run reports the test failure, not a coverage shortfall caused by it. It reads + # the profile data the step above already wrote, so it re-runs nothing. + - name: Check native libs/ branch coverage + timeout-minutes: 10 + run: mise run cargo-llvm-cov-branch-check + # Fold the instrumented wasm guest modules' coverage into lcov.info so it rides the same `rust` flag. # The WASI + browser modules dumped their .profraw to target/wasi-cov during the runner tests above. # @@ -104,31 +128,47 @@ jobs: # still upload -- publishing a Rust-only report as though it were the whole picture, which reads downstream # as a large coverage drop rather than as a failed run. `hashFiles` keeps them off a run that produced no # report at all, where there is nothing to merge into. + # + # Linux only, here and for the two browser merges below. The instrumented guest builds need the + # wasm-capable conda clang resolving as bare `clang`, and the headless-Chrome steps need the image's + # chromedriver; neither holds on the macOS lane, which is why wasm-cov-branch-check is Linux-gated too. - name: Merge wasm guest coverage into lcov.info - if: ${{ !cancelled() && hashFiles('lcov.info') != '' }} + if: ${{ !cancelled() && runner.os == 'Linux' && hashFiles('lcov.info') != '' }} timeout-minutes: 10 run: mise run wasm-cov + # Both browser steps below want the image's chromedriver, so it is resolved once here. + # CHROMEWEBDRIVER is the GitHub image's own, matched to its preinstalled Chrome. Leaving CHROMEDRIVER + # empty when the image does not set it is deliberate: the tasks read `${CHROMEDRIVER:-...}`, which treats + # empty and unset alike and falls back to the mise-pinned http:chromedriver either way. Carries the same + # `!cancelled()` as the steps it feeds, so a failing test run still merges against the matched driver. + - name: Point the browser coverage tasks at the runner's chromedriver + if: ${{ !cancelled() && runner.os == 'Linux' }} + run: echo "CHROMEDRIVER=${CHROMEWEBDRIVER:+$CHROMEWEBDRIVER/chromedriver}" >>"$GITHUB_ENV" + # The browser ws-wasm-agent has no native tests, so it is invisible to the cargo-llvm-cov run above. # This runs its wasm-bindgen tests in the runner's headless Chrome against an in-process ws-server and folds - # the agent lib's coverage into the same lcov.info. CHROMEWEBDRIVER is the GitHub image's chromedriver that - # matches its preinstalled Chrome; the task falls back to the mise-pinned http:chromedriver when it is unset. + # the agent lib's coverage into the same lcov.info. - name: Merge ws-wasm-agent coverage into lcov.info - if: ${{ !cancelled() && hashFiles('lcov.info') != '' }} + if: ${{ !cancelled() && runner.os == 'Linux' && hashFiles('lcov.info') != '' }} timeout-minutes: 20 - run: | - export CHROMEDRIVER="${CHROMEWEBDRIVER:+$CHROMEWEBDRIVER/chromedriver}" - mise run wasm-agent-cov + run: mise run wasm-agent-cov # The pic-viewer display path only runs in a browser, so it is invisible to the cargo-llvm-cov run. # Its native parse tests already ride that run; this drives the module's wasm-bindgen display tests in # the same headless Chrome as the ws-wasm-agent step above and folds their lcov into lcov.info. - name: Merge pic-viewer coverage into lcov.info - if: ${{ !cancelled() && hashFiles('lcov.info') != '' }} + if: ${{ !cancelled() && runner.os == 'Linux' && hashFiles('lcov.info') != '' }} timeout-minutes: 20 - run: | - export CHROMEDRIVER="${CHROMEWEBDRIVER:+$CHROMEWEBDRIVER/chromedriver}" - mise run pic-viewer-cov + run: mise run pic-viewer-cov + + # After all three merges, because it reads what they leave in target/wasi-cov. + # The task is Linux-gated internally as well as here: this `if` keeps the step off the macOS lane at all, + # and the gate inside it keeps a direct `mise run` on a workstation from passing on an empty report. + - name: Check wasm libs/ branch coverage + if: runner.os == 'Linux' + timeout-minutes: 10 + run: mise run wasm-cov-branch-check # Python coverage does not depend on the Rust result, so a failing Rust test must not skip it. - name: Collect Python coverage @@ -138,11 +178,12 @@ jobs: GITHUB_TOKEN: ${{ github.token }} run: mise run pytest-cov + # Per-OS name because both lanes produce one and artifact names have to be unique within a run. - name: Upload coverage reports as artifacts if: ${{ !cancelled() }} uses: actions/upload-artifact@v7 with: - name: coverage-reports + name: coverage-reports-${{ matrix.os }} path: | lcov.info coverage-python.xml @@ -153,8 +194,14 @@ jobs: # re-raising the test status. The `hashFiles` half keeps a genuinely absent report from turning into a # second, misleading failure: when an earlier step (a tool install, say) means nothing was ever produced, # these skip instead of erroring on a missing file and burying the real cause. + # + # All four carry `runner.os == 'Linux'` so only the canonical lane reports outward. + # Both lanes land on the same commit, so an unguarded upload would send two partial pictures under one + # `rust` flag / one `--key rust`: the macOS lcov.info covers the native half only, and whichever arrived + # second would read as a coverage collapse rather than as a second platform's result. The macOS numbers + # are kept as that lane's artifact instead, and its branch gate fails the job directly when they slip. - name: Upload Rust coverage to Codecov (OIDC) - if: ${{ !cancelled() && hashFiles('lcov.info') != '' }} + if: ${{ !cancelled() && runner.os == 'Linux' && hashFiles('lcov.info') != '' }} uses: codecov/codecov-action@v7 with: use_oidc: true @@ -163,7 +210,7 @@ jobs: fail_ci_if_error: true - name: Upload Python coverage to Codecov (OIDC) - if: ${{ !cancelled() && hashFiles('coverage-python.xml') != '' }} + if: ${{ !cancelled() && runner.os == 'Linux' && hashFiles('coverage-python.xml') != '' }} uses: codecov/codecov-action@v7 with: use_oidc: true @@ -172,18 +219,18 @@ jobs: fail_ci_if_error: true - name: Upload Rust coverage to DeepSource (OIDC) - if: ${{ !cancelled() && hashFiles('lcov.info') != '' }} + if: ${{ !cancelled() && runner.os == 'Linux' && hashFiles('lcov.info') != '' }} run: deepsource report --analyzer test-coverage --key rust --value-file lcov.info --use-oidc - name: Upload Python coverage to DeepSource (OIDC) - if: ${{ !cancelled() && hashFiles('coverage-python.xml') != '' }} + if: ${{ !cancelled() && runner.os == 'Linux' && hashFiles('coverage-python.xml') != '' }} run: deepsource report --analyzer test-coverage --key python --value-file coverage-python.xml --use-oidc # Upload test results even when tests failed. # nextest still wrote the JUnit report, and a failing run is exactly when the per-test results on Codecov # matter most; `report_type: test_results` selects the JUnit upload path. - name: Upload test results to Codecov (OIDC) - if: ${{ !cancelled() }} + if: ${{ !cancelled() && runner.os == 'Linux' }} uses: codecov/codecov-action@v7 with: use_oidc: true diff --git a/.github/workflows/k3s.yaml b/.github/workflows/k3s.yaml index fae5d009..27ff6ffd 100644 --- a/.github/workflows/k3s.yaml +++ b/.github/workflows/k3s.yaml @@ -64,9 +64,6 @@ jobs: - name: Show MISE_ENV run: echo "MISE_ENV=$MISE_ENV" - - name: Free disk space - uses: jlumbroso/free-disk-space@v1.3.1 - - name: Install mise + tools uses: ./.github/actions/install-mise-tools timeout-minutes: 60 diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index a554f1ab..35fcedd7 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -107,19 +107,9 @@ jobs: fetch-depth: 1 persist-credentials: false - - name: Free disk space on Linux - if: runner.os == 'Linux' - uses: jlumbroso/free-disk-space@v1.3.1 - - - name: Free disk space on Windows - if: runner.os == 'Windows' - uses: ./.github/actions/free-disk-space-windows - - name: Install Mesa Vulkan drivers (lavapipe) if: runner.os == 'Linux' - run: | - sudo apt-get update - sudo apt-get install -y --no-install-recommends mesa-vulkan-drivers + uses: ./.github/actions/install-lavapipe # Windows tools source-build under mise 2026.7.1, so this step runs long (bumped from 20). # The per-tool msvc `install_env` override no longer reaches cargo-binstall there, so cargo tools like @@ -131,8 +121,6 @@ jobs: - name: Install mise + tools uses: ./.github/actions/install-mise-tools timeout-minutes: ${{ matrix.install_timeout || 60 }} - with: - github-token: ${{ github.token }} # GITHUB_TOKEN raises the rate-limit ceiling on mise's fetch of the rp-v rustpython_wasm tarball. # (The fetch is [tools."http:rp-wasm"], via mise's http backend against github.com release assets.) @@ -207,17 +195,9 @@ jobs: fetch-depth: 1 persist-credentials: false - - name: Free disk space on Windows - uses: ./.github/actions/free-disk-space-windows - - # Windows tools source-build under mise 2026.7.1, so this step runs long (bumped from 25). - # The per-tool msvc `install_env` override no longer reaches cargo-binstall there, so cargo tools like - # action-validator compile from source instead of fetching their msvc prebuilts. Revert once mise fixes it. - name: Install mise + tools uses: ./.github/actions/install-mise-tools timeout-minutes: 60 - with: - github-token: ${{ github.token }} - name: Prefetch dependencies timeout-minutes: 20 diff --git a/.mise/config.coverage.toml b/.mise/config.coverage.toml index c8092235..3244145d 100644 --- a/.mise/config.coverage.toml +++ b/.mise/config.coverage.toml @@ -31,6 +31,16 @@ "aqua:hhatto/gocloc" = "latest" "conda:clang" = { version = "latest", os = ["linux", "macos"] } +[vars] +# The libs/ crates each branch-coverage check is answerable for, as directory names under libs/. +# The split is which pipeline carries the crate's coverage, not a judgement about how well tested it is. +# The native set is everything cargo-llvm-cov's own run instruments and reports; the wasm set is the two +# crates that only ever compile to wasm, whose covmaps reach lcov.info through wasm-cov's .ll-gutting path +# and so can only be asserted where that path runs. A crate belongs to exactly one list: naming it in both +# would let a gap in one pipeline be answered by the other. +native_cov_libs = "edge-toolkit et-otlp path test-helpers test-otlp ws-runner-common" +wasm_cov_libs = "wasi-guest web" + # The wasm coverage builds run on nightly (-Zno-profiler-runtime) with the wasm-capable conda clang on PATH. # It is set env-wide here because this env loads only under the coverage workflow; per-command RUSTFLAGS and # the minicov feature come from the wasm_cov / *_cov_feat vars so uninstrumented builds stay clean. @@ -49,6 +59,17 @@ RUSTUP_TOOLCHAIN = "{{ vars.rust_nightly }}" # Pointing the loader straight at the install dir is layout-independent, so it holds however cargo lays the # build dir out. Scoped to this env because only the coverage lane runs binaries under nightly. Drop it together # with the cargo-doc-check mkdir once ort stops hardcoding the OUT_DIR depth. +# Keep host links on Apple's cc, which the conda-clang prepend below otherwise shadows. +# That prepend is there so minicov's build.rs finds a bare `clang` with a wasm32 backend, but conda-clang ships +# a `cc` too and wins the lookup for it -- and conda's clang knows nothing of the macOS SDK, so every host link +# in the coverage env dies with libSystem unresolved, one undefined symbol per libstd call: +# "_writev", referenced from: ...std::sys::stdio::unix::Stderr...write_vectored in libstd-*.rlib +# ld: symbol(s) not found for architecture arm64 +# Naming the linker outright is narrower than reordering PATH: `clang` stays conda's for the wasm guest builds +# that need it, and only rustc's choice of linker driver moves back. The var is per-target, so the Linux lane +# ignores both lines. Drop them if the conda-clang bin dir ever stops carrying a `cc`. +CARGO_TARGET_AARCH64_APPLE_DARWIN_LINKER = "/usr/bin/cc" +CARGO_TARGET_X86_64_APPLE_DARWIN_LINKER = "/usr/bin/cc" DYLD_LIBRARY_PATH = "{{ vars.ort_loc_unix }}/lib" LD_LIBRARY_PATH = "{{ vars.ort_loc_unix }}/lib" _.path = "{{ env.HOME }}/.local/share/mise/installs/conda-clang/latest/bin" @@ -139,6 +160,80 @@ exit "$test_status" """ shell = "{{ vars.task_shell }}" +# The assertion both branch-coverage checks make, kept in one place so the two differ only in what they feed it. +# Hidden because it is meaningless without a report: the caller decides which pipeline produced one and which +# crates that pipeline answers for, and this only reads the result. +# +# Two ways to fail, and the second is the one that matters. +# A crate whose files are all at 100% passes, and so does a file with no branch regions at all (`count` 0 is +# vacuously complete). But a crate with NO records in the report also has nothing under 100%, so a check that +# only looked for shortfalls would go green for a crate the run never executed -- exactly backwards, since a +# crate dropping out of the report is how coverage silently stops being measured. A named crate contributing +# no records is therefore an error, which is why the lists are spelled out rather than globbed from libs/. +[tasks."_cov-branch-assert"] +description = "Assert every named libs/ crate is at 100% branch coverage in an llvm-cov JSON summary" +hide = true +# The whole assertion is one jaq query over llvm-cov's own JSON, with no parsing of our own. +# That JSON (`--format=text`, confusingly) carries a per-file `summary.branches` object, so `.data[0].files[]` +# already holds everything the check reads. The query is written to a file with a quoted heredoc, as wasm-cov +# does with its awk programs: no shell expansion, and no backslash survives the TOML string otherwise. It +# builds its strings with `+` for that reason -- jq's `"\(...)"` interpolation would need every backslash +# doubled to reach the shell. +run = """ +report="$usage_report" +if [ ! -f "$report" ]; then + echo "cov-branch-assert: no coverage report at $report" >&2 + echo "Run the coverage task that produces it first; a missing report is a failure, not a skip." >&2 + exit 1 +fi +coreutils mkdir -p target/cov +prog=target/cov/branch-assert.jaq +coreutils cat >"$prog" <<'JAQ' +($libs | split(" ")) as $names +| .data[0].files as $files +| [ $names[] + | . as $n + | ("/libs/" + $n + "/") as $dir + | ($files | map(select(.filename | contains($dir)))) as $hit + | if ($hit | length) == 0 + then "libs/" + $n + ": no records in the report -- this run never exercised the crate" + else ($hit[] + | select(.summary.branches.covered < .summary.branches.count) + | "libs/" + $n + ": " + .filename + " " + + (.summary.branches.covered | tostring) + "/" + + (.summary.branches.count | tostring) + " branches") + end ] +| .[] +JAQ +shortfall="$(jaq -r --arg libs "$usage_libs" -f "$prog" "$report")" +if [ -n "$shortfall" ]; then + echo "cov-branch-assert: libs/ must be at 100% branch coverage, and is not:" >&2 + echo "$shortfall" >&2 + exit 1 +fi +echo "cov-branch-assert: 100% branch coverage across $usage_libs" +""" +shell = "{{ vars.task_shell }}" +usage = """ +arg "" help="llvm-cov JSON summary (--format=text) to assert over" +arg "" help="Space-separated libs/ directory names that must be at 100% branch coverage" +""" + +# Reports on the profile data cargo-llvm-cov already wrote; it does not re-run the tests. +# `report` re-reads that data, so this is cheap and must follow cargo-llvm-cov in the same job. It needs no +# `--branch` flag: branch regions are baked into the covmap by the `-Zcoverage-options=branch` that task +# appends, and every export carries them from there. With no profile data the underlying command fails on its +# own, which is the right answer -- there is nothing to assert. +[tasks.cargo-llvm-cov-branch-check] +description = "Fail unless every native libs/ crate is at 100% branch coverage" +run = """ +coreutils mkdir -p target/cov +report=target/cov/native-libs.json +cargo llvm-cov report --json --summary-only --output-path "$report" +mise run _cov-branch-assert "$report" "{{ vars.native_cov_libs }}" +""" +shell = "{{ vars.task_shell }}" + [tasks.wasm-cov] description = "Turn instrumented wasm guest .profraw into lcov (llc + llvm-cov) and merge into the workspace lcov.info" # The instrumented wasm builds drop their minicov .profraw into target/wasi-cov during the runner tests. @@ -217,6 +312,53 @@ goawk -f "$keep" "$covdir/wasi.lcov" >>lcov.info """ shell = "{{ vars.task_shell }}" +# Linux-only, because the pipeline feeding it is. +# The guest .profraw this reads exist only if the instrumented wasm builds ran, and those need the wasm-capable +# conda clang genuinely first on PATH -- which this env's `_.path` prepend does not achieve on macOS (see the +# header). Windows never runs coverage at all. So on any other OS there is no data to judge and the check says +# so and stops, rather than passing on an empty report. +# +# One export over every module at once, not one per module. +# libs/web and libs/wasi-guest are linked into several guests, so each module's covmap holds its own partial +# view of the same source file. Exported separately those views would be judged separately and a branch +# covered by one guest would still read as missed in another; merging the profraw first makes the export the +# union, which is what "did anything exercise this branch" means. Same shape wasm-agent-cov already uses. +[tasks.wasm-cov-branch-check] +description = "Linux-only: fail unless every wasm libs/ crate is at 100% branch coverage" +run = """ +os="$(coreutils uname -s)" +if [ "$os" != "Linux" ]; then + echo "wasm-cov-branch-check: the wasm coverage pipeline only runs on Linux; nothing to check on $os" + exit 0 +fi +covdir=target/wasi-cov +if [ -z "$(find "$covdir" -maxdepth 1 -name '*.profraw' -print -quit 2>/dev/null)" ]; then + echo "wasm-cov-branch-check: no $covdir/*.profraw -- run the instrumented wasm build and wasm-cov first" >&2 + exit 1 +fi +found="$(find "$covdir" -maxdepth 1 -name '*.o' -print 2>/dev/null | coreutils sort)" +if [ -z "$found" ]; then + echo "wasm-cov-branch-check: no $covdir/*.o -- wasm-cov has not gutted the guest .ll into objects yet" >&2 + exit 1 +fi +objs=() +while IFS= read -r obj; do + [ -n "$obj" ] || continue + objs+=("-object" "$obj") +done <"$report" +mise run _cov-branch-assert "$report" "{{ vars.wasm_cov_libs }}" +""" +shell = "{{ vars.task_shell }}" + [tasks.pytest-cov] description = "Combined Python coverage (pytest + web-runner Pyodide runs) -> Cobertura for Codecov/DeepSource" # One Python report is combined from several data sources: the pytest runs plus the web-runner Pyodide runs. diff --git a/.mise/config.linux.toml b/.mise/config.linux.toml index 3c1b96b1..1add51ff 100644 --- a/.mise/config.linux.toml +++ b/.mise/config.linux.toml @@ -74,8 +74,8 @@ BINDGEN_EXTRA_CLANG_ARGS = "{{ vars.c_bindgen_args }}" # rpath/pylib flags are shared, from config.toml. # OPENSSL_DIR points openssl-sys at conda's OpenSSL, so anything linking it records conda's libssl soname; # without an rpath the loader only finds it when conda's soname matches the system one (it broke when conda -# bumped to libssl.so.4). pylib_flag does the same for the CPython lib dir. No `-fuse-ld=lld` needed: the -# system linker accepts `-Wl,-rpath` directly. +# bumped to libssl.so.4). pylib_flag does the same for the CPython lib dir -- unlike darwin, where pyo3's +# build script already emits that rpath itself and a second copy draws a linker warning. CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUSTFLAGS = "{{ vars.rpath_flag }} {{ vars.pylib_flag }}" CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS = "{{ vars.rpath_flag }} {{ vars.pylib_flag }}" LIBCLANG_PATH = "{{ vars.a_conda_clangxx }}/lib" diff --git a/.mise/config.macos.toml b/.mise/config.macos.toml index d5b9fd36..e26f1215 100644 --- a/.mise/config.macos.toml +++ b/.mise/config.macos.toml @@ -3,17 +3,10 @@ # loads first; PYO3_PYTHON keeps its config.toml default (py3_unix). [tools] -# The LLD linker (ships ld64.lld) the RUSTFLAGS below use. -# Apple's /usr/bin/clang can't resolve `-fuse-ld=lld` without it. # gpg for mise's gpg_verify signature checks; _setup_all installs it up front. "conda:gnupg" = "latest" -"conda:lld" = "latest" [vars] -conda_lld = "{{ env.HOME }}/.local/share/mise/installs/conda-lld/latest" -# Point `-fuse-ld=` at the absolute ld64.lld so it resolves without a PATH lookup. -# The cargo subprocess mise spawns for a `cargo:` source build doesn't have sibling tools like conda:lld on PATH. -lld_flag = "-C link-arg=-fuse-ld={{ vars.conda_lld }}/bin/ld64.lld" # Xcode's libclang (the lib dir beside `xcrun`'s clang). # clang-sys doesn't always auto-find it on CI. The system libclang knows the SDK, so no bindgen args. mac_libclang = "{{ exec(command='dirname $(dirname $(xcrun --find clang))') }}/lib" @@ -22,19 +15,20 @@ mac_libclang = "{{ exec(command='dirname $(dirname $(xcrun --find clang))') }}/l # `rustc -Z unstable-options --print target-spec-json` describes aarch64-apple-darwin as "11.0+, Big Sur+" and # x86_64-apple-darwin as "10.12+, Sierra+". cc-rs (the `cc` crate) reads this env var when compiling aws-lc- # sys/tree-sitter's C and assembly sources, but without it clang stamps those .o files with the full installed -# SDK version instead -- e.g. `ld64.lld: .../libaws_lc_sys-*.rlib(*-jitterentropy-base.o) has version 26.5.0, -# which is newer than target minimum of 11.0.0`, one line per object file, surfaced as a build warning by -# rustc's `linker_messages` lint. Setting it here makes cc-rs target the same minimum rustc's own codegen -# already uses, so the .o files' stamped version and the linked binary's requested minimum agree and ld64.lld -# has nothing to warn about -- fixing the mismatch instead of just silencing the lint that reports it. +# SDK version instead: `otool -l` on aws-lc-sys's jitterentropy-base.o then reports `minos 27.0` inside a +# binary whose own LC_BUILD_VERSION asks for 11.0, which is a real portability bug -- those objects use +# whatever the newest SDK offers while the binary claims to run on Big Sur. Apple's ld links the mismatch +# without complaint, so nothing surfaces it; only the stamps disagree. Setting it here makes cc-rs target the +# same minimum rustc's own codegen already uses, so the two agree. macosx_deployment_target = "{% if arch() == 'arm64' %}11.0{% else %}10.12{% endif %}" [env] -# lld_flag points Apple's clang at ld64.lld (see [vars]). -# The rpath/pylib flags (from config.toml) record conda's OpenSSL soname + the CPython lib dir so the runtime -# loader finds them. Applies to every rustc call, including the `cargo:` source-build subprocess. -CARGO_TARGET_AARCH64_APPLE_DARWIN_RUSTFLAGS = "{{ vars.lld_flag }} {{ vars.rpath_flag }} {{ vars.pylib_flag }}" -CARGO_TARGET_X86_64_APPLE_DARWIN_RUSTFLAGS = "{{ vars.lld_flag }} {{ vars.rpath_flag }} {{ vars.pylib_flag }}" +# rpath_flag (from config.toml) records conda's OpenSSL soname so the runtime loader finds it. +# Applies to every rustc call, including the `cargo:` source-build subprocess. config.toml's pylib_flag is +# deliberately absent: pyo3's build script already emits that same rpath on darwin, and passing it twice makes +# Apple's linker report `ld: duplicate -rpath '/lib' ignored` through rustc's `linker_messages` lint. +CARGO_TARGET_AARCH64_APPLE_DARWIN_RUSTFLAGS = "{{ vars.rpath_flag }}" +CARGO_TARGET_X86_64_APPLE_DARWIN_RUSTFLAGS = "{{ vars.rpath_flag }}" # See the macosx_deployment_target comment in [vars] above. LIBCLANG_PATH = "{{ vars.mac_libclang }}" MACOSX_DEPLOYMENT_TARGET = "{{ vars.macosx_deployment_target }}" @@ -55,10 +49,9 @@ _.path = ["/Library/Developer/CommandLineTools/usr/bin", "/bin", "/sbin", "/usr/ # Linux. The package list has a single source of truth -- the Dockerfile's APT_PACKAGES ARG: a workstation # reads it back from the Dockerfile in the checkout, so adding/removing a prereq there propagates here # automatically. Most apt names map cleanly to Xcode CLT binaries or macOS pre-installed tools; the case -# patterns below adjust the few that don't (cert path, SDK headers via xcrun). Finally install conda:lld (the -# darwin linker the RUSTFLAGS use). +# patterns below adjust the few that don't (cert path, SDK headers via xcrun). depends = ["_setup_all"] -description = "Preinstall: verify Xcode CLT + Dockerfile prereqs + conda:lld" +description = "Preinstall: verify Xcode CLT + Dockerfile prereqs" run = """ xcode-select -p >/dev/null 2>&1 || { echo "preinstall: Xcode command-line tools not found." >&2 @@ -128,6 +121,5 @@ if [ -n "$brewy" ]; then echo "Adjust PATH so Xcode CLT / macOS-native tools resolve first." >&2 exit 1 fi -mise install conda:lld """ shell = "{{ vars.task_shell }}" diff --git a/.mise/config.toml b/.mise/config.toml index 762314b5..00e5907a 100644 --- a/.mise/config.toml +++ b/.mise/config.toml @@ -153,7 +153,7 @@ ripgrep = "latest" "cargo:ryl" = { version = "latest", os = ["macos/x64"] } "github:microsoft/onnxruntime" = "1.22.0" "github:owenlamont/ryl" = { version = "latest", os = ["linux", "macos/arm64", "windows"] } -"github:wasm-bindgen/wasm-bindgen" = "0.2.127" +"github:wasm-bindgen/wasm-bindgen" = "0.2.128" # go-containerregistry's crane fetches the per-platform ghcr.io mise-tools stores. # Its `crane export` rootfs-flatten is what the pull-mise-tools task and CI's install-mise-tools restore # step extract tool trees with. @@ -247,9 +247,9 @@ version = "{{ vars.rust_nightly }}" # # Capping every lint to `warn` for this one install leaves the deny in place for our own crates. It has to # ride on plain RUSTFLAGS rather than joining config.macos.toml's CARGO_TARGET_X86_64_APPLE_DARWIN_RUSTFLAGS, -# because install_env values are passed through literally -- no `{{ vars.x }}`, no `$HOME` -- so the lld and -# rpath flags that value is built from cannot be restated here. Plain RUSTFLAGS wins over the target-specific -# form, so this install alone links with the default linker instead of conda:lld. +# because install_env values are passed through literally -- no `{{ vars.x }}`, no `$HOME` -- so the rpath +# flag that value is built from cannot be restated here. Plain RUSTFLAGS wins over the target-specific form, +# so this install alone links without the conda OpenSSL rpath. [tools."cargo:vectordotdev/vector"] crate = "vector" install_env = { RUSTFLAGS = "--cap-lints=warn" } @@ -512,6 +512,16 @@ a_et_cli_target_dir = "target{% if os() == 'windows' %}/x86_64-pc-windows-gnullv # web_cov_wrapper only adds the ET_TEST_COVERAGE conditional around this value. a_web_cov_wrapper_env = 'RUSTC_WORKSPACE_WRAPPER={{ config_root }}/target/debug/int-wasm-cov-wrapper ' et_cli = "{{ vars.config_root_fwd }}/{{ vars.a_et_cli_target_dir }}/{{ vars.a_et_cli_exe }}" +# Oldest macOS SDK macos-sdk-check accepts, held as high as CI can actually satisfy. +# This is a track-the-latest policy rather than a floor the build forces: the deployment target (11.0 on +# arm64) is the only figure the repo itself requires, and every SDK since is technically sufficient. Pinning +# near the newest keeps contributors on one known SDK, so a build difference is never "which Xcode is this". +# +# 26.0 is that policy capped by the runners. GitHub's macOS images are macOS 26 -- macos-latest is macOS 26 +# arm64, macos-26-intel its x64 twin -- so they carry a 26.x SDK, and 27.0 would fail every macOS lane rather +# than any real defect. Raise this to 27.0 once those images ship Xcode 27; a contributor already on 27 keeps +# passing meanwhile, since the check is a floor and not an equality. +macos_min_sdk = "26.0" # mise-managed CPython (Linux/macOS); the PYO3_PYTHON default below. # Windows overrides PYO3_PYTHON with its own py3_win in config.windows.toml. The version # segment must equal the `python` [tools] pin (conftest enforces both). @@ -778,6 +788,7 @@ depends = [ "kubeconform-check", "link-check", "ls-lint-check", + "macos-sdk-check", "mise-lock-check", "readme-mise-version-check", "regal-check", @@ -841,6 +852,45 @@ run = "zizmor --offline --no-progress -c config/zizmor.yaml .github/workflows .g description = "Lint file and directory naming conventions (ls-lint)" run = "ls-lint --config config/ls-lint.yaml" +# Verify the host's macOS SDK before anything tries to link against it. +# Two failures, and the first is the common one. `xcrun` reporting no SDK at all means the command-line tools +# are absent or the active developer dir points at nothing -- config.macos.toml's `SDKROOT` is computed by +# exactly this command, so that state poisons every cc-rs build script with "'stdlib.h' file not found" +# rather than saying what is wrong. Catching it here names it once, up front. The second is an SDK older than +# the deployment target the build asks for, which is incoherent: you cannot target macOS 11 with a 10.x SDK. +# +# Lives in the always-loaded config rather than config.macos.toml, and no-ops elsewhere. +# `check:rust` and `test:rust` both depend on it, and their depends lists are static -- a task defined only in +# the macOS config would be an unresolvable dependency on every other platform. +# +# This does NOT exist to demand a recent Xcode. Apple's `ld` reads its own SDK whatever its vintage, so there +# is no upper-bound problem to guard against; the floor is only a floor. +[tasks.macos-sdk-check] +description = "Fail if the macOS SDK is missing or older than the minimum this repo builds against" +run = """ +if [ "$(coreutils uname -s)" != "Darwin" ]; then + echo "macos-sdk-check: not macOS; nothing to verify" + exit 0 +fi +sdk="$(xcrun --show-sdk-version 2>/dev/null || true)" +if [ -z "$sdk" ]; then + echo "macos-sdk-check: xcrun reported no SDK version." >&2 + echo "Install the Xcode command-line tools with: xcode-select --install" >&2 + exit 1 +fi +min="{{ vars.macos_min_sdk }}" +# `sort -V` puts the lower version first, so the minimum staying first means the SDK is at least it. +# Equal versions sort to the same value, which lands on the pass side, as an exact match should. +lowest="$(printf '%s\\n%s\\n' "$min" "$sdk" | coreutils sort -V | coreutils head -n 1)" +if [ "$lowest" != "$min" ]; then + echo "macos-sdk-check: SDK $sdk is older than the required $min." >&2 + echo "Update the Xcode command-line tools via Software Update, or: xcode-select --install" >&2 + exit 1 +fi +echo "macos-sdk-check: SDK $sdk (minimum $min)" +""" +shell = "{{ vars.task_shell }}" + [tasks.ast-grep-check] # --no-ignore hidden so the gha-* YAML rules reach .github/workflows. # ast-grep skips dot-dirs by default; gitignored paths like target/ stay skipped. @@ -889,6 +939,16 @@ shell = "{{ vars.task_shell }}" # config, still shows the whole picture instead of filtering to new clones. That config's `ignore` list covers # only generator-owned output nobody hand-writes -- note the files under a module's pkg/ other than package.json # are hand-written shims, so they stay in scope. +# +# The one `ignorePattern` entry, whose reason has to live here because that config is plain JSON. +# It matches the `actions/checkout` step, which every workflow is obliged to write out in full: a composite +# action is referenced by a path inside the repo, so it cannot run until the repo has been checked out, which +# makes this the one step that can never be factored into `.github/actions/`. Six workflows therefore repeat the +# same five lines with the same `fetch-depth` / `persist-credentials` pins, and reported as a clone it asks for a +# refactor that the platform does not permit. `ignorePattern` drops those tokens before detection, which is +# narrower than excluding the workflow files: everything else in them -- job bodies, step sequences, run blocks +# -- is still compared. Delete this the day a checkout can be shared, and expect the clone count to rise by the +# handful of pairs it is currently hiding. [tasks.jscpd-check] description = "Detect copy/pasted code across every language (jscpd)" run = "jscpd . --config config/jscpd.json --baseline config/jscpd-baseline.json --fail-on-new-clones" @@ -1031,6 +1091,17 @@ run = "conftest test --namespace jscpd -p config/conftest/policy config/jscpd-ba description = "Run the Rego unit tests beside the conftest policies (conftest verify)" run = "conftest verify -p config/conftest/policy" +# The policy tree checked against itself: every policy must have a test file beside it, or a recorded reason. +# `--parser ignore` because .rego has no parser and only the paths are read; the policy contents are never +# inspected, so the entries the parser produces per line go unused. +[tasks.conftest-check-policy-pairs] +description = "Check every conftest policy has a test file beside it (conftest)" +run = """ +find config/conftest/policy -name '*.rego' -print0 | + xargs -0 conftest test --combine --parser ignore --namespace policy_tests -p config/conftest/policy +""" +shell = "{{ vars.task_shell }}" + [tasks.conftest-check-generated-trees] description = "Cross-check config/generated-trees.toml against the linter configs it governs (conftest)" # --combine over a deliberately mixed file set, with conftest auto-detecting each parser (TOML, YAML, JSON). @@ -2243,7 +2314,11 @@ run = "cargo nextest run --config-file config/nextest.toml -p edge-toolkit -p et [tasks."test:rust"] # Rust tests. Always-loaded, so `test`'s glob always matches it. -depends = ["cargo-test"] +# macos-sdk-check rides here as well as on check:rust so both GHA workflows get it without a workflow edit: +# `check` globs check:*, `test` globs test:*, and these two are the members that are always loaded. It is a +# host precondition rather than a test, but a broken SDK fails the compile it precedes with a far worse +# message, so naming it up front is worth the slight category stretch. +depends = ["cargo-test", "macos-sdk-check"] description = "Run the Rust tests" [tasks.test] diff --git a/.mise/config.windows.toml b/.mise/config.windows.toml index 5c68f23e..3ecbd774 100644 --- a/.mise/config.windows.toml +++ b/.mise/config.windows.toml @@ -156,9 +156,8 @@ py3_win_fwd = "{{ vars.a_mise_installs_fwd }}/python/3.13.14/python.exe" ort_loc_win = '{{ vars.mise_installs }}\github-microsoft-onnxruntime\1.22.0' # llvm-mingw's bin dir (the github:mstorsjo/llvm-mingw install above). # The linker/dlltool [env] vars below point at absolute exes in here rather than bare names because mise's -# cargo backend doesn't activate llvm-mingw onto the cargo: source-build subprocess's PATH (same problem -# config.macos.toml's absolute ld64.lld path solves): a bare `clang` would resolve to a runner's system -# LLVM, which defaults to the MSVC target and can't link gnullvm objects. +# cargo backend doesn't activate llvm-mingw onto the cargo: source-build subprocess's PATH: a bare `clang` +# would resolve to a runner's system LLVM, which defaults to the MSVC target and can't link gnullvm objects. win_llvm_bin = '{{ vars.mise_installs }}\github-mstorsjo-llvm-mingw\20260602\bin' # conda:m2-gnupg's msys2 binary dir. # Cargo's own bin dir, prepended by the [env] `_.path` below so `cargo` resolves inside a task body. diff --git a/.mise/mise.lock b/.mise/mise.lock index 5c3a08f2..bf7effb8 100644 --- a/.mise/mise.lock +++ b/.mise/mise.lock @@ -1196,39 +1196,38 @@ url = "https://github.com/vladkens/macmon/releases/download/v0.7.2/macmon-v0.7.2 url_api = "https://api.github.com/repos/vladkens/macmon/releases/assets/410521089" [[tools."github:wasm-bindgen/wasm-bindgen"]] -version = "0.2.127" +version = "0.2.128" backend = "github:wasm-bindgen/wasm-bindgen" [tools."github:wasm-bindgen/wasm-bindgen"."platforms.linux-arm64"] -checksum = "sha256:1ce0ebd74e378d989651091f93c917bd7700996945d4c5ce37ec9507f149225c" -url = "https://github.com/wasm-bindgen/wasm-bindgen/releases/download/0.2.127/wasm-bindgen-0.2.127-aarch64-unknown-linux-gnu.tar.gz" -url_api = "https://api.github.com/repos/wasm-bindgen/wasm-bindgen/releases/assets/505858464" +checksum = "sha256:2f61267bbef279c2d6357b199649468b073223febde4ae108d30ac3d6b5540af" +url = "https://github.com/wasm-bindgen/wasm-bindgen/releases/download/0.2.128/wasm-bindgen-0.2.128-aarch64-unknown-linux-gnu.tar.gz" +url_api = "https://api.github.com/repos/wasm-bindgen/wasm-bindgen/releases/assets/545035805" provenance = "github-attestations" [tools."github:wasm-bindgen/wasm-bindgen"."platforms.linux-x64"] -checksum = "sha256:61d4a7dc85acfa0d2354ccc0b8361928c7e52a746d17f28ebaa795ed3dc1614a" -url = "https://github.com/wasm-bindgen/wasm-bindgen/releases/download/0.2.127/wasm-bindgen-0.2.127-x86_64-unknown-linux-musl.tar.gz" -url_api = "https://api.github.com/repos/wasm-bindgen/wasm-bindgen/releases/assets/505858453" +checksum = "sha256:b51f0208fdff83515a787bd8ab9ac5865ed84dabb66d0c709957bb59793c645f" +url = "https://github.com/wasm-bindgen/wasm-bindgen/releases/download/0.2.128/wasm-bindgen-0.2.128-x86_64-unknown-linux-musl.tar.gz" +url_api = "https://api.github.com/repos/wasm-bindgen/wasm-bindgen/releases/assets/545035799" provenance = "github-attestations" -provenance_verified = true [tools."github:wasm-bindgen/wasm-bindgen"."platforms.macos-arm64"] -checksum = "sha256:cd93e691eb5953ace5d8ffce52a20b024077a3dac3e2215b8224136b0efb7585" -url = "https://github.com/wasm-bindgen/wasm-bindgen/releases/download/0.2.127/wasm-bindgen-0.2.127-aarch64-apple-darwin.tar.gz" -url_api = "https://api.github.com/repos/wasm-bindgen/wasm-bindgen/releases/assets/505858463" +checksum = "sha256:67ba17f260977725c0b541b516dbb5153538140f079a900329fb6077661b47ab" +url = "https://github.com/wasm-bindgen/wasm-bindgen/releases/download/0.2.128/wasm-bindgen-0.2.128-aarch64-apple-darwin.tar.gz" +url_api = "https://api.github.com/repos/wasm-bindgen/wasm-bindgen/releases/assets/545035806" provenance = "github-attestations" provenance_verified = true [tools."github:wasm-bindgen/wasm-bindgen"."platforms.macos-x64"] -checksum = "sha256:81049c79f4e283e1725e6582a0528af1301a70ad23add1df1c4d042ec825263d" -url = "https://github.com/wasm-bindgen/wasm-bindgen/releases/download/0.2.127/wasm-bindgen-0.2.127-x86_64-apple-darwin.tar.gz" -url_api = "https://api.github.com/repos/wasm-bindgen/wasm-bindgen/releases/assets/505858457" +checksum = "sha256:59d9af11d0a61b8019898d555de31153c3a50e7f1797e9849fb38589d16add43" +url = "https://github.com/wasm-bindgen/wasm-bindgen/releases/download/0.2.128/wasm-bindgen-0.2.128-x86_64-apple-darwin.tar.gz" +url_api = "https://api.github.com/repos/wasm-bindgen/wasm-bindgen/releases/assets/545035803" provenance = "github-attestations" [tools."github:wasm-bindgen/wasm-bindgen"."platforms.windows-x64"] -checksum = "sha256:fd52ef9896cbb0f4f59ef115bdf2f4664a76d716b6201c35e5936018ad680cfc" -url = "https://github.com/wasm-bindgen/wasm-bindgen/releases/download/0.2.127/wasm-bindgen-0.2.127-x86_64-pc-windows-msvc.tar.gz" -url_api = "https://api.github.com/repos/wasm-bindgen/wasm-bindgen/releases/assets/505858456" +checksum = "sha256:8fd8e2165da16b21ee3f5efd19e7f97d8d27cb7832f54edaef4b18e830283ec0" +url = "https://github.com/wasm-bindgen/wasm-bindgen/releases/download/0.2.128/wasm-bindgen-0.2.128-x86_64-pc-windows-msvc.tar.gz" +url_api = "https://api.github.com/repos/wasm-bindgen/wasm-bindgen/releases/assets/545035796" provenance = "github-attestations" [[tools.go]] diff --git a/.mise/mise.macos.lock b/.mise/mise.macos.lock index 28fa4ac2..f2174192 100644 --- a/.mise/mise.macos.lock +++ b/.mise/mise.macos.lock @@ -52,14 +52,6 @@ checksum = "sha256:1473451cd282b48d24515795a595801c9b65b567fe399d7e12d50b2d6cdb0 url = "https://conda.anaconda.org/conda-forge/linux-aarch64/libksba-1.6.7-h0a1ffab_0.conda" checksum = "sha256:6bd8c23157e3afa89d56ff876993232d94dc796e1ad190f3cd56148815c7004f" -[conda-packages.linux-arm64."libllvm22-22.1.8-hfd2ba90_1"] -url = "https://conda.anaconda.org/conda-forge/linux-aarch64/libllvm22-22.1.8-hfd2ba90_1.conda" -checksum = "sha256:dc943f1730a27f5fb36682c9852f7196f671c3a9df4a0a7a8f7ba665637c9151" - -[conda-packages.linux-arm64."liblzma-5.8.3-he30d5cf_1"] -url = "https://conda.anaconda.org/conda-forge/linux-aarch64/liblzma-5.8.3-he30d5cf_1.conda" -checksum = "sha256:f760669fd1cea27f689f411c0d5488e26ce50e382c9b63e4ad348f959850124a" - [conda-packages.linux-arm64."libsqlite-3.53.4-h022381a_0"] url = "https://conda.anaconda.org/conda-forge/linux-aarch64/libsqlite-3.53.4-h022381a_0.conda" checksum = "sha256:da46b52f6815e9771f4e21e3332c423c88644e91ac96b0534e85158adc88a8b4" @@ -72,14 +64,6 @@ checksum = "sha256:81ef9a10a0e01ffc7e03d429ed048410153411b2d8bbf95c334bdf3dcf175 url = "https://conda.anaconda.org/conda-forge/linux-aarch64/libstdcxx-ng-16.1.0-hdbbeba8_1.conda" checksum = "sha256:cb88eb500022e01f835209e771d455d2296e10a18a749f518c257dfcab7d46ba" -[conda-packages.linux-arm64."libxml2-16-2.15.3-h064b767_0"] -url = "https://conda.anaconda.org/conda-forge/linux-aarch64/libxml2-16-2.15.3-h064b767_0.conda" -checksum = "sha256:96e9bd642c013ce3407c23b3819204ea8a591567d62b49baa4bafb2f1487326c" - -[conda-packages.linux-arm64."libxml2-2.15.3-h9ba8346_0"] -url = "https://conda.anaconda.org/conda-forge/linux-aarch64/libxml2-2.15.3-h9ba8346_0.conda" -checksum = "sha256:74aa3c62e0ec4491343b95ce4ca8812ad2f9bb015369e29cb3b4b9b4302d8cb0" - [conda-packages.linux-arm64."libzlib-1.3.2-hdc9db2a_3"] url = "https://conda.anaconda.org/conda-forge/linux-aarch64/libzlib-1.3.2-hdc9db2a_3.conda" checksum = "sha256:76efa6cc9d7e6f5ee3bbca0939f64054af2bdfb3c3632531f8e633cf6c2ea41e" @@ -108,10 +92,6 @@ checksum = "sha256:70bc70e05ec16173ad15e13624d051588dcd2c2f01ef12171234ab04d4c7d url = "https://conda.anaconda.org/conda-forge/linux-aarch64/zlib-1.3.2-hdc9db2a_3.conda" checksum = "sha256:3f3c296477cf26474660942f4d8ec104a8eee2cb1abc7c5bfec9ac1102c66451" -[conda-packages.linux-arm64."zstd-1.5.7-h9d15635_7"] -url = "https://conda.anaconda.org/conda-forge/linux-aarch64/zstd-1.5.7-h9d15635_7.conda" -checksum = "sha256:427fd14bcb3b8659796fecc682716617409350fb5a98e5b7b47558a10d1a2fc7" - [conda-packages.linux-x64."_openmp_mutex-4.5-20_gnu"] url = "https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda" checksum = "sha256:1dd3fffd892081df9726d7eb7e0dea6198962ba775bd88842135a4ddb4deb3c9" @@ -168,14 +148,6 @@ checksum = "sha256:f943117edb9cd4d9c61cc972eee5a34291dc55ea7a6e9e38da104995841cb url = "https://conda.anaconda.org/conda-forge/linux-64/libksba-1.6.7-hac33072_0.conda" checksum = "sha256:289d4f2bf33134c4f5565f772829cb66c996485723bd9c299a21f73ff8aed77a" -[conda-packages.linux-x64."libllvm22-22.1.8-h474f4eb_2"] -url = "https://conda.anaconda.org/conda-forge/linux-64/libllvm22-22.1.8-h474f4eb_2.conda" -checksum = "sha256:cfc90781f703b8b8cc35694d46c9a200e3cf66657f55517f8b1ec1f672c42752" - -[conda-packages.linux-x64."liblzma-5.8.3-hb03c661_1"] -url = "https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.3-hb03c661_1.conda" -checksum = "sha256:9787df8c22a59c9a70d3e5a10db9ad663485e75e9ccc3f09bd092cb7b95e0dab" - [conda-packages.linux-x64."libsqlite-3.53.4-h13e7031_1"] url = "https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.53.4-h13e7031_1.conda" checksum = "sha256:f20d70da54e5b31dd4a51fb1efeaafafd5ab6dd8d7ac9d1438eaa2526ac4ed3d" @@ -188,14 +160,6 @@ checksum = "sha256:40b792b0186c1e8859280a1f6f19a54fc50a11b32724fc7b637009c1a9bd3 url = "https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-16.2.0-hdf11a46_4.conda" checksum = "sha256:4cdebd87b76cf53a58a08ebd6d15336daa79c5f0aa34d83a895ac503c0203632" -[conda-packages.linux-x64."libxml2-16-2.15.3-hca6bf5a_1"] -url = "https://conda.anaconda.org/conda-forge/linux-64/libxml2-16-2.15.3-hca6bf5a_1.conda" -checksum = "sha256:087d4de023f22d6a28b9e1b818961dd41e9bda6e9793590600ff16ca150bf9cb" - -[conda-packages.linux-x64."libxml2-2.15.3-h49c6c72_1"] -url = "https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.15.3-h49c6c72_1.conda" -checksum = "sha256:a16a576a5844a3a0e1cdfc5e162b9fe9c64dccf6a93f44c293bb42851aebe2b4" - [conda-packages.linux-x64."libzlib-1.3.2-h25fd6f3_3"] url = "https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_3.conda" checksum = "sha256:eb8a0db0aa570124f7d2a93d7c7f596e3390df5e047818d873baad32985fc736" @@ -224,10 +188,6 @@ checksum = "sha256:b01812eff4e950a73933cb3dc14647a97ee377c7fab4858495b80a749ebfb url = "https://conda.anaconda.org/conda-forge/linux-64/zlib-1.3.2-h25fd6f3_3.conda" checksum = "sha256:16080a1c7724f7d25727cdc23c7658e0cec2db52448c1dc0c33467ee2c6e1c62" -[conda-packages.linux-x64."zstd-1.5.7-hb78ec9c_7"] -url = "https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_7.conda" -checksum = "sha256:47d682b9f6d6ec9eb1a6e6c3e75ea6273e899e78fb7fc59f81d39745009fbc60" - [conda-packages.macos-arm64."bzip2-1.0.8-h4e30115_10"] url = "https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-h4e30115_10.conda" checksum = "sha256:8ec22f0ba25cbfc2e64d70cf29459eccd7ffdf6436f6a6ff15bbfef799f7d4f6" @@ -276,26 +236,10 @@ checksum = "sha256:99d2cebcd8f84961b86784451b010f5f0a795ed1c08f1e7c76fbb3c22abf0 url = "https://conda.anaconda.org/conda-forge/osx-arm64/libksba-1.6.7-h00cdb27_0.conda" checksum = "sha256:1fb2da81dd151baf940bfb90b9bfe8b14a57ab75d28b95fd5adde89e14b8b288" -[conda-packages.macos-arm64."libllvm22-22.1.8-h89af1be_1"] -url = "https://conda.anaconda.org/conda-forge/osx-arm64/libllvm22-22.1.8-h89af1be_1.conda" -checksum = "sha256:9c0ece5160e3d8749f43f8dd86777be4cfdb51490fb32f4d8269f8fd9866ce55" - -[conda-packages.macos-arm64."liblzma-5.8.3-h8088a28_1"] -url = "https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.3-h8088a28_1.conda" -checksum = "sha256:23d0630046a3e8b164d8f80f2b74ed2605af2e7050ab9913018056402fae4311" - [conda-packages.macos-arm64."libsqlite-3.53.4-h1ae2325_0"] url = "https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.53.4-h1ae2325_0.conda" checksum = "sha256:745662565e103f290e9dc4263bbd88285082f8cf699854fe2d5f1e35a4a0d326" -[conda-packages.macos-arm64."libxml2-16-2.15.3-h6967ea9_0"] -url = "https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-16-2.15.3-h6967ea9_0.conda" -checksum = "sha256:43895a7517c055b8893531290f9dc48bd751eb04be04f14bbce3b6c71b052be6" - -[conda-packages.macos-arm64."libxml2-2.15.3-heed7d32_0"] -url = "https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-2.15.3-heed7d32_0.conda" -checksum = "sha256:4d9c117b2dd222cf891710d5f6a570ebb275479979843a1477ac54ed50907b40" - [conda-packages.macos-arm64."libzlib-1.3.2-h8088a28_3"] url = "https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_3.conda" checksum = "sha256:a18fa5d5bac452401459f966cf0d872224e8080c4ff93c77e168d43ab42ef9d7" @@ -324,10 +268,6 @@ checksum = "sha256:fffebc6bbe45dd4462d3a701a0426c224ad75f8d8bcd174860d7f1b277d4e url = "https://conda.anaconda.org/conda-forge/osx-arm64/zlib-1.3.2-h8088a28_3.conda" checksum = "sha256:ab46d85e4fcffff1b1c5cac517afa40b7ae784cf76fc9da1f55f6a6934291eb6" -[conda-packages.macos-arm64."zstd-1.5.7-hf451053_7"] -url = "https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hf451053_7.conda" -checksum = "sha256:da867f5092eb0cb746d353694f0098031fd9817a4ce7d5743121209ae0f406ca" - [conda-packages.macos-x64."bzip2-1.0.8-h374f1ed_10"] url = "https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-h374f1ed_10.conda" checksum = "sha256:4ed83961876dc8844a6f0df49c07b408efbaea275ffb0b37133e24c006990b3a" @@ -372,26 +312,10 @@ checksum = "sha256:8c352744517bc62d24539d1ecc813b9fdc8a785c780197c5f0b84ec5b0dfe url = "https://conda.anaconda.org/conda-forge/osx-64/libksba-1.6.7-hf036a51_0.conda" checksum = "sha256:cbeb2ae0d4b6e3f2abfcb1ae13c92e84e3cdd87caaad7a7e99e6a80edd35deb1" -[conda-packages.macos-x64."libllvm22-22.1.8-hab754da_1"] -url = "https://conda.anaconda.org/conda-forge/osx-64/libllvm22-22.1.8-hab754da_1.conda" -checksum = "sha256:c2bd652c5a6c4f0e6029786c4e59c54c09018049664006e94d674db4561238ea" - -[conda-packages.macos-x64."liblzma-5.8.3-hbb4bfdb_1"] -url = "https://conda.anaconda.org/conda-forge/osx-64/liblzma-5.8.3-hbb4bfdb_1.conda" -checksum = "sha256:7915dac7c71c208e40e716e2f6d3eff41a8d5584e0e7c2d46f9bf9bd5f9aa739" - [conda-packages.macos-x64."libsqlite-3.53.4-h77d7759_0"] url = "https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.53.4-h77d7759_0.conda" checksum = "sha256:5725d44a17d196adba9798a5fd9f692b7039a827cd6145556a072a80e1931c49" -[conda-packages.macos-x64."libxml2-16-2.15.3-h0d7f165_0"] -url = "https://conda.anaconda.org/conda-forge/osx-64/libxml2-16-2.15.3-h0d7f165_0.conda" -checksum = "sha256:daa69a1dd887b2dbac44327bc5af73a4d41fa63bc6dc609782fdda9aec187895" - -[conda-packages.macos-x64."libxml2-2.15.3-h0712280_0"] -url = "https://conda.anaconda.org/conda-forge/osx-64/libxml2-2.15.3-h0712280_0.conda" -checksum = "sha256:caf6e73fa53c3dec3227ea67de231b0f7009544252684e816c6f2f414aeb55c9" - [conda-packages.macos-x64."libzlib-1.3.2-hbb4bfdb_3"] url = "https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.3.2-hbb4bfdb_3.conda" checksum = "sha256:b2dba286dd6632292b12296e761193b5ef9fb0eaeecaa481f5ba9af72c0c18e1" @@ -420,50 +344,6 @@ checksum = "sha256:f791a0485dc7ed05e1ba7230149d2922427777370c74e98df6384b0f73216 url = "https://conda.anaconda.org/conda-forge/osx-64/zlib-1.3.2-hbb4bfdb_3.conda" checksum = "sha256:d46ba65a5ebcb4f682f9f0f21943c427eb56e7f9d15aeab8b823294c787dbb0b" -[conda-packages.macos-x64."zstd-1.5.7-hbc1a06c_7"] -url = "https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-hbc1a06c_7.conda" -checksum = "sha256:5277886d9704a624dc9b79ac985861e1e431bdb39a57c082507ab577a138ec6c" - -[conda-packages.windows-x64."libiconv-1.18-hc1393d2_2"] -url = "https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-hc1393d2_2.conda" -checksum = "sha256:0dcdb1a5f01863ac4e8ba006a8b0dc1a02d2221ec3319b5915a1863254d7efa7" - -[conda-packages.windows-x64."liblzma-5.8.3-hfd05255_1"] -url = "https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.3-hfd05255_1.conda" -checksum = "sha256:d36c4a1e1f80fd08e18a407e03622ff2f34dfdd022da6488ad19603dea19e6d5" - -[conda-packages.windows-x64."libxml2-16-2.15.3-h692994f_0"] -url = "https://conda.anaconda.org/conda-forge/win-64/libxml2-16-2.15.3-h692994f_0.conda" -checksum = "sha256:8038084c60eda2006d0122d05e3364fe8db0a18935ca6ed0168b5ba5aa33f904" - -[conda-packages.windows-x64."libxml2-2.15.3-hbc0d294_0"] -url = "https://conda.anaconda.org/conda-forge/win-64/libxml2-2.15.3-hbc0d294_0.conda" -checksum = "sha256:da68af9d9d28d65a6916db1bef68f8a25c64c4fdcf759f32a2d2f2f143220adf" - -[conda-packages.windows-x64."libzlib-1.3.2-hfd05255_3"] -url = "https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.2-hfd05255_3.conda" -checksum = "sha256:0629c2cc0404d3bb29d6baa7b4ba62da80797015e86de050db81ea5a07050527" - -[conda-packages.windows-x64."ucrt-10.0.26100.0-h57928b3_0"] -url = "https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda" -checksum = "sha256:3005729dce6f3d3f5ec91dfc49fc75a0095f9cd23bab49efb899657297ac91a5" - -[conda-packages.windows-x64."vc-14.5-ha367084_41"] -url = "https://conda.anaconda.org/conda-forge/win-64/vc-14.5-ha367084_41.conda" -checksum = "sha256:35444c55a92e2f7f7ba26bc70f81e56e52344f7d064c0fd4b40a46a58517b79c" - -[conda-packages.windows-x64."vc14_runtime-14.51.36247-habf1de7_41"] -url = "https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.51.36247-habf1de7_41.conda" -checksum = "sha256:4e4cb599cdc41bf2109d1464c127b5bcbddf548ce3e322e612afb691338b48f8" - -[conda-packages.windows-x64."vcomp14-14.51.36247-habf1de7_41"] -url = "https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.51.36247-habf1de7_41.conda" -checksum = "sha256:731e043390c9457299484d39e427221fc868a9249540a498a5a4f6456c7744d1" - -[conda-packages.windows-x64."zstd-1.5.7-h534d264_7"] -url = "https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_7.conda" -checksum = "sha256:ca7daae4f218a11fab82cc2857f0ea518ec3f46acec60490485347a4c22c6b3e" - [[tools."conda:gnupg"]] version = "2.5.20" backend = "conda:gnupg" @@ -580,89 +460,3 @@ conda_deps = [ "libgcrypt-tools-1.12.2-ha1e9b39_0", "libgcrypt-devel-1.12.2-ha1e9b39_0", ] - -[[tools."conda:lld"]] -version = "22.1.6" -backend = "conda:lld" - -[tools."conda:lld".options] -channel = "conda-forge" - -[tools."conda:lld"."platforms.linux-arm64"] -checksum = "sha256:90896d2e1d56044f054544f7be70f4cda3ce2c341596c7731b80345adc2423ed" -url = "https://conda.anaconda.org/conda-forge/linux-aarch64/lld-22.1.6-hddaf64f_0.conda" -conda_deps = [ - "libgcc-16.1.0-h205dda4_1", - "libstdcxx-16.1.0-hef695bb_1", - "libllvm22-22.1.8-hfd2ba90_1", - "libzlib-1.3.2-hdc9db2a_3", - "zstd-1.5.7-h9d15635_7", - "_openmp_mutex-4.5-20_gnu", - "libgomp-16.1.0-h8acb6b2_1", - "libxml2-2.15.3-h9ba8346_0", - "libxml2-16-2.15.3-h064b767_0", - "libiconv-1.18-h90929bb_2", - "liblzma-5.8.3-he30d5cf_1", -] - -[tools."conda:lld"."platforms.linux-x64"] -checksum = "sha256:9d70f486ee21c56f3ee8eea86e9edc92c12da4ea216d6ea88a2161092336e5bd" -url = "https://conda.anaconda.org/conda-forge/linux-64/lld-22.1.6-hef48ded_0.conda" -conda_deps = [ - "libzlib-1.3.2-h25fd6f3_3", - "libgcc-16.2.0-ha9f2e26_4", - "libstdcxx-16.2.0-h934c35e_4", - "libllvm22-22.1.8-h474f4eb_2", - "zstd-1.5.7-hb78ec9c_7", - "_openmp_mutex-4.5-20_gnu", - "libgomp-16.2.0-he0feb66_4", - "libxml2-2.15.3-h49c6c72_1", - "libxml2-16-2.15.3-hca6bf5a_1", - "liblzma-5.8.3-hb03c661_1", - "icu-78.3-py310h44b86e0_2", - "libiconv-1.18-h0cb94f2_3", -] - -[tools."conda:lld"."platforms.macos-arm64"] -checksum = "sha256:77efb8f120bc6b6f6a7e984ca0f0a6a6acd431d59577302596771f6cbd7f09d0" -url = "https://conda.anaconda.org/conda-forge/osx-arm64/lld-22.1.6-hdeb7e3d_0.conda" -conda_deps = [ - "libzlib-1.3.2-h8088a28_3", - "libcxx-22.1.8-h55c6f16_0", - "libllvm22-22.1.8-h89af1be_1", - "zstd-1.5.7-hf451053_7", - "libxml2-2.15.3-heed7d32_0", - "libxml2-16-2.15.3-h6967ea9_0", - "libiconv-1.18-h23cfdf5_2", - "liblzma-5.8.3-h8088a28_1", -] - -[tools."conda:lld"."platforms.macos-x64"] -checksum = "sha256:d74ea5241e4cd807072519f84e03754a6f6d26be9b66f2d8769a62c5b02bc5d7" -url = "https://conda.anaconda.org/conda-forge/osx-64/lld-22.1.6-hf72bb31_0.conda" -conda_deps = [ - "libzlib-1.3.2-hbb4bfdb_3", - "libcxx-22.1.8-h19cb2f5_0", - "libllvm22-22.1.8-hab754da_1", - "zstd-1.5.7-hbc1a06c_7", - "libxml2-2.15.3-h0712280_0", - "libxml2-16-2.15.3-h0d7f165_0", - "libiconv-1.18-h57a12c2_2", - "liblzma-5.8.3-hbb4bfdb_1", -] - -[tools."conda:lld"."platforms.windows-x64"] -checksum = "sha256:783dcde239d5280cfdf807f84c296c9094b82e74d2e23ea638dc7d7ac5c78243" -url = "https://conda.anaconda.org/conda-forge/win-64/lld-22.1.6-hc465015_0.conda" -conda_deps = [ - "libxml2-2.15.3-hbc0d294_0", - "libxml2-16-2.15.3-h692994f_0", - "libzlib-1.3.2-hfd05255_3", - "ucrt-10.0.26100.0-h57928b3_0", - "vc-14.5-ha367084_41", - "vc14_runtime-14.51.36247-habf1de7_41", - "vcomp14-14.51.36247-habf1de7_41", - "zstd-1.5.7-h534d264_7", - "libiconv-1.18-hc1393d2_2", - "liblzma-5.8.3-hfd05255_1", -] diff --git a/Cargo.lock b/Cargo.lock index 1982b1d6..63c691cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,7 +11,7 @@ dependencies = [ "actix-macros", "actix-rt", "actix_derive", - "bitflags 2.13.1", + "bitflags 2.13.2", "bytes", "crossbeam-channel", "futures-core", @@ -22,18 +22,18 @@ dependencies = [ "once_cell", "parking_lot", "pin-project-lite", - "smallvec 1.15.2", + "smallvec 1.16.1", "tokio", "tokio-util", ] [[package]] name = "actix-codec" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31404e1443b7b7bcaa311c1af456775cc3f668e34573f1503d7ce4844327629e" +checksum = "4c13df95297bcf9014dc89162b0cc69431e192e34e3b419612fc124cfcd45dbf" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "bytes", "futures-core", "futures-sink", @@ -46,15 +46,15 @@ dependencies = [ [[package]] name = "actix-files" -version = "0.6.10" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8c4f30e3272d7c345f88ae0aac3848507ef5ba871f9cc2a41c8085a0f0523b" +checksum = "ed87c765e9e5be096cc29b43d04b932209892ec871d05fe255f0ae1ccfed9a06" dependencies = [ "actix-http", "actix-service", "actix-utils", "actix-web", - "bitflags 2.13.1", + "bitflags 2.13.2", "bytes", "derive_more", "futures-core", @@ -69,17 +69,16 @@ dependencies = [ [[package]] name = "actix-http" -version = "3.13.3" +version = "3.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11004b0e9b44b4eb3d15e0c3132b96fb178c7e50a74758b2f17bb9cc9a7fb4f6" +checksum = "86d62d1a48894ec9450bcde7ef1e3681205771ff6ea9c61cc5ae031145192787" dependencies = [ "actix-codec", - "actix-rt", "actix-service", "actix-tls", "actix-utils", "base64 0.22.1", - "bitflags 2.13.1", + "bitflags 2.13.2", "brotli 8.0.4", "bytes", "bytestring", @@ -100,11 +99,11 @@ dependencies = [ "pin-project-lite", "rand 0.10.2", "sha1 0.11.0", - "smallvec 1.15.2", + "smallvec 1.16.1", "tokio", "tokio-util", "tracing", - "zstd", + "zstd 0.13.2", ] [[package]] @@ -134,9 +133,9 @@ dependencies = [ [[package]] name = "actix-rt" -version = "2.13.0" +version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a16bf2f19c2ad84842bdfe6f3665620e93197d5c607889bfa3aaac45e762fd1" +checksum = "e5f794807f82bbd36430c12cd600c73bbab0f52fdde4f0ed49978df113f4807f" dependencies = [ "actix-macros", "futures-core", @@ -145,13 +144,12 @@ dependencies = [ [[package]] name = "actix-server" -version = "2.9.1" +version = "2.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d44ae8a6516f4ac7bfc7b61aabcd286104e96b4b24c747ce220832a016056d9" +checksum = "aec21d555b23ee78a1c0e4ed61667935de7d61664caba878226a081b1ac20a74" dependencies = [ "actix-rt", "actix-service", - "actix-utils", "futures-core", "futures-util", "mio", @@ -172,15 +170,15 @@ dependencies = [ [[package]] name = "actix-tls" -version = "3.5.0" +version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6176099de3f58fbddac916a7f8c6db297e021d706e7a6b99947785fee14abe9f" +checksum = "21d0a5de50893252dfa9d7c4bf83761bcc1920651916cd9da24c974cf8ee1b92" dependencies = [ "actix-rt", "actix-service", "actix-utils", "futures-core", - "impl-more 0.1.9", + "impl-more", "pin-project-lite", "rustls-pki-types", "tokio", @@ -224,7 +222,7 @@ dependencies = [ "foldhash 0.2.0", "futures-core", "futures-util", - "impl-more 0.3.5", + "impl-more", "itoa", "language-tags", "log", @@ -236,7 +234,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "smallvec 1.15.2", + "smallvec 1.16.1", "socket2 0.6.5", "time", "tracing", @@ -664,9 +662,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.43" +version = "0.4.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3976abdc8fe7d1133d43d304afd42abdf5bc3e1319d263d223bde07b5efc4be8" +checksum = "ef217a77a86a6e3dab9a5b3c81dc445b603fe743a90c1cb10a2f2144628d8cfa" dependencies = [ "compression-codecs", "compression-core", @@ -710,7 +708,7 @@ checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] @@ -766,11 +764,11 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-lc-rs" -version = "1.18.0" +version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce2b2dcc879c3bae0d371e77c99f2238400ef24ec001394befa67b6e543add9e" +checksum = "b281d307588d634de920874890732659e2e7672f72b5e10e81badc1a8a83621e" dependencies = [ - "aws-lc-sys 0.44.0", + "aws-lc-sys 0.45.0", "untrusted 0.7.1", "zeroize", ] @@ -789,9 +787,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.44.0" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f09fae7be8bb3174e05c6afdb34199e6dc0c7c04ba9fa237b1967adfbde27483" +checksum = "9bff6c3b54fad79a2e60b8102caf565819711497c1f5f092f49508e2f5c31b27" dependencies = [ "cc", "cmake", @@ -938,7 +936,7 @@ version = "0.71.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "cexpr", "clang-sys", "itertools 0.13.0", @@ -958,7 +956,7 @@ version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "cexpr", "clang-sys", "itertools 0.13.0", @@ -1013,9 +1011,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.13.1" +version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" dependencies = [ "serde_core", ] @@ -1168,13 +1166,13 @@ dependencies = [ [[package]] name = "bytemuck_derive" -version = "1.12.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0e56a716f1e132ff6bf4bdac1c944a3fcdc1cae65f70a4a2a1ac3b401d2d1f" +checksum = "6a1f896587b6f2c069c73d2f0913e2d590c3990285cd2f0b6aa02b786b4c679c" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] @@ -1315,12 +1313,6 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "cast" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" - [[package]] name = "castaway" version = "0.2.4" @@ -1341,9 +1333,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.4.4" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273" +checksum = "a3eb0f42d6c360dc3f8a821f6bf2fdea7f72bfd36b3076eb0e6d1e9e0752fff4" dependencies = [ "find-msvc-tools", "jobserver", @@ -1430,9 +1422,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.6" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" +checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" dependencies = [ "clap_builder", "clap_derive", @@ -1449,9 +1441,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.6" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" +checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" dependencies = [ "anstream", "anstyle", @@ -1461,21 +1453,21 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.4" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" +checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0" dependencies = [ "heck", "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] name = "clap_lex" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486" [[package]] name = "clipboard-win" @@ -1589,23 +1581,33 @@ dependencies = [ [[package]] name = "compression-codecs" -version = "0.4.38" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf" +checksum = "257c7085cbb71be72d8fb97edff08b03d86d5d9f2222b9cc34a6bec87093bc10" dependencies = [ "brotli 8.0.4", "compression-core", "flate2", "memchr", - "zstd", - "zstd-safe", + "zstd 0.14.0", + "zstd-safe 8.0.0", ] [[package]] name = "compression-core" -version = "0.4.32" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e8ccc4ea9f6acc32d102c0f6d471d11d913ad15f20c04de743374861fa1d414" + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] [[package]] name = "const-hex" @@ -1832,7 +1834,7 @@ dependencies = [ "regalloc2 0.11.2", "rustc-hash 2.1.3", "serde", - "smallvec 1.15.2", + "smallvec 1.16.1", "target-lexicon", ] @@ -1861,8 +1863,8 @@ dependencies = [ "rustc-hash 2.1.3", "serde", "serde_derive", - "sha2", - "smallvec 1.15.2", + "sha2 0.10.9", + "smallvec 1.16.1", "target-lexicon", "wasmtime-internal-core", ] @@ -1949,7 +1951,7 @@ checksum = "9fd44e7e5dcea20ca104d45894748205c51365ce4cdb18f4418e3ba955971d1b" dependencies = [ "cranelift-codegen 0.117.2", "log", - "smallvec 1.15.2", + "smallvec 1.16.1", "target-lexicon", ] @@ -1962,7 +1964,7 @@ dependencies = [ "cranelift-codegen 0.134.4", "hashbrown 0.17.1", "log", - "smallvec 1.15.2", + "smallvec 1.16.1", "target-lexicon", ] @@ -2029,9 +2031,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.5.1" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" dependencies = [ "cfg-if", ] @@ -2044,18 +2046,18 @@ checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" [[package]] name = "crossbeam-channel" -version = "0.5.16" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" +checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-deque" -version = "0.8.7" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -2063,18 +2065,18 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.20" +version = "0.9.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.22" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" [[package]] name = "crossterm" @@ -2082,7 +2084,7 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "crossterm_winapi", "parking_lot", "rustix 0.38.44", @@ -2164,7 +2166,7 @@ dependencies = [ "dtoa-short", "itoa", "phf 0.13.1", - "smallvec 1.15.2", + "smallvec 1.16.1", ] [[package]] @@ -2400,7 +2402,7 @@ dependencies = [ "hyper-util", "log", "rusqlite", - "sha2", + "sha2 0.10.9", "slab", "thiserror 2.0.20", "tokio", @@ -2428,7 +2430,7 @@ dependencies = [ "parking_lot", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "sys_traits", "thiserror 2.0.20", "url", @@ -2507,7 +2509,7 @@ dependencies = [ "serde", "serde_json", "serde_v8", - "smallvec 1.15.2", + "smallvec 1.16.1", "sourcemap", "static_assertions", "sys_traits", @@ -2572,7 +2574,7 @@ dependencies = [ "serde", "serde_bytes", "sha1 0.10.7", - "sha2", + "sha2 0.10.9", "sha3", "signature 2.2.0", "spki 0.7.3", @@ -2760,7 +2762,7 @@ dependencies = [ "phf 0.11.3", "pin-project", "scopeguard", - "smallvec 1.15.2", + "smallvec 1.16.1", "thiserror 2.0.20", "tokio", "tokio-util", @@ -2961,7 +2963,7 @@ dependencies = [ "quinn", "rustls-tokio-stream", "serde", - "sha2", + "sha2 0.10.9", "socket2 0.5.10", "thiserror 2.0.20", "tokio", @@ -3016,7 +3018,7 @@ dependencies = [ "rustls-tokio-stream", "rustls-webpki", "serde", - "smallvec 1.15.2", + "smallvec 1.16.1", "socket2 0.5.10", "sys_traits", "thiserror 2.0.20", @@ -3025,7 +3027,7 @@ dependencies = [ "url", "webpki-root-certs 0.26.11", "windows-sys 0.59.0", - "zstd", + "zstd 0.13.2", ] [[package]] @@ -3079,7 +3081,7 @@ dependencies = [ "sec1 0.7.3", "serde", "sha1 0.10.7", - "sha2", + "sha2 0.10.9", "sha3", "sm3", "spki 0.7.3", @@ -3377,7 +3379,7 @@ dependencies = [ "rustyline", "same-file", "serde", - "sha2", + "sha2 0.10.9", "sys_traits", "thiserror 2.0.20", "tokio", @@ -3714,9 +3716,9 @@ dependencies = [ [[package]] name = "der" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69dedd701da44b0536442edf09c81a64b0ab97a7a4a5e3d1971f00027cbc63d" +checksum = "a878c850e9e421b20262e9b41f9c860e4785fa07541c266b62ff9d1ef998a80a" dependencies = [ "const-oid 0.10.2", "zeroize", @@ -3894,7 +3896,7 @@ dependencies = [ "diplomat_core", "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] @@ -3912,9 +3914,9 @@ dependencies = [ "proc-macro2", "quote", "serde", - "smallvec 1.15.2", + "smallvec 1.16.1", "strck", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] @@ -3944,7 +3946,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "objc2", ] @@ -3956,7 +3958,7 @@ checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] @@ -4039,7 +4041,7 @@ dependencies = [ "num-traits", "pkcs8 0.10.2", "rfc6979", - "sha2", + "sha2 0.10.9", "signature 2.2.0", "zeroize", ] @@ -4123,7 +4125,7 @@ dependencies = [ "ed25519", "rand_core 0.6.4", "serde", - "sha2", + "sha2 0.10.9", "signature 2.2.0", "subtle", "zeroize", @@ -4314,16 +4316,16 @@ dependencies = [ "fs-err", "k8s-openapi", "pretty_yaml", - "rand 0.9.5", - "rand_chacha 0.9.0", + "rand 0.10.2", + "rand_chacha 0.10.0", "serde", "serde_json", "serde_yaml", - "sha2", + "sha2 0.11.0", "tempfile", "textwrap", "thiserror 2.0.20", - "toml 0.8.23", + "toml 1.1.6+spec-1.1.0", ] [[package]] @@ -4355,7 +4357,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", - "syn 3.0.4", + "syn 3.0.5", "thiserror 2.0.20", "tree-sitter", "tree-sitter-zig", @@ -4498,7 +4500,7 @@ dependencies = [ "et-path", "fs-err", "minicov", - "wit-bindgen", + "wit-bindgen 0.62.0", ] [[package]] @@ -5114,13 +5116,13 @@ dependencies = [ [[package]] name = "fake" -version = "4.4.0" +version = "5.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2b0902eb36fbab51c14eda1c186bda119fcff91e5e4e7fc2dd2077298197ce8" +checksum = "ea6be833b323a56361118a747470a45a1bcd5c52a2ec9b1e40c83dafe687e453" dependencies = [ "deunicode", "either", - "rand 0.9.5", + "rand 0.10.2", ] [[package]] @@ -5253,9 +5255,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" +checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d" [[package]] name = "fips203" @@ -5276,7 +5278,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f5626bf5534df4ebdbd2536465d7eaa8a9dc2cdeb7e036e0ecf291dcc80ffb6" dependencies = [ "rand_core 0.6.4", - "sha2", + "sha2 0.10.9", "sha3", "zeroize", ] @@ -5340,7 +5342,7 @@ checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] @@ -5481,7 +5483,7 @@ checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] @@ -5525,7 +5527,7 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25234f20a3ec0a962a61770cfe39ecf03cb529a6e474ad8cff025ed497eda557" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "debugid", "rustc-hash 2.1.3", "serde", @@ -5708,7 +5710,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "gpu-descriptor-types", "hashbrown 0.15.5", ] @@ -5719,7 +5721,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", ] [[package]] @@ -5868,9 +5870,9 @@ dependencies = [ [[package]] name = "hashlink" -version = "0.12.1" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32069d97bb81e38fa67eab65e3393bf804bb85969f2bc06bf13f64aef5aba248" +checksum = "a596f1b20ed2cc5ecac41a164aaebc7258057060f06c0cf7a2ba3991ee7990fb" dependencies = [ "hashbrown 0.17.1", ] @@ -5907,9 +5909,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" [[package]] name = "hex" @@ -5965,7 +5967,7 @@ dependencies = [ "rand 0.9.5", "resolv-conf", "serde", - "smallvec 1.15.2", + "smallvec 1.16.1", "thiserror 2.0.20", "tokio", "tracing", @@ -6115,9 +6117,9 @@ dependencies = [ [[package]] name = "hybrid-array" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" dependencies = [ "subtle", "typenum", @@ -6141,7 +6143,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "smallvec 1.15.2", + "smallvec 1.16.1", "tokio", "want", ] @@ -6301,7 +6303,7 @@ dependencies = [ "icu_normalizer_data", "icu_properties", "icu_provider", - "smallvec 1.15.2", + "smallvec 1.16.1", "zerovec", ] @@ -6368,7 +6370,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", - "smallvec 1.15.2", + "smallvec 1.16.1", "utf8_iter", ] @@ -6444,15 +6446,9 @@ dependencies = [ [[package]] name = "impl-more" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" - -[[package]] -name = "impl-more" -version = "0.3.5" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "277ff51754a3f68f12f58446c5d006aa8baa4914ea273cce24a599cfaff33d4f" +checksum = "30c0cddce6b7505483307994f60d97c4b156020e1504d0cf1f05a21b1b325e64" [[package]] name = "import_map" @@ -6473,9 +6469,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.14.1" +version = "2.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07aa2048142242915a31d35844fb311e0e53fcca590c3a0a40dcf1b841fa09eb" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" dependencies = [ "equivalent", "hashbrown 0.17.1", @@ -6489,7 +6485,7 @@ version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cc00ea907cab49550b7da656f80ebb97be1b997d931fbcd28d39734e17ce592" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "inotify-sys", "libc", ] @@ -6566,9 +6562,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.12.1" +version = "2.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" [[package]] name = "is-macro" @@ -6649,9 +6645,9 @@ checksum = "4d3667095d64c3ecffc96463a21157b04bf3e252f6e8d5750b20c02e33c194e3" [[package]] name = "jiff" -version = "0.2.35" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc" +checksum = "0ab1baf72f08796de0260609515130699b890ac25f30e610ad894bc5856cafdb" dependencies = [ "defmt", "jiff-core", @@ -6663,18 +6659,19 @@ dependencies = [ [[package]] name = "jiff-core" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09" +checksum = "5e52fe76043ccecc9005d2305ebaadf7d7fc0cc89ca6baa10a94d6bc68c7128c" dependencies = [ "defmt", + "log", ] [[package]] name = "jiff-static" -version = "0.2.35" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204" +checksum = "378268a1116ad67ae6228701118ac9f491d78fda38a40a1f1a9e1348de6f7212" dependencies = [ "jiff-core", "proc-macro2", @@ -6752,9 +6749,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.104" +version = "0.3.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" dependencies = [ "cfg-if", "futures-util", @@ -6790,7 +6787,7 @@ dependencies = [ "ecdsa", "elliptic-curve 0.13.8", "once_cell", - "sha2", + "sha2 0.10.9", "signature 2.2.0", ] @@ -6882,7 +6879,7 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "libc", ] @@ -6983,9 +6980,9 @@ dependencies = [ [[package]] name = "libffi-sys" -version = "4.2.1" +version = "4.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54d39d034f5ea2662814789448722078d01cc9431a2a09ff15e55f0df7120bae" +checksum = "eab801a4ae9065bfea87f37204a7940523e752056e4a090eaaaab9b47fc826b5" dependencies = [ "cc", ] @@ -7029,14 +7026,14 @@ dependencies = [ [[package]] name = "libredox" -version = "0.1.21" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7955dfc218a8afb29dfeffd540e3a6e96baeb94fe7138228dd7cc6937fbbf96" +checksum = "6480ccc157a1389bb2e4891b24751b0f798ba640d22386f23143fbcc89da195a" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "libc", "plain", - "redox_syscall 0.9.3", + "redox_syscall 0.9.4", ] [[package]] @@ -7131,9 +7128,9 @@ checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" [[package]] name = "lru-slab" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f" [[package]] name = "mach2" @@ -7143,13 +7140,13 @@ checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b" [[package]] name = "macro-string" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59a9dbbfc75d2688ed057456ce8a3ee3f48d12eec09229f560f3643b9f275653" +checksum = "6e7b3a5bbb2f63aaa223a671ea1bd0e7bc16bd30ce387324cf63995deaddbbd2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.5", ] [[package]] @@ -7226,9 +7223,9 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "memfd" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227" +checksum = "57804b2c9b69967f1536a56f86297e367a33b19e98852ed624b84551cdbc0d90" dependencies = [ "rustix 1.1.4", ] @@ -7338,9 +7335,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.2" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" dependencies = [ "libc", "log", @@ -7364,7 +7361,7 @@ dependencies = [ "tokio", "tokio-stream", "tonic", - "tower-http 0.7.0", + "tower-http 0.7.1", ] [[package]] @@ -7379,7 +7376,7 @@ dependencies = [ "equivalent", "parking_lot", "portable-atomic", - "smallvec 1.15.2", + "smallvec 1.16.1", "tagptr", "uuid", ] @@ -7420,7 +7417,7 @@ checksum = "b2bf919621e7975acb27d881bae2fb993e0d45c8e0446e85e6272971e00dc8df" dependencies = [ "arrayvec", "bit-set 0.9.1", - "bitflags 2.13.1", + "bitflags 2.13.2", "cfg-if", "cfg_aliases", "codespan-reporting", @@ -7466,7 +7463,7 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22f9786d56d972959e1408b6a93be6af13b9c1392036c5c1fafa08a1b0c6ee87" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "byteorder", "derive_builder", "getset", @@ -7511,7 +7508,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" dependencies = [ - "smallvec 1.15.2", + "smallvec 1.16.1", ] [[package]] @@ -7520,7 +7517,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "cfg-if", "cfg_aliases", "libc", @@ -7533,7 +7530,7 @@ version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "cfg-if", "cfg_aliases", "libc", @@ -7606,7 +7603,7 @@ version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "fsevent-sys", "inotify", "kqueue", @@ -7624,7 +7621,7 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", ] [[package]] @@ -7661,7 +7658,7 @@ dependencies = [ "num-traits", "rand 0.8.5", "serde", - "smallvec 1.15.2", + "smallvec 1.16.1", "zeroize", ] @@ -7725,7 +7722,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "dispatch2", "objc2", ] @@ -7742,7 +7739,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "objc2", "objc2-core-foundation", ] @@ -7753,7 +7750,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "block2", "objc2", "objc2-foundation", @@ -7765,7 +7762,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "objc2", "objc2-core-foundation", "objc2-foundation", @@ -7883,21 +7880,15 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "onnx-extractor" -version = "0.4.5" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18c9fb03edc0726bae09c309a6f69aa24f8e50de2cd408620d9451276f2d556d" +checksum = "d95e8bba8b0d0d02e75f360841d276c677a2bdde3fab6e4579fe688ac37313e8" dependencies = [ "memmap2", "prost", "prost-build", ] -[[package]] -name = "oorandom" -version = "11.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" - [[package]] name = "opaque-debug" version = "0.3.1" @@ -8096,7 +8087,7 @@ dependencies = [ "ecdsa", "elliptic-curve 0.13.8", "primeorder", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -8108,7 +8099,7 @@ dependencies = [ "ecdsa", "elliptic-curve 0.13.8", "primeorder", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -8120,7 +8111,7 @@ dependencies = [ "ecdsa", "elliptic-curve 0.13.8", "primeorder", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -8134,7 +8125,7 @@ dependencies = [ "elliptic-curve 0.13.8", "primeorder", "rand_core 0.6.4", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -8171,7 +8162,7 @@ dependencies = [ "cfg-if", "libc", "redox_syscall 0.5.18", - "smallvec 1.15.2", + "smallvec 1.16.1", "windows-link 0.2.1", ] @@ -8407,7 +8398,7 @@ dependencies = [ "der 0.7.10", "pbkdf2", "scrypt", - "sha2", + "sha2 0.10.9", "spki 0.7.3", ] @@ -8429,7 +8420,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" dependencies = [ - "der 0.8.1", + "der 0.8.2", "spki 0.8.0", ] @@ -8451,7 +8442,7 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "crc32fast", "fdeflate", "flate2", @@ -8460,9 +8451,9 @@ dependencies = [ [[package]] name = "pollster" -version = "0.4.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" +checksum = "bc6355899e1c9462875b6757c79f3caa011a1fdae12bbb1a2e72dd1f234f8336" [[package]] name = "polyval" @@ -8579,7 +8570,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bfe0f4c752e450fc2faf62654f1c134747922825d5b04ca717b8874f41a40c0" dependencies = [ "proc-macro2", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] @@ -8597,7 +8588,7 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit 0.25.13+spec-1.1.0", + "toml_edit", ] [[package]] @@ -8629,9 +8620,9 @@ checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" [[package]] name = "progenitor" -version = "0.14.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8ba1d77160e6d5c95bdf0792527f76bf528791093fa83015bc2908a0ba9d076" +checksum = "b5e58f442cdad0db9db03ac94cc7b65938dccf1b282a56c440528a7da41ec947" dependencies = [ "progenitor-client", "progenitor-impl", @@ -8640,9 +8631,9 @@ dependencies = [ [[package]] name = "progenitor-client" -version = "0.14.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e8a874cf25a33cac7a01b9c1de87bcfbc8aea93f3156d09dcc3bee516a78926" +checksum = "53034c7e99439e366d243ed9847977d349243a862284d2e509a2abba7d003654" dependencies = [ "bytes", "futures-core", @@ -8655,9 +8646,9 @@ dependencies = [ [[package]] name = "progenitor-impl" -version = "0.14.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6e349eed84b9a1a6a5dbe478d335e3df73d32a93c5eefe571c9b8cb298aab5d" +checksum = "83aef4388cf237992f3f55aeead8417f45e9305fdbb77e97a551e8166629c692" dependencies = [ "heck", "http 1.5.0", @@ -8669,7 +8660,7 @@ dependencies = [ "schemars 0.8.22", "serde", "serde_json", - "syn 2.0.119", + "syn 3.0.5", "thiserror 2.0.20", "typify", "unicode-ident", @@ -8677,9 +8668,9 @@ dependencies = [ [[package]] name = "progenitor-macro" -version = "0.14.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efa969a1349979c5f64347f204e794781a86d738206d75672e6c9493f5910002" +checksum = "dafac71de913f41e24fa43c9460308f3b85f687bcbb03bac42c3dd2b0ae82bdd" dependencies = [ "openapiv3", "proc-macro2", @@ -8690,7 +8681,7 @@ dependencies = [ "serde_json", "serde_tokenstream", "serde_yaml", - "syn 2.0.119", + "syn 3.0.5", ] [[package]] @@ -8699,7 +8690,7 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "num-traits", "rand 0.9.5", "rand_chacha 0.9.0", @@ -8901,9 +8892,9 @@ dependencies = [ [[package]] name = "quinn" -version = "0.11.11" +version = "0.11.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +checksum = "4051e23e9185c255a7e33ef59cdbca87a22d359052eecd22fc6b901fb37d9d11" dependencies = [ "bytes", "cfg_aliases", @@ -8921,9 +8912,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.17" +version = "0.11.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04759210543be93709136e28212294a659ef5001836ff4eab4d663e4529bba83" +checksum = "a9746dbde176634f4f2f1faf2404e30a31b2bc1e9cafb5329c95d8177a18c9fc" dependencies = [ "aws-lc-rs", "bytes", @@ -9046,6 +9037,16 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rand_chacha" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e6af7f3e25ded52c41df4e0b1af2d047e45896c2f3281792ed68a1c243daedb" +dependencies = [ + "ppv-lite86", + "rand_core 0.10.1", +] + [[package]] name = "rand_core" version = "0.6.4" @@ -9161,16 +9162,16 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", ] [[package]] name = "redox_syscall" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d678d17679829e73d371e96880897e98fee2ded7acc0a50bdf8af2affa4b2fe5" +checksum = "737970939a87c6fa31e7acad13307bccbb017a073b695b6089a2c484f929e20e" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", ] [[package]] @@ -9201,7 +9202,7 @@ checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] @@ -9215,7 +9216,7 @@ dependencies = [ "hashbrown 0.15.5", "log", "rustc-hash 2.1.3", - "smallvec 1.15.2", + "smallvec 1.16.1", ] [[package]] @@ -9230,7 +9231,7 @@ dependencies = [ "log", "rustc-hash 2.1.3", "serde", - "smallvec 1.15.2", + "smallvec 1.16.1", ] [[package]] @@ -9270,11 +9271,10 @@ checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "regress" -version = "0.11.1" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "158a764437582235e3501f683b93a0a6f8d825d04a789dbe5ed30b8799b8908a" +checksum = "32eef8b209c3c1c15dbad02c1f30f9539f00dc7253e0cbcdaae442a50a09d7c1" dependencies = [ - "hashbrown 0.16.1", "memchr", ] @@ -9286,11 +9286,11 @@ checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" [[package]] name = "reqwest" -version = "0.13.4" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029" dependencies = [ - "base64 0.22.1", + "base64 0.23.1", "bytes", "futures-channel", "futures-core", @@ -9397,7 +9397,7 @@ version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81116b9531d61eabc41aeb228e4b6b2435bcca3233b98cf3b3077d4e6e9debb3" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "once_cell", "serde", "serde_derive", @@ -9462,9 +9462,9 @@ dependencies = [ [[package]] name = "rstest" -version = "0.26.1" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5a3193c063baaa2a95a33f03035c8a72b83d97a54916055ba22d35ed3839d49" +checksum = "948203e6d13b83e51a90d7cf236dd58a0065f23f4b902f00f306e7eb2bd09b9c" dependencies = [ "futures-timer", "futures-util", @@ -9473,9 +9473,9 @@ dependencies = [ [[package]] name = "rstest_macros" -version = "0.26.1" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c845311f0ff7951c5506121a9ad75aec44d083c31583b2ea5a30bcb0b0abba0" +checksum = "a8d92eaf7b6e51e12471d71ddc72608e7151573de44099aacfbb1128177c0da4" dependencies = [ "cfg-if", "glob", @@ -9495,12 +9495,12 @@ version = "0.40.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23f2a97da3e3873c73cb2a2e71b35c40ff95e0b1eefa8d72d8499a6928c3b5b3" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "fallible-iterator", "fallible-streaming-iterator", "hashlink", "libsqlite3-sys", - "smallvec 1.15.2", + "smallvec 1.16.1", "sqlite-wasm-rs", ] @@ -9546,7 +9546,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "errno", "libc", "linux-raw-sys 0.4.15", @@ -9559,7 +9559,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "errno", "libc", "linux-raw-sys 0.12.1", @@ -9699,7 +9699,7 @@ version = "17.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed34fbd08950d17f8297e738d5b76acd4baab50c8d45008d498b4327feb43ea1" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "cfg-if", "clipboard-win", "fd-lock", @@ -9778,12 +9778,10 @@ version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" dependencies = [ - "chrono", "dyn-clone", "schemars_derive 0.8.22", "serde", "serde_json", - "uuid", ] [[package]] @@ -9820,7 +9818,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals 0.30.0", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] @@ -9843,7 +9841,7 @@ checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" dependencies = [ "pbkdf2", "salsa20", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -9869,7 +9867,7 @@ checksum = "d56d437c2f19203ce5f7122e507831de96f3d2d4d3be5af44a0b0a09d8a80e4d" dependencies = [ "base16ct 1.0.0", "ctutils", - "der 0.8.1", + "der 0.8.2", "hybrid-array", "subtle", "zeroize", @@ -9891,7 +9889,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -9904,7 +9902,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -9964,7 +9962,7 @@ checksum = "7a024ee82abb97858516d2539fa845e6ef82ef57fdede6833ca24ed26425b614" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] @@ -10017,7 +10015,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] @@ -10039,7 +10037,7 @@ checksum = "f852137cce035d6a4df67ccce505ff6b3e9fd3a10e3e52b24dc71e650bb1a9bd" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] @@ -10080,15 +10078,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "serde_spanned" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" -dependencies = [ - "serde", -] - [[package]] name = "serde_spanned" version = "1.1.1" @@ -10100,14 +10089,14 @@ dependencies = [ [[package]] name = "serde_tokenstream" -version = "0.2.3" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c49585c52c01f13c5c2ebb333f14f6885d76daa768d8a037d28017ec538c69" +checksum = "db95b8acf6eca5bc1bc771a7d7fb1a0d1485bdef0c46eec2f573bbaf4b4118f8" dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.119", + "syn 3.0.5", ] [[package]] @@ -10131,7 +10120,7 @@ dependencies = [ "deno_error", "num-bigint", "serde", - "smallvec 1.15.2", + "smallvec 1.16.1", "thiserror 2.0.20", "v8", ] @@ -10202,6 +10191,17 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "digest 0.11.3", +] + [[package]] name = "sha3" version = "0.10.9" @@ -10350,9 +10350,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.15.2" +version = "1.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891" dependencies = [ "serde", ] @@ -10430,7 +10430,7 @@ version = "0.4.0+sdk-1.4.341.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9571ea910ebd84c86af4b3ed27f9dbdc6ad06f17c5f96146b2b671e2976744f" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", ] [[package]] @@ -10450,7 +10450,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" dependencies = [ "base64ct", - "der 0.8.1", + "der 0.8.2", ] [[package]] @@ -10679,7 +10679,7 @@ version = "18.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a573a0c72850dec8d4d8085f152d5778af35a2520c3093b242d2d1d50776da7c" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "is-macro", "num-bigint", "once_cell", @@ -10734,13 +10734,13 @@ version = "26.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e82f7747e052c6ff6e111fa4adeb14e33b46ee6e94fe5ef717601f651db48fc" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "either", "num-bigint", "rustc-hash 2.1.3", "seq-macro", "serde", - "smallvec 1.15.2", + "smallvec 1.16.1", "smartstring", "stacker", "swc_atoms", @@ -10771,7 +10771,7 @@ version = "27.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f1a51af1a92cd4904c073b293e491bbc0918400a45d58227b34c961dd6f52d7" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "either", "num-bigint", "phf 0.11.3", @@ -11022,9 +11022,9 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.4" +version = "3.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" +checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" dependencies = [ "proc-macro2", "quote", @@ -11208,9 +11208,9 @@ dependencies = [ [[package]] name = "textwrap" -version = "0.16.2" +version = "0.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" +checksum = "6ecfad6c3abc80a577f2b91c1e412ee57e7a060d430b553c1b0c940974ebcd49" [[package]] name = "thiserror" @@ -11249,7 +11249,7 @@ checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] @@ -11331,18 +11331,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" [[package]] name = "tokio" @@ -11379,7 +11370,7 @@ checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] @@ -11396,9 +11387,9 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.4" +version = "0.26.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67" dependencies = [ "rustls", "tokio", @@ -11429,9 +11420,9 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.24.0" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" +checksum = "17a073bfed563fa236697a068031408a93cd9522e08abf9933ead3e73411bd71" dependencies = [ "futures-util", "log", @@ -11470,18 +11461,6 @@ dependencies = [ "vsock", ] -[[package]] -name = "toml" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" -dependencies = [ - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.11", - "toml_edit 0.22.27", -] - [[package]] name = "toml" version = "0.9.12+spec-1.1.0" @@ -11490,7 +11469,7 @@ checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" dependencies = [ "indexmap", "serde_core", - "serde_spanned 1.1.1", + "serde_spanned", "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "toml_writer", @@ -11498,12 +11477,18 @@ dependencies = [ ] [[package]] -name = "toml_datetime" -version = "0.6.11" +name = "toml" +version = "1.1.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +checksum = "920602543f0911ab71da12c50d59701da54c196d1a2bf5cb4b75667f137a406a" dependencies = [ - "serde", + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.4", ] [[package]] @@ -11526,23 +11511,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" -dependencies = [ - "indexmap", - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.11", - "toml_write", - "winnow 0.7.15", -] - -[[package]] -name = "toml_edit" -version = "0.25.13+spec-1.1.0" +version = "0.25.15+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +checksum = "1340ea94a5856333492c9064b02c778b191dd2c853778d9609debdcdfea3a614" dependencies = [ "indexmap", "toml_datetime 1.1.1+spec-1.1.0", @@ -11559,12 +11530,6 @@ dependencies = [ "winnow 1.0.4", ] -[[package]] -name = "toml_write" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" - [[package]] name = "toml_writer" version = "1.1.2+spec-1.1.0" @@ -11599,7 +11564,7 @@ dependencies = [ "tower-layer", "tower-service", "tracing", - "zstd", + "zstd 0.13.2", ] [[package]] @@ -11649,7 +11614,7 @@ version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "bytes", "futures-util", "http 1.5.0", @@ -11663,12 +11628,12 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b11f75e912b0c2be01b63d8cf8057b8c3f97cf34abb3d431a3a4c8675498e233" +checksum = "08a05a66a4fdd61cbbe0a1d755ffe0ca6aba159dd4820936a0ff8a8278245b9c" dependencies = [ "async-compression", - "bitflags 2.13.1", + "bitflags 2.13.2", "bytes", "futures-core", "http 1.5.0", @@ -11761,7 +11726,7 @@ checksum = "adbc64cba7137545b8044cb1fe9814f7aacf3c6b5f9b45be8bb5db538befdb26" dependencies = [ "js-sys", "opentelemetry", - "smallvec 1.15.2", + "smallvec 1.16.1", "tracing", "tracing-core", "tracing-log", @@ -11780,7 +11745,7 @@ dependencies = [ "once_cell", "regex-automata", "sharded-slab", - "smallvec 1.15.2", + "smallvec 1.16.1", "thread_local", "tracing", "tracing-core", @@ -11800,13 +11765,12 @@ dependencies = [ [[package]] name = "tree-sitter" -version = "0.26.13" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17ebdd3a5a7e28a1890b876fdbd0c3c0fe0a6336cffaa104f11b9f720c9daa29" +checksum = "2038684e0058edba0d17302619f62eabce4a8e11c6ac59506996a8d79848851d" dependencies = [ "cc", "regex", - "regex-syntax", "serde_json", "streaming-iterator", "tree-sitter-language", @@ -11846,20 +11810,18 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "tungstenite" -version = "0.24.0" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" +checksum = "e48ac77174b19c110a50ab2128b24215ac9cb40e0e12e093fb602d175c569d22" dependencies = [ - "byteorder", "bytes", "data-encoding", "http 1.5.0", "httparse", "log", - "rand 0.8.5", - "sha1 0.10.7", - "thiserror 1.0.69", - "utf-8", + "rand 0.10.2", + "sha1 0.11.0", + "thiserror 2.0.20", ] [[package]] @@ -11882,9 +11844,9 @@ checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "typify" -version = "0.6.2" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc0b89f47309feaeb23c4509c15c9a04234f7deccef6f96c3bfe95319819a304" +checksum = "71187d2e983363e9a6ca884f7b061620bc6da4316902058e74bbc9d61c347696" dependencies = [ "typify-impl", "typify-macro", @@ -11892,9 +11854,9 @@ dependencies = [ [[package]] name = "typify-impl" -version = "0.6.2" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7b026f540b148b81043c720889dbb942b08659aa8a43f624ac4f04dbfc1861" +checksum = "1cbc3132686085f84a068ad86fc281cb3d566dd940c8911fd3562013851e9ae8" dependencies = [ "heck", "log", @@ -11905,16 +11867,16 @@ dependencies = [ "semver", "serde", "serde_json", - "syn 2.0.119", + "syn 3.0.5", "thiserror 2.0.20", "unicode-ident", ] [[package]] name = "typify-macro" -version = "0.6.2" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39ed96c57f06ae0839416b986921a98f18b220da63bbb243a8570a00c8492183" +checksum = "b3a7f8d08ec85254fb8b383a6c66fe43b81f7f6b288fa71eb365f0867fc8f3a6" dependencies = [ "proc-macro2", "quote", @@ -11923,7 +11885,7 @@ dependencies = [ "serde", "serde_json", "serde_tokenstream", - "syn 2.0.119", + "syn 3.0.5", "typify-impl", ] @@ -12094,9 +12056,9 @@ dependencies = [ [[package]] name = "uuid" -version = "1.26.0" +version = "1.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812" +checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" dependencies = [ "getrandom 0.4.3", "js-sys", @@ -12111,7 +12073,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebfd8d6919bfb4b627ca778a67f85ca45e5fb442d352947ba093798bdf9b07e0" dependencies = [ "bindgen 0.72.1", - "bitflags 2.13.1", + "bitflags 2.13.2", "fslock", "gzip-header", "home", @@ -12127,7 +12089,7 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e05741b8524f73cbf239bea12239458d3a835246c5c637cc9e7e601eac60770" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "encoding_rs", "indexmap", "num-bigint", @@ -12136,11 +12098,20 @@ dependencies = [ "wtf8", ] +[[package]] +name = "v_escape-base" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1212fce830b75af194b578e55b3db9049f2c8c45f58d397fb25602fdb50fb3d" + [[package]] name = "v_htmlescape" -version = "0.15.8" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e8257fbc510f0a46eb602c10215901938b5c2a7d5e70fc11483b1d3c9b5b18c" +checksum = "befb3d53c9e3ec641417685896cbc8cc5bd264d6a2e190c56aaef1af24740d99" +dependencies = [ + "v_escape-base", +] [[package]] name = "valuable" @@ -12237,7 +12208,7 @@ version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.57.1", ] [[package]] @@ -12248,9 +12219,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.127" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" dependencies = [ "cfg-if", "once_cell", @@ -12261,9 +12232,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.77" +version = "0.4.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950" +checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928" dependencies = [ "js-sys", "wasm-bindgen", @@ -12271,9 +12242,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.127" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -12281,65 +12252,52 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.127" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.5", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.127" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" dependencies = [ "unicode-ident", ] [[package]] name = "wasm-bindgen-test" -version = "0.3.77" +version = "0.3.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "895a2607575412a4eda1df892084a375ea10dfeadc4d7d2ab87b854e4ddc7ba1" +checksum = "d381749acb0943d357dcbd8f0b100640679883fcdeeef04def49daf8d33a5426" dependencies = [ - "async-trait", - "cast", + "console_error_panic_hook", "js-sys", - "libm", "minicov", - "nu-ansi-term", - "num-traits", - "oorandom", - "serde", - "serde_json", + "scoped-tls", "wasm-bindgen", "wasm-bindgen-futures", "wasm-bindgen-test-macro", - "wasm-bindgen-test-shared", ] [[package]] name = "wasm-bindgen-test-macro" -version = "0.3.77" +version = "0.3.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288cb0ebe215033bf949ae1fd046726daa4c32a157f24b9dc6ac387a52aa759" +checksum = "c97b2ef2c8d627381e51c071c2ab328eac606d3f69dd82bcbca20a9e389d95f0" dependencies = [ "proc-macro2", "quote", "syn 2.0.119", ] -[[package]] -name = "wasm-bindgen-test-shared" -version = "0.2.127" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ff1c1b360982e93b6d8ea9c04836f71dba0817a16f91e229cf3a51bdd9d987" - [[package]] name = "wasm-compose" version = "0.252.0" @@ -12351,22 +12309,12 @@ dependencies = [ "indexmap", "log", "petgraph 0.6.5", - "smallvec 1.15.2", + "smallvec 1.16.1", "wasm-encoder 0.252.0", "wasmparser 0.252.0", "wat", ] -[[package]] -name = "wasm-encoder" -version = "0.247.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b6733b8b91d010a6ac5b0fb237dc46a19650bc4c67db66857e2e787d437204" -dependencies = [ - "leb128fmt", - "wasmparser 0.247.0", -] - [[package]] name = "wasm-encoder" version = "0.252.0" @@ -12379,24 +12327,24 @@ dependencies = [ [[package]] name = "wasm-encoder" -version = "0.258.0" +version = "0.259.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e974fe6821a8cf64575d51ea2194e2c8f77e7b66e9afe7419ce8a97f9ee0d251" +checksum = "b1d0246511d901aacf25d2dc9111f0054947de5e093fe662099e709ff7530dc3" dependencies = [ "leb128fmt", - "wasmparser 0.258.0", + "wasmparser 0.259.0", ] [[package]] name = "wasm-metadata" -version = "0.247.0" +version = "0.259.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "665fe59e56cc9b419ca6fcca56673e3421d1a5011e3b65caf6b726fd9e041d10" +checksum = "4e7bf119eb01f4a246864c10bd87c8f26c566350abb25aa4d5273c415bbab338" dependencies = [ "anyhow", "indexmap", - "wasm-encoder 0.247.0", - "wasmparser 0.247.0", + "wasm-encoder 0.259.0", + "wasmparser 0.259.0", ] [[package]] @@ -12422,25 +12370,13 @@ dependencies = [ "thiserror 2.0.20", ] -[[package]] -name = "wasmparser" -version = "0.247.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e6fb4c2bee46c5ea4d40f8cdb5c131725cd976718ec56f1c8e82fbde5fa2a80" -dependencies = [ - "bitflags 2.13.1", - "hashbrown 0.17.1", - "indexmap", - "semver", -] - [[package]] name = "wasmparser" version = "0.252.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3eb099dcadcde5be9eef55e3a337128efd4e44b4c93122487e4d2e4e1c6627c" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "hashbrown 0.17.1", "indexmap", "semver", @@ -12449,11 +12385,12 @@ dependencies = [ [[package]] name = "wasmparser" -version = "0.258.0" +version = "0.259.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9a61719f93a87b16d325921e251800c4833f8fab50fa21c7de73aed50086313" +checksum = "0f7c12eac7bb587801590f6a67ff0bc84d0748513864b71108e4b311cc3df694" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", + "hashbrown 0.17.1", "indexmap", "semver", ] @@ -12477,7 +12414,7 @@ checksum = "4cdaf5b8af5713146e3a62d940c71e3eb2ad1ebacfa6b55f0c8509a5b4b87718" dependencies = [ "addr2line", "async-trait", - "bitflags 2.13.1", + "bitflags 2.13.2", "bumpalo", "cc", "cfg-if", @@ -12500,7 +12437,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "smallvec 1.15.2", + "smallvec 1.16.1", "target-lexicon", "tempfile", "wasm-compose", @@ -12543,8 +12480,8 @@ dependencies = [ "semver", "serde", "serde_derive", - "sha2", - "smallvec 1.15.2", + "sha2 0.10.9", + "smallvec 1.16.1", "target-lexicon", "wasm-encoder 0.252.0", "wasmparser 0.252.0", @@ -12566,11 +12503,11 @@ dependencies = [ "rustix 1.1.4", "serde", "serde_derive", - "sha2", + "sha2 0.10.9", "toml 0.9.12+spec-1.1.0", "wasmtime-environ", "windows-sys 0.61.2", - "zstd", + "zstd 0.13.2", ] [[package]] @@ -12623,7 +12560,7 @@ dependencies = [ "log", "object 0.39.1", "pulley-interpreter", - "smallvec 1.15.2", + "smallvec 1.16.1", "target-lexicon", "thiserror 2.0.20", "wasmparser 0.252.0", @@ -12703,7 +12640,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfdbdd9d6fab1ecb67a7dc189e32c9e341444945fb709437bf8aab25d9a88459" dependencies = [ "anyhow", - "bitflags 2.13.1", + "bitflags 2.13.2", "heck", "indexmap", "wit-parser 0.252.0", @@ -12716,7 +12653,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b373095a6dc8382da56882ad16d7ea16771dd6927dd480b1ce3fe7c4b816d2cf" dependencies = [ "async-trait", - "bitflags 2.13.1", + "bitflags 2.13.2", "bytes", "cap-fs-ext", "cap-std", @@ -12773,24 +12710,24 @@ dependencies = [ [[package]] name = "wast" -version = "258.0.0" +version = "259.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97f7defc7ecca8b19ac7f824598eadd0c53985ee00c74060d65051e9da5b58a1" +checksum = "c69beba8d9da07af9a0971b559149a2ac4729895b55144de4fddbf5a02660648" dependencies = [ "bumpalo", "leb128fmt", "memchr", "unicode-width 0.2.2", - "wasm-encoder 0.258.0", + "wasm-encoder 0.259.0", ] [[package]] name = "wat" -version = "1.258.0" +version = "1.259.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7555c008cca87f2ac58d9f83ccda7e7b44611093ce28eb28f052e7c78024b9bf" +checksum = "c6eec44b0c80391b20fb7ad9ea440c72c5d382385bdf74444bb18321c425017e" dependencies = [ - "wast 258.0.0", + "wast 259.0.0", ] [[package]] @@ -12807,9 +12744,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.104" +version = "0.3.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30" +checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8" dependencies = [ "js-sys", "wasm-bindgen", @@ -12888,7 +12825,7 @@ dependencies = [ "arrayvec", "bit-set 0.9.1", "bit-vec 0.9.1", - "bitflags 2.13.1", + "bitflags 2.13.2", "bytemuck", "cfg_aliases", "document-features", @@ -12904,7 +12841,7 @@ dependencies = [ "ron", "rustc-hash 1.1.0", "serde", - "smallvec 1.15.2", + "smallvec 1.16.1", "thiserror 2.0.20", "wgpu-core-deps-apple", "wgpu-core-deps-emscripten", @@ -12951,7 +12888,7 @@ dependencies = [ "arrayvec", "ash", "bit-set 0.9.1", - "bitflags 2.13.1", + "bitflags 2.13.2", "block2", "bytemuck", "cfg-if", @@ -12980,7 +12917,7 @@ dependencies = [ "range-alloc", "raw-window-handle", "raw-window-metal", - "smallvec 1.15.2", + "smallvec 1.16.1", "thiserror 2.0.20", "wasm-bindgen", "wayland-sys", @@ -13008,7 +12945,7 @@ version = "29.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec2675540fb1a5cfa5ef122d3d5f390e2c75711a0b946410f2d6ac3a0f77d1f6" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "bytemuck", "js-sys", "log", @@ -13068,7 +13005,7 @@ version = "47.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c08bd1d66a10c7d4deaeb746cb1c5b5c62fb631907f18dfe058ecba71c99002" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "thiserror 2.0.20", "tracing", "wasmtime", @@ -13547,7 +13484,7 @@ version = "0.36.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "windows-sys 0.59.0", ] @@ -13556,33 +13493,39 @@ name = "wit-bindgen" version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen" +version = "0.62.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53cb4b5556c3a791e86838ea287782bdafa704d55b0e68b5b81a3a16b9ea5f4b" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "wit-bindgen-rust-macro", ] [[package]] name = "wit-bindgen-core" -version = "0.57.1" +version = "0.62.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02dee27a2dc20d1008016c742ec9fc6ea498492994ba3750be7454cbc97ff04c" +checksum = "72ad22a37ecbc0e1fdca34d2b670ba41368cb0920735643e00fdf39a16ee5431" dependencies = [ "anyhow", "heck", - "wit-parser 0.247.0", + "wit-parser 0.259.0", ] [[package]] name = "wit-bindgen-rust" -version = "0.57.1" +version = "0.62.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5007dae772945b7a5003d69d90a3a4a78929d41f19d004e980c4259a6af4484" +checksum = "84ba5213d4332c260a0d4e69e34b0f7e4b85ddae10b6ebfbac00d9c185ba628a" dependencies = [ "anyhow", "heck", "indexmap", - "prettyplease 0.2.37", - "syn 2.0.119", + "prettyplease 0.3.0", + "syn 3.0.5", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -13590,37 +13533,37 @@ dependencies = [ [[package]] name = "wit-bindgen-rust-macro" -version = "0.57.1" +version = "0.62.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af9237d678e3513ad24e96fe98beacdc0db6405284ba2a2400418cf0d42caa89" +checksum = "8f19d63da8e478a370ef8192d90ccb517965b7747d6b46cd2d831dd0ef4d2541" dependencies = [ "anyhow", "macro-string", - "prettyplease 0.2.37", + "prettyplease 0.3.0", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.5", "wit-bindgen-core", "wit-bindgen-rust", ] [[package]] name = "wit-component" -version = "0.247.0" +version = "0.259.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d567162a6b9843080e5e0053f696623ff694bae8ae017c9ec536d1873bbe3d8" +checksum = "092f783dee3253265fcde131475055f8c275ac0176a9c0396db1dc7b5544bfb4" dependencies = [ "anyhow", - "bitflags 2.13.1", + "bitflags 2.13.2", "indexmap", "log", "serde", "serde_derive", "serde_json", - "wasm-encoder 0.247.0", + "wasm-encoder 0.259.0", "wasm-metadata", - "wasmparser 0.247.0", - "wit-parser 0.247.0", + "wasmparser 0.259.0", + "wit-parser 0.259.0", ] [[package]] @@ -13638,9 +13581,9 @@ dependencies = [ [[package]] name = "wit-parser" -version = "0.247.0" +version = "0.252.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ffe4064318cdf3c08cb99343b44c039fcefe61ccdf58aa9975285f13d74d1fc" +checksum = "4266bea110371c620ccf3201c5023676046bc4556e5c7cfb5d500bda5ebc162d" dependencies = [ "anyhow", "hashbrown 0.17.1", @@ -13651,15 +13594,15 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "unicode-xid", - "wasmparser 0.247.0", + "unicode-ident", + "wasmparser 0.252.0", ] [[package]] name = "wit-parser" -version = "0.252.0" +version = "0.259.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4266bea110371c620ccf3201c5023676046bc4556e5c7cfb5d500bda5ebc162d" +checksum = "6390f4f02493ce678d509c859cf1698e38929fb75c8012a6ee2f7d6b6cb66cd7" dependencies = [ "anyhow", "hashbrown 0.17.1", @@ -13671,7 +13614,7 @@ dependencies = [ "serde_derive", "serde_json", "unicode-ident", - "wasmparser 0.252.0", + "wasmparser 0.259.0", ] [[package]] @@ -13851,18 +13794,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.56" +version = "0.8.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" +checksum = "d35102a9f36d089ccae9e4c6802bc118be4487b80aaffc0ab4e0cf5ce92d2873" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.56" +version = "0.8.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" +checksum = "146c01f5ab44258da43cf276c74a2763db2ff3969c9c652c3f2de07041d0b2bc" dependencies = [ "proc-macro2", "quote", @@ -13942,7 +13885,7 @@ checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] @@ -13976,23 +13919,41 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" dependencies = [ - "zstd-safe", + "zstd-safe 7.3.0", +] + +[[package]] +name = "zstd" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf06bd8162af0734b344780deb55b42a2429ae430870d13fcc12f238e880fe6e" +dependencies = [ + "zstd-safe 8.0.0", ] [[package]] name = "zstd-safe" -version = "7.2.4" +version = "7.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64d80649ab6db9d9f6f9c80a40becd948eda4714a0a5ac8c4d157a32231c7882" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-safe" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +checksum = "ae42c0555055784c70058d19ba8e275528e8a99a706684868ace5da4e716a4ab" dependencies = [ "zstd-sys", ] [[package]] name = "zstd-sys" -version = "2.0.16+zstd.1.5.7" +version = "2.1.0+zstd.1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +checksum = "0ef0a8027ec3ee71300ab3bcbcd0393f434aa72b91ca6d635a39941deae8eea0" dependencies = [ "cc", "pkg-config", diff --git a/Cargo.toml b/Cargo.toml index da18ee3e..15e38426 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,7 +55,7 @@ resolver = "2" [workspace.dependencies] actix = "0.13" -actix-files = "0.6" +actix-files = "0.7" actix-rt = "2" actix-web = { version = "4", features = ["rustls-0_23"] } actix-web-thiserror = "0.2" @@ -74,17 +74,17 @@ bytesize = { version = "2", features = ["serde"] } cc = "1" chrono = { version = "0.4", features = ["serde"] } clap = { version = "4.4", features = ["derive"] } -clap-markdown = "0.1.5" +clap-markdown = "0.1" command-error = "0.8" const-hex = "1.19" -deno_core = "0.407.0" +deno_core = "0.407" deno_error = "=0.7.1" -deno_resolver = "0.85.0" +deno_resolver = "0.85" # `transpile` enables on-the-fly TS transpilation (deno_node ships its own TS). # `hmr` makes `op_snapshot_options` use `state.try_take(...).unwrap_or_default()` # instead of panicking when no SnapshotOptions struct is in OpState -- without # baking our own startup snapshot we'd otherwise hit the panic on first run. -deno_runtime = { version = "0.262.0", features = ["transpile", "hmr"] } +deno_runtime = { version = "0.262", features = ["transpile", "hmr"] } edge-toolkit = { path = "libs/edge-toolkit", version = "0.2.0" } et-modules-service = { path = "services/modules", version = "0.1.0" } et-otlp = { path = "libs/et-otlp", version = "0.1.0" } @@ -101,7 +101,7 @@ et-ws-server = { path = "services/ws-server", version = "0.1.0" } et-ws-service = { path = "services/ws", version = "0.1.0" } et-ws-test-server = { path = "services/ws-test-server", version = "0.1.0" } et-ws-wasm-agent = { path = "services/ws-wasm-agent", version = "0.1.0" } -fake = "4" +fake = "5" fs-err = "3" futures-core = "0.3" futures-util = "0.3" @@ -133,7 +133,7 @@ minicov = "0.3" # Serves the spec paths (/v1/traces, ...), so a consumer points `collector_url` at `http://host:port/v1` # for `et-otlp`'s `{collector_url}/traces` shape to land on them. mock-collector = "0.2" -onnx-extractor = "0.4" +onnx-extractor = "0.5" # Storage backend abstraction: local disk by default, any object_store backend via a URL. # `aws` pulls the S3 client (which also covers S3-compatible servers such as the rustfs mise tool the backend # test runs against). Only `aws` is enabled -- adding `gcp`/`azure`/`http` is a one-word change if a consumer @@ -160,12 +160,12 @@ opentelemetry-proto = { version = "0.32", default-features = false, features = [ ] } opentelemetry_sdk = "0.32" ort = { version = "=2.0.0-rc.10", default-features = false, features = ["copy-dylibs"] } -pollster = "0.4" +pollster = "1.0" port_check = "0.3" pretty_yaml = "0.6" prettyplease = "0.3" -progenitor = "0.14" -progenitor-client = "0.14" +progenitor = "0.15" +progenitor-client = "0.15" prost = "0.14" # `auto-initialize` starts the embedded CPython interpreter on first use. # The pyo3 runner never calls `Py_Initialize` itself. @@ -176,23 +176,23 @@ qr2term = "0.3" quote = "1" # Kept in lockstep with whichever rand major `fake` builds against. # The generated deployments seed a fake-driven RNG, so the two crates have to agree on the `Rng` trait. -rand = "0.9" +rand = "0.10" # Reproducibility is the whole point of this dependency, not randomness quality. # ChaCha is the one rand generator whose output is guaranteed stable across releases for a given seed, so a # regenerated deployment keeps the credentials it had. `rand`'s own SmallRng/StdRng explicitly do not promise # that, and StepRng (the obvious "just count up" answer) is deprecated without replacement in rand 0.9. -rand_chacha = "0.9" +rand_chacha = "0.10" rcgen = "0.14" regex = { version = "1.12", default-features = false } reqwest = { version = "0.13", default-features = false } retry = { version = "2", default-features = false } retry-policies = "0.5" -rstest = "0.26" +rstest = "0.27" rustls = "0.23" -schemars = { version = "1.1", features = ["derive"] } -secrecy = { version = "0.10.3", features = ["serde"] } +schemars = { version = "1.2", features = ["derive"] } +secrecy = { version = "0.10", features = ["serde"] } semver = "1" -serde = { version = "1.0.228", features = ["derive"] } +serde = { version = "1.0", features = ["derive"] } serde-env = "0.3" serde-inline-default = "1.0" serde-wasm-bindgen = "0.6" @@ -201,18 +201,18 @@ serde_json = "1" serde_path_to_error = "0.1" serde_urlencoded = "0.7" serde_yaml = "0.9" -sha2 = { version = "0.10", default-features = false } +sha2 = { version = "0.11", default-features = false } strum = { version = "0.28", features = ["derive"] } syn = "3" -sys_traits = { version = "0.1.28", features = ["libc", "real"] } +sys_traits = { version = "0.1", features = ["libc", "real"] } temp-env = "0.3" tempfile = "3" testing_logger = "0.1" textwrap = { version = "0.16", default-features = false } thiserror = "2" tokio = "1" -tokio-tungstenite = { version = "0.24", default-features = false } -toml = "0.8" +tokio-tungstenite = { version = "0.30", default-features = false } +toml = "1.1" tracing = "0.1" tracing-actix-web = { version = "0.7", default-features = false, features = [ "emit_event_on_error", @@ -223,14 +223,14 @@ tracing-log = "0.2" tracing-opentelemetry = "0.33" tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-wasm = "0.2" -tree-sitter = "0.26" +tree-sitter = "0.27" tree-sitter-zig = "1" # Already resolved transitively (object_store, reqwest, ...), so this adds no new crate or license to review. # object_store's parse_url_opts takes a `&Url` and does not re-export the type, so a backend-URL config needs it. url = "2" utoipa = { version = "5", features = ["actix_extras", "yaml"] } uuid = { version = "1", features = ["serde", "v4", "v7"] } -wasi-webgpu-wasmtime = "0.2.0" +wasi-webgpu-wasmtime = "0.2" wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" wasm-bindgen-test = "0.3" @@ -253,8 +253,8 @@ web-sys = "0.3" wgpu-core = { version = "29", default-features = false } wgpu-types = "29" # Windows-only feature-unification shim: forces `winapi/std` on for deno_io. -winapi = { version = "0.3.9", features = ["std"] } -wit-bindgen = "0.57" +winapi = { version = "0.3", features = ["std"] } +wit-bindgen = "0.62" wit-encoder = "0.252" wit-parser = "0.252" diff --git a/config/conftest/policy/cargo/cargo.rego b/config/conftest/policy/cargo/cargo.rego index 4913ca72..db2c036f 100644 --- a/config/conftest/policy/cargo/cargo.rego +++ b/config/conftest/policy/cargo/cargo.rego @@ -221,6 +221,65 @@ deny contains msg if { msg := sprintf("Cargo.toml: %q pins version %q but that crate declares %q", [name, spec.version, member_version[name]]) } +# Every requirement in the root [workspace.dependencies] names a major and a minor, and stops there. +# A third component is a claim that one specific release is required. Left on by default it claims nothing -- +# it is only whatever happened to be current the day the dep was added -- and once most entries carry one, a +# reader can no longer tell the load-bearing pins from the incidental ones, while every routine bump has to +# rewrite a digit that never meant anything. A dep that genuinely needs the patch component says so below, +# in a line that has to name the release and the reason it cannot move. +# +# Path deps are exempt by construction rather than by entry: cargo wants their `version` to equal the member +# crate's own `package.version` -- the rule above enforces exactly that -- which is always a full triple. +patch_pin_exception := { + "deno_error": "exact pin: JsErrorBox must be the type deno_core holds, so a 0.7.2 would be a second copy", + "ort": "exact pin on a prerelease, which has no two-part form; rc.11+ moved API wasmtime-wasi-nn calls", + "wasmtime": "47.0.3 is the security floor; RUSTSEC-2026-0222 has no fix anywhere below it in the 47 line", + "wasmtime-internal-wit-bindgen": "47.0.4 tracks the wasmtime release; the crate is internal and its API can move", + "wasmtime-wasi": "47.0.3 carries the same RUSTSEC-2026-0222 floor as the wasmtime entry it ships beside", +} + +# Matches the requirement's leading version token rather than splitting the whole string on dots. +# That keeps a comparator (`=`, `>=`, `~`) and a prerelease suffix (`-rc.10`, whose dot is its own) out of the +# component count, so only the version core decides. +patch_pinned(req) if regex.match(`^[^0-9]*[0-9]+\.[0-9]+\.[0-9]+`, req) + +requirement(spec) := spec if is_string(spec) + +requirement(spec) := spec.version if is_object(spec) + +path_dep(spec) if { + is_object(spec) + spec.path +} + +root_dep[name] := spec if { + some file in input + file.path == "Cargo.toml" + some name, spec in file.contents.workspace.dependencies +} + +deny contains msg if { + some name, spec in root_dep + not path_dep(spec) + not patch_pin_exception[name] + patch_pinned(requirement(spec)) + msg := sprintf( + "Cargo.toml: %q pins %q to a patch version; use major.minor, or add a reasoned patch_pin_exception", + [name, requirement(spec)], + ) +} + +# The exception map is a two-way contract. +# An entry that no longer describes the manifest misleads exactly as much as a missing one would, so a dep that +# has since been trimmed back to major.minor, or dropped altogether, takes its exception entry with it. +exception_is_live(name) if patch_pinned(requirement(root_dep[name])) + +deny contains msg if { + some name, reason in patch_pin_exception + not exception_is_live(name) + msg := sprintf("Cargo.toml: patch_pin_exception entry %q is stale (%s); remove it", [name, reason]) +} + # crates.io rejects an upload whose manifest carries no description, so every publishable crate has one. # cargo only warns locally, which means a missing description fails at upload time -- partway through a # workspace release, leaving some crates published at the new version and the rest behind. diff --git a/config/conftest/policy/cargo/cargo_test.rego b/config/conftest/policy/cargo/cargo_test.rego new file mode 100644 index 00000000..98daeb8e --- /dev/null +++ b/config/conftest/policy/cargo/cargo_test.rego @@ -0,0 +1,98 @@ +# Unit tests for the patch-pin rule, run by `conftest verify`. +# +# They exist because that rule turns entirely on the form of a version requirement, and most of the forms that +# decide it are ones the manifest does not hold: a bare `1.2.3`, a comparator in front of one, a prerelease tail +# whose own dots must not be counted, a path dep's mandatory full triple. Running the real task over the real +# manifest reaches at most one of those at a time, and only for as long as that dep survives a bump, so they are +# fed in synthetically here instead. +package cargo_test + +import data.cargo + +# A root manifest carrying nothing but the dependency table under test. +manifest(deps) := [{"path": "Cargo.toml", "contents": {"workspace": {"dependencies": deps}}}] + +flagged(msgs, name) if { + some msg in msgs + contains(msg, sprintf("%q", [name])) + contains(msg, "to a patch version") +} + +stale(msgs, name) if { + some msg in msgs + contains(msg, sprintf("patch_pin_exception entry %q is stale", [name])) +} + +# The two shapes the rule exists to require, and the one it exists to reject. +test_bare_major_is_accepted if { + msgs := cargo.deny with input as manifest({"crate-a": "1"}) + not flagged(msgs, "crate-a") +} + +test_major_minor_is_accepted if { + msgs := cargo.deny with input as manifest({"crate-a": "1.2"}) + not flagged(msgs, "crate-a") +} + +test_major_minor_patch_is_rejected if { + msgs := cargo.deny with input as manifest({"crate-a": "1.2.3"}) + flagged(msgs, "crate-a") +} + +# The table form carries the requirement under `version`, and must be read the same way as the string form. +test_table_form_is_rejected if { + msgs := cargo.deny with input as manifest({"crate-a": {"version": "1.2.3", "features": ["derive"]}}) + flagged(msgs, "crate-a") +} + +# A comparator must not launder a patch pin past the rule, nor shift the components it counts. +test_comparator_does_not_launder_a_patch_pin if { + msgs := cargo.deny with input as manifest({"crate-a": "=1.2.3"}) + flagged(msgs, "crate-a") +} + +test_comparator_on_major_minor_is_accepted if { + msgs := cargo.deny with input as manifest({"crate-a": ">=1.2"}) + not flagged(msgs, "crate-a") +} + +# A prerelease tail contributes dots of its own, which belong to the tail rather than to the version core. +test_prerelease_counts_only_the_version_core if { + msgs := cargo.deny with input as manifest({"crate-a": "=2.0.0-rc.10"}) + flagged(msgs, "crate-a") +} + +# A path dep has no two-part form available to it, so it is exempt without needing an exception entry. +test_path_dep_keeps_its_full_triple if { + msgs := cargo.deny with input as manifest({"et-a": {"path": "libs/a", "version": "0.1.0"}}) + not flagged(msgs, "et-a") +} + +# An excepted dep keeps its pin, and the entry that permits it is what stops the denial. +test_excepted_dep_keeps_its_patch_pin if { + msgs := cargo.deny with input as manifest({"deno_error": "=0.7.1", "ort": "=2.0.0-rc.10"}) + not flagged(msgs, "deno_error") +} + +test_exception_only_covers_the_dep_it_names if { + deps := {"deno_error": "=0.7.1", "ort": "=2.0.0-rc.10", "other": "1.2.3"} + msgs := cargo.deny with input as manifest(deps) + flagged(msgs, "other") +} + +# The map is only honest while every entry still describes the manifest, so both ways of going stale report. +test_trimmed_dep_reports_its_stale_exception if { + msgs := cargo.deny with input as manifest({"deno_error": "=0.7.1", "ort": "2.0"}) + stale(msgs, "ort") +} + +test_dropped_dep_reports_its_stale_exception if { + msgs := cargo.deny with input as manifest({"deno_error": "=0.7.1"}) + stale(msgs, "ort") +} + +test_live_exceptions_report_nothing if { + msgs := cargo.deny with input as manifest({"deno_error": "=0.7.1", "ort": "=2.0.0-rc.10"}) + not stale(msgs, "deno_error") + not stale(msgs, "ort") +} diff --git a/config/conftest/policy/checksums/checksums_test.rego b/config/conftest/policy/checksums/checksums_test.rego new file mode 100644 index 00000000..f6db4956 --- /dev/null +++ b/config/conftest/policy/checksums/checksums_test.rego @@ -0,0 +1,79 @@ +# Unit tests for the upstream-cache cross-reference rule, run by `conftest verify`. +# +# They exist because both directions of the cross-reference fail open when their key matching breaks: a var +# whose `_asset` suffix stops being recognised, or an asset table that stops being read, produces an empty set +# on one side and an empty set cross-references cleanly against anything. The real task only ever sees a +# manifest that already agrees, so it cannot tell a working cross-check from one that compares nothing. +package checksums_test + +import data.checksums + +entry := { + "sha256": "3b2c1d", + "url": "https://example.invalid/releases/download/v1/tool.tar.gz", + "upstream": "https://example.invalid/tool", + "license": "Apache-2.0", +} + +scenario(vars, assets) := [ + {"path": ".mise/config.toml", "contents": {"vars": vars}}, + {"path": "config/upstream-cache/data.toml", "contents": {"asset": assets}}, +] + +says(msgs, fragment) if { + some msg in msgs + contains(msg, fragment) +} + +test_matched_var_and_asset_table_are_clean if { + msgs := checksums.deny with input as scenario({"tool_asset": "tool.tar.gz"}, {"tool.tar.gz": entry}) + count(msgs) == 0 +} + +test_var_without_an_asset_table_is_flagged if { + msgs := checksums.deny with input as scenario({"tool_asset": "tool.tar.gz"}, {}) + says(msgs, "missing [asset.") +} + +test_asset_table_without_a_var_is_flagged if { + msgs := checksums.deny with input as scenario({}, {"tool.tar.gz": entry}) + says(msgs, "is recorded but no") +} + +# Only a `_asset` var names an asset; every other var in the same table is someone else's. +test_other_vars_are_not_read_as_asset_names if { + msgs := checksums.deny with input as scenario({"tool_url": "tool.tar.gz"}, {}) + count(msgs) == 0 +} + +# An empty sha256 is the documented bootstrap state, held open until the first upload lands. +test_empty_sha256_is_allowed_while_bootstrapping if { + bootstrapping := object.union(entry, {"sha256": ""}) + msgs := checksums.deny with input as scenario({"tool_asset": "tool.tar.gz"}, {"tool.tar.gz": bootstrapping}) + count(msgs) == 0 +} + +# An absent sha256 is not the same as an empty one -- it means nobody has decided yet. +test_absent_sha256_is_flagged if { + stripped := object.remove(entry, ["sha256"]) + msgs := checksums.deny with input as scenario({"tool_asset": "tool.tar.gz"}, {"tool.tar.gz": stripped}) + says(msgs, "is missing `sha256`") +} + +test_absent_url_is_flagged if { + stripped := object.remove(entry, ["url"]) + msgs := checksums.deny with input as scenario({"tool_asset": "tool.tar.gz"}, {"tool.tar.gz": stripped}) + says(msgs, "is missing `url`") +} + +test_absent_upstream_is_flagged if { + stripped := object.remove(entry, ["upstream"]) + msgs := checksums.deny with input as scenario({"tool_asset": "tool.tar.gz"}, {"tool.tar.gz": stripped}) + says(msgs, "is missing `upstream`") +} + +test_absent_license_is_flagged if { + stripped := object.remove(entry, ["license"]) + msgs := checksums.deny with input as scenario({"tool_asset": "tool.tar.gz"}, {"tool.tar.gz": stripped}) + says(msgs, "is missing `license`") +} diff --git a/config/conftest/policy/dockerfile_heredoc/dockerfile_heredoc.rego b/config/conftest/policy/dockerfile_heredoc/dockerfile_heredoc.rego index 4c2d8974..5c3ad8b0 100644 --- a/config/conftest/policy/dockerfile_heredoc/dockerfile_heredoc.rego +++ b/config/conftest/policy/dockerfile_heredoc/dockerfile_heredoc.rego @@ -17,7 +17,11 @@ deny contains msg if { items := entries(file) some i, entry in items is_string(entry.Original) - regex.match(`^RUN[^\n]*<<[A-Z]`, entry.Original) + + # Everything between `<<` and the delimiter tag's first character is optional. + # The tag may be quoted (`<<'EOF'`, the form this repo requires) or bare, and `<<-` strips leading tabs, so + # a pattern that insists on the tag starting immediately after `<<` sees only the bare form. + regex.match(`^RUN[^\n]*<<-?['"]?[A-Za-z_]`, entry.Original) # Bounds-check before indexing to avoid a `panic: slice bounds out of range`. # An unterminated heredoc at EOF would otherwise trip it. diff --git a/config/conftest/policy/dockerfile_heredoc/dockerfile_heredoc_test.rego b/config/conftest/policy/dockerfile_heredoc/dockerfile_heredoc_test.rego new file mode 100644 index 00000000..9da965b9 --- /dev/null +++ b/config/conftest/policy/dockerfile_heredoc/dockerfile_heredoc_test.rego @@ -0,0 +1,61 @@ +# Unit tests for the heredoc first-body-line rule, run by `conftest verify`. +# +# They exist because the rule recognises a heredoc by matching one regex against a raw source line, and a regex +# that stops matching fails open: every Dockerfile then passes the check while nothing is being checked. Running +# the real task over the real Dockerfiles cannot tell that apart from the files being correct, since both look +# like zero denials. Feeding a body that is known to be wrong is what separates the two. +package dockerfile_heredoc_test + +import data.dockerfile_heredoc + +# One Dockerfile, shaped the way the `ignore` parser hands it over. +# That parser emits every source line as its own {Kind, Original, Value} entry, and conftest wraps a file's +# entries in one more array. +dockerfile(originals) := [{"path": "Dockerfile", "contents": [lines]}] if { + lines := [line | some o in originals; line := {"Original": o}] +} + +# The form CLAUDE.md requires and every Dockerfile in this repo uses: bash first, delimiter quoted. +test_quoted_delimiter_accepts_a_correct_body if { + msgs := dockerfile_heredoc.deny with input as dockerfile([ + "RUN bash <<'EOF'", + "set -euo pipefail", + "apt-get update", + "EOF", + ]) + count(msgs) == 0 +} + +test_quoted_delimiter_rejects_a_wrong_first_body_line if { + msgs := dockerfile_heredoc.deny with input as dockerfile([ + "RUN bash <<'EOF'", + "apt-get update", + "EOF", + ]) + count(msgs) == 1 +} + +# The unquoted form is banned elsewhere, but while it parses as a heredoc this rule still has to see it. +test_unquoted_delimiter_rejects_a_wrong_first_body_line if { + msgs := dockerfile_heredoc.deny with input as dockerfile(["RUN bash <], or one entry of a list. +test_deeply_nested_leaf_is_reached if { + nested := {"a": {"b": [{"c": ["ok", "cmd \\\n more"]}]}} + msgs := no_trailing_backslash.deny with input as parsed(nested) + count(msgs) == 1 +} + +# A multi-line body without continuations is the normal case and must stay quiet. +test_plain_newline_is_not_a_continuation if { + msgs := no_trailing_backslash.deny with input as parsed({"run": "set -euo pipefail\ncargo build"}) + count(msgs) == 0 +} + +# A backslash inside a value is not a continuation: Windows paths and regex escapes are both legitimate. +test_backslash_inside_a_value_is_not_a_continuation if { + msgs := no_trailing_backslash.deny with input as parsed({"env": {"DIR": "C:\\Users\\runner", "RE": "\\d+"}}) + count(msgs) == 0 +} + +# A backslash as the last character of the whole value has no newline after it, so it joins nothing. +test_backslash_at_end_of_value_is_not_a_continuation if { + msgs := no_trailing_backslash.deny with input as parsed({"env": {"DIR": "C:\\Users\\runner\\"}}) + count(msgs) == 0 +} diff --git a/config/conftest/policy/policy_tests/policy_tests.rego b/config/conftest/policy/policy_tests/policy_tests.rego new file mode 100644 index 00000000..b7174125 --- /dev/null +++ b/config/conftest/policy/policy_tests/policy_tests.rego @@ -0,0 +1,76 @@ +# Every conftest policy carries an accompanying `_test.rego`, or an entry saying why it does not. +# Run with `--namespace policy_tests` over the policy tree itself, parsed with `ignore` so each file arrives as +# a path; only the paths are read here, never the contents. +# +# A policy rule that stops matching does not report anything -- it reports nothing, which is the same output as +# a clean repo. The real `conftest-check-*` tasks only ever run over files that already comply, so they cannot +# tell those two apart, and a rule can sit dead for months looking like a passing check. Feeding a known-bad +# input is the only thing that distinguishes them, and a test file is where that input lives. +package policy_tests + +# conftest reports native separators when it walks a directory, so a Windows lane sees backslashes. +normalised(path) := replace(path, "\\", "/") + +rego_files contains path if { + some file in input + path := normalised(file.path) + endswith(path, ".rego") +} + +# Named away from a `test_` prefix: conftest verify treats any rule so named as a unit test to execute. +paired_tests contains path if { + some path in rego_files + endswith(path, "_test.rego") +} + +policy_files contains path if { + some path in rego_files + not endswith(path, "_test.rego") +} + +expected_test(path) := sprintf("%s_test.rego", [trim_suffix(path, ".rego")]) + +# Policies deliberately left without tests, each with the reason. +# +# The first group is exempt for good: their matching is direct -- a named key read and compared against another +# named key -- so a break is a rename, which turns the check red or shows up in the diff that caused it. There is +# no silent-failure mode for synthetic input to catch, and a test would only restate the rule. +# +# The second group is not a judgement, it is debt. These are pattern-driven in the same way as the policies that +# do carry tests, and the same silent-failure mode applies to them; they simply have not been written yet. Delete +# the entry as each one gets its test rather than letting the group settle in. +untested_policy := { + "config/conftest/policy/cross/wasm-bindgen-sync.rego": "one equality between two named values", + "config/conftest/policy/gha_action/gha_action.rego": "reads named keys off one action file, no matching", + "config/conftest/policy/jscpd/jscpd.rego": "two arithmetic comparisons against one declared number", + "config/conftest/policy/pyproject/pyproject.rego": "direct key presence checks over one table", + "config/conftest/policy/dockerfile/dockerfile.rego": "pattern-driven; predates the rule, write tests", + "config/conftest/policy/gha/gha.rego": "pattern-driven; predates the rule, write tests", + "config/conftest/policy/gha_combined/gha_combined.rego": "pattern-driven; predates the rule, write tests", + "config/conftest/policy/gha_mise/gha_mise.rego": "pattern-driven; predates the rule, write tests", + "config/conftest/policy/gha_uses/gha_uses.rego": "pattern-driven; predates the rule, write tests", +} + +deny contains msg if { + some path in policy_files + not untested_policy[path] + expected := expected_test(path) + not expected in paired_tests + msg := sprintf("%s: add %s, or record the policy in untested_policy with its reason", [path, expected]) +} + +# The map is a two-way contract, the same way a lint suppression is. +# An entry kept past the test that answers it reads as a standing decision not to test a policy that is in fact +# tested, so writing the test has to be what removes the entry. +deny contains msg if { + some path, reason in untested_policy + expected_test(path) in paired_tests + msg := sprintf("%s: now has a test, so drop its untested_policy entry (%s)", [path, reason]) +} + +# An entry naming a policy that is no longer there has outlived what it described. +deny contains msg if { + some path, reason in untested_policy + not path in policy_files + msg := sprintf("%s: untested_policy names a policy that no longer exists (%s)", [path, reason]) +} diff --git a/config/conftest/policy/policy_tests/policy_tests_test.rego b/config/conftest/policy/policy_tests/policy_tests_test.rego new file mode 100644 index 00000000..23560f0a --- /dev/null +++ b/config/conftest/policy/policy_tests/policy_tests_test.rego @@ -0,0 +1,95 @@ +# Unit tests for the policy/test pairing rule, run by `conftest verify`. +# +# They exist for the reason the rule itself exists: the pairing is decided by suffix matching over paths, which +# fails open. If `_test.rego` stopped being recognised, every policy would look untested and the check would go +# loudly red -- but the opposite break, a policy file that stops being recognised as one, makes the requirement +# silently apply to nothing. The real run over the real tree passes either way once the tree complies. +# +# A fixture holds a handful of paths rather than the whole policy tree, so every real `untested_policy` entry is +# absent from it and the stale-entry rule reports each one. That noise is expected here, and is why these assert +# on the message they are about rather than on a count. +package policy_tests_test + +import data.policy_tests + +files(paths) := [entry | some p in paths; entry := {"path": p, "contents": [[]]}] + +names(msgs, fragment) if { + some msg in msgs + contains(msg, fragment) +} + +demo_wanted := "add config/conftest/policy/demo/demo_test.rego" + +test_policy_with_its_test_is_accepted if { + msgs := policy_tests.deny with input as files([ + "config/conftest/policy/demo/demo.rego", + "config/conftest/policy/demo/demo_test.rego", + ]) + not names(msgs, demo_wanted) +} + +test_policy_without_a_test_is_flagged if { + msgs := policy_tests.deny with input as files(["config/conftest/policy/demo/demo.rego"]) + names(msgs, demo_wanted) +} + +# A test file is not itself a policy, so it must not demand a test of its own. +test_test_file_does_not_demand_its_own_test if { + msgs := policy_tests.deny with input as files(["config/conftest/policy/demo/demo_test.rego"]) + not names(msgs, "demo_test_test.rego") +} + +# Pairing is per policy, so another policy's test does not answer for this one. +test_another_policy_test_does_not_count if { + msgs := policy_tests.deny with input as files([ + "config/conftest/policy/demo/demo.rego", + "config/conftest/policy/other/other_test.rego", + ]) + names(msgs, demo_wanted) +} + +# conftest reports native separators when walking a directory, which a Windows lane hands over as backslashes. +# Unnormalised, the `.rego` suffix still matches, so the policy is still demanded -- but its test is never +# recognised as the answer, and the check goes red on that lane alone for files that are perfectly paired. +test_windows_separators_still_pair_up if { + msgs := policy_tests.deny with input as files([ + "config\\conftest\\policy\\demo\\demo.rego", + "config\\conftest\\policy\\demo\\demo_test.rego", + ]) + not names(msgs, demo_wanted) +} + +# Anything that is not a .rego is none of this rule's business. +test_non_rego_files_are_ignored if { + msgs := policy_tests.deny with input as files(["config/conftest/policy/demo/README.md"]) + not names(msgs, "README") +} + +# An exception excuses the policy it names, and only that one. +test_excepted_policy_needs_no_test if { + msgs := policy_tests.deny with input as files(["config/conftest/policy/jscpd/jscpd.rego"]) + not names(msgs, "add config/conftest/policy/jscpd/jscpd_test.rego") +} + +test_exception_does_not_cover_other_policies if { + msgs := policy_tests.deny with input as files([ + "config/conftest/policy/jscpd/jscpd.rego", + "config/conftest/policy/demo/demo.rego", + ]) + names(msgs, demo_wanted) +} + +# Writing the test is what retires the entry, so an entry that outlives its answer reports. +test_excepted_policy_that_gained_a_test_reports_the_stale_entry if { + msgs := policy_tests.deny with input as files([ + "config/conftest/policy/jscpd/jscpd.rego", + "config/conftest/policy/jscpd/jscpd_test.rego", + ]) + names(msgs, "drop its untested_policy entry") +} + +test_excepted_policy_that_was_deleted_reports_the_stale_entry if { + msgs := policy_tests.deny with input as files(["config/conftest/policy/demo/demo.rego"]) + names(msgs, "names a policy that no longer exists") +} diff --git a/config/jscpd.json b/config/jscpd.json index e9e5b2e6..e66a321f 100644 --- a/config/jscpd.json +++ b/config/jscpd.json @@ -2,6 +2,7 @@ "minTokens": 40, "minLines": 5, "mode": "mild", + "ignorePattern": ["- name: Checkout[\\s\\S]*?persist-credentials: false"], "ignore": [ "generated/**", "verification/**", diff --git a/config/semgrep/no-jscpd-mentions.yaml b/config/semgrep/no-jscpd-mentions.yaml new file mode 100644 index 00000000..04fe8812 --- /dev/null +++ b/config/semgrep/no-jscpd-mentions.yaml @@ -0,0 +1,46 @@ +rules: + - id: no-jscpd-mentions-outside-its-own-wiring + languages: [generic] + paths: + # Repo-wide, with the allowlist below naming every file the word is allowed to appear in. + # Each entry is there because the name is load-bearing -- a task that invokes the tool, a config it reads, + # a policy package, an exclude another linter needs for its files, the ratchet's own documentation. A file + # that merely mentions it in passing does not belong here; the whole point is that the duplication checker + # stays invisible to everything it is not wired into. Adding an entry to explain that some code was + # written a certain way to satisfy the tool is precisely the thing this rule exists to prevent: it + # documents a tool's reaction rather than the code, and goes stale the moment the thresholds move. + exclude: + # The tool pin, the tasks that run it, and the lockfile entry for the pinned binary. + - "/.mise/config.toml" + - "/.mise/mise.lock" + # Its own config and the clone-fingerprint baseline it reads. + - "/config/jscpd.json" + - "/config/jscpd-baseline.json" + # The conftest policy that ratchets the baseline; `jscpd` is its package name, so `--namespace` needs it. + - "/config/conftest/policy/jscpd/jscpd.rego" + # Name the policy file above by path, as the pairing rule's exception list and its tests. + - "/config/conftest/policy/policy_tests/policy_tests.rego" + - "/config/conftest/policy/policy_tests/policy_tests_test.rego" + # Map the generated trees onto each linter's exclude key, the tool's among them. + - "/config/conftest/policy/generated_trees/generated_trees.rego" + - "/config/generated-trees.toml" + # Other linters' carve-outs for its two JSON files, which cannot be YAML or carry comments. + - "/config/semgrep/prefer-yaml-toml.yaml" + - "/config/typos.toml" + # Skips the tool in the Nano Server image, whose attestation verifier cannot install it. + - "/Dockerfile.nanoserver" + # Documents the baseline-is-a-debt-ledger rule, which is where that policy belongs. + - "/CLAUDE.md" + # This rule, which cannot help but name what it bans. + - "/config/semgrep/no-jscpd-mentions.yaml" + pattern-regex: (?i)jscpd + message: >- + Do not name the duplication checker outside the files it is wired into, and never in a comment explaining + that something was written a certain way to satisfy it -- not in source, not in a workflow, not in an + ignore marker. Such a comment documents a tool's reaction rather than the code, reads as noise to everyone + who does not run that tool, and goes stale the moment its thresholds move. A reported clone is a request + to remove duplication: factor the shared part out so it exists once. Where a clone is genuinely + irreducible -- the same few lines every caller must write verbatim -- leave the code plain and raise it, + rather than annotating the source. If a new file genuinely has to wire the tool up, add it to this rule's + paths.exclude allowlist with a comment saying which part of the wiring needs it. + severity: ERROR diff --git a/generated/rust-rest/src/lib.rs b/generated/rust-rest/src/lib.rs index 744b1572..013dbb26 100644 --- a/generated/rust-rest/src/lib.rs +++ b/generated/rust-rest/src/lib.rs @@ -8,6 +8,14 @@ use progenitor_client::{ClientHooks, OperationInfo, RequestBuilderExt, encode_pa /// Types used as operation parameters and responses. #[allow(clippy::all)] pub mod types { + /**Server liveness probe response. + + Returned by `GET /health`.*/ + #[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)] + pub struct HealthResponse { + pub service: ::std::string::String, + pub status: ::std::string::String, + } /// Error types. pub mod error { /// Error from a `TryFrom` or `FromStr` implementation. @@ -34,36 +42,6 @@ pub mod types { } } } - /**Server liveness probe response. - - Returned by `GET /health`.*/ - /// - ///
JSON schema - /// - /// ```json - ///{ - /// "description": "Server liveness probe response.\n\nReturned by `GET /health`.", - /// "type": "object", - /// "required": [ - /// "service", - /// "status" - /// ], - /// "properties": { - /// "service": { - /// "type": "string" - /// }, - /// "status": { - /// "type": "string" - /// } - /// } - ///} - /// ``` - ///
- #[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)] - pub struct HealthResponse { - pub service: ::std::string::String, - pub status: ::std::string::String, - } } #[derive(Clone, Debug)] /**Client for Edge Toolkit REST API diff --git a/libs/ws-runner-common/src/lib.rs b/libs/ws-runner-common/src/lib.rs index d6b841eb..c8c01d67 100644 --- a/libs/ws-runner-common/src/lib.rs +++ b/libs/ws-runner-common/src/lib.rs @@ -190,7 +190,7 @@ async fn register_once( let connect = serde_json::to_string(&ClientMessage::Connect { agent_id: requested_agent_id, })?; - socket.send(tungstenite::Message::Text(connect)).await?; + socket.send(tungstenite::Message::text(connect)).await?; while let Some(frame) = socket.next().await { let tungstenite::Message::Text(text) = frame? else { continue; diff --git a/services/websockify/tests/relay.rs b/services/websockify/tests/relay.rs index 1a797076..c21d1a51 100644 --- a/services/websockify/tests/relay.rs +++ b/services/websockify/tests/relay.rs @@ -151,7 +151,7 @@ async fn relay_echoes_bytes_both_directions() { let port = start_relay(start_echo_target().await).await; let mut ws = open_ws(port).await; - ws.send(Message::Binary(b"hello relay".to_vec())).await.unwrap(); + ws.send(Message::binary(b"hello relay".to_vec())).await.unwrap(); let echoed = recv_bytes(&mut ws, b"hello relay".len()).await; assert_eq!(echoed, b"hello relay"); @@ -164,7 +164,7 @@ async fn relay_carries_a_full_http_get() { // Exactly what libcurl/httr2 would write over the tunnel. let request = "GET /storage/agent/data.txt HTTP/1.1\r\nHost: relay\r\nConnection: close\r\n\r\n"; - ws.send(Message::Binary(request.as_bytes().to_vec())).await.unwrap(); + ws.send(Message::binary(request.as_bytes().to_vec())).await.unwrap(); let response = String::from_utf8(recv_bytes(&mut ws, 48).await).unwrap(); assert!( @@ -181,7 +181,7 @@ async fn relay_handles_a_large_chunked_payload() { // Larger than the relay's 16 KiB read buffer, so both directions must span multiple reads/frames. let payload = vec![0xAB_u8; 200 * 1024]; - ws.send(Message::Binary(payload.clone())).await.unwrap(); + ws.send(Message::binary(payload.clone())).await.unwrap(); let echoed = recv_bytes(&mut ws, payload.len()).await; assert_eq!(echoed.len(), payload.len()); @@ -193,7 +193,7 @@ async fn relay_closes_when_target_closes() { let port = start_relay(start_closing_target().await).await; let mut ws = open_ws(port).await; // Send a (non-SOCKS5) byte so the relay sniffs the protocol and connects to the target, which closed. - ws.send(Message::Binary(b"GET / HTTP/1.1\r\n\r\n".to_vec())) + ws.send(Message::binary(b"GET / HTTP/1.1\r\n\r\n".to_vec())) .await .unwrap(); @@ -208,7 +208,7 @@ async fn relay_closes_when_target_unreachable() { let port = start_relay(unreachable_target().await).await; let mut ws = open_ws(port).await; // The relay only connects to the target after the client's first byte; send one so it tries (and fails). - ws.send(Message::Binary(b"GET / HTTP/1.1\r\n\r\n".to_vec())) + ws.send(Message::binary(b"GET / HTTP/1.1\r\n\r\n".to_vec())) .await .unwrap(); @@ -227,7 +227,7 @@ async fn relay_accepts_an_appended_target_path() { .await .unwrap(); - ws.send(Message::Binary(b"suffixed".to_vec())).await.unwrap(); + ws.send(Message::binary(b"suffixed".to_vec())).await.unwrap(); let echoed = recv_bytes(&mut ws, b"suffixed".len()).await; assert_eq!(echoed, b"suffixed"); @@ -237,13 +237,13 @@ async fn relay_accepts_an_appended_target_path() { async fn socks5_connect(port: u16, request: &[u8]) -> (WsStream, Vec) { let mut ws = open_ws(port).await; // Method selection: version 5, one method, "no authentication". - ws.send(Message::Binary(vec![0x05, 0x01, 0x00])).await.unwrap(); + ws.send(Message::binary(vec![0x05, 0x01, 0x00])).await.unwrap(); assert_eq!( recv_bytes(&mut ws, 2).await, vec![0x05, 0x00], "SOCKS5 method-selection reply" ); - ws.send(Message::Binary(request.to_vec())).await.unwrap(); + ws.send(Message::binary(request.to_vec())).await.unwrap(); let reply = recv_bytes(&mut ws, 10).await; (ws, reply) } @@ -261,7 +261,7 @@ async fn relay_socks5_connect_to_loopback_bridges() { ); // Tunnel is live -> bytes echo back through the bridged target. - ws.send(Message::Binary(b"socks-hi".to_vec())).await.unwrap(); + ws.send(Message::binary(b"socks-hi".to_vec())).await.unwrap(); assert_eq!(recv_bytes(&mut ws, b"socks-hi".len()).await, b"socks-hi"); } diff --git a/services/ws-pyo3-runner/src/agent.rs b/services/ws-pyo3-runner/src/agent.rs index 3d901750..9a634e22 100644 --- a/services/ws-pyo3-runner/src/agent.rs +++ b/services/ws-pyo3-runner/src/agent.rs @@ -29,8 +29,8 @@ use crate::python::{AgentIdSlot, Dispatcher, OutboundFrame, StorageError, Storag #[derive(Debug)] enum InboundEvent { Connect(String), - Text(String), - Binary(Vec), + Text(tungstenite::Utf8Bytes), + Binary(tungstenite::Bytes), Shutdown, } @@ -322,14 +322,14 @@ async fn drive( // sends or return-value replies). Some(out) = outbound_rx.recv() => { let msg = match out { - OutboundFrame::Text(text) => tungstenite::Message::Text(text), - OutboundFrame::Binary(bytes) => tungstenite::Message::Binary(bytes), + OutboundFrame::Text(text) => tungstenite::Message::text(text), + OutboundFrame::Binary(bytes) => tungstenite::Message::binary(bytes), }; socket.send(msg).await?; } // Keepalive ping; the server treats it as activity and pongs back. _ = heartbeat.tick() => { - socket.send(tungstenite::Message::Ping(Vec::new())).await?; + socket.send(tungstenite::Message::Ping(tungstenite::Bytes::new())).await?; } } } diff --git a/services/ws-pyo3-runner/src/lib.rs b/services/ws-pyo3-runner/src/lib.rs index 06d733fd..c4caba3a 100644 --- a/services/ws-pyo3-runner/src/lib.rs +++ b/services/ws-pyo3-runner/src/lib.rs @@ -14,7 +14,6 @@ #![expect( clippy::single_call_fn, clippy::integer_division_remainder_used, - clippy::result_large_err, reason = "register/drive/storage_worker/python_worker single-use; select! uses %; RunnerError: tungstenite::Error" )] diff --git a/services/ws-pyo3-runner/tests/modules.rs b/services/ws-pyo3-runner/tests/modules.rs index cb8676b4..e40bda2e 100644 --- a/services/ws-pyo3-runner/tests/modules.rs +++ b/services/ws-pyo3-runner/tests/modules.rs @@ -231,7 +231,7 @@ fn skipped(module: &str, gate: &Gate) -> bool { reason = "one Exchange variant's protocol; kept separate so run_exchange stays a dispatcher" )] async fn exchange_text_contains(control: &mut ControlSocket, send: &str, extra: &[&str]) -> Result<(), Box> { - control.send(tungstenite::Message::Text(send.to_string())).await?; + control.send(tungstenite::Message::text(send)).await?; let reply = drain_text(control).await?; if !reply.contains(send) { return Err(format!("reply {reply:?} did not contain the sent {send:?}").into()); @@ -254,7 +254,7 @@ async fn exchange_text_json( payload: &str, check: fn(&serde_json::Value) -> Result<(), Box>, ) -> Result<(), Box> { - control.send(tungstenite::Message::Text(payload.to_string())).await?; + control.send(tungstenite::Message::text(payload)).await?; let reply = drain_text(control).await?; let value: serde_json::Value = match serde_json::from_str(&reply) { Ok(value) => value, @@ -273,11 +273,11 @@ async fn exchange_storage_put_get(control: &mut ControlSocket, key: &str, value: put_frame.extend_from_slice(key.as_bytes()); put_frame.push(0); put_frame.extend_from_slice(value); - control.send(tungstenite::Message::Binary(put_frame)).await?; + control.send(tungstenite::Message::binary(put_frame)).await?; // Let the storage worker PUT to disk before we GET. tokio::time::sleep(Duration::from_millis(200)).await; control - .send(tungstenite::Message::Binary(key.as_bytes().to_vec())) + .send(tungstenite::Message::binary(key.as_bytes().to_vec())) .await?; let reply = drain_binary(control).await?; if reply.as_slice() != value { @@ -292,7 +292,7 @@ async fn exchange_storage_put_get(control: &mut ControlSocket, key: &str, value: reason = "one Exchange variant's protocol; kept separate so run_exchange stays a dispatcher" )] async fn exchange_fanout(control: &mut ControlSocket, count: u8) -> Result<(), Box> { - control.send(tungstenite::Message::Binary(vec![count])).await?; + control.send(tungstenite::Message::binary(vec![count])).await?; let frames = collect_binary(control, usize::from(count)).await?; let expected: Vec = (0..count).collect(); if frames != expected { @@ -390,7 +390,7 @@ fn spawn_runner(module: &str, ws_url: &str) -> Child { async fn control_client(ws_url: &str) -> Result<(ControlSocket, String), Box> { let (mut socket, _) = connect_async(ws_url).await?; let connect = serde_json::to_string(&ClientMessage::Connect { agent_id: None })?; - socket.send(tungstenite::Message::Text(connect)).await?; + socket.send(tungstenite::Message::text(connect)).await?; loop { let Some(frame) = socket.next().await else { return Err("control socket closed before connect-ack".into()); @@ -441,7 +441,7 @@ async fn poll_for_peer_once(control: &mut ControlSocket, self_id: &str) -> Resul let Ok(req) = serde_json::to_string(&ClientMessage::ListAgents) else { return Err(()); }; - if control.send(tungstenite::Message::Text(req)).await.is_err() { + if control.send(tungstenite::Message::text(req)).await.is_err() { return Err(()); } let poll_until = Instant::now() + POLL_DRAIN_WINDOW; @@ -462,23 +462,35 @@ async fn poll_for_peer_once(control: &mut ControlSocket, self_id: &str) -> Resul Err(()) } +/// Await one frame from `control`, failing if `deadline` passes first. +/// +/// Every drain below wants the same four outcomes off the socket -- a frame, a recv error, a closed +/// socket, or the deadline -- and differs only in which frames it keeps. `what` names the wait in the +/// two error strings, so each caller still reports what it was waiting for. +async fn next_frame( + control: &mut ControlSocket, + deadline: Instant, + what: &str, +) -> Result> { + match tokio::time::timeout(deadline - Instant::now(), control.next()).await { + Ok(Some(Ok(frame))) => Ok(frame), + Ok(Some(Err(err))) => Err(format!("recv error: {err}").into()), + Ok(None) => Err("control socket closed".into()), + Err(_) => Err(format!("timed out waiting for {what}").into()), + } +} + /// Drain frames until the first non-protocol text frame (skipping typed et-* envelopes). async fn drain_text(control: &mut ControlSocket) -> Result> { let deadline = Instant::now() + REPLY_TIMEOUT; while Instant::now() < deadline { - let remaining = deadline - Instant::now(); - match tokio::time::timeout(remaining, control.next()).await { - Ok(Some(Ok(tungstenite::Message::Text(text)))) => { - if serde_json::from_str::(&text).is_ok() { - continue; // typed et-* envelope (status / list / ack), keep draining - } - return Ok(text); - } - Ok(Some(Ok(_))) => {} - Ok(Some(Err(err))) => return Err(format!("recv error: {err}").into()), - Ok(None) => return Err("control socket closed".into()), - Err(_) => return Err("timed out waiting for text reply".into()), + let tungstenite::Message::Text(text) = next_frame(control, deadline, "text reply").await? else { + continue; + }; + if serde_json::from_str::(&text).is_ok() { + continue; // typed et-* envelope (status / list / ack), keep draining } + return Ok(text.to_string()); } Err("deadline exceeded waiting for text reply".into()) } @@ -487,14 +499,10 @@ async fn drain_text(control: &mut ControlSocket) -> Result Result, Box> { let deadline = Instant::now() + REPLY_TIMEOUT; while Instant::now() < deadline { - let remaining = deadline - Instant::now(); - match tokio::time::timeout(remaining, control.next()).await { - Ok(Some(Ok(tungstenite::Message::Binary(bytes)))) => return Ok(bytes), - Ok(Some(Ok(_))) => {} - Ok(Some(Err(err))) => return Err(format!("recv error: {err}").into()), - Ok(None) => return Err("control socket closed".into()), - Err(_) => return Err("timed out waiting for binary reply".into()), - } + let tungstenite::Message::Binary(bytes) = next_frame(control, deadline, "binary reply").await? else { + continue; + }; + return Ok(bytes.to_vec()); } Err("deadline exceeded waiting for binary reply".into()) } @@ -504,19 +512,13 @@ async fn collect_binary(control: &mut ControlSocket, count: usize) -> Result { - let [byte] = bytes.as_slice() else { - return Err(format!("fan-out produced a {}-byte frame, expected 1", bytes.len()).into()); - }; - received.push(*byte); - } - Ok(Some(Ok(_))) => {} - Ok(Some(Err(err))) => return Err(format!("recv error: {err}").into()), - Ok(None) => return Err("control socket closed".into()), - Err(_) => return Err("timed out waiting for fan-out frames".into()), - } + let tungstenite::Message::Binary(bytes) = next_frame(control, deadline, "fan-out frames").await? else { + continue; + }; + let [byte] = &*bytes else { + return Err(format!("fan-out produced a {}-byte frame, expected 1", bytes.len()).into()); + }; + received.push(*byte); } if received.len() != count { return Err(format!("got {} frames, expected {count}", received.len()).into()); diff --git a/services/ws-test-server/src/math1.rs b/services/ws-test-server/src/math1.rs index ec1c95d0..882c655b 100644 --- a/services/ws-test-server/src/math1.rs +++ b/services/ws-test-server/src/math1.rs @@ -76,7 +76,7 @@ pub async fn drive_math1_exchange( ) -> Result<(f64, f64), Math1Error> { let (mut socket, _response) = connect_async(ws_url).await?; let connect = serde_json::to_string(&ClientMessage::Connect { agent_id: None })?; - socket.send(Message::Text(connect)).await?; + socket.send(Message::text(connect)).await?; let deadline = tokio::time::Instant::now() + budget; let mut fake_id = String::default(); @@ -140,9 +140,9 @@ pub async fn drive_math1_exchange( // Ask for the roster and re-broadcast the pointer; both are safe to repeat. let list = serde_json::to_string(&ClientMessage::ListAgents)?; - socket.send(Message::Text(list)).await?; + socket.send(Message::text(list)).await?; if !pointer.is_empty() { - socket.send(Message::Text(pointer.clone())).await?; + socket.send(Message::text(pointer.clone())).await?; } } } diff --git a/services/ws-test-server/tests/helpers.rs b/services/ws-test-server/tests/helpers.rs index 18372dd0..4b9693e7 100644 --- a/services/ws-test-server/tests/helpers.rs +++ b/services/ws-test-server/tests/helpers.rs @@ -8,7 +8,7 @@ use edge_toolkit::ws::{ConnectStatus, ServerMessage}; use et_ws_test_server::{connect_agent, next_payload}; use futures_util::SinkExt as _; use tokio::net::TcpListener; -use tokio_tungstenite::tungstenite::Message; +use tokio_tungstenite::tungstenite::{Bytes, Message}; use tokio_tungstenite::{accept_async, connect_async}; /// Start a ws server on a free port that accepts one connection, sends `frames` in order, then holds the socket open. @@ -54,7 +54,7 @@ async fn next_payload_skips_control_frames_and_protocol_acks() { .unwrap(); let frames = vec![ // A control frame and a protocol ack both precede the real payload; next_payload must skip both. - Message::Ping(Vec::new()), + Message::Ping(Bytes::new()), Message::text(ack), Message::text("actual-payload"), ]; diff --git a/services/ws-test-server/tests/math1_exchange.rs b/services/ws-test-server/tests/math1_exchange.rs index 9fc3260c..16e24925 100644 --- a/services/ws-test-server/tests/math1_exchange.rs +++ b/services/ws-test-server/tests/math1_exchange.rs @@ -117,10 +117,8 @@ async fn reads_and_verifies_a_peer_output() { let noise_then_output = tokio::spawn(async move { // Let the fake agent connect and start draining, then relay noise before the output lands. tokio::time::sleep(Duration::from_millis(600)).await; - peer.send(Message::Text(r#"{"type":"noise"}"#.to_string())) - .await - .unwrap(); - peer.send(Message::Binary(vec![1, 2, 3])).await.unwrap(); + peer.send(Message::text(r#"{"type":"noise"}"#)).await.unwrap(); + peer.send(Message::binary(vec![1, 2, 3])).await.unwrap(); tokio::time::sleep(Duration::from_millis(300)).await; let bucket = storage_dir.join(&peer_id); fs_err::create_dir_all(&bucket).unwrap(); diff --git a/services/ws-wasi-runner/src/host/ws.rs b/services/ws-wasi-runner/src/host/ws.rs index 3e87a7a8..c9b98c1e 100644 --- a/services/ws-wasi-runner/src/host/ws.rs +++ b/services/ws-wasi-runner/src/host/ws.rs @@ -101,7 +101,7 @@ impl WsBackend { continue; } }, - tungstenite::Message::Binary(bytes) => ServerMessage::from_binary_frame(bytes.clone()), + tungstenite::Message::Binary(bytes) => ServerMessage::from_binary_frame(bytes.to_vec()), _ => continue, }; if tx.send(parsed).is_err() { @@ -129,7 +129,11 @@ impl WsBackend { break; } let mut guard = pinger_sink.lock().await; - if guard.send(tungstenite::Message::Ping(Vec::new())).await.is_err() { + if guard + .send(tungstenite::Message::Ping(tungstenite::Bytes::new())) + .await + .is_err() + { break; } } From 82eb3c9394c6b8b52bdcbb1e66920055f29f2c45 Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Tue, 15 Sep 2026 13:55:46 +0800 Subject: [PATCH 02/24] Fixes and more checks --- .github/actions/free-disk-space/action.yaml | 11 +- .github/actions/install-lavapipe/action.yaml | 11 +- .mise/config.coverage.toml | 159 +---- .mise/config.linux.toml | 3 +- .mise/config.macos.toml | 21 +- .mise/config.maint.toml | 5 +- .mise/config.toml | 33 +- .mise/config.windows.toml | 5 +- .mise/cov-branch-assert.jq | 19 + .mise/lcov-drop-external.awk | 7 + .mise/lcov-keep-want.awk | 6 + .mise/llvm-cov-gut.awk | 18 + .mise/osv-ids.awk | 35 + .mise/pip-shim.sh | 3 + .mise/wasm-ll-objs.sh | 29 + CLAUDE.md | 27 + Cargo.lock | 2 +- Cargo.toml | 4 +- .../no-string-literal-line-continuation.yaml | 4 +- config/ast-grep/rules/no-string-new.yaml | 2 +- config/clang-tidy.yaml | 2 +- config/clippy.toml | 4 +- .../policy/advisories/advisories.rego | 54 ++ .../policy/advisories/advisories_test.rego | 77 ++ config/conftest/policy/cargo/cargo.rego | 4 +- .../policy/dockerfile/dockerfile.rego | 4 +- config/conftest/policy/gha/gha.rego | 4 +- .../no_trailing_backslash.rego | 8 +- config/deny.toml | 59 +- config/dprint.jsonc | 2 +- config/generated-trees.toml | 4 + config/gitleaks.toml | 5 +- config/hadolint.yaml | 2 +- config/osv-scanner.toml | 29 +- config/oxlintrc.jsonc | 6 +- config/pyrefly.toml | 6 +- config/semgrep/comment-summary-line.yaml | 17 +- .../dockerfile-heredoc-set-euo-pipefail.yaml | 8 +- config/semgrep/no-cat-heredoc.yaml | 20 + config/semgrep/no-check-tool-mentions.yaml | 658 ++++++++++++++++++ config/semgrep/no-jscpd-mentions.yaml | 46 -- config/semgrep/no-scripts-dir.yaml | 15 + config/semgrep/no-shell-scripts.yaml | 17 + config/semgrep/no-task-name-mentions.yaml | 235 +++++++ config/semgrep/no-type-comments.yaml | 2 +- config/taplo.toml | 2 +- .../require-lib-doctest-false.schema.json | 2 +- config/trivyignore.yaml | 3 +- config/upstream-cache/data.toml | 2 +- generated/README.md | 3 +- libs/path/src/lib.rs | 2 +- libs/wasi-guest/src/coverage.rs | 4 +- libs/wasi-guest/src/lib.rs | 2 +- libs/web/src/error.rs | 3 +- libs/web/src/lib.rs | 6 +- ruff.toml | 6 +- .../ws-modules/face-detection/tests/web.rs | 1 + services/ws-modules/har1/tests/web.rs | 1 + .../ws-modules/pic-viewer/tests/show_image.rs | 1 + .../ws-modules/rcomm1/pkg/et_ws_rcomm1.js | 2 +- .../ws-modules/rdata1/pkg/et_ws_rdata1.js | 2 +- .../ws-modules/rmath1/pkg/et_ws_rmath1.js | 4 +- services/ws-pyo3-runner/tests/modules.rs | 2 +- services/ws-server/Dockerfile | 12 +- services/ws-test-server/src/bin/cov-server.rs | 2 +- services/ws-wasi-runner/src/host/mod.rs | 2 +- services/ws-wasi-runner/src/host/ws.rs | 22 +- services/ws-wasi-runner/tests/ws_backend.rs | 106 +++ services/ws-wasm-agent/tests/client.rs | 4 + services/ws-wasm-agent/tests/web.rs | 1 + services/ws-web-runner/build.rs | 7 +- .../ws-web-runner/mingw-shim/msvc_crt_alloc.c | 4 +- .../mingw-shim/msvc_crt_locale_cache.c | 10 +- .../ws-web-runner/mingw-shim/msvc_crt_shim.c | 6 +- services/ws-web-runner/src/shims/xhr.js | 2 +- utilities/cli/src/deployment_types/k3s.rs | 12 +- utilities/cli/src/deployment_types/mise.rs | 9 +- .../src/deployment_types/scenario_image.rs | 2 +- utilities/cli/src/lib.rs | 10 +- utilities/int-gen/src/error.rs | 2 +- utilities/int-gen/src/lib.rs | 10 +- utilities/int-gen/src/wit/bindings.rs | 4 +- utilities/wasm-cov-wrapper/src/main.rs | 2 +- 83 files changed, 1579 insertions(+), 390 deletions(-) create mode 100644 .mise/cov-branch-assert.jq create mode 100644 .mise/lcov-drop-external.awk create mode 100644 .mise/lcov-keep-want.awk create mode 100644 .mise/llvm-cov-gut.awk create mode 100644 .mise/osv-ids.awk create mode 100644 .mise/pip-shim.sh create mode 100644 .mise/wasm-ll-objs.sh create mode 100644 config/conftest/policy/advisories/advisories.rego create mode 100644 config/conftest/policy/advisories/advisories_test.rego create mode 100644 config/semgrep/no-cat-heredoc.yaml create mode 100644 config/semgrep/no-check-tool-mentions.yaml delete mode 100644 config/semgrep/no-jscpd-mentions.yaml create mode 100644 config/semgrep/no-scripts-dir.yaml create mode 100644 config/semgrep/no-shell-scripts.yaml create mode 100644 config/semgrep/no-task-name-mentions.yaml create mode 100644 services/ws-wasi-runner/tests/ws_backend.rs diff --git a/.github/actions/free-disk-space/action.yaml b/.github/actions/free-disk-space/action.yaml index 46307d6a..e88390e1 100644 --- a/.github/actions/free-disk-space/action.yaml +++ b/.github/actions/free-disk-space/action.yaml @@ -1,14 +1,5 @@ name: Free disk space -description: >- - Reclaim runner disk before the long build and test steps, on whichever - platform the job is running: jlumbroso/free-disk-space on Linux, this - repo's free-disk-space-windows on Windows. macOS runners need neither. - - Each step carries its own `runner.os` guard, so a caller needs none -- - that is the point of one entry rather than a matched pair at every call - site. A caller wanting the Windows reclaim's opt-in removals (the - visual-studio / windows-kits inputs) calls free-disk-space-windows - directly instead, as docker-windows.yaml does. +description: Reclaim runner disk before the long build and test steps runs: using: composite diff --git a/.github/actions/install-lavapipe/action.yaml b/.github/actions/install-lavapipe/action.yaml index ae4ce16a..90107640 100644 --- a/.github/actions/install-lavapipe/action.yaml +++ b/.github/actions/install-lavapipe/action.yaml @@ -1,14 +1,5 @@ name: Install Mesa Vulkan drivers (lavapipe) -description: >- - Install Mesa's lavapipe software Vulkan driver on a GHA Linux runner, so - the wasi-webgpu tests find a wgpu adapter on an image that has no GPU. - - There is no counterpart on the other platforms: macOS runners reach Metal - through the OS itself, and no Windows lane runs these tests. - - Caller is responsible for the `if: runner.os == 'Linux'` guard, the same - way free-disk-space-windows expects its own -- the apt-get calls below - mean nothing on an image that has no apt. +description: Install Mesa's lavapipe Vulkan driver on Linux for WebGPU w/o GPU. runs: using: composite diff --git a/.mise/config.coverage.toml b/.mise/config.coverage.toml index 3244145d..812e2cbe 100644 --- a/.mise/config.coverage.toml +++ b/.mise/config.coverage.toml @@ -59,6 +59,7 @@ RUSTUP_TOOLCHAIN = "{{ vars.rust_nightly }}" # Pointing the loader straight at the install dir is layout-independent, so it holds however cargo lays the # build dir out. Scoped to this env because only the coverage lane runs binaries under nightly. Drop it together # with the cargo-doc-check mkdir once ort stops hardcoding the OUT_DIR depth. +# # Keep host links on Apple's cc, which the conda-clang prepend below otherwise shadows. # That prepend is there so minicov's build.rs finds a bare `clang` with a wasm32 backend, but conda-clang ships # a `cc` too and wins the lookup for it -- and conda's clang knows nothing of the macOS SDK, so every host link @@ -174,11 +175,6 @@ shell = "{{ vars.task_shell }}" description = "Assert every named libs/ crate is at 100% branch coverage in an llvm-cov JSON summary" hide = true # The whole assertion is one jaq query over llvm-cov's own JSON, with no parsing of our own. -# That JSON (`--format=text`, confusingly) carries a per-file `summary.branches` object, so `.data[0].files[]` -# already holds everything the check reads. The query is written to a file with a quoted heredoc, as wasm-cov -# does with its awk programs: no shell expansion, and no backslash survives the TOML string otherwise. It -# builds its strings with `+` for that reason -- jq's `"\(...)"` interpolation would need every backslash -# doubled to reach the shell. run = """ report="$usage_report" if [ ! -f "$report" ]; then @@ -186,26 +182,7 @@ if [ ! -f "$report" ]; then echo "Run the coverage task that produces it first; a missing report is a failure, not a skip." >&2 exit 1 fi -coreutils mkdir -p target/cov -prog=target/cov/branch-assert.jaq -coreutils cat >"$prog" <<'JAQ' -($libs | split(" ")) as $names -| .data[0].files as $files -| [ $names[] - | . as $n - | ("/libs/" + $n + "/") as $dir - | ($files | map(select(.filename | contains($dir)))) as $hit - | if ($hit | length) == 0 - then "libs/" + $n + ": no records in the report -- this run never exercised the crate" - else ($hit[] - | select(.summary.branches.covered < .summary.branches.count) - | "libs/" + $n + ": " + .filename + " " - + (.summary.branches.covered | tostring) + "/" - + (.summary.branches.count | tostring) + " branches") - end ] -| .[] -JAQ -shortfall="$(jaq -r --arg libs "$usage_libs" -f "$prog" "$report")" +shortfall="$(jaq -r --arg libs "$usage_libs" -f .mise/cov-branch-assert.jq "$report")" if [ -n "$shortfall" ]; then echo "cov-branch-assert: libs/ must be at 100% branch coverage, and is not:" >&2 echo "$shortfall" >&2 @@ -240,15 +217,11 @@ description = "Turn instrumented wasm guest .profraw into lcov (llc + llvm-cov) # WASI guests (wasi-comm1/wasi-data1) dump via the runner's /cov preopen; browser modules (data1, comm1, ...) go # through et-web's __et_capture_coverage -> the module's ws-server storage bucket -> the web-runner test (see # config.rust.toml's coverage wrapper). Each instrumented build also emits an .ll next to its .wasm (release/deps). -# llvm-cov cannot read a coverage map from a .wasm, so (per taiki-e/cargo-llvm-cov#337 + hknio/wasmcov) we gut -# every function body in the .ll to `unreachable` -- keeping only the signatures and the __llvm_covmap records -- -# then compile that to an ELF object with llc. The gut also strips each function's "target-cpu"/"target-features" -# attributes: browser .ll carries wasm `mvp` cpu + wasm features that make llc reject the x86_64 target -# ("64-bit code requested on a subtarget that doesn't support it"); the WASI guest .ll lacks them. The hit counts -# come from the .profraw, so the object needs no code; the fixed x86_64 ELF target avoids the macOS Mach-O -# AsmPrinter aborting on the covmap globals, and llvm-cov reads the covmap from any object format regardless of -# host. llc/llvm-cov/llvm-profdata are the version-matched nightly llvm-tools that built the guests. Path remap of -# the emitted SF: lines to repo-relative may need a CI follow-up. +# The .ll is gutted and compiled to an ELF object with llc (per taiki-e/cargo-llvm-cov#337 + hknio/wasmcov). +# The fixed x86_64 ELF target avoids the macOS Mach-O AsmPrinter aborting on the covmap globals, and llvm-cov +# reads the covmap from any object format regardless of host. llc/llvm-cov/llvm-profdata are the version-matched +# nightly llvm-tools that built the guests. Path remap of the emitted SF: lines to repo-relative may need a CI +# follow-up. run = """ covdir=target/wasi-cov if [ -z "$(find "$covdir" -maxdepth 1 -name '*.profraw' -print -quit 2>/dev/null)" ]; then @@ -257,15 +230,6 @@ if [ -z "$(find "$covdir" -maxdepth 1 -name '*.profraw' -print -quit 2>/dev/null fi host="$(rustc -vV | goawk '/^host:/ { print $2 }')" bin="$(rustc +{{ vars.rust_nightly }} --print sysroot)/lib/rustlib/$host/bin" -gut="$covdir/gut.awk" -coreutils cat >"$gut" <<'AWK' -/^target datalayout/ { next } -/^target triple/ { next } -/^define/ { print; print "start:"; print " unreachable"; print "}"; skip = 1; next } -skip && /^}/ { skip = 0; next } -skip { next } -{ gsub(/"target-cpu"="[^"]*"/, ""); gsub(/"target-features"="[^"]*"/, ""); print } -AWK : >"$covdir/wasi.lcov" for profraw in "$covdir"/*.profraw; do name="$(coreutils basename "$profraw" .profraw)" @@ -293,22 +257,12 @@ for profraw in "$covdir"/*.profraw; do fi pd="$covdir/$name.profdata" obj="$covdir/$name.o" - goawk -f "$gut" "$ll" >"$covdir/$name.gutted.ll" + goawk -f .mise/llvm-cov-gut.awk "$ll" >"$covdir/$name.gutted.ll" "$bin/llc" -filetype=obj -mtriple=x86_64-unknown-linux-gnu -o "$obj" "$covdir/$name.gutted.ll" "$bin/llvm-profdata" merge -sparse -o "$pd" "$profraw" "$bin/llvm-cov" export --format=lcov --instr-profile "$pd" "$obj" >>"$covdir/wasi.lcov" done -# The wasm covmap records reference every source each module linked. -# Dependency crates under ~/.cargo/registry and toolchain std under ~/.rustup are not in VCS -- DeepSource -# flags them and they skew the aggregate metric -- so keep only workspace records (dropping .cargo/.rustup/rustc -# blocks) before merging the wasm lcov into lcov.info. -keep="$covdir/keep.awk" -coreutils cat >"$keep" <<'AWK' -{ buf = buf $0 ORS } -/^SF:/ { p = substr($0, 4); drop = (index(p, "/.cargo/") || index(p, "/.rustup/") || index(p, "/rustc/")) } -/^end_of_record$/ { if (!drop) printf "%s", buf; buf = ""; drop = 0 } -AWK -goawk -f "$keep" "$covdir/wasi.lcov" >>lcov.info +goawk -f .mise/lcov-drop-external.awk "$covdir/wasi.lcov" >>lcov.info """ shell = "{{ vars.task_shell }}" @@ -484,65 +438,23 @@ trap - EXIT host="$(rustc +{{ vars.rust_nightly }} -vV | goawk '/^host:/ { print $2 }')" llbin="$(rustc +{{ vars.rust_nightly }} --print sysroot)/lib/rustlib/$host/bin" -gut="$covdir/gut.awk" -coreutils cat >"$gut" <<'AWK' -/^target datalayout/ { next } -/^target triple/ { next } -/^define/ { print; print "start:"; print " unreachable"; print "}"; skip = 1; next } -skip && /^}/ { skip = 0; next } -skip { next } -{ - gsub(/"target-cpu"="[^"]*"/, "") - gsub(/"target-features"="[^"]*"/, "") - if ($0 ~ /^attributes #/ && $0 ~ /\\{[[:space:]]*\\}/) sub(/\\{[[:space:]]*\\}/, "{ nounwind }") - print -} -AWK "$llbin/llvm-profdata" merge -sparse -o "$covdir/agent.profdata" "$covdir"/wasm-agent-*.profraw -objs=() -# Collect the instrumented .ll from whichever build-dir layout the toolchain produced. -# A plain `deps/*.ll` glob broke when nightly made `-Z build-dir-new-layout` the default: per-crate -# intermediates moved to `/build///out/`, the glob matched nothing, and the unexpanded -# pattern reached goawk as a literal filename -- `file "target/wasm32-unknown-unknown/debug/deps/*.ll" not -# found` on commit 12bfe419 at https://github.com/edge-toolkit/core/actions/runs/30797697402/job/91634929015. -# `find` also avoids the silent-nullglob trap the literal-pattern failure exposed. -lls="$(find target/wasm32-unknown-unknown/debug -path '*/build/*' -name '*.ll' 2>/dev/null)" -if [ -z "$lls" ]; then - lls="$(find target/wasm32-unknown-unknown/debug/deps -name '*.ll' 2>/dev/null)" -fi -if [ -z "$lls" ]; then - echo "wasm-agent-cov: no instrumented .ll under target/wasm32-unknown-unknown/debug" >&2 - exit 1 -fi -while IFS= read -r ll; do - [ -n "$ll" ] || continue - name="$(coreutils basename "$ll" .ll)" - goawk -f "$gut" "$ll" >"$covdir/$name.g.ll" - "$llbin/llc" -filetype=obj -mtriple=x86_64-unknown-linux-gnu -o "$covdir/$name.o" "$covdir/$name.g.ll" - objs+=("-object" "$covdir/$name.o") -done <"$covdir/all.lcov" -keep="$covdir/keep.awk" -coreutils cat >"$keep" <<'AWK' -{ buf = buf $0 ORS } -/^SF:/ { keep = index($0, want) > 0 } -/^end_of_record$/ { if (keep) printf "%s", buf; buf = ""; keep = 0 } -AWK coreutils touch lcov.info -goawk -v want="ws-wasm-agent/src/" -f "$keep" "$covdir/all.lcov" >>lcov.info +goawk -v want="ws-wasm-agent/src/" -f .mise/lcov-keep-want.awk "$covdir/all.lcov" >>lcov.info # Second pass for et-web, whose browser helpers tests/web.rs drives. # One `want` substring per pass, matching the cov-server fold below, rather than a combined pattern the filter # cannot express. -goawk -v want="libs/web/src/" -f "$keep" "$covdir/all.lcov" >>lcov.info +goawk -v want="libs/web/src/" -f .mise/lcov-keep-want.awk "$covdir/all.lcov" >>lcov.info # Fold cov-server's own native coverage in from the instrumented run above (its cov-server-*.profraw). # The export object links its whole dep tree, so keep only the cov-server.rs launcher records. csp="$covdir/cov-server" "$llbin/llvm-profdata" merge -sparse -o "$csp.profdata" "$covdir"/cov-server-*.profraw "$llbin/llvm-cov" export --format=lcov --instr-profile "$csp.profdata" -object target/debug/cov-server >"$csp.lcov" -goawk -v want="ws-test-server/src/bin/cov-server.rs" -f "$keep" "$csp.lcov" >>lcov.info +goawk -v want="ws-test-server/src/bin/cov-server.rs" -f .mise/lcov-keep-want.awk "$csp.lcov" >>lcov.info rpt="$covdir/report.txt" "$llbin/llvm-cov" report --instr-profile "$covdir/agent.profdata" "${objs[@]}" >"$rpt" 2>/dev/null || true @@ -581,49 +493,12 @@ cargo test -p et-ws-pic-viewer --features et-web/coverage --target wasm32-unknow host="$(rustc +{{ vars.rust_nightly }} -vV | goawk '/^host:/ { print $2 }')" llbin="$(rustc +{{ vars.rust_nightly }} --print sysroot)/lib/rustlib/$host/bin" -gut="$covdir/gut.awk" -coreutils cat >"$gut" <<'AWK' -/^target datalayout/ { next } -/^target triple/ { next } -/^define/ { print; print "start:"; print " unreachable"; print "}"; skip = 1; next } -skip && /^}/ { skip = 0; next } -skip { next } -{ - gsub(/"target-cpu"="[^"]*"/, "") - gsub(/"target-features"="[^"]*"/, "") - if ($0 ~ /^attributes #/ && $0 ~ /\\{[[:space:]]*\\}/) sub(/\\{[[:space:]]*\\}/, "{ nounwind }") - print -} -AWK "$llbin/llvm-profdata" merge -sparse -o "$covdir/pic-viewer.profdata" "$covdir"/pic-viewer-*.profraw -objs=() -# Same build-dir-layout handling as wasm-agent-cov above; this task shares the failure mode. -lls="$(find target/wasm32-unknown-unknown/debug -path '*/build/*' -name '*.ll' 2>/dev/null)" -if [ -z "$lls" ]; then - lls="$(find target/wasm32-unknown-unknown/debug/deps -name '*.ll' 2>/dev/null)" -fi -if [ -z "$lls" ]; then - echo "pic-viewer-cov: no instrumented .ll under target/wasm32-unknown-unknown/debug" >&2 - exit 1 -fi -while IFS= read -r ll; do - [ -n "$ll" ] || continue - name="$(coreutils basename "$ll" .ll)" - goawk -f "$gut" "$ll" >"$covdir/$name.g.ll" - "$llbin/llc" -filetype=obj -mtriple=x86_64-unknown-linux-gnu -o "$covdir/$name.o" "$covdir/$name.g.ll" - objs+=("-object" "$covdir/$name.o") -done <"$covdir/all.lcov" -keep="$covdir/keep.awk" -coreutils cat >"$keep" <<'AWK' -{ buf = buf $0 ORS } -/^SF:/ { keep = index($0, want) > 0 } -/^end_of_record$/ { if (keep) printf "%s", buf; buf = ""; keep = 0 } -AWK coreutils touch lcov.info -goawk -v want="ws-modules/pic-viewer/src/" -f "$keep" "$covdir/all.lcov" >>lcov.info +goawk -v want="ws-modules/pic-viewer/src/" -f .mise/lcov-keep-want.awk "$covdir/all.lcov" >>lcov.info rpt="$covdir/report.txt" "$llbin/llvm-cov" report --instr-profile "$covdir/pic-viewer.profdata" "${objs[@]}" >"$rpt" 2>/dev/null || true diff --git a/.mise/config.linux.toml b/.mise/config.linux.toml index 1add51ff..c1deecd8 100644 --- a/.mise/config.linux.toml +++ b/.mise/config.linux.toml @@ -74,8 +74,7 @@ BINDGEN_EXTRA_CLANG_ARGS = "{{ vars.c_bindgen_args }}" # rpath/pylib flags are shared, from config.toml. # OPENSSL_DIR points openssl-sys at conda's OpenSSL, so anything linking it records conda's libssl soname; # without an rpath the loader only finds it when conda's soname matches the system one (it broke when conda -# bumped to libssl.so.4). pylib_flag does the same for the CPython lib dir -- unlike darwin, where pyo3's -# build script already emits that rpath itself and a second copy draws a linker warning. +# bumped to libssl.so.4). pylib_flag does the same for the CPython lib dir. CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUSTFLAGS = "{{ vars.rpath_flag }} {{ vars.pylib_flag }}" CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS = "{{ vars.rpath_flag }} {{ vars.pylib_flag }}" LIBCLANG_PATH = "{{ vars.a_conda_clangxx }}/lib" diff --git a/.mise/config.macos.toml b/.mise/config.macos.toml index e26f1215..100c79b1 100644 --- a/.mise/config.macos.toml +++ b/.mise/config.macos.toml @@ -23,12 +23,21 @@ mac_libclang = "{{ exec(command='dirname $(dirname $(xcrun --find clang))') }}/l macosx_deployment_target = "{% if arch() == 'arm64' %}11.0{% else %}10.12{% endif %}" [env] -# rpath_flag (from config.toml) records conda's OpenSSL soname so the runtime loader finds it. -# Applies to every rustc call, including the `cargo:` source-build subprocess. config.toml's pylib_flag is -# deliberately absent: pyo3's build script already emits that same rpath on darwin, and passing it twice makes -# Apple's linker report `ld: duplicate -rpath '/lib' ignored` through rustc's `linker_messages` lint. -CARGO_TARGET_AARCH64_APPLE_DARWIN_RUSTFLAGS = "{{ vars.rpath_flag }}" -CARGO_TARGET_X86_64_APPLE_DARWIN_RUSTFLAGS = "{{ vars.rpath_flag }}" +# The rpath/pylib flags (from config.toml) point the runtime loader at conda's OpenSSL and CPython libraries. +# They record the OpenSSL soname and the CPython lib dir, and apply to every rustc call, including the `cargo:` +# source-build subprocess. +# +# pylib_flag stays even though Apple's linker reports `ld: duplicate -rpath '/lib' ignored` on the +# et-ws-pyo3-runner binary, which pyo3's build script has already rpath'd. That warning is only true of the +# binary: the crate's TEST executables get no such rpath from pyo3, so dropping the flag leaves them unable to +# start at all, and nextest cannot even enumerate them -- +# error: creating test list failed +# dyld[94600]: Library not loaded: @rpath/libpython3.13.dylib +# on commit 8e1d10ba04c3baa27ba3ca6d29e9dd7b9d9d7b5a at +# https://github.com/edge-toolkit/core/actions/runs/34925257378/job/104241837125 (and the coverage lane's +# nightly build alongside it). A duplicate-rpath warning on one binary is the cheaper of the two. +CARGO_TARGET_AARCH64_APPLE_DARWIN_RUSTFLAGS = "{{ vars.rpath_flag }} {{ vars.pylib_flag }}" +CARGO_TARGET_X86_64_APPLE_DARWIN_RUSTFLAGS = "{{ vars.rpath_flag }} {{ vars.pylib_flag }}" # See the macosx_deployment_target comment in [vars] above. LIBCLANG_PATH = "{{ vars.mac_libclang }}" MACOSX_DEPLOYMENT_TARGET = "{{ vars.macosx_deployment_target }}" diff --git a/.mise/config.maint.toml b/.mise/config.maint.toml index f273f043..e9223b6a 100644 --- a/.mise/config.maint.toml +++ b/.mise/config.maint.toml @@ -475,10 +475,7 @@ if [ ! -f "$shared_venv/bin/pip" ]; then shared_site="$(coreutils ls -d "$shared_venv"/lib/*/site-packages)" coreutils cp -R "$pip_extracted/pip" "$shared_site/" coreutils cp -R "$pip_extracted"/pip-*.dist-info "$shared_site/" - coreutils cat >"$shared_venv/bin/pip" <<'PIP_SHIM' -#!/bin/sh -exec "$(dirname "$0")/python" -m pip "$@" -PIP_SHIM + coreutils cp "{{ config_root }}/.mise/pip-shim.sh" "$shared_venv/bin/pip" coreutils chmod +x "$shared_venv/bin/pip" fi diff --git a/.mise/config.toml b/.mise/config.toml index 00e5907a..a29a6ce0 100644 --- a/.mise/config.toml +++ b/.mise/config.toml @@ -534,13 +534,20 @@ rpath_flag = "-C link-arg=-Wl,-rpath,{{ vars.conda_openssl }}/lib" # `python` pin), since the minor-version alias dir isn't created everywhere. pylib_flag = "-C link-arg=-Wl,-rpath,{{ vars.a_home }}/.local/share/mise/installs/python/3.13.14/lib" wasm_rustflags = "-C target-cpu=mvp -C target-feature=+mutable-globals,+sign-ext,+nontrapping-fptoint,+reference-types" -# Instrumentation flag pair (leaf var) shared by the wasm coverage builds. +# Instrumentation flags (leaf var) shared by the wasm coverage builds. # The nightly -Z flag adds branch regions to the covmap so the lcov exports carry BRDA records. `a_` prefix so # mise's alphabetical [vars] render defines it before wasm_cov_rustflags uses it. -a_wasm_cov_instr = "-Cinstrument-coverage -Zcoverage-options=branch" +# +# `coverage_nightly` is cargo-llvm-cov's convention, set here by hand because these builds do not go through +# it. The wasm-bindgen-test macro expands to `#[coverage(off)]`, whose feature is still unstable (rust-lang +# /rust#84605 is open, so no nightly stabilises it), and the test crates gate the matching `feature(...)` on +# this cfg. Paired with its own --check-cfg so the cfg stays declared rather than tripping unexpected_cfgs. +a_wasm_cov_ccfg = "--check-cfg=cfg(coverage_nightly)" +a_wasm_cov_instr = "-Cinstrument-coverage -Zcoverage-options=branch --cfg=coverage_nightly" # RUSTFLAGS for the wasm coverage builds: instrument + emit LLVM IR, no profiler runtime (minicov provides it). # Single codegen unit + LTO off so each crate emits one .ll the wasm-cov task can gut and feed to llc/llvm-cov. -wasm_cov_rustflags = "--emit=llvm-ir {{ vars.a_wasm_cov_instr }} -Ccodegen-units=1 -Clto=off -Zno-profiler-runtime" +a_wasm_cov_tail = "-Ccodegen-units=1 -Clto=off -Zno-profiler-runtime" +wasm_cov_rustflags = "--emit=llvm-ir {{ vars.a_wasm_cov_instr }} {{ vars.a_wasm_cov_ccfg }} {{ vars.a_wasm_cov_tail }}" # Coverage fragments the wasm module build commands append unconditionally (empty unless ET_TEST_COVERAGE is set). # wasm_cov is an inline RUSTFLAGS env prefix; the nightly toolchain + conda clang come from the coverage [env]. # The *_feat vars turn on minicov: cargo `--features` for the WASI cdylibs, wasm-pack `-- --features` for browsers @@ -1069,12 +1076,14 @@ description = "Run conftest OPA/Rego policies over the TOML config + lock files" # namespaces are listed explicitly, so the per-file `gha` (YAML) namespace is never evaluated against this # combined TOML input. run = """ -ns_list="cross cargo mise pyproject no_trailing_backslash checksums" -# $ns_list iteration relies on word-splitting; $ns expands to repeated `--namespace ` pairs. +ns_list="cross cargo mise pyproject no_trailing_backslash checksums advisories" +named="Cargo.lock config/upstream-cache/data.toml config/deny.toml" +# Both lists rely on word-splitting when they expand. +# $ns expands to repeated `--namespace ` pairs; $named is a space-separated path list. # shellcheck disable=SC2086 ns=$(for n in $ns_list; do echo -n " --namespace $n"; done) # shellcheck disable=SC2086 -git ls-files '*Cargo.toml' '.mise/config*.toml' '*pyproject.toml' Cargo.lock config/upstream-cache/data.toml | +git ls-files '*Cargo.toml' '.mise/config*.toml' '*pyproject.toml' $named | xargs conftest test --combine --parser toml $ns -p config/conftest/policy """ shell = "{{ vars.task_shell }}" @@ -1187,6 +1196,8 @@ description = "shellcheck every bash-shell mise task body across .mise/config*.t # .mise/shellcheck-mise.jq emits one ` ` line per eligible task (base64 so newlines + # quotes survive the pipe). goawk emits CRLF on Windows, so `tr -d '\r'` normalises each generated script # back to LF (a no-op on Unix) -- without it shellcheck flags every body with SC1017 (literal carriage return). +# `-x` with the repo root as source path follows what a body sources, so a shared fragment is linted with it +# rather than reported unread; the extracted scripts sit under target/, so a relative source needs that root. run = ''' boot="{{ vars.config_root_fwd }}/target/scratch/shellcheck-mise" coreutils rm -rf "$boot" @@ -1203,7 +1214,7 @@ for cfg in .mise/config*.toml; do } | coreutils tr -d '\r' >"$out" done done -shellcheck --shell=bash --exclude=SC2034,SC2154 "$boot"/*.sh +shellcheck --shell=bash -x --source-path="{{ vars.config_root_fwd }}" --exclude=SC2034,SC2154 "$boot"/*.sh ''' shell = "{{ vars.task_shell }}" @@ -1544,16 +1555,20 @@ run = "osv-scanner {{ vars.osv_locks }} --config config/osv-scanner.toml" [tasks."gen:osv-scanner"] description = "Regenerate config/osv-scanner.toml from config/deny.toml's [advisories].ignore list" # osv-scanner and cargo-deny must ignore the same advisory IDs. -# config/deny.toml is the source of truth (it carries the per-ID rationale). rg + coreutils only, so +# config/deny.toml is the source of truth (it carries the per-ID rationale). goawk + coreutils only, so # the `dependencies` workflow can run it (via mise) next to the audit binaries. Both RUSTSEC-YYYY-NNNN and # GHSA-xxxx-xxxx-xxxx ids are extracted, so a GHSA-only advisory (one RustSec hasn't assigned) still filters. +# +# An entry's `expires` date rides along as `ignoreUntil`, so the entry stops being honoured the day it lapses. +# An id that appears only in prose (the GHSA ones, which have no entry of their own) carries no date and stays +# ignored indefinitely. run = """ { # backticks in the echoed line are literal markdown, not command substitution. # shellcheck disable=SC2016 echo '# AUTO-GENERATED from config/deny.toml by `mise run gen:osv-scanner`.' echo 'IgnoredVulns = [' - rg -oN -r ' { id = $0 },' '"(RUSTSEC-[0-9]{4}-[0-9]{4}|GHSA(-[0-9a-z]{4}){3})"' config/deny.toml | coreutils sort -u + goawk -f .mise/osv-ids.awk config/deny.toml | coreutils sort -u echo ']' } >config/osv-scanner.toml """ diff --git a/.mise/config.windows.toml b/.mise/config.windows.toml index 3ecbd774..b06d105f 100644 --- a/.mise/config.windows.toml +++ b/.mise/config.windows.toml @@ -415,10 +415,7 @@ elif [ "${ENABLE_RUSTPYTHON_PIPX_BOOTSTRAP:-}" = "1" ]; then shared_site="$(coreutils ls -d "$shared_venv"/lib/*/site-packages)" coreutils cp -R "$pip_extracted/pip" "$shared_site/" coreutils cp -R "$pip_extracted"/pip-*.dist-info "$shared_site/" - coreutils cat >"$shared_venv/bin/pip" <<'PIP_SHIM' -#!/bin/sh -exec "$(dirname "$0")/python" -m pip "$@" -PIP_SHIM + coreutils cp "{{ vars.config_root_fwd }}/.mise/pip-shim.sh" "$shared_venv/bin/pip" coreutils chmod +x "$shared_venv/bin/pip" fi else diff --git a/.mise/cov-branch-assert.jq b/.mise/cov-branch-assert.jq new file mode 100644 index 00000000..1d13de5b --- /dev/null +++ b/.mise/cov-branch-assert.jq @@ -0,0 +1,19 @@ +# Names every libs/ crate in $libs that an llvm-cov JSON summary does not show at 100% branch coverage. +# That JSON (`--format=text`, confusingly) carries a per-file `summary.branches` object, so `.data[0].files[]` +# already holds everything this reads. A crate with no records at all is reported rather than passing +# vacuously: a crate dropping out of the report is how coverage silently stops being measured. +($libs | split(" ")) as $names +| .data[0].files as $files +| [ $names[] + | . as $n + | ("/libs/" + $n + "/") as $dir + | ($files | map(select(.filename | contains($dir)))) as $hit + | if ($hit | length) == 0 + then "libs/" + $n + ": no records in the report -- this run never exercised the crate" + else ($hit[] + | select(.summary.branches.covered < .summary.branches.count) + | "libs/" + $n + ": " + .filename + " " + + (.summary.branches.covered | tostring) + "/" + + (.summary.branches.count | tostring) + " branches") + end ] +| .[] diff --git a/.mise/lcov-drop-external.awk b/.mise/lcov-drop-external.awk new file mode 100644 index 00000000..53e95dfc --- /dev/null +++ b/.mise/lcov-drop-external.awk @@ -0,0 +1,7 @@ +# Keeps only the workspace records of an lcov file, dropping dependency and toolchain sources. +# A wasm covmap references every source its module linked, so dependency crates under ~/.cargo/registry and +# toolchain std under ~/.rustup ride along. Neither is in VCS and both skew the aggregate metric, so a record +# whose SF: path sits in one of those trees never reaches lcov.info. +{ buf = buf $0 ORS } +/^SF:/ { p = substr($0, 4); drop = (index(p, "/.cargo/") || index(p, "/.rustup/") || index(p, "/rustc/")) } +/^end_of_record$/ { if (!drop) printf "%s", buf; buf = ""; drop = 0 } diff --git a/.mise/lcov-keep-want.awk b/.mise/lcov-keep-want.awk new file mode 100644 index 00000000..8bb5c0bd --- /dev/null +++ b/.mise/lcov-keep-want.awk @@ -0,0 +1,6 @@ +# Keeps only the lcov records whose SF: path contains the `want` substring, passed with `goawk -v want=...`. +# One substring per pass, which is all this filter can express: a caller that needs several source trees runs +# it once per tree and appends each result. +{ buf = buf $0 ORS } +/^SF:/ { keep = index($0, want) > 0 } +/^end_of_record$/ { if (keep) printf "%s", buf; buf = ""; keep = 0 } diff --git a/.mise/llvm-cov-gut.awk b/.mise/llvm-cov-gut.awk new file mode 100644 index 00000000..f2f8b251 --- /dev/null +++ b/.mise/llvm-cov-gut.awk @@ -0,0 +1,18 @@ +# Guts every function body in an instrumented .ll, keeping only the signatures and the __llvm_covmap records. +# llvm-cov cannot read a coverage map from a .wasm, so the gutted module is compiled to a fixed x86_64 ELF +# object instead; the hit counts come from the .profraw, so the object needs no code. The target-cpu and +# target-features attributes go with the bodies: browser .ll carries a wasm `mvp` cpu and wasm features that +# make llc reject the x86_64 target with `64-bit code requested on a subtarget that doesn't support it`. The +# WASI guest .ll carries neither attribute, so there the strip is a no-op. An attribute group left empty by it +# is refilled with `nounwind`, which llc parses; an empty `{ }` group it rejects. +/^target datalayout/ { next } +/^target triple/ { next } +/^define/ { print; print "start:"; print " unreachable"; print "}"; skip = 1; next } +skip && /^}/ { skip = 0; next } +skip { next } +{ + gsub(/"target-cpu"="[^"]*"/, "") + gsub(/"target-features"="[^"]*"/, "") + if ($0 ~ /^attributes #/ && $0 ~ /\{[[:space:]]*\}/) sub(/\{[[:space:]]*\}/, "{ nounwind }") + print +} diff --git a/.mise/osv-ids.awk b/.mise/osv-ids.awk new file mode 100644 index 00000000..6ea57a8f --- /dev/null +++ b/.mise/osv-ids.awk @@ -0,0 +1,35 @@ +# Prints one `IgnoredVulns` row per advisory id found in config/deny.toml. +# Both RUSTSEC-YYYY-NNNN and GHSA-xxxx-xxxx-xxxx ids are extracted, so a GHSA-only advisory (one RustSec has +# not assigned) still filters. An entry's `expires` date rides along as an `ignoreUntil` value. +BEGIN { q = "\"" } +{ + # An id can appear more than once on a line, and in prose as well as in an entry, so scan the whole line. + # The expiry is read from the line as a whole: only a real ignore entry carries a `reason`, so a bare id + # mentioned in a comment keeps the undated form and stays ignored indefinitely. + rest = $0 + while (match(rest, /"(RUSTSEC-[0-9]{4}-[0-9]{4}|GHSA(-[0-9a-z]{4}){3})"/)) { + # Save this match's span before any other match() call, which would overwrite RSTART/RLENGTH. + # Advancing `rest` by the expiry match's span instead leaves the id still in the remainder, so the loop + # matches it again and never terminates -- goawk spins on the first dated entry rather than failing. + s = RSTART + l = RLENGTH + id = substr(rest, s + 1, l - 2) + seen[id] = 1 + if (match($0, /reason = "expires [0-9]{4}-[0-9]{2}-[0-9]{2}"/)) { + d = substr($0, RSTART, RLENGTH) + sub(/.*expires /, "", d) + sub(/"$/, "", d) + expiry[id] = d + } + rest = substr(rest, s + l) + } +} +END { + for (id in seen) { + if (id in expiry) { + print " { id = " q id q ", ignoreUntil = " expiry[id] " }," + } else { + print " { id = " q id q " }," + } + } +} diff --git a/.mise/pip-shim.sh b/.mise/pip-shim.sh new file mode 100644 index 00000000..1399ad1c --- /dev/null +++ b/.mise/pip-shim.sh @@ -0,0 +1,3 @@ +#!/bin/sh +# `bin/pip` for a pipx shared-libs venv created with `--without-pip`, alongside the dropped-in site-packages. +exec "$(dirname "$0")/python" -m pip "$@" diff --git a/.mise/wasm-ll-objs.sh b/.mise/wasm-ll-objs.sh new file mode 100644 index 00000000..a14ed2cf --- /dev/null +++ b/.mise/wasm-ll-objs.sh @@ -0,0 +1,29 @@ +# Turns a wasm-bindgen test build's instrumented .ll into the llvm-cov objects an export needs. +# Sourced by the coverage tasks that instrument a browser module, which differ only in which module they +# build: the caller sets `task` (named in the failure message), `covdir` and `llbin` beforehand, and reads +# the populated `objs` array afterwards. +# +# Both build-dir layouts are searched. A plain `deps/*.ll` glob broke when nightly made +# `-Z build-dir-new-layout` the default: per-crate intermediates moved to `/build///out/`, +# the glob matched nothing, and the unexpanded pattern reached goawk as a literal filename -- `file +# "target/wasm32-unknown-unknown/debug/deps/*.ll" not found` on commit 12bfe419 at +# https://github.com/edge-toolkit/core/actions/runs/30797697402/job/91634929015. `find` also avoids the +# silent-nullglob trap the literal-pattern failure exposed. +objs=() +lls="$(find target/wasm32-unknown-unknown/debug -path '*/build/*' -name '*.ll' 2>/dev/null)" +if [ -z "$lls" ]; then + lls="$(find target/wasm32-unknown-unknown/debug/deps -name '*.ll' 2>/dev/null)" +fi +if [ -z "$lls" ]; then + echo "$task: no instrumented .ll under target/wasm32-unknown-unknown/debug" >&2 + exit 1 +fi +while IFS= read -r ll; do + [ -n "$ll" ] || continue + name="$(coreutils basename "$ll" .ll)" + goawk -f .mise/llvm-cov-gut.awk "$ll" >"$covdir/$name.g.ll" + "$llbin/llc" -filetype=obj -mtriple=x86_64-unknown-linux-gnu -o "$covdir/$name.o" "$covdir/$name.g.ll" + objs+=("-object" "$covdir/$name.o") +done <` impl, which has to name the type. anyhow = "1" @@ -244,7 +244,7 @@ wasmtime = { version = "47.0.3", features = ["async", "component-model"] } # Emitting the runner's host bindings as a checked-in source file keeps ws-wasi-runner self-contained: the # macro read WIT from `../../generated/specs/wit`, which `cargo package` cannot include, so the crate failed # to build from its own tarball. Named `internal` upstream, so treat its API as unstable and pinned to the -# wasmtime version above; gen-specs-check catches any drift in what it emits. +# wasmtime version above; the drift check catches any change in what it emits. wasmtime-internal-wit-bindgen = { version = "47.0.4", features = ["async", "component-model-async"] } wasmtime-wasi = "47.0.3" wasmtime-wasi-nn = { version = "47", default-features = false, features = ["onnx"] } diff --git a/config/ast-grep/rules/no-string-literal-line-continuation.yaml b/config/ast-grep/rules/no-string-literal-line-continuation.yaml index c2ab0b62..0eb555d9 100644 --- a/config/ast-grep/rules/no-string-literal-line-continuation.yaml +++ b/config/ast-grep/rules/no-string-literal-line-continuation.yaml @@ -5,8 +5,8 @@ message: | Backslash-newline inside a Rust string literal is a line continuation (the `\` + newline + leading whitespace all get elided at parse time). Banned per the repo-wide no-trailing-backslash rule. Use `concat!("a", "b")` to assemble a multi-line string literal, keep it on one line, or drop to a raw string `r"..."` if the content really - needs literal backslashes at line ends. The semgrep `no-trailing-backslash` rule catches the same pattern - repo-wide via generic regex; this ast-grep variant is the Rust-AST-aware companion (and runs faster). + needs literal backslashes at line ends. A generic-regex rule catches the same pattern repo-wide; this + variant is the Rust-AST-aware companion, and runs faster. rule: kind: string_literal regex: '\\\n' diff --git a/config/ast-grep/rules/no-string-new.yaml b/config/ast-grep/rules/no-string-new.yaml index 600b1ec6..fe1d4388 100644 --- a/config/ast-grep/rules/no-string-new.yaml +++ b/config/ast-grep/rules/no-string-new.yaml @@ -2,7 +2,7 @@ id: no-string-new language: Rust severity: error message: | - `String::new()` is banned (DeepSource RS-W1079): construct empty strings with `String::default()` instead, so + `String::new()` is banned (`RS-W1079`): construct empty strings with `String::default()` instead, so the zero value routes through the `Default` impl like every other default-constructed value in the codebase. rule: pattern: String::new() diff --git a/config/clang-tidy.yaml b/config/clang-tidy.yaml index 79372cb9..ea261b5c 100644 --- a/config/clang-tidy.yaml +++ b/config/clang-tidy.yaml @@ -2,6 +2,6 @@ # Applied via `mise run clang-tidy-check` (passed as --config-file=). # Bug-finding analysis only; opinionated readability/style checks are left off to avoid churn on the small C surface. # cppcoreguidelines-avoid-non-const-global-variables is pulled in specifically (not the whole cppcoreguidelines set) -# to mirror DeepSource's cxx CXX-W2009, so the mingw-shim's ABI globals are caught here too, not only on DeepSource. +# to mirror `CXX-W2009`, so the mingw-shim's ABI globals are caught here too, not only after a push. Checks: "clang-analyzer-*,bugprone-*,performance-*,portability-*,cppcoreguidelines-avoid-non-const-global-variables" WarningsAsErrors: "*" diff --git a/config/clippy.toml b/config/clippy.toml index 6b2f2908..a6e95de4 100644 --- a/config/clippy.toml +++ b/config/clippy.toml @@ -6,13 +6,13 @@ allow-print-in-tests = true allow-unwrap-in-tests = true allow-useless-vec-in-tests = true -# Compile-time bans -- a second layer under the matching config/ast-grep/rules/*, which catch these at diff time. +# Compile-time bans -- a second layer under the structural rules that catch the same shapes at diff time. # `.expect()` itself is NOT listed here. `disallowed_methods` has no test exemption and does not skip # proc-macro-generated code, so it would flag the `.build().expect("Failed building the Runtime")` that the # `#[tokio::test]` macro expands into -- breaking every async test, even though that `.expect()` isn't ours. The # `clippy::expect_used` restriction lint (denied workspace-wide) is the right tool instead: it bans hand-written # `.expect()` everywhere but skips macro-generated calls, so `#[tokio::test]` is unaffected. `disallowed-methods` -# covers `.expect_err()` (which `expect_used` misses -- use `.unwrap_err()`) and mirrors ast-grep's `no-current-dir`. +# covers `.expect_err()` (which `expect_used` misses -- use `.unwrap_err()`) and the current-dir ban below. disallowed-methods = [ { path = "std::env::current_dir", reason = "use get_project_root() / et_path::find_project_root()" }, { path = "std::result::Result::expect_err", reason = "use .unwrap_err() -- no message string needed" }, diff --git a/config/conftest/policy/advisories/advisories.rego b/config/conftest/policy/advisories/advisories.rego new file mode 100644 index 00000000..eb4b11c7 --- /dev/null +++ b/config/conftest/policy/advisories/advisories.rego @@ -0,0 +1,54 @@ +# Review dates on the advisory ignores in config/deny.toml, evaluated from the combined TOML input. +# Run with `--namespace advisories` by conftest-check-toml, which feeds deny.toml alongside the other config +# TOML, so `input` is the --combine array of {path, contents} rather than one file's contents. +# +# Every entry in `[advisories].ignore` is an accepted finding, and an accepted finding that nobody looks at +# again is indistinguishable from one nobody noticed. So each carries a review date in its `reason`, and this +# fails the build once that date passes -- the entry then has to be dropped, fixed, or consciously re-dated, +# which is a reviewable one-line diff rather than silence. +# +# The undated rule is what keeps that true of future entries: a new ignore added as a bare id, or with a +# reason this cannot parse, fails immediately rather than quietly becoming the one exception with no deadline. +package advisories + +# The `reason` spelling the date is read from, anchored so trailing prose cannot hide a second date. +expiry_pattern := `^expires ([0-9]{4}-[0-9]{2}-[0-9]{2})$` + +undated_msg := "config/deny.toml: advisory ignore %q has no `reason = \"expires YYYY-MM-DD\"` review date" + +lapsed_msg := "config/deny.toml: advisory ignore %q lapsed on %s -- drop it, fix the finding, or re-date it" + +entries contains entry if { + some file in input + file.path == "config/deny.toml" + some entry in file.contents.advisories.ignore +} + +# An entry is either an `{ id, reason }` object or a bare id string; both need naming in a message. +advisory_id(entry) := entry.id if is_object(entry) + +advisory_id(entry) := entry if is_string(entry) + +dated[id] := date if { + some entry in entries + is_object(entry) + [[_, date]] := regex.find_all_string_submatch_n(expiry_pattern, entry.reason, 1) + id := entry.id +} + +deny contains msg if { + some entry in entries + id := advisory_id(entry) + not dated[id] + msg := sprintf(undated_msg, [id]) +} + +# Nanoseconds in a day, added to the parsed midnight so an entry survives the whole of its review date. +day_ns := 86400000000000 + +deny contains msg if { + some id, date in dated + midnight := time.parse_rfc3339_ns(concat("", [date, "T00:00:00Z"])) + time.now_ns() >= midnight + day_ns + msg := sprintf(lapsed_msg, [id, date]) +} diff --git a/config/conftest/policy/advisories/advisories_test.rego b/config/conftest/policy/advisories/advisories_test.rego new file mode 100644 index 00000000..214cd80b --- /dev/null +++ b/config/conftest/policy/advisories/advisories_test.rego @@ -0,0 +1,77 @@ +# Unit tests for the advisory review-date rule, run by `conftest verify`. +# +# The lapse rule reads the wall clock, which makes it the one policy here whose result changes on its own: a +# real run passes today and fails after the date without anything in the repo changing. So every case pins +# `time.now_ns` with `with`, and the dates in the fixtures are chosen relative to that pinned instant rather +# than to today -- otherwise the tests would themselves expire. +package advisories_test + +import data.advisories + +# 2026-10-15T00:00:00Z in nanoseconds, the instant the fixtures are written around. +review_day := 1792022400000000000 + +# One day either side, for the boundary cases. +day_ns := 86400000000000 + +deny_file(entries) := [{"path": "config/deny.toml", "contents": {"advisories": {"ignore": entries}}}] + +dated(id, date) := {"id": id, "reason": sprintf("expires %s", [date])} + +names(msgs, fragment) if { + some msg in msgs + contains(msg, fragment) +} + +test_entry_still_inside_its_review_date_passes if { + msgs := advisories.deny with input as deny_file([dated("RUSTSEC-2026-0285", "2026-10-15")]) + with time.now_ns as review_day + count(msgs) == 0 +} + +# The entry survives the whole of its review date, so the last second of that day is still fine. +test_entry_passes_until_the_end_of_its_review_date if { + msgs := advisories.deny with input as deny_file([dated("RUSTSEC-2026-0285", "2026-10-15")]) + with time.now_ns as (review_day + day_ns) - 1 + count(msgs) == 0 +} + +test_entry_lapses_the_day_after if { + msgs := advisories.deny with input as deny_file([dated("RUSTSEC-2026-0285", "2026-10-15")]) + with time.now_ns as review_day + day_ns + names(msgs, "lapsed on 2026-10-15") +} + +test_bare_id_without_a_review_date_is_flagged if { + msgs := advisories.deny with input as deny_file(["RUSTSEC-2026-0285"]) + with time.now_ns as review_day + names(msgs, "has no") +} + +# A reason that says something else is not a date, and must not be read as one. +test_entry_whose_reason_carries_no_date_is_flagged if { + entry := {"id": "RUSTSEC-2026-0285", "reason": "upstream has not released a fix"} + msgs := advisories.deny with input as deny_file([entry]) with time.now_ns as review_day + names(msgs, "has no") +} + +# Anchored, so trailing prose cannot smuggle a date past the check. +test_entry_with_prose_after_the_date_is_flagged if { + entry := {"id": "RUSTSEC-2026-0285", "reason": "expires 2026-10-15 unless upstream moves"} + msgs := advisories.deny with input as deny_file([entry]) with time.now_ns as review_day + names(msgs, "has no") +} + +test_each_lapsed_entry_is_reported_separately if { + entries := [dated("RUSTSEC-2026-0285", "2026-10-15"), dated("GHSA-3w8q-xq97-5j7x", "2026-10-15")] + msgs := advisories.deny with input as deny_file(entries) with time.now_ns as review_day + day_ns + names(msgs, "RUSTSEC-2026-0285") + names(msgs, "GHSA-3w8q-xq97-5j7x") +} + +# Only deny.toml carries this table; another TOML file in the combined input is none of this rule's business. +test_other_files_in_the_combined_input_are_ignored if { + other := [{"path": "Cargo.toml", "contents": {"advisories": {"ignore": ["RUSTSEC-2026-0285"]}}}] + msgs := advisories.deny with input as other with time.now_ns as review_day + count(msgs) == 0 +} diff --git a/config/conftest/policy/cargo/cargo.rego b/config/conftest/policy/cargo/cargo.rego index db2c036f..90cab5fc 100644 --- a/config/conftest/policy/cargo/cargo.rego +++ b/config/conftest/policy/cargo/cargo.rego @@ -33,8 +33,8 @@ dep contains [file.path, name, spec] if { # Banned crates -> rejection reason. # Members must use workspace = true, so the root's [workspace.dependencies] is the only place a ban can bite. # -# `anyhow` is not listed here, and is instead constrained at the source level by the semgrep rule -# `anyhow-only-in-error-rs`: a crate may depend on it, but may only name it in an `error.rs` `#[from]` variant. +# `anyhow` is not listed here, and is instead constrained at the source level: a crate may depend on it, but +# may only name it in an `error.rs` `#[from]` variant. # A blanket dependency ban was unworkable because `?` on a foreign `anyhow::Result` needs # `From`, which cannot be written without naming the type. banned := { diff --git a/config/conftest/policy/dockerfile/dockerfile.rego b/config/conftest/policy/dockerfile/dockerfile.rego index 8ee4c3f2..3f175668 100644 --- a/config/conftest/policy/dockerfile/dockerfile.rego +++ b/config/conftest/policy/dockerfile/dockerfile.rego @@ -124,8 +124,8 @@ deny contains msg if { # expansion to bash, which only evaluates the line inside the correct package-manager branch. ARG values needed # inside the body are promoted to ENV before the RUN so bash can resolve them from the environment. # -# The `set -euo pipefail` first-line check itself is a semgrep rule (the Dockerfile parser flattens heredoc bodies out -# of the AST, so conftest can't see them; semgrep operates on the raw file text). +# The `set -euo pipefail` first-line check itself lives in a raw-text rule: the Dockerfile parser flattens heredoc +# bodies out of the AST, so this policy never sees them. deny contains msg if { some file in input is_array(file.contents) diff --git a/config/conftest/policy/gha/gha.rego b/config/conftest/policy/gha/gha.rego index cc5e1955..5422a2c6 100644 --- a/config/conftest/policy/gha/gha.rego +++ b/config/conftest/policy/gha/gha.rego @@ -1,6 +1,6 @@ # GitHub Actions workflow policy, evaluated per file. # conftest reads each .yaml independently (no --combine, since there are no cross-file YAML rules). Replicates the -# gha-* ast-grep rules; running both is fine. Selected with `--namespace gha`, so it only runs against workflow YAML, +# structural gha-* rules; running both is fine. Selected with `--namespace gha`, so it only runs against workflow YAML, # never the TOML inputs. package gha @@ -33,7 +33,7 @@ step_shell_allowed(step) if { } # Every workflow must declare MISE_ENV at the workflow level so the set of loaded language envs is visible at a glance. -# This avoids per-job `mise run print-all-langs` runtime resolution. The matching `Show MISE_ENV` step in each job +# That avoids resolving the list per job at runtime. The matching `Show MISE_ENV` step in each job # echoes the value into the CI log. deny contains msg if { not input.env.MISE_ENV diff --git a/config/conftest/policy/no_trailing_backslash/no_trailing_backslash.rego b/config/conftest/policy/no_trailing_backslash/no_trailing_backslash.rego index 4d0b6c2d..75e1646f 100644 --- a/config/conftest/policy/no_trailing_backslash/no_trailing_backslash.rego +++ b/config/conftest/policy/no_trailing_backslash/no_trailing_backslash.rego @@ -1,4 +1,4 @@ -# Defense-in-depth duplicate of config/semgrep/no-trailing-backslash.yaml. +# Defense-in-depth duplicate of the repo-wide raw-text trailing-backslash ban. # Flags the trailing-backslash line continuation we don't want anywhere in the repo, by walking every string # anywhere in the combined input and flagging any literal `\` immediately followed by a newline. # @@ -9,8 +9,8 @@ # block-scalar values, similarly preserved by the YAML parser. # - Dockerfile: NOT covered here -- conftest's dockerfile parser already consumes line-continuation backslashes when # it joins each instruction's value, so by the time Rego sees the parsed input the backslashes are gone. The -# semgrep rule (which scans the raw text) is the source of truth for Dockerfile coverage, allowlisting Dockerfile* -# per its `paths.exclude` (Dockerfile RUN bodies legitimately need line continuations). +# raw-text rule is the source of truth for Dockerfile coverage, and allowlists `Dockerfile*` there because +# Dockerfile RUN bodies legitimately need line continuations. package no_trailing_backslash # Flag any string leaf containing `\` followed by `\n` (LF), reporting its path key in the message. @@ -23,7 +23,7 @@ deny contains msg if { is_string(value) regex.match(`\\\n`, value) msg := sprintf( - "%s: trailing-backslash line continuation in string at %v -- not allowed (see no-trailing-backslash semgrep rule)", + "%s: trailing-backslash line continuation in string at %v -- the form is banned repo-wide", [file.path, path], ) } diff --git a/config/deny.toml b/config/deny.toml index e4df4c86..50f40105 100644 --- a/config/deny.toml +++ b/config/deny.toml @@ -14,34 +14,47 @@ ignore = [ # Pulled by deno_core for snapshot / cache serialization -- its only consumer, requiring ^1 through its # newest release. Unmaintained != vulnerable. No upgrade clears this: the advisory covers every version of # the crate, 2.x and 3.0.0 included, so only deno_core dropping bincode outright would. - "RUSTSEC-2025-0141", + { id = "RUSTSEC-2025-0141", reason = "expires 2026-10-15" }, # paste 1.0.15 (unmaintained). # Proc-macro pulled by v8 (rusty_v8) via deno_core / serde_v8; expands at compile time only, so there is no # runtime exposure. Every published v8 release through 152.2.0 declares it as a non-optional dependency, so no # upgrade clears this -- only a rusty_v8 move to the maintained pastey fork. Awaiting that migration. - "RUSTSEC-2024-0436", + { id = "RUSTSEC-2024-0436", reason = "expires 2026-10-15" }, # rustls-pemfile 2.2.0 (unmaintained; final release -- parsing moved into rustls-pki-types). # PEM cert parser for loading the system CA bundle. Unmaintained, not vulnerable. Pulled directly by both # deno_native_certs and deno_tls, each at its newest release, so no bump drops it. rustls-native-certs # migrated at 0.8.1 and our 0.8.4 copy is clean; the affected 0.7.3 copy is pinned by deno_native_certs. - "RUSTSEC-2025-0134", + { id = "RUSTSEC-2025-0134", reason = "expires 2026-10-15" }, + # rustls 0.23.40 -- TLS 1.3 handshake messages accepted across encryption-level boundaries. + # The only real vulnerability in this list rather than an unmaintained-crate notice, so unlike its neighbours + # it is time-boxed instead of held open until upstream moves. RFC 8446 section 5.1 requires a connection to + # terminate with `unexpected_message` when a handshake message spans a key change; rustls accepted one + # instead, for example a plaintext `EncryptedExtensions` packed into the same record as the `ServerHello`. + # CVSS 5.3 MEDIUM, low confidentiality impact, client and server roles alike; fixed in rustls 0.23.45. + # No bump reaches that fix: deno_tls pins `rustls = "=0.23.40"` by equality, and every release through + # 0.245.0 still does, so bumping deno_runtime changes nothing. A [patch.crates-io] on rustls does not help + # either -- a patch replaces a source, not a requirement, so a 0.23.45 patch is reported unused and the + # pinned copy stays. Only patching deno_tls itself clears it, which means carrying a fork of it. + # When the expiry below lapses the answer is one of two things: deno_tls has relaxed the pin, so drop this + # entry; or it has not, so carry a fork of deno_tls rather than extend the date again. + { id = "RUSTSEC-2026-0285", reason = "expires 2026-10-15" }, # smartstring 1.0.1 (unmaintained; repo archived 2026-05-03). # Pulled only by the swc stack (swc_ecma_lexer/parser 26/27) under deno_ast -> deno_runtime. Maintenance # notice, not a vulnerability. Upstream swc migrated to compact_str at swc_ecma_lexer 41.0.0 and # swc_ecma_parser 42.0.0, but deno_ast pins both by equality (=26.0.0 / =27.0.7) through its newest release, # so no bump on our side reaches them. Drop when a deno_runtime/deno_ast bump pulls the migrated swc. - "RUSTSEC-2026-0249", + { id = "RUSTSEC-2026-0249", reason = "expires 2026-10-15" }, # hickory-proto 0.25.2 NSEC3 closest-encloser proof unbounded loop (DoS on DNSSEC validation). # Pulled by deno_net for resolver. The advisory scopes reachability to builds carrying dnssec-ring or # dnssec-aws-lc-rs, and our resolution enables neither -- cargo tree -i hickory-proto -f "{p} [{f}]" reports # default,futures-io,serde,std,tokio -- so DnssecDnsHandle is never compiled in. (Cargo.lock listing ring # under hickory-proto is not evidence otherwise; it records optional deps regardless of activation.) The fix # landed in 0.26.0-beta.1, but deno_net requires ^0.25.2 through its newest release, so it is out of reach. - "RUSTSEC-2026-0118", + { id = "RUSTSEC-2026-0118", reason = "expires 2026-10-15" }, # hickory-proto 0.25.2 O(n^2) name compression during message encode. # Encoder side, hit only when we *send* a DNS message. The runner uses the OS resolver via hickory. It # never crafts outbound DNS frames with attacker-controlled name lists. - "RUSTSEC-2026-0119", + { id = "RUSTSEC-2026-0119", reason = "expires 2026-10-15" }, # rand 0.8.5 unsound ThreadRng reseed when a custom `log` logger calls rand::rng() (UB via aliased &mut). # Transitive via deno_crypto / deno_fs / deno_node (web-runner), used for non-cryptographic randomness # (jitter, salt scratchpads). The advisory's first precondition is the `log` feature, which our resolution @@ -49,7 +62,7 @@ ignore = [ # Fixed in the 0.8 line at 0.8.6, which deleted the log optional dep -- but deno_crypto pins rand "=0.8.5" # by equality through its newest release, so neither a lockfile bump nor a deno bump moves it. cargo-deny # does not match this id and warns advisory-not-detected; the entry stays because osv-scanner does flag it. - "RUSTSEC-2026-0097", + { id = "RUSTSEC-2026-0097", reason = "expires 2026-10-15" }, # rsa 0.9.10 Marvin Attack timing sidechannel on RSA decryption. # No patched version exists -- the advisory lists none, upstream is still migrating to a constant-time # implementation, and 0.9.10 is the newest stable (the 0.10 line is release candidates, equally affected), @@ -57,7 +70,7 @@ ignore = [ # RSA keys or decrypt RSA payloads. Only TLS server cert verification, which uses *signature* checks, no # decryption. Not reachable unless guest JS explicitly imports crypto.subtle and performs RSA-OAEP decrypt # with a private key. Our modules don't. - "RUSTSEC-2023-0071", + { id = "RUSTSEC-2023-0071", reason = "expires 2026-10-15" }, # h2 0.3.27 unbounded empty DATA frames (low-severity HTTP/2 DoS: undrained streams grow memory). # Accepted for the 0.3 copy only. hyperium patched the 0.4 line (>= 0.4.16, which Cargo.lock now pins for the # hyper/reqwest path) and shipped no 0.3 backport, so 0.3.27 is the last release of a dead line. It arrives @@ -67,7 +80,7 @@ ignore = [ # this when it ships one built on h2 0.4. cargo-deny 0.20 ignores by id only (no version scoping), so this # also mutes the 0.4 line -- Cargo.lock pins 0.4.16 there, and a bump below that would have to come back # through this entry, so re-check both copies when touching it. - "RUSTSEC-2026-0258", + { id = "RUSTSEC-2026-0258", reason = "expires 2026-10-15" }, # org.mozilla:rhino 1.7.15 -- high-CPU/DoS in toFixed. Maven ecosystem: the Java toolchain, not a Rust crate. # Build-time only: pulled by the teavm-maven-plugin (TeaVM's Java->JS compiler) via teavm-relocated-libs-rhino; # not shipped in any artifact, and its only input is our own trusted Java, so unreachable. pom.xml overrides the @@ -75,7 +88,7 @@ ignore = [ # relocated lib. A TeaVM bump does not help either: teavm-relocated-libs-rhino still declares rhino 1.7.15 at # 0.15.0, the newest release. Flagged now that osv-scanner-check scans pom.xml; a Maven id so cargo-deny reports # it unknown (osv-scanner honours it). Drop when a TeaVM release declares 1.7.15.1 or later. - "GHSA-3w8q-xq97-5j7x", + { id = "GHSA-3w8q-xq97-5j7x", reason = "expires 2026-10-15" }, # adm-zip 0.5.18 arbitrary file write via a crafted archive entry (npm ecosystem, Node.js; fixed 0.6.0). # Reaches us only through onnxruntime-node, which @huggingface/transformers declares for its Node backend -- # llm1 serves the browser bundle (dist/transformers.web.js) instead, and that file is self-contained and @@ -86,17 +99,17 @@ ignore = [ # target/. `osv-scanner-check` scans the committed lockfiles instead, does not see these packages, and so # reports both ids under "unused ignores" -- that report is NOT grounds for deleting them. Doing so turns # the dependencies workflow red, because the npm scan then has nothing to filter these findings with. - "GHSA-xcpc-8h2w-3j85", + { id = "GHSA-xcpc-8h2w-3j85", reason = "expires 2026-10-15" }, # adm-zip 0.5.18 extraction follows symlinks in the destination directory, allowing arbitrary file overwrite. # Same package and same unreachable path as the entry above -- nothing we ship opens an archive at all. This # one has no patched release: the advisory covers >= 0.5.9 through 0.6.0 inclusive, so the 0.6.0 that clears # GHSA-xcpc-8h2w-3j85 does not clear this, and only onnxruntime-node dropping adm-zip will. - "GHSA-vwc7-r8mq-g2x9", + { id = "GHSA-vwc7-r8mq-g2x9", reason = "expires 2026-10-15" }, # sharp 0.34.5 heap overflow in bundled libvips decoding a malformed image (npm ecosystem, Node.js). # Fixed in 0.35.0. Same path and reasoning as adm-zip above: sharp is transformers' Node-side image decoder, # and the served browser bundle decodes images through the DOM instead. No bump helps here either -- # transformers 4.2.0 requires sharp ^0.34.5, which excludes 0.35.0, so widening it is upstream's call. - "GHSA-f88m-g3jw-g9cj", + { id = "GHSA-f88m-g3jw-g9cj", reason = "expires 2026-10-15" }, # sharp 0.34.5 bundles a libheif carrying GHSA-g89c-p67h-r497 and GHSA-2jg2-4ch7-h545. # Reached only by decoding a HEIF/AVIF image, on the same package and the same unreachable path as the entry # above. Fixed in 0.35.4 rather than the 0.35.0 that clears GHSA-f88m-g3jw-g9cj, so it is the later bound @@ -106,7 +119,7 @@ ignore = [ # onnxruntime-node release stops carrying adm-zip -- no adm-zip version fixes GHSA-vwc7-r8mq-g2x9, so waiting # for one is not a plan. All four are npm-ecosystem ids that cargo-deny reports unknown; osv-scanner honours # them. - "GHSA-rgj7-g3m4-5g8c", + { id = "GHSA-rgj7-g3m4-5g8c", reason = "expires 2026-10-15" }, ] [licenses] @@ -153,12 +166,12 @@ license-files = [{ path = "LICENSE", hash = 0x0bcebb0f }] # Warn (not deny) on multiple-versions. # Useful to surface duplicate transitive dep trees without blocking on every cargo update. multiple-versions = "warn" -# Wildcard version specs in member crates are already forbidden by the taplo `no-wildcard-or-git-deps` schema. -# Double-cover here so it shows up in a security-style report too. +# Wildcard version specs in member crates are already forbidden where they are declared. +# Double-cover here so they show up in a security-style report too. wildcards = "deny" -# Crate-by-crate bans live in `config/taplo/no-banned-deps.schema.json`. -# That is a declaration-time check on workspace [dependencies]. -# The `deny` entries below catch *transitive* uses that the taplo rule can't see. +# Crate-by-crate bans are declared twice over, and the two halves see different things. +# A declaration-time check on the workspace `[dependencies]` table catches a direct dep, while the `deny` +# entries below are the only thing that catches a *transitive* one. deny = [ # `openssl` and `openssl-sys` link to libssl/libcrypto. # We use rustls + aws-lc-rs as the single TLS+crypto stack across the workspace. Any new transitive dep @@ -175,12 +188,10 @@ deny = [ # Current allowed parents: # * `rcgen` -- server-side TLS cert generation in et-ws-server. # Intentional; rcgen's ring feature is what we want for the cert generator. - # * `rustls-webpki` -- pulled by deno_fetch + deno_tls (default features include `ring`). - # Drop once Deno's TLS stack migrates to aws-lc-rs defaults. # * `quinn-proto` -- pulled by deno_net via quinn (default features include `ring`). # Drop once Deno's quinn config switches to the `rustls-aws-lc-rs` feature. - { crate = "ring", wrappers = ["quinn-proto", "rcgen", "rustls-webpki"] }, - # `ureq` is forbidden -- direct use is blocked by the taplo no-banned-deps schema. + { crate = "ring", wrappers = ["quinn-proto", "rcgen"] }, + # `ureq` is forbidden -- direct use is blocked where deps are declared. # This entry catches transitive paths the same way as `ring` above. No wrappers allowed. The only previous # parent (`ort-sys` via the `download-binaries` feature) is gone. The workspace `ort` dep now stages ONNX # Runtime through mise instead. @@ -189,7 +200,7 @@ deny = [ # It arrives only transitively through progenitor -> typify -> typify-impl, which uses it to validate the # JSON-Schema `pattern` regexes in our generated REST client. We standardise on the `regex` crate everywhere # else; any new parent that drags `regress` back in should migrate to `regex`. Direct workspace declaration is - # separately blocked by config/taplo/no-banned-deps.schema.json. + # separately blocked where deps are declared. { crate = "regress", use-instead = "regex", wrappers = ["typify-impl"] }, ] diff --git a/config/dprint.jsonc b/config/dprint.jsonc index 22fa777d..e2e2689c 100644 --- a/config/dprint.jsonc +++ b/config/dprint.jsonc @@ -5,7 +5,7 @@ "java": {}, "json": {}, // Match the editorconfig line length so dprint's bundled ruff agrees with it. - // Otherwise it reformats Python files to its own default line-length and fights with `mise run ruff-fmt`. + // Otherwise it reformats Python files to its own default line-length and fights with the standalone ruff run. "ruff": { "lineLength": 120, }, diff --git a/config/generated-trees.toml b/config/generated-trees.toml index b7966b1b..e98ea9aa 100644 --- a/config/generated-trees.toml +++ b/config/generated-trees.toml @@ -63,7 +63,9 @@ required_in = [ "config/pyrefly.toml", "config/semgrep/anyhow-only-in-error-rs.yaml", "config/semgrep/comment-summary-line.yaml", + "config/semgrep/no-check-tool-mentions.yaml", "config/semgrep/no-non-ascii.yaml", + "config/semgrep/no-task-name-mentions.yaml", "config/semgrep/no-todo.yaml", "config/semgrep/no-type-comments.yaml", ] @@ -74,7 +76,9 @@ regenerated_by = "mise run regen-verification" required_in = [ "config/jscpd.json", "config/semgrep/comment-summary-line.yaml", + "config/semgrep/no-check-tool-mentions.yaml", "config/semgrep/no-non-ascii.yaml", + "config/semgrep/no-task-name-mentions.yaml", "config/semgrep/no-todo.yaml", "config/semgrep/no-trailing-backslash.yaml", ] diff --git a/config/gitleaks.toml b/config/gitleaks.toml index 4c28c96f..c12772a4 100644 --- a/config/gitleaks.toml +++ b/config/gitleaks.toml @@ -7,8 +7,9 @@ # # `useDefault` takes gitleaks' full upstream ruleset. Do not trim it to silence a finding -- fix the finding, or # suppress the single line with a trailing `gitleaks:allow`. Note that a marker only works where the scanner -# reads it: gitleaks honours it on the secret's own line and nowhere else, and Codacy's scan ignores it entirely, -# so a committed credential cannot be made to pass everywhere by annotation. Don't commit one. +# reads it: gitleaks honours it on the secret's own line and nowhere else, and a pushed branch is scanned again +# by a service that ignores it entirely, so a committed credential cannot be made to pass everywhere by +# annotation. Don't commit one. [extend] useDefault = true diff --git a/config/hadolint.yaml b/config/hadolint.yaml index 965cd62b..31f731cf 100644 --- a/config/hadolint.yaml +++ b/config/hadolint.yaml @@ -15,7 +15,7 @@ ignored: # Consecutive RUNs are kept separate on purpose. # For layer caching and so a change to one step doesn't bust the others. - DL3059 - # Our RUN <- A multi-line `#` comment block must open with a single summary line ending in a full stop (PEP 257 style). The summary describes the whole block in one sentence; continuation lines follow. End the first comment line @@ -41,8 +48,8 @@ rules: - "*.h" - "*.jsonc" # The C-style twin of comment-summary-line above, covering `//` and `/* */` comments (so it also covers .jsonc). - # No dedicated linter has this check (clang-tidy's comment checks stop at namespace-closing and task-tag - # formatting; cpplint only checks comment whitespace), so the generic-mode regexes carry it. Two alternatives, + # No dedicated C linter has this check -- their comment rules stop at namespace-closing markers, task-tag + # formatting and whitespace -- so the generic-mode regexes carry it. Two alternatives, # one per comment shape: # - `/* ... */` blocks: a line opening `/*` WITHOUT `*/` on the same line (the negative lookahead # `(?![^\n]*\*/)` is the multi-line test, mirroring the next-line-is-also-comment test of the `#` rule) must diff --git a/config/semgrep/dockerfile-heredoc-set-euo-pipefail.yaml b/config/semgrep/dockerfile-heredoc-set-euo-pipefail.yaml index 6377cb0a..8bd33b6a 100644 --- a/config/semgrep/dockerfile-heredoc-set-euo-pipefail.yaml +++ b/config/semgrep/dockerfile-heredoc-set-euo-pipefail.yaml @@ -7,13 +7,13 @@ rules: - "**/Dockerfile.*" # Match a heredoc RUN whose first body line ISN'T `set -euo pipefail`. # The dotfile-style `<- Dockerfile RUN heredocs must begin with `set -euo pipefail` as the first body line. BuildKit's default heredoc shell carries neither -e nor -o pipefail (and dash on Debian/Ubuntu doesn't support -o pipefail at all), so the strict-mode invariant has to be re-declared per body. Paired with the - conftest rule that requires `RUN <- + Writing a file by piping a heredoc through `cat` is banned in this repo. A program embedded that way is + invisible to every checker: it is a string as far as the surrounding file is concerned, so nothing lints + it, formats it, or reports it in a diff as the language it is written in, and the quoting rules of two + layers have to be satisfied before its first byte reaches the interpreter. Put the content in its own + file beside whatever runs it -- `.mise/` for a mise task body -- named with the extension of whatever + reads it (`.awk`, `.jq`, `.sh`), and point the tool at that path instead. It then reads as the program + it is, and a path is all the caller needs. + severity: ERROR diff --git a/config/semgrep/no-check-tool-mentions.yaml b/config/semgrep/no-check-tool-mentions.yaml new file mode 100644 index 00000000..63140441 --- /dev/null +++ b/config/semgrep/no-check-tool-mentions.yaml @@ -0,0 +1,658 @@ +# One rule per check tool, each banning that tool's name everywhere except the files that wire it up. +# +# A checker should be invisible to everything it is not wired into. A comment saying some code is shaped a +# certain way to satisfy one documents the tool's reaction rather than the code, reads as noise to everyone who +# does not run it, and goes stale the moment its config moves. A cross-reference to another checker is worse +# still: it duplicates a rule that already documents itself where it lives, and the two then drift apart. +# +# Two narrow exceptions run through the allowlists below. A suppression pragma only works spelled out, so the +# file carrying one keeps the name. And a local rule mirroring an external analyzer's finding may name the code +# it mirrors so the pair stays recognisable -- the code, never the vendor. +# +# To wire a checker into a new file, add that file to the matching rule's `paths.exclude` with a comment saying +# which part of the wiring needs it. Do not add one so a comment can explain why some code looks the way it does; +# that is the thing these rules exist to prevent. One tool is deliberately absent: clippy, whose lint names are +# operational at nearly every site that writes one, so a ban on the word would be almost entirely allowlist. +rules: + - id: no-action-validator-mentions + languages: [generic] + paths: + exclude: + - "/.mise/**" # Tool pin, lockfile entry, and the task that runs it. + - "/CLAUDE.md" # Documents which checks an agent runs for which file types. + - "/.codacy.yaml" # Names the workflow linters Codacy defers to, as its exclusion rationale. + - "/.github/actions/install-mise-tools/action.yaml" # Stages the tool for CI. + - "/.github/workflows/test.yaml" # Notes the Windows lane's source-build fallback for it. + - "/Dockerfile" # Disables the tool for an image whose build does not need it. + - "/config/conftest/policy/mise/mise.rego" # Allowlists it as a `cargo:` tool with no prebuilt. + - "/config/taplo/mise-cargo-backend-allowlist.schema.json" # Same allowlist, as a TOML schema enum. + - "/config/semgrep/no-*-mentions.yaml" # The mention bans, which cannot help but name what they ban. + pattern-regex: "(?i)action-validator" + message: >- + Do not name the workflow schema validator outside the files that install and run it. Whether a workflow + key is spelled a certain way to keep a validator happy is not what a reader of that workflow needs; say + what the key does. + severity: ERROR + + - id: no-actionlint-mentions + languages: [generic] + paths: + exclude: + - "/.mise/**" # Tool pin, lockfile entry, and the task that runs it. + - "/.codacy.yaml" # Names the workflow linters Codacy defers to, as its exclusion rationale. + - "/config/semgrep/no-*-mentions.yaml" # The mention bans, which cannot help but name what they ban. + pattern-regex: "(?i)actionlint" + message: >- + Do not name the GitHub Actions linter outside the files that install and run it. A workflow comment + crediting it for a shell quoting or expression change says nothing a reader of the workflow can use. + severity: ERROR + + - id: no-ast-grep-mentions + languages: [generic] + paths: + exclude: + - "/.mise/**" # Tool pin, lockfile entry, and the scan/fix tasks. + - "/CLAUDE.md" # Documents which checks an agent runs for which file types. + - "/.codacy.yaml" # Names the workflow linters Codacy defers to, as its exclusion rationale. + - "/config/ast-grep/**" # Its own ruleset, where the tool's own syntax is the subject. + - "/config/generated-trees.toml" # Maps each generated tree onto the exclusion key this tool needs. + - "/config/conftest/policy/generated_trees/generated_trees.rego" # Enforces that mapping. + - "/config/conftest/policy/generated_trees/generated_trees_test.rego" # Unit tests for it. + - "/config/semgrep/no-*-mentions.yaml" # The mention bans, which cannot help but name what they ban. + pattern-regex: "(?i)ast-grep" + message: >- + Do not name the structural search tool outside its own ruleset and the configs that scope it. Pointing at + one of its rules from the code that rule governs duplicates the rule's own message, which is where the + explanation belongs and where it stays correct. + severity: ERROR + + - id: no-cargo-deny-mentions + languages: [generic] + paths: + exclude: + - "/.mise/**" # Tool pin, lockfile entry, and the task that runs it. + - "/.github/workflows/dependencies.yaml" # Runs the audit on CI. + - "/config/deny.toml" # Its own config, including which scanner honours which advisory id. + - "/config/semgrep/no-*-mentions.yaml" # The mention bans, which cannot help but name what they ban. + pattern-regex: "(?i)cargo-deny" + message: >- + Do not name the dependency auditor outside its config and the job that runs it. A dependency is chosen or + avoided for a licensing or advisory reason, which stands on its own; the auditor that would have noticed + is not part of it. + severity: ERROR + + - id: no-cargo-unmaintained-mentions + languages: [generic] + paths: + exclude: + - "/.mise/**" # Tool pin and the task that runs it. + - "/.github/workflows/dependencies.yaml" # Runs the scan on CI. + - "/Cargo.toml" # Its per-crate ignore list, where each entry's reason is the false positive it hit. + - "/config/semgrep/no-*-mentions.yaml" # The mention bans, which cannot help but name what they ban. + pattern-regex: "(?i)cargo-unmaintained" + message: >- + Do not name the unmaintained-crate scanner outside its ignore list and the job that runs it. Its ignore + entries need the tool's own heuristics explained; nothing else in the tree does. + severity: ERROR + + - id: no-clang-format-mentions + languages: [generic] + paths: + exclude: + - "/.mise/**" # Tool pin, lockfile entry, and the format/check tasks. + - "/CLAUDE.md" # Documents which checks an agent runs for which file types. + - "/config/clang-format.yaml" # Its own config, including the settings held to agree with the C linters. + - "/config/semgrep/no-*-mentions.yaml" # The mention bans, which cannot help but name what they ban. + pattern-regex: "(?i)clang-format" + message: >- + Do not name the C formatter outside its own config. Formatted code carries no evidence of who formatted + it, and a comment claiming a brace or a blank line is the formatter's doing is unverifiable noise. + severity: ERROR + + - id: no-clang-tidy-mentions + languages: [generic] + paths: + exclude: + - "/.mise/**" # Tool pin, lockfile entry, and the check/fix tasks. + - "/CLAUDE.md" # Documents which checks an agent runs for which file types. + - "/.github/workflows/codeql.yaml" # Names the check that keeps the coverage an excluded query gave up. + - "/config/clang-tidy.yaml" # Its own config, including which external code each check mirrors. + - "/config/conftest/policy/mise/mise.rego" # Allowlists it as a `cargo:` tool with no prebuilt. + - "/config/semgrep/no-*-mentions.yaml" # The mention bans, which cannot help but name what they ban. + pattern-regex: "(?i)clang-tidy" + message: >- + Do not name the C static analyzer outside its config. Its suppression marker is a bare `// NOLINT` naming + the check, which is all a reader needs -- spelling out the analyzer beside it adds nothing and dates the + comment to whichever analyzer happened to run that week. + severity: ERROR + + - id: no-codacy-mentions + languages: [generic] + paths: + exclude: + - "/.mise/**" # Names the tool in the task that mirrors its secret scan locally. + - "/CLAUDE.md" # Documents how a finding from an external analyzer must reach a local checker. + - "/.codacy.yaml" # Its own config, whose path excludes each carry their reason. + - "/libs/wasi-guest/src/coverage.rs" # Path-excluded there; its header is that exclusion's only home. + - "/services/ws-test-server/src/bin/cov-server.rs" # Likewise path-excluded, rationale in its header. + - "/services/ws-web-runner/mingw-shim/msvc_crt_alloc.c" # Likewise, and the reason the file exists apart. + - "/services/ws-web-runner/mingw-shim/msvc_crt_locale_cache.c" # Likewise. + - "/utilities/cli/src/hub_ws_url.rs" # Likewise. + - "/utilities/wasm-cov-wrapper/src/main.rs" # Likewise. + - "/config/semgrep/no-*-mentions.yaml" # The mention bans, which cannot help but name what they ban. + pattern-regex: "(?i)codacy" + message: >- + Do not name this analyzer outside its own config and the few files it excludes by path, where the header + is the exclusion's canonical home. It runs after a push, so a source comment about it describes something + the reader cannot see, run, or check; state the property the code actually has. + severity: ERROR + + - id: no-codecov-mentions + languages: [generic] + paths: + exclude: + - "/.mise/**" # The coverage tasks that produce and upload its reports. + - "/CLAUDE.md" # Records which coverage check on a PR is the one that has to pass. + - "/.deepsource.toml" # Declares the coverage artifact both services read. + - "/.github/workflows/coverage.yaml" # Uploads to it on CI. + - "/.gitignore" # Ignores its downloaded uploader binary. + - "/.dockerignore" # The same ignore, generated from it. + - "/verification/**" # Generator-owned output, carrying whatever the generator wrote. + - "/config/semgrep/no-*-mentions.yaml" # The mention bans, which cannot help but name what they ban. + pattern-regex: "(?i)codecov" + message: >- + Do not name the coverage service outside the jobs that upload to it. A test exists to pin behaviour, and + saying it was added for a coverage percentage tells the next reader to delete it once the number moves. + severity: ERROR + + - id: no-conftest-mentions + languages: [generic] + paths: + exclude: + - "/.mise/**" # Tool pin, lockfile entries, and the per-format policy tasks. + - "/CLAUDE.md" # Documents which checks an agent runs for which file types. + - "/.codacy.yaml" # Names the config linters Codacy defers to, as its exclusion rationale. + - "/.editorconfig" # `conftest fmt` owns Rego indentation and is not configurable, hence the override. + - "/Dockerfile" # Stages or disables the tool for the image build. + - "/Dockerfile.nanoserver" # Same, for the Nano Server image. + - "/config/conftest/**" # Its own policy tree, where the tool's input shape is the subject. + - "/config/generated-trees.toml" # Declares the policy that enforces the linter/tree mapping. + - "/config/regal.yaml" # Carves out the conftest-specific Rego patterns vanilla regal would flag. + - "/config/semgrep/no-*-mentions.yaml" # The mention bans, which cannot help but name what they ban. + pattern-regex: "(?i)conftest" + message: >- + Do not name the policy runner outside its own policy tree and the tasks that feed it. A config entry is + shaped by what the config has to express; a note that some policy would otherwise object belongs in that + policy's own deny message, where it is shown to the person who trips it. + severity: ERROR + + - id: no-cpplint-mentions + languages: [generic] + paths: + exclude: + - "/.mise/**" # Tool pin, lockfile entry, and the task that runs it. + - "/CLAUDE.md" # Documents which checks an agent runs for which file types. + - "/.github/workflows/codeql.yaml" # Names the check that keeps the coverage an excluded query gave up. + - "/Dockerfile.nanoserver" # Disables the tool for an image whose build does not need it. + - "/config/clang-format.yaml" # The formatter setting chosen so the two agree on comment spacing. + - "/config/semgrep/no-*-mentions.yaml" # The mention bans, which cannot help but name what they ban. + pattern-regex: "(?i)cpplint" + message: >- + Do not name the C++ style checker outside the configs that run it or defer to it. Its suppression marker + carries the rule name already, and nothing else in a C file is the checker's business. + severity: ERROR + + - id: no-deepsource-mentions + languages: [generic] + paths: + exclude: + - "/.mise/**" # The coverage tasks that produce the reports it ingests. + - "/CLAUDE.md" # Documents how a finding from an external analyzer must reach a local checker. + - "/.deepsource.toml" # Its own config. + - "/.codacy.yaml" # Names it as the analyzer still covering what Codacy skips. + - "/.github/workflows/coverage.yaml" # Reports coverage to it on CI. + - "/services/ws-modules/dotnet-data1/pkg/et_ws_dotnet_data1.js" # A `skipcq` pragma's own justification. + - "/services/ws-modules/dotnet-math1/pkg/et_ws_dotnet_math1.js" # The same pragma in the twin shim. + - "/config/semgrep/no-*-mentions.yaml" # The mention bans, which cannot help but name what they ban. + pattern-regex: "(?i)deepsource" + message: >- + Do not name this analyzer outside its own config and the pragmas it reads. A local rule mirroring one of + its findings names the code -- `RS-W1079`, `TYP-025`, `CXX-W2009` -- and stops there: the code identifies + the finding, survives a change of vendor, and is what a search will actually turn up. + severity: ERROR + + - id: no-dprint-mentions + languages: [generic] + paths: + exclude: + - "/.mise/**" # Tool pin, lockfile entry, and the format/check tasks. + - "/CLAUDE.md" # Documents which checks an agent runs for which file types. + - "/.dprint.jsonc" # The root config, which is also this repo's root-marker file. + - "/.codacy.yaml" # Names the formatters Codacy defers to, as its exclusion rationale. + - "/.gitattributes" # Records that the formatters write LF, which the checkout must preserve. + - "/Cargo.lock" # Carries its plugin crates as dependency names. + - "/config/dprint.jsonc" # Its own config, including the settings held to agree with the JS formatter. + - "/config/oxfmtrc.jsonc" # The other half of that agreement. + - "/config/ryl.yaml" # Defers to it on inline-comment spacing so the two cannot fight. + - "/config/semgrep/no-line-length-in-comment.yaml" # Names the configs that hold the line-length value. + - "/config/semgrep/prefer-yaml-toml.yaml" # Allowlists its config, which can only be JSONC. + - "/config/ast-grep/rules/no-current-dir.yaml" # Names the root-marker file, not the tool. + - "/libs/path/src/lib.rs" # Holds that marker filename as a constant. + - "/libs/path/tests/find.rs" # Writes the marker file to build a fixture root. + - "/config/semgrep/no-*-mentions.yaml" # The mention bans, which cannot help but name what they ban. + pattern-regex: "(?i)dprint" + message: >- + Do not name the markdown/YAML/JSON formatter outside its config and the configs held to agree with it. + Where a generator has to emit already-formatted output, say which shape the output must have; naming the + formatter dates the comment to a config the generator never reads. + severity: ERROR + + - id: no-editorconfig-check-mentions + languages: [generic] + paths: + exclude: + - "/.mise/**" # Tool pin, lockfile entry, and the task that runs it. + - "/CLAUDE.md" # Documents which checks an agent runs for which file types. + - "/config/semgrep/no-*-mentions.yaml" # The mention bans, which cannot help but name what they ban. + pattern-regex: "(?i)editorconfig-check" + message: >- + Do not name the editorconfig checker outside the task that runs it. Write "the editorconfig line length" + when a comment genuinely has to mention the limit -- that names the constraint and its source, which is + what a reader can act on. + severity: ERROR + + - id: no-flawfinder-mentions + languages: [generic] + paths: + exclude: + - "/.mise/**" # Tool pin, lockfile entry, and the task that runs it. + - "/CLAUDE.md" # Documents which checks an agent runs for which file types. + - "/Dockerfile.nanoserver" # Disables the tool for an image whose build does not need it. + - "/services/ws-modules/zig-except1/src/exceptions.cpp" # A `flawfinder: ignore` pragma, spelled out. + - "/config/semgrep/no-*-mentions.yaml" # The mention bans, which cannot help but name what they ban. + pattern-regex: "(?i)flawfinder" + message: >- + Do not name the C/C++ security scanner except in the `flawfinder: ignore` pragma, which only works spelled + out on the line it covers. Its justification belongs on that same line and says why the call is safe. + severity: ERROR + + - id: no-gitleaks-mentions + languages: [generic] + paths: + exclude: + - "/.mise/**" # Tool pin, lockfile entry, and the task that runs it. + - "/CLAUDE.md" # Documents which checks an agent runs for which file types. + - "/config/gitleaks.toml" # Its own config, including how far its inline marker reaches. + - "/config/semgrep/no-*-mentions.yaml" # The mention bans, which cannot help but name what they ban. + pattern-regex: "(?i)gitleaks" + message: >- + Do not name the secret scanner outside its config and the `gitleaks:allow` marker it reads. A credential + is kept out of a tracked file because it is a credential, not because a scanner would find it. + severity: ERROR + + - id: no-hadolint-mentions + languages: [generic] + paths: + exclude: + - "/.mise/**" # Tool pin, lockfile entry, and the task that runs it. + - "/CLAUDE.md" # Documents which checks an agent runs for which file types. + - "/.github/workflows/docker-linux.yaml" # Stages every Dockerfile where the tool can find it. + - "/Dockerfile" # Carries `# hadolint ignore=` pragmas, which only work spelled out. + - "/Dockerfile.nanoserver" # Same, plus the tool's own MISE_DISABLE_TOOLS entry. + - "/Dockerfile.windows" # Same pragmas. + - "/services/ws-server/Dockerfile" # Same, and why one code is suppressed inline rather than in config. + - "/config/hadolint.yaml" # Its own config, and the shellcheck code it emits from its embedded copy. + - "/utilities/cli/src/deployment_types/scenario_image.rs" # Emits those pragmas into generated images. + - "/verification/**" # The generated images holding them. + - "/config/semgrep/no-*-mentions.yaml" # The mention bans, which cannot help but name what they ban. + pattern-regex: "(?i)hadolint" + message: >- + Do not name the Dockerfile linter outside its config, its `hadolint ignore=` pragmas, and the code that + emits them. Say what the instruction does and why it is written that way; a pragma already carries the + code it suppresses. + severity: ERROR + + - id: no-jscpd-mentions + languages: [generic] + paths: + exclude: + - "/.mise/**" # Tool pin, lockfile entry, and the tasks that run and report it. + - "/CLAUDE.md" # Documents the baseline-is-a-debt-ledger rule, which is where that policy belongs. + - "/config/jscpd.json" # Its own config. + - "/config/jscpd-baseline.json" # The clone-fingerprint baseline it reads. + - "/config/conftest/policy/jscpd/jscpd.rego" # Ratchets that baseline, and takes the tool as its package. + - "/config/conftest/policy/policy_tests/policy_tests.rego" # Names that policy path in its exception list. + - "/config/conftest/policy/policy_tests/policy_tests_test.rego" # Unit tests for it. + - "/config/conftest/policy/generated_trees/generated_trees.rego" # Enforces the linter/tree mapping. + - "/config/generated-trees.toml" # Maps each generated tree onto the exclusion key this tool needs. + - "/config/semgrep/prefer-yaml-toml.yaml" # Allowlists its two files, which cannot be YAML or take comments. + - "/config/typos.toml" # The same carve-out for those two files. + - "/Dockerfile.nanoserver" # Disables the tool for an image whose verifier cannot install it. + - "/config/semgrep/no-*-mentions.yaml" # The mention bans, which cannot help but name what they ban. + pattern-regex: "(?i)jscpd" + message: >- + Do not name the duplication checker outside its config, its baseline, and the policy that ratchets them. A + reported clone is a request to remove duplication: factor the shared part out so it exists once. Where a + clone is genuinely irreducible -- the same few lines every caller must write verbatim -- leave the code + plain and raise it, rather than annotating the source with the tool's name. + severity: ERROR + + - id: no-kubeconform-mentions + languages: [generic] + paths: + exclude: + - "/.mise/**" # Tool pin, lockfile entry, and the task that runs it. + - "/README.md" # Tells a contributor which checks validate the generated manifests. + - "/.codacy.yaml" # Names the manifest checks Codacy defers to, as its exclusion rationale. + - "/.github/workflows/k3s.yaml" # Runs the validation on CI. + - "/config/semgrep/no-*-mentions.yaml" # The mention bans, which cannot help but name what they ban. + pattern-regex: "(?i)kubeconform" + message: >- + Do not name the manifest schema validator outside the job that runs it. A manifest field is set because + Kubernetes requires or honours it, which is what the comment beside it should say. + severity: ERROR + + - id: no-ls-lint-mentions + languages: [generic] + paths: + exclude: + - "/.mise/**" # Tool pin, lockfile entry, and the task that runs it. + - "/CLAUDE.md" # Documents which checks an agent runs, and keeping its ignores in sync with .gitignore. + - "/config/ls-lint.yaml" # Its own config. + - "/config/semgrep/ls-lint-no-glob.yaml" # The rule holding that config to literal paths. + - "/config/semgrep/no-*-mentions.yaml" # The mention bans, which cannot help but name what they ban. + pattern-regex: "(?i)ls-lint" + message: >- + Do not name the file-naming linter outside its config and the rule that governs it. A file or directory is + named for what it holds; the checker that would object to another name is not part of that. + severity: ERROR + + - id: no-lychee-mentions + languages: [generic] + paths: + exclude: + - "/.mise/**" # Tool pin, lockfile entry, and the link-check task. + - "/CLAUDE.md" # Documents which checks an agent runs, and keeping its ignores in sync with .gitignore. + - "/config/lychee.toml" # Its own config. + - "/Dockerfile.nanoserver" # Disables the tool for an image whose build does not need it. + - "/.gitignore" # Ignores its on-disk cache. + - "/.dockerignore" # The same ignore, generated from it. + - "/verification/**" # Generator-owned output, carrying whatever the generator wrote. + - "/config/semgrep/no-*-mentions.yaml" # The mention bans, which cannot help but name what they ban. + pattern-regex: "(?i)lychee" + message: >- + Do not name the link checker outside its config and the task that runs it. A link is written to point + somewhere useful, and a note that the checker accepts or skips it will outlive the reason it did. + severity: ERROR + + - id: no-nextest-mentions + languages: [generic] + paths: + exclude: + - "/.mise/**" # Tool pin, lockfile entry, and every task that runs the test suite. + - "/.github/workflows/coverage.yaml" # Runs the suite under it on CI. + - "/.github/workflows/test.yaml" # Same. + - "/config/nextest.toml" # Its own config, including the test groups that serialise port users. + # Every test here wants one fixed port, and only that config's test group keeps them apart. + # That is a constraint nothing in the file itself can express, and a silent corruption when bypassed. + - "/utilities/cli/tests/scenario_runners.rs" + - "/config/semgrep/no-*-mentions.yaml" # The mention bans, which cannot help but name what they ban. + pattern-regex: "(?i)nextest" + message: >- + Do not name the test runner outside its config and the jobs that invoke it. A test asserts what the code + must do, and how it happens to be scheduled is the runner's business rather than the test's -- unless the + schedule is load-bearing, which is a constraint worth stating and worth allowlisting for. + severity: ERROR + + - id: no-osv-scanner-mentions + languages: [generic] + paths: + exclude: + - "/.mise/**" # Tool pin, lockfile entry, and the tasks that scan each ecosystem. + - "/.github/workflows/dependencies.yaml" # Runs the scan on CI. + - "/config/osv-scanner.toml" # Its own config. + - "/config/deny.toml" # Records which of the two scanners honours each advisory id. + - "/pnpm-workspace.yaml" # One root lockfile exists so the npm scan has a single committed target. + - "/pubspec.yaml" # The same, for the Dart ecosystem. + - "/config/semgrep/no-*-mentions.yaml" # The mention bans, which cannot help but name what they ban. + pattern-regex: "(?i)osv-scanner" + message: >- + Do not name the vulnerability scanner outside its config, the job that runs it, and the ignore lists that + have to say which scanner honours which id. Everywhere else, describe the dependency, not the scan. + severity: ERROR + + - id: no-oxfmt-mentions + languages: [generic] + paths: + exclude: + - "/.mise/**" # Tool pin, lockfile entry, and the format/check tasks. + - "/CLAUDE.md" # Documents how to write JS that both formatters accept. + - "/config/oxfmtrc.jsonc" # Its own config. + - "/config/dprint.jsonc" # The other formatter, tuned to agree with it. + - "/config/semgrep/prefer-yaml-toml.yaml" # Allowlists its config, which can only be JSONC. + - "/config/conftest/policy/mise/mise.rego" # Allowlists it as a tool with no prebuilt at some triple. + - "/config/semgrep/no-*-mentions.yaml" # The mention bans, which cannot help but name what they ban. + pattern-regex: "(?i)oxfmt" + message: >- + Do not name the JS formatter outside its config and the config held to agree with it. When a statement has + to be split to keep both formatters happy, the fix is in the source -- a shorter line, an extracted local + -- and the split stands on its own without a comment naming either tool. + severity: ERROR + + - id: no-oxlint-mentions + languages: [generic] + paths: + exclude: + - "/.mise/**" # Tool pin, lockfile entry, and the lint/fix tasks. + - "/CLAUDE.md" # Documents which checks an agent runs for which file types. + - "/config/oxlintrc.jsonc" # Its own config, including which external code each rule mirrors. + - "/config/oxfmtrc.jsonc" # Names the shared JSONC configs the formatter must skip. + - "/config/semgrep/prefer-yaml-toml.yaml" # Allowlists its config, which can only be JSONC. + - "/config/conftest/policy/mise/mise.rego" # Allowlists it as a tool with no prebuilt at some triple. + - "/config/semgrep/no-*-mentions.yaml" # The mention bans, which cannot help but name what they ban. + pattern-regex: "(?i)oxlint" + message: >- + Do not name the JS linter outside its config. A `skipcq` or eslint-style disable comment already carries + the rule it silences plus the reason it is safe; adding the linter's name tells the reader nothing more. + severity: ERROR + + - id: no-pyrefly-mentions + languages: [generic] + paths: + exclude: + - "/.mise/**" # Tool pin, lockfile entry, and the Python check task. + - "/config/pyrefly.toml" # Its own config. + - "/config/semgrep/no-type-comments.yaml" # The rule keeping annotations the only form it can check. + - "/config/generated-trees.toml" # Maps each generated tree onto the exclusion key this tool needs. + - "/config/conftest/policy/generated_trees/generated_trees.rego" # Enforces that mapping. + - "/config/semgrep/no-*-mentions.yaml" # The mention bans, which cannot help but name what they ban. + pattern-regex: "(?i)pyrefly" + message: >- + Do not name the Python type checker outside its config and the rules that protect what it can see. An + annotation states the type the code requires; that it also satisfies a checker follows, and need not be + written down. + severity: ERROR + + - id: no-regal-mentions + languages: [generic] + paths: + exclude: + - "/.mise/**" # Tool pin, lockfile entry, and the check/fix tasks. + - "/CLAUDE.md" # Documents which checks an agent runs for which file types. + - "/config/regal.yaml" # Its own config. + - "/config/semgrep/no-*-mentions.yaml" # The mention bans, which cannot help but name what they ban. + pattern-regex: '(?i)\bregal\b' + message: >- + Do not name the Rego linter outside its own config. A policy is written the way Rego reads best; if one of + its idioms is deliberate, say what the idiom does rather than which linter asked for it. + severity: ERROR + + - id: no-roslynator-mentions + languages: [generic] + paths: + exclude: + - "/.mise/**" # Tool pin, lockfile entry, and the .NET check task. + - "/Dockerfile.nanoserver" # Disables the tool for an image whose build does not need it. + - "/Dockerfile.windows" # Same. + - "/config/semgrep/no-*-mentions.yaml" # The mention bans, which cannot help but name what they ban. + pattern-regex: "(?i)roslynator" + message: >- + Do not name the C# analyzer outside the tasks and images that install it. A `#pragma warning disable` + already names the diagnostic, which is the part a reader can look up. + severity: ERROR + + - id: no-ruff-mentions + languages: [generic] + paths: + exclude: + - "/.mise/**" # Tool pin, lockfile entry, and the format/check/fix tasks. + - "/CLAUDE.md" # Documents which checks an agent runs for which file types. + - "/ruff.toml" # Its own config, kept at the root so editors discover it. + - "/.editorconfig" # Records that the formatter owns docstring and doc-comment reflow. + - "/config/dprint.jsonc" # Skips the Python files this tool formats instead. + - "/config/oxfmtrc.jsonc" # Names the root config it must not treat as JS. + - "/config/semgrep/no-line-length-in-comment.yaml" # Names the configs that hold the line-length value. + - "/config/openapi-python-client.yaml" # Turns off the generator's own hooks, which a task already runs. + - "/generated/**" # Generator-owned output, carrying whatever the generator wrote. + - "/config/semgrep/no-*-mentions.yaml" # The mention bans, which cannot help but name what they ban. + pattern-regex: '(?i)\bruff\b' + message: >- + Do not name the Python linter/formatter outside its config and the configs that defer to it. A `# noqa` + carries the code it silences and should carry the reason too; the tool that issued the code does not need + naming beside it. + severity: ERROR + + - id: no-ryl-mentions + languages: [generic] + paths: + exclude: + - "/.mise/**" # Tool pin, lockfile entry, and the task that runs it. + - "/CLAUDE.md" # Documents which checks an agent runs for which file types. + - "/config/ryl.yaml" # Its own config. + - "/config/conftest/policy/mise/mise.rego" # Allowlists it as a tool with no prebuilt at some triple. + - "/config/taplo/mise-cargo-backend-allowlist.schema.json" # The same allowlist, as a TOML schema enum. + - "/config/semgrep/no-*-mentions.yaml" # The mention bans, which cannot help but name what they ban. + pattern-regex: '(?i)\bryl\b' + message: >- + Do not name the YAML linter outside its own config. YAML is written to be read; where its shape is forced, + say what forces it, which is almost always the schema the file answers to rather than a linter. + severity: ERROR + + - id: no-semgrep-mentions + languages: [generic] + paths: + exclude: + - "/.mise/**" # Tool pin, lockfile entry, and the scan/fix tasks. + - "/CLAUDE.md" # Documents which checks an agent runs for which file types. + - "/.codacy.yaml" # Excludes this ruleset, which its own copy of the tool misparses. + - "/.github/workflows/test.yaml" # Notes the Windows lane's handling of the tool. + - "/Dockerfile.nanoserver" # Disables the tool for an image whose build does not need it. + - "/config/semgrep/**" # This ruleset, where the tool's own rule syntax is the subject. + - "/config/generated-trees.toml" # Maps each generated tree onto the exclusion key this tool needs. + - "/config/conftest/policy/generated_trees/generated_trees.rego" # Enforces that mapping. + - "/config/conftest/policy/generated_trees/generated_trees_test.rego" # Unit tests for it. + pattern-regex: "(?i)semgrep" + message: >- + Do not name this tool outside its own ruleset and the configs that scope it. A `nosemgrep` marker is the + one place the name belongs in source, and it needs the rule id and a reason beside it; anywhere else, + pointing at a rule duplicates the message that rule already shows whoever trips it. + severity: ERROR + + - id: no-shellcheck-mentions + languages: [generic] + paths: + exclude: + - "/.mise/**" # Tool pin, the task-body extractor, and the `# shellcheck disable=` pragmas in task bodies. + - "/CLAUDE.md" # Documents which checks an agent runs for which file types. + - "/.github/workflows/docker-linux.yaml" # Carries those pragmas in its run bodies. + - "/.github/workflows/docker-windows.yaml" # Same. + - "/.github/workflows/upstream-cache.yaml" # Same. + - "/Dockerfile.nanoserver" # Same, plus the tool's own MISE_DISABLE_TOOLS entry. + - "/Dockerfile.windows" # Same pragmas. + - "/config/hadolint.yaml" # Ignores a code raised by the copy embedded in the Dockerfile linter. + - "/utilities/cli/src/deployment_types/scenario_image.rs" # Emits those pragmas into generated images. + - "/verification/**" # The generated images holding them. + - "/config/semgrep/no-*-mentions.yaml" # The mention bans, which cannot help but name what they ban. + pattern-regex: "(?i)shellcheck" + message: >- + Do not name the shell linter except in the `# shellcheck disable=` pragma, which only works spelled out + above the line it covers, and in the configs that run it. The pragma needs the code and the reason the + construct is deliberate; nothing else in the script does. + severity: ERROR + + - id: no-shfmt-mentions + languages: [generic] + paths: + exclude: + - "/.mise/**" # Tool pin, lockfile entry, and the extractor that formats mise task bodies with it. + - "/CLAUDE.md" # Documents which checks an agent runs for which file types. + - "/config/semgrep/no-*-mentions.yaml" # The mention bans, which cannot help but name what they ban. + pattern-regex: "(?i)shfmt" + message: >- + Do not name the shell formatter outside the tasks that run it. Formatted shell carries no evidence of who + formatted it, so a comment claiming an indent is the formatter's doing cannot be checked. + severity: ERROR + + - id: no-taplo-mentions + languages: [generic] + paths: + exclude: + - "/.mise/**" # Tool pin, lockfile entry, and the format/check tasks. + - "/CLAUDE.md" # Documents which checks an agent runs for which file types. + - "/config/taplo.toml" # Its own config. + - "/config/taplo/**" # The JSON schemas it lints TOML against. + - "/config/conftest/policy/mise/mise.rego" # Allowlists it as a tool with no prebuilt at some triple. + - "/config/semgrep/no-*-mentions.yaml" # The mention bans, which cannot help but name what they ban. + pattern-regex: "(?i)taplo" + message: >- + Do not name the TOML formatter and schema linter outside its config and schemas. Where a generator has to + emit TOML the formatter will accept unchanged, say which shape the output must have -- sorted arrays, + aligned comments -- rather than naming the tool that would otherwise rewrite it. + severity: ERROR + + - id: no-trivy-mentions + languages: [generic] + paths: + exclude: + - "/.mise/**" # Tool pin, lockfile entry, and the task that runs it. + - "/README.md" # Tells a contributor which checks validate the generated manifests. + - "/.codacy.yaml" # Names the manifest checks Codacy defers to, as its exclusion rationale. + - "/config/trivyignore.yaml" # Its own ignore file, where each entry states why the finding is an artifact. + - "/config/semgrep/no-*-mentions.yaml" # The mention bans, which cannot help but name what they ban. + pattern-regex: "(?i)trivy" + message: >- + Do not name the container and manifest scanner outside its ignore file and the task that runs it. Its + ignore entries need its own resolution behaviour explained; nothing it scans does. + severity: ERROR + + - id: no-typos-check-mentions + languages: [generic] + paths: + exclude: + - "/.mise/**" # Tool pin and the check/fix tasks. + - "/CLAUDE.md" # Documents which checks an agent runs for which file types. + - "/config/semgrep/no-*-mentions.yaml" # The mention bans, which cannot help but name what they ban. + pattern-regex: "(?i)typos-(check|fix)" + message: >- + Do not name the spelling checker's tasks outside the config that defines them. An identifier spelled an + unusual way is either correct or a typo; if it is correct, say what it means, and add it to the checker's + own allowlist rather than explaining it where it appears. + severity: ERROR + + - id: no-zizmor-mentions + languages: [generic] + paths: + exclude: + - "/.mise/**" # Tool pin, lockfile entry, and the task that runs it. + - "/CLAUDE.md" # Documents which checks an agent runs for which file types. + - "/config/zizmor.yaml" # Its own config. + - "/.codacy.yaml" # Names the workflow linters Codacy defers to, as its exclusion rationale. + - "/.github/actions/install-mise/action.yaml" # A `zizmor: ignore[...]` pragma, which only works spelled out. + - "/.github/workflows/upstream-cache.yaml" # The same pragma. + - "/config/semgrep/no-*-mentions.yaml" # The mention bans, which cannot help but name what they ban. + pattern-regex: "(?i)zizmor" + message: >- + Do not name the Actions security auditor except in the `zizmor: ignore[...]` pragma, which only works + spelled out, and in the configs that run it. A workflow step is written to be safe; say why it is, and let + the pragma carry the audit it waives. + severity: ERROR diff --git a/config/semgrep/no-jscpd-mentions.yaml b/config/semgrep/no-jscpd-mentions.yaml deleted file mode 100644 index 04fe8812..00000000 --- a/config/semgrep/no-jscpd-mentions.yaml +++ /dev/null @@ -1,46 +0,0 @@ -rules: - - id: no-jscpd-mentions-outside-its-own-wiring - languages: [generic] - paths: - # Repo-wide, with the allowlist below naming every file the word is allowed to appear in. - # Each entry is there because the name is load-bearing -- a task that invokes the tool, a config it reads, - # a policy package, an exclude another linter needs for its files, the ratchet's own documentation. A file - # that merely mentions it in passing does not belong here; the whole point is that the duplication checker - # stays invisible to everything it is not wired into. Adding an entry to explain that some code was - # written a certain way to satisfy the tool is precisely the thing this rule exists to prevent: it - # documents a tool's reaction rather than the code, and goes stale the moment the thresholds move. - exclude: - # The tool pin, the tasks that run it, and the lockfile entry for the pinned binary. - - "/.mise/config.toml" - - "/.mise/mise.lock" - # Its own config and the clone-fingerprint baseline it reads. - - "/config/jscpd.json" - - "/config/jscpd-baseline.json" - # The conftest policy that ratchets the baseline; `jscpd` is its package name, so `--namespace` needs it. - - "/config/conftest/policy/jscpd/jscpd.rego" - # Name the policy file above by path, as the pairing rule's exception list and its tests. - - "/config/conftest/policy/policy_tests/policy_tests.rego" - - "/config/conftest/policy/policy_tests/policy_tests_test.rego" - # Map the generated trees onto each linter's exclude key, the tool's among them. - - "/config/conftest/policy/generated_trees/generated_trees.rego" - - "/config/generated-trees.toml" - # Other linters' carve-outs for its two JSON files, which cannot be YAML or carry comments. - - "/config/semgrep/prefer-yaml-toml.yaml" - - "/config/typos.toml" - # Skips the tool in the Nano Server image, whose attestation verifier cannot install it. - - "/Dockerfile.nanoserver" - # Documents the baseline-is-a-debt-ledger rule, which is where that policy belongs. - - "/CLAUDE.md" - # This rule, which cannot help but name what it bans. - - "/config/semgrep/no-jscpd-mentions.yaml" - pattern-regex: (?i)jscpd - message: >- - Do not name the duplication checker outside the files it is wired into, and never in a comment explaining - that something was written a certain way to satisfy it -- not in source, not in a workflow, not in an - ignore marker. Such a comment documents a tool's reaction rather than the code, reads as noise to everyone - who does not run that tool, and goes stale the moment its thresholds move. A reported clone is a request - to remove duplication: factor the shared part out so it exists once. Where a clone is genuinely - irreducible -- the same few lines every caller must write verbatim -- leave the code plain and raise it, - rather than annotating the source. If a new file genuinely has to wire the tool up, add it to this rule's - paths.exclude allowlist with a comment saying which part of the wiring needs it. - severity: ERROR diff --git a/config/semgrep/no-scripts-dir.yaml b/config/semgrep/no-scripts-dir.yaml new file mode 100644 index 00000000..67175d56 --- /dev/null +++ b/config/semgrep/no-scripts-dir.yaml @@ -0,0 +1,15 @@ +rules: + - id: no-scripts-dir + languages: [generic] + paths: + include: + - "**/scripts/**" + pattern-regex: \A + message: >- + A `scripts/` directory is banned in this repo. Every script belongs in one of exactly two places, and + neither of them is a loose tree of files that nothing points at. Short and simple goes inline in a task + body under `.mise/`, where it is listed alongside every other entry point and runs by name. Anything + more involved gets its own directory under `utilities/`, with a `README.md` saying what it does and how + to run it. A file under `scripts/` has neither: no caller, no documentation, and nothing that fails when + it stops working. Move it to whichever of the two fits, and drop the directory. + severity: ERROR diff --git a/config/semgrep/no-shell-scripts.yaml b/config/semgrep/no-shell-scripts.yaml new file mode 100644 index 00000000..9d60eaf1 --- /dev/null +++ b/config/semgrep/no-shell-scripts.yaml @@ -0,0 +1,17 @@ +rules: + - id: no-shell-scripts + languages: [generic] + paths: + include: + - "*.sh" + exclude: + - "/.mise/**" # A task body split out of its TOML string, which is the one shell this repo asks for. + pattern-regex: \A + message: >- + A `.sh` file outside `.mise/` is banned. Shell that is short and simple belongs inline in a task body, + where it is discoverable and runs by name; shell that has outgrown that is a program, and belongs in its + own directory under `utilities/`, written in a language with types, tests and a linter behind it. The + one exception is `.mise/`, where a task body too long to sit inside a TOML string is split into a file + beside the config that invokes it, named for the interpreter that reads it -- that file is formatted and + linted as the shell it is, which a string embedded in a config never can be. + severity: ERROR diff --git a/config/semgrep/no-task-name-mentions.yaml b/config/semgrep/no-task-name-mentions.yaml new file mode 100644 index 00000000..ade60c21 --- /dev/null +++ b/config/semgrep/no-task-name-mentions.yaml @@ -0,0 +1,235 @@ +# One rule per family of mise task, each banning those task names everywhere except the files that need them. +# +# A task name is an entry point, not a fact about the code. Writing one into a source file, a manifest or a +# neighbouring config makes that file depend on a name it never invokes: the task can be renamed, split, folded +# into an aggregate or dropped, and nothing fails -- the reference just quietly becomes a lie. Worse, it usually +# stands in for something the reader actually needs. "Sorted because reformats it" hides the shape the +# file has to have; "this broke once" hides the invariant that broke. +# +# The name is critical, and so allowlisted, in four places. A task definition or its `depends`. A command that +# actually runs it -- CI, an image build, a process manager. A generated file's header, which is the only way a +# reader can rebuild it. And documentation whose subject is what to run. Everything else states the constraint +# and leaves the task out of it. +# +# The families are split by what the tasks do, because that decides where they can legitimately appear: a build +# task belongs in an image, a generator in the header of what it wrote, an observability task in the collector +# config it loads. Names that are also a crate, module, directory or package name are absent -- `ws-server` and +# `pyo3-math1` name components far more often than tasks -- as are the aggregates whose names are ordinary words +# (`check`, `fmt`, `test`, `all`). The `(?x)` flag is what lets each alternation wrap inside the line limit. +rules: + - id: no-generator-task-name-mentions + languages: [generic] + paths: + exclude: + - "/.mise/**" # Where the tasks are defined and depend on each other. + - "/.github/**" # Runs them, and fails a PR on drift between their output and what is committed. + - "/CLAUDE.md" # Documents which generator to run after changing a source of truth. + - "/README.md" # Same, for a contributor. + - "**/README.md" # A generated tree's README names the command that rewrites it. + - "/generated/**" # Generator-owned output, whose headers name the task that wrote it. + - "/verification/**" # The same, for the scenario outputs. + - "/.gitignore" # Half of a generated pair, naming the task that derives the other half. + - "/.dockerignore" # The generated half, with the same header. + - "/Dockerfile" # Runs the drift checks against the in-image source. + - "/config/generated-trees.toml" # `regenerated_by` is a value, read by the policy that enforces it. + - "/config/osv-scanner.toml" # Generated, and says so in its first line. + - "/config/openapi-python-client.yaml" # Disables hooks a generator task already performs itself. + - "/config/typos.toml" # A vendored word that cannot be corrected here, only where it is fetched from. + - "/services/ws-wasi-runner/src/bindings.rs" # Generated; the header is how a reader rebuilds it. + - "/utilities/int-gen/src/**" # The generator itself, whose entry points are these tasks. + - "/config/semgrep/no-*-mentions.yaml" # The mention bans, which cannot help but name what they ban. + pattern-regex: >- + (?ix)(?- + Do not name a generator task outside the generator, the files it writes, and the docs that say when to run + it. A generated file's own header is where the name belongs, because a reader who must not hand-edit the + file needs the command that rebuilds it. Elsewhere, "the drift check fails" says everything the reader can + act on, and survives the task being renamed or folded into an aggregate. + severity: ERROR + + - id: no-build-task-name-mentions + languages: [generic] + paths: + exclude: + - "/.mise/**" # Where the tasks are defined and depend on each other. + - "/.github/**" # Builds the modules on CI. + - "/CLAUDE.md" # Documents how to build one module rather than the whole set. + - "/README.md" # Tells a contributor how to build a module. + - "**/README.md" # A module's own README names the command that builds it. + - "/Dockerfile" # Builds the modules into the image. + - "/Dockerfile.nanoserver" # Same, for the Nano Server image. + - "/Dockerfile.windows" # Same, for the Windows image. + - "/generated/**" # Generator-owned output, carrying whatever the generator wrote. + - "/config/conftest/policy/mise/mise.rego" # Names the task a shell builtin was verified to break in. + - "/pnpm-workspace.yaml" # The lockfile layout exists because one module's build depends on it. + - "/services/ws-modules/pyface1/pyproject.toml" # Names the task that puts a bundled asset in `pkg/`. + - "/services/ws-modules/wasi-graphics-info/.gitignore" # Names the task that writes the ignored files. + # These three skip when a module's `pkg/` is absent, and print or explain which build fills it. + - "/services/ws-wasi-runner/tests/modules.rs" + - "/services/ws-wasi-runner/tests/otel_propagation.rs" + - "/services/ws-web-runner/tests/modules.rs" + - "/config/semgrep/no-*-mentions.yaml" # The mention bans, which cannot help but name what they ban. + pattern-regex: >- + (?ix)(?- + Do not name a module build task outside the images, jobs and READMEs that run it. A skip message telling a + developer which build to run is the exception, and the reason it works is that it is addressed to someone + at a terminal; a comment in the module's own source is not. + severity: ERROR + + - id: no-coverage-task-name-mentions + languages: [generic] + paths: + exclude: + - "/.mise/**" # Where the tasks are defined and depend on each other. + - "/.github/**" # Runs the coverage pipeline and uploads its reports. + - "/CLAUDE.md" # Documents how coverage is collected for each runtime. + - "/.codacy.yaml" # Names the task whose trusted argv is why an excluded file is a false positive. + - "/services/ws-test-server/src/bin/cov-server.rs" # The same rationale, in the excluded file's header. + - "/utilities/wasm-cov-wrapper/src/main.rs" # Likewise. + - "/services/ws-wasm-agent/Cargo.toml" # Names the task that turns the instrumentation feature on. + - "/services/ws-wasm-agent/tests/client.rs" # Names the task that must have a backend listening. + - "/services/ws-web-runner/tests/modules.rs" # Names where each profile artifact is routed for processing. + - "/config/semgrep/no-*-mentions.yaml" # The mention bans, which cannot help but name what they ban. + pattern-regex: >- + (?ix)(?- + Do not name a coverage task outside the pipeline that runs it. Where one is a genuine precondition -- it + starts the server a test needs, or it is the trusted caller that makes an argv-derived path safe -- the + name is the precondition and belongs there. A test's own assertions never are. + severity: ERROR + + - id: no-observability-task-name-mentions + languages: [generic] + paths: + exclude: + - "/.mise/**" # Where the tasks are defined and depend on each other. + - "/.github/**" # Skips the opener one of them uses on a lane that cannot install it. + - "/CLAUDE.md" # Documents how to bring the local collector up. + - "/README.md" # Same, for a contributor. + - "/Dockerfile" # Skips the runtime services it would otherwise stage. + - "/Dockerfile.windows" # Notes which opener is unavailable on that lane. + - "/mprocs.yaml" # Process entries are the tasks it launches. + - "/verification/**" # Generated deployments whose own task list includes them. + - "/config/otelcol-hostmetrics.yaml" # Loaded by the task named in its header. + - "/config/otelcol-macmon.yaml" # Likewise. + - "/config/otelcol-nvidia.yaml" # Likewise. + - "/config/otelcol-winmetrics.yaml" # Likewise. + - "/config/otelcol-winperf.yaml" # Likewise. + - "/config/conftest/policy/mise/mise.rego" # Names the task each os-scoped exporter tool exists for. + - "/services/ws-wasi-runner/tests/o2_ingest_search.rs" # Its subject is the server those tasks run. + - "/utilities/cli/src/lib.rs" # Emits those task names into the deployments it generates. + - "/utilities/cli/src/deployment_types/mise.rs" # Likewise. + - "/config/semgrep/no-*-mentions.yaml" # The mention bans, which cannot help but name what they ban. + pattern-regex: >- + (?ix)(?- + Do not name an observability task outside the configs it loads and the things that launch it. A collector + config naming the task that reads it is how a reader finds its way in; a comment elsewhere saying some + value exists for one of these tasks is not. + severity: ERROR + + - id: no-maintainer-task-name-mentions + languages: [generic] + paths: + exclude: + - "/.mise/**" # Where the tasks are defined and depend on each other. + - "/.github/**" # Runs the install and asset jobs, and records what only a maintainer may run. + - "/CLAUDE.md" # Documents the bootstrap-then-publish sequence for a new cached asset. + - "/README.md" # Tells a contributor which install to run first. + - "/Dockerfile" # Installs the toolchain and prefetches models. + - "/Dockerfile.nanoserver" # Same, with a narrower prefetch. + - "/Dockerfile.windows" # Same. + - "/config/upstream-cache/data.toml" # Names the task that mirrors each asset to our own release. + - "/data/model-modules/.gitignore" # Names the task that fetches the ignored weights. + - "/config/semgrep/no-*-mentions.yaml" # The mention bans, which cannot help but name what they ban. + pattern-regex: >- + (?ix)(?- + Do not name an install, prefetch or asset-publishing task outside the jobs, images and asset tables that + invoke it. These run on a maintainer's machine or a runner, never from the code, so a source file naming + one is describing an operation it has no part in. + severity: ERROR + + - id: no-test-task-name-mentions + languages: [generic] + paths: + exclude: + - "/.mise/**" # Where the tasks are defined and depend on each other. + - "/.github/**" # Runs the suites on CI. + - "/CLAUDE.md" # Documents which suite covers which language. + - "/README.md" # Tells a contributor how to run them. + - "**/README.md" # A module's own README names the command that exercises it. + - "/services/ws-modules/pic-viewer/tests/show_image.rs" # Needs a browser harness no plain test run gives. + - "/services/ws-test-server/src/bin/cov-server.rs" # Names the end-to-end run sharing its fixed port. + - "/services/ws-test-server/src/bin/verify-math1.rs" # Names the task that pipes cluster output into it. + - "/services/ws-wasm-agent/tests/client.rs" # Names the runs that must have a backend on its port. + - "/utilities/cli/tests/scenario_runners.rs" # Drives the generated tasks, and needs the group config. + - "/config/semgrep/no-*-mentions.yaml" # The mention bans, which cannot help but name what they ban. + pattern-regex: >- + (?ix)(?- + Do not name a test or server task outside the jobs and docs that run it. A test file may name the task + that supplies something it cannot create for itself -- a browser harness, a listening backend, a port + group -- because that is a precondition. Naming one to explain why an assertion exists is not. + severity: ERROR + + - id: no-check-task-name-mentions + languages: [generic] + paths: + exclude: + - "/.mise/**" # Where the tasks are defined and depend on each other. + - "/.github/**" # Runs the battery on CI. + - "/CLAUDE.md" # Documents which check an agent runs for which file type. + - "/README.md" # Tells a contributor which checks gate a change. + - "/Dockerfile" # Runs the battery against the in-image source. + - "/Dockerfile.nanoserver" # Names the tasks a skipped tool would otherwise gate. + - "/generated/**" # Generator-owned output, carrying whatever the generator wrote. + - "/.codacy.yaml" # Names the local checks it defers to, as its exclusion rationale. + - "/.deepsource.toml" # Likewise, for the language it leaves to the local pipeline. + # Each tool config's header names the task that applies it, which is how a reader learns what runs it. + - "/config/clang-format.yaml" + - "/config/clang-tidy.yaml" + - "/config/deny.toml" + - "/config/gitleaks.toml" + - "/config/ls-lint.yaml" + - "/config/lychee.toml" + - "/config/pyrefly.toml" + - "/config/trivyignore.yaml" + - "/config/typos.toml" + - "/config/zizmor.yaml" + - "/config/conftest/policy/**" # Each policy names the task that feeds it, which fixes what it can see. + - "/config/semgrep/ls-lint-no-glob.yaml" # Its worked example is two checks disagreeing about a path. + - "/config/semgrep/no-type-comments.yaml" # Names the check whose coverage the rule exists to protect. + - "/pubspec.yaml" # One root lockfile exists so a single scan has a committed target. + - "/config/semgrep/no-*-mentions.yaml" # The mention bans, which cannot help but name what they ban. + pattern-regex: >- + (?ix)(?- + Do not name a check or formatter task outside its own config, the images and jobs that run it, and the + docs that say when to. Saying code is written a certain way because one of these would object describes + the check rather than the code, and it is the first thing to rot: the task gets renamed, folded into an + aggregate, or its rule moves, and the comment keeps asserting the old arrangement. State the constraint. + severity: ERROR diff --git a/config/semgrep/no-type-comments.yaml b/config/semgrep/no-type-comments.yaml index 411e85ab..3ea19fe2 100644 --- a/config/semgrep/no-type-comments.yaml +++ b/config/semgrep/no-type-comments.yaml @@ -18,6 +18,6 @@ rules: message: >- Legacy `# type:` comment. Use an inline annotation (`x: T = ...`, `def f(a: T) -> R:`) instead. The compiled Pyrefly type checker that backs `check:python` parses annotations but silently ignores legacy type comments, - so a broken one (an undefined name, a stray `--`) rots unseen -- exactly DeepSource's TYP-025. Annotations are + so a broken one (an undefined name, a stray `--`) rots unseen -- exactly `TYP-025`. Annotations are the only form both tools check. `# type: ignore` (the suppression pragma) is exempt. severity: ERROR diff --git a/config/taplo.toml b/config/taplo.toml index f735c571..e6422b5f 100644 --- a/config/taplo.toml +++ b/config/taplo.toml @@ -64,7 +64,7 @@ include = ["**/Cargo.toml"] schema = { path = "config/taplo/no-banned-deps.schema.json" } # Require `[lib] doctest = false` for every member crate with a [lib]. -# Pairs with the `no-doctest` ast-grep rule: that one bans ``` fenced blocks inside doc comments; this one +# Pairs with the structural ban on ``` fenced blocks inside doc comments: this one # disables the doctest harness at the crate level so the prohibition has cargo-level parity. Runnable # examples belong in `tests/` files (each with `#![cfg(test)]`). [[rule]] diff --git a/config/taplo/require-lib-doctest-false.schema.json b/config/taplo/require-lib-doctest-false.schema.json index c52dc6e8..e2268676 100644 --- a/config/taplo/require-lib-doctest-false.schema.json +++ b/config/taplo/require-lib-doctest-false.schema.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "title": "Workspace member Cargo.toml -- [lib] must set doctest = false", - "description": "Pairs with the no-doctest ast-grep rule; runnable examples go in tests/.", + "description": "Pairs with the ban on fenced blocks in doc comments; runnable examples go in tests/.", "type": "object", "properties": { "lib": { diff --git a/config/trivyignore.yaml b/config/trivyignore.yaml index 9dccc645..5d269989 100644 --- a/config/trivyignore.yaml +++ b/config/trivyignore.yaml @@ -53,8 +53,7 @@ misconfigurations: # a plain `docker build` supplies with `--build-context`. Trivy reads that as an image reference it cannot # fetch, and so reports it as untagged (DS-0001), as running whatever user it cannot see (DS-0002), and as # lacking the healthcheck it cannot read (DS-0026). The hub Dockerfile actually declares `USER 10001`, and a - # build context has no tag to give. hadolint needs `DL3006` suppressed at the same `FROM` for the same - # reason, which is stated in the generated file itself. + # build context has no tag to give. - id: DS-0001 paths: - "verification/*/output/*/Dockerfile" diff --git a/config/upstream-cache/data.toml b/config/upstream-cache/data.toml index 81c82a44..e23ecd68 100644 --- a/config/upstream-cache/data.toml +++ b/config/upstream-cache/data.toml @@ -13,7 +13,7 @@ # - license: SPDX expression for the upstream content. Bump alongside `upstream` whenever upstream # re-licenses. # -# config/conftest/policy/checksums/checksums.rego enforces a bidirectional cross-reference: every +# A policy enforces a bidirectional cross-reference: every # `_asset` var in .mise/config*.toml must have a matching `[asset.]` table here, and vice # versa. Filename keys pin the upstream commit SHA (short) or version; bump in lockstep with the var and the # release tag. diff --git a/generated/README.md b/generated/README.md index dfe539df..b9bc5cf1 100644 --- a/generated/README.md +++ b/generated/README.md @@ -38,8 +38,7 @@ directly: omitted — progenitor's generated source contains patterns the workspace lint set flags (doc-comment shapes that trip rustdoc's invalid-code-block lint, etc.), so the lint-inheritance schema - (`config/taplo/require-lints-section.schema.json`) exempts this - Cargo.toml. The deps-inheritance schema does not. + exempts this Cargo.toml. The deps-inheritance schema does not. - `zig-rest/build.zig.zon` — Zig package manifest (name, version, fingerprint). Regen writes only `src/et_rest_client.zig`. - `specs/wit/world.wit` — The top-level `et:ws-wasi@0.1.0` package diff --git a/libs/path/src/lib.rs b/libs/path/src/lib.rs index 9b7f9afe..f5869d90 100644 --- a/libs/path/src/lib.rs +++ b/libs/path/src/lib.rs @@ -28,7 +28,7 @@ pub fn find_project_root(start: &Path) -> PathBuf { /// Locate the repository root from a build script. /// /// Reads `CARGO_MANIFEST_DIR` (which cargo sets for build scripts), so this is the single sanctioned use -/// of that variable (see the `no-cargo-manifest-dir` ast-grep rule). Outside a build script the variable +/// of that variable, and every other read of it is banned. Outside a build script the variable /// is unset and this returns a meaningless path; use [`find_project_root`] with an explicit start, or /// `edge_toolkit::config::get_project_root`, instead. #[must_use] diff --git a/libs/wasi-guest/src/coverage.rs b/libs/wasi-guest/src/coverage.rs index 504f74ef..9c6170f5 100644 --- a/libs/wasi-guest/src/coverage.rs +++ b/libs/wasi-guest/src/coverage.rs @@ -3,8 +3,8 @@ //! minicov's `capture_coverage` is an `unsafe fn` (it reads the raw instrumented counter buffers), which //! Codacy flags for audit. Codacy can only exclude whole paths in-repo, not suppress per line, so this //! one-function file is the single thing excluded from Codacy (see .codacy.yaml) while the rest of this crate -//! and every guest that depends on it stays analyzed. The unsafe is still covered by the repo's own clippy and -//! by DeepSource. Each guest calls this at the end of `run()`, naming the profile its own build should write. +//! and every guest that depends on it stays analyzed. Every other analyzer the repo runs over Rust still reads +//! the unsafe. Each guest calls this at the end of `run()`, naming the profile its own build should write. /// Dump the calling guest's coverage profile to the runner's `/cov` preopen, under `profraw_name`. #[expect( diff --git a/libs/wasi-guest/src/lib.rs b/libs/wasi-guest/src/lib.rs index 26c6c037..097d6afc 100644 --- a/libs/wasi-guest/src/lib.rs +++ b/libs/wasi-guest/src/lib.rs @@ -46,7 +46,7 @@ const AGENT_ID_POLL_INTERVAL_MS: u64 = 50; const NANOS_PER_MILLI: u64 = 1_000_000; // Lets `?` lift a `ws-error` into `entry-error.ws(...)` so a guest's `run` body stays free of explicit -// `.map_err`s (which the workspace's no-map-err ast-grep rule bans outside listed error.rs files anyway). +// `.map_err`s, which the workspace bans outside the error.rs files it names anyway. impl From for EntryError { fn from(err: WsError) -> Self { Self::Ws(err) diff --git a/libs/web/src/error.rs b/libs/web/src/error.rs index 41e7ebc9..b654daa8 100644 --- a/libs/web/src/error.rs +++ b/libs/web/src/error.rs @@ -7,8 +7,7 @@ //! -- replace with `.js_context("ctx")` (it formats the inner `JsValue` //! into the prefix string itself). //! -//! This is the only file in the workspace where `map_err` is permitted -- -//! the `no-map-err` ast-grep rule exempts it. +//! This is the only file in the workspace where `map_err` is permitted. use wasm_bindgen::{JsCast, JsValue}; diff --git a/libs/web/src/lib.rs b/libs/web/src/lib.rs index cc78206b..49eddb54 100644 --- a/libs/web/src/lib.rs +++ b/libs/web/src/lib.rs @@ -8,8 +8,8 @@ pub const SENSOR_PERMISSION_GRANTED: &str = "granted"; /// Discard `value`, marking a `Result` (or other `#[must_use]`) as intentionally ignored. /// -/// The workspace denies `let_underscore*` and `unused_results`, and `DeepSource`'s RS-E1021 flags `drop()` on a -/// non-`Drop` type (e.g. `Result`), so neither `let _ = expr` nor `drop(expr)` is available for discarding one. +/// The workspace denies `let_underscore*` and `unused_results`, and `RS-E1021` flags `drop()` on a non-`Drop` +/// type (e.g. `Result`), so neither `let _ = expr` nor `drop(expr)` is available for discarding one. /// Passing the value here consumes it -- satisfying `must_use` / `unused_results` -- via neither. Intended for /// best-effort JS DOM calls in `()`-returning closures and event handlers where the error is deliberately dropped. pub fn ignore(_value: T) {} @@ -19,7 +19,7 @@ pub fn ignore(_value: T) {} /// Present only in the `coverage` build. `wasm-bindgen` collects this export into every dependent browser /// module's JS glue, so the web-runner can pull each module's coverage after running it -- `wasm32-unknown-unknown` /// has no filesystem, so the bytes come back through JS rather than a file. The web-runner then routes them -/// through the same llc + llvm-cov pipeline the WASI guests use (see the `wasi-cov` mise task). +/// through the same llc + llvm-cov pipeline the WASI guests use. #[cfg(feature = "coverage")] #[wasm_bindgen] #[expect( diff --git a/ruff.toml b/ruff.toml index cedacce9..f160c9d6 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,11 +1,11 @@ -# Kept at the repo root on purpose (unlike config/deny.toml, config/osv-scanner.toml, config/taplo.toml). +# Kept at the repo root on purpose, unlike every other tool config in this repo, which lives under config/. # Editors/IDEs (the VS Code Ruff extension, PyCharm) and pre-commit auto-discover ruff config only as a root # ruff.toml / .ruff.toml / pyproject.toml. ruff has no config-path env var, so moving this under config/ # would silently drop every editor back to ruff's defaults (line-length 88, no isort). The mise tasks could # pass --config, but the live editor experience can't, so it stays here. # # Match the editorconfig line length. Without this, ruff would use its default of 88, so files -# that ruff considered "already formatted" could still trip the editorconfig-check. +# that ruff considered "already formatted" could still sit over the limit. line-length = 120 # Exclude componentize-py's generated bindings, which we don't own and don't ship. @@ -24,7 +24,7 @@ line-ending = "lf" [lint] # On top of the default rule set (E, F). Codes are kept alphabetically sorted. # "ARG" -- flake8-unused-arguments: unused function/method/lambda arguments. This is the local equivalent of -# DeepSource's PYL-W0613; ruff honours the underscore-prefix convention (`dummy-variable-rgx`), so a param kept +# `PYL-W0613`; ruff honours the underscore-prefix convention (`dummy-variable-rgx`), so a param kept # only to satisfy a fixed callback signature (the pyo3-runner `init`/`on_text_frame` hooks) is silenced by # renaming it `_send` / `_text` rather than deleting it. # "B" -- flake8-bugbear: likely bugs (mutable defaults, `zip` without `strict=`, loop-variable shadowing, ...). diff --git a/services/ws-modules/face-detection/tests/web.rs b/services/ws-modules/face-detection/tests/web.rs index a0e16f96..62569a49 100644 --- a/services/ws-modules/face-detection/tests/web.rs +++ b/services/ws-modules/face-detection/tests/web.rs @@ -1,5 +1,6 @@ #![cfg(test)] #![cfg(target_arch = "wasm32")] +#![cfg_attr(coverage_nightly, feature(coverage_attribute))] use et_ws_face_detection::{init, is_running, run, stop}; use wasm_bindgen_test::*; diff --git a/services/ws-modules/har1/tests/web.rs b/services/ws-modules/har1/tests/web.rs index b03cb5fc..8070dc04 100644 --- a/services/ws-modules/har1/tests/web.rs +++ b/services/ws-modules/har1/tests/web.rs @@ -1,5 +1,6 @@ #![cfg(test)] #![cfg(target_arch = "wasm32")] +#![cfg_attr(coverage_nightly, feature(coverage_attribute))] use et_ws_har1::{init, run}; use wasm_bindgen_test::*; diff --git a/services/ws-modules/pic-viewer/tests/show_image.rs b/services/ws-modules/pic-viewer/tests/show_image.rs index 23a1c404..e8eddee3 100644 --- a/services/ws-modules/pic-viewer/tests/show_image.rs +++ b/services/ws-modules/pic-viewer/tests/show_image.rs @@ -7,6 +7,7 @@ #![cfg(test)] #![cfg(target_arch = "wasm32")] +#![cfg_attr(coverage_nightly, feature(coverage_attribute))] use et_ws_pic_viewer::show_image; use wasm_bindgen::JsCast; diff --git a/services/ws-modules/rcomm1/pkg/et_ws_rcomm1.js b/services/ws-modules/rcomm1/pkg/et_ws_rcomm1.js index 821c1baf..7c3c774b 100644 --- a/services/ws-modules/rcomm1/pkg/et_ws_rcomm1.js +++ b/services/ws-modules/rcomm1/pkg/et_ws_rcomm1.js @@ -4,7 +4,7 @@ // webR cannot open the agent WebSocket itself, so the transport (the shared et-ws-wasm-agent WsClient) lives // here and is exposed on globalThis.__etAgent for R to drive. The on_message callback stashes the latest agent // list (R selects the peer from it) and logs inbound messages. All sequencing and message composition happen in -// module.R. webR is vendored under pkg/webr/ (see build-ws-rcomm1-module). +// module.R. webR is vendored under pkg/webr/ and served at the path below. const WEBR_BASE_URL = "/modules/et-ws-rcomm1/webr/"; const R_SOURCE_URL = "/modules/et-ws-rcomm1/module.R"; diff --git a/services/ws-modules/rdata1/pkg/et_ws_rdata1.js b/services/ws-modules/rdata1/pkg/et_ws_rdata1.js index e9156535..6915f4ea 100644 --- a/services/ws-modules/rdata1/pkg/et_ws_rdata1.js +++ b/services/ws-modules/rdata1/pkg/et_ws_rdata1.js @@ -4,7 +4,7 @@ // webR cannot open the agent WebSocket itself, so the transport (the shared et-ws-wasm-agent WsClient) lives // here and is exposed on globalThis.__etAgent for R to drive. Everything else -- sequencing, the storage // round-trip (httr2 over the /websockify relay), verification -- happens in module.R. webR is vendored under -// pkg/webr/ (see build-ws-rdata1-module) and served at the path below. +// pkg/webr/ and served at the path below. const WEBR_BASE_URL = "/modules/et-ws-rdata1/webr/"; const R_SOURCE_URL = "/modules/et-ws-rdata1/module.R"; diff --git a/services/ws-modules/rmath1/pkg/et_ws_rmath1.js b/services/ws-modules/rmath1/pkg/et_ws_rmath1.js index bce02f13..dcab8842 100644 --- a/services/ws-modules/rmath1/pkg/et_ws_rmath1.js +++ b/services/ws-modules/rmath1/pkg/et_ws_rmath1.js @@ -4,8 +4,8 @@ // webR cannot open the agent WebSocket itself, so the transport (the shared et-ws-wasm-agent WsClient) lives // here and is exposed on globalThis.__etAgent for R to drive; the broadcast math1-input pointer is captured // onto __etAgent.input for R to poll. Everything else -- sequencing, the storage reads/writes (httr2 over the -// /websockify relay), the FedAvg kernel -- happens in module.R. webR is vendored under pkg/webr/ (see -// build-ws-rmath1-module) and served at the path below. +// /websockify relay), the FedAvg kernel -- happens in module.R. webR is vendored under pkg/webr/ and served +// at the path below. const WEBR_BASE_URL = "/modules/et-ws-rmath1/webr/"; const R_SOURCE_URL = "/modules/et-ws-rmath1/module.R"; diff --git a/services/ws-pyo3-runner/tests/modules.rs b/services/ws-pyo3-runner/tests/modules.rs index e40bda2e..d60d1622 100644 --- a/services/ws-pyo3-runner/tests/modules.rs +++ b/services/ws-pyo3-runner/tests/modules.rs @@ -304,7 +304,7 @@ async fn exchange_fanout(control: &mut ControlSocket, count: u8) -> Result<(), B /// Send the module's trigger and assert on its reply. /// /// Each variant's send-and-check sequence lives in its own function above rather than in an arm here. Inline, -/// the four of them made one 55-line body whose cyclomatic complexity was 19 against Codacy's limit of 10, and +/// the four of them made one 55-line body whose cyclomatic complexity was 19 against a ceiling of 10, and /// they share nothing but the socket -- the reply is text, JSON, one binary frame or many, per variant. async fn run_exchange( control: &mut ControlSocket, diff --git a/services/ws-server/Dockerfile b/services/ws-server/Dockerfile index 3d255402..4b73f6e4 100644 --- a/services/ws-server/Dockerfile +++ b/services/ws-server/Dockerfile @@ -72,8 +72,8 @@ WORKDIR /workspace COPY . . # The wasm flags mirror CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUSTFLAGS in .mise/config.toml. -# That is what `mise run build-ws-wasm-agent` builds the agent with on a workstation, so the served artifact is the -# same one either way. They are split across two ARGs purely so the composed ENV fits the line limit. +# A workstation builds the agent with those same flags, so the served artifact is the same one either way. +# They are split across two ARGs purely so the composed ENV fits the line limit. ARG WASM_TARGET_CPU="-C target-cpu=mvp" ARG WASM_FEATURES="+mutable-globals,+sign-ext,+nontrapping-fptoint,+reference-types" ENV CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUSTFLAGS="${WASM_TARGET_CPU} -C target-feature=${WASM_FEATURES}" @@ -86,10 +86,10 @@ EOF # The browser client is built in its own layer, deliberately not folded into the cargo build above. # The two compile different targets from different inputs, so keeping them apart means editing the agent does # not invalidate the server's release build and vice versa. Three analyzers raise that split as the same -# consecutive-RUN anti-pattern under three different codes, so it takes three pragmas: DeepSource's DOK-W1001 -# (DOK-DL3059 is not a code DeepSource issues, so it suppressed nothing), and hadolint's DL3059 -- named inline -# here rather than left to config/hadolint.yaml's ignore list, because Codacy runs its own hadolint and reads -# neither that file nor a skipcq. +# consecutive-RUN anti-pattern under three different codes, so it takes three pragmas: `DOK-W1001` +# (`DOK-DL3059` is not a code anything here issues, so it suppressed nothing), and hadolint's DL3059 -- named +# inline here rather than left to config/hadolint.yaml's ignore list, because one of the three runs its own +# hadolint and reads neither that file nor a skipcq. # skipcq: DOK-W1001 # hadolint ignore=DL3059 RUN bash <<'EOF' diff --git a/services/ws-test-server/src/bin/cov-server.rs b/services/ws-test-server/src/bin/cov-server.rs index aed2289d..70cacfc3 100644 --- a/services/ws-test-server/src/bin/cov-server.rs +++ b/services/ws-test-server/src/bin/cov-server.rs @@ -9,7 +9,7 @@ //! operation as a path-traversal shape and cannot suppress it per line. That is a false positive here -- the single //! argument is the marker path the trusted `wasm-agent-cov` mise task passes -- and taking that path as an argument //! (rather than hardcoding one) is the point. The crate exposes this as its own minimal file so the path exclude stays -//! narrow; it is still covered by clippy and `DeepSource`'s Rust analyzer. +//! narrow; every other analyzer the repo runs over Rust still reads it. use std::error::Error; use std::path::PathBuf; diff --git a/services/ws-wasi-runner/src/host/mod.rs b/services/ws-wasi-runner/src/host/mod.rs index 31e70920..43f2408c 100644 --- a/services/ws-wasi-runner/src/host/mod.rs +++ b/services/ws-wasi-runner/src/host/mod.rs @@ -69,7 +69,7 @@ impl HostState { builder.inherit_stdio().inherit_env(); } // Instrumented guests write their minicov `.profraw` to `/cov`; map it to target/wasi-cov so the - // wasi-cov task finds it. Repo-root-anchored (not CWD) so it lands consistently under nextest. + // wasi-cov task finds it. Repo-root-anchored (not CWD) so it lands consistently under the test harness. #[cfg(feature = "coverage")] if coverage { #[expect( diff --git a/services/ws-wasi-runner/src/host/ws.rs b/services/ws-wasi-runner/src/host/ws.rs index c9b98c1e..d5368d16 100644 --- a/services/ws-wasi-runner/src/host/ws.rs +++ b/services/ws-wasi-runner/src/host/ws.rs @@ -54,11 +54,11 @@ pub struct WsBackend { } impl WsBackend { - #[expect( - clippy::single_call_fn, - reason = "inherent constructor; used once by ::connect" - )] - async fn connect(ws_url: &str, ack_timeout: Option) -> Result { + /// Open the socket and complete the et-connect handshake. + /// + /// Public so `tests/ws_backend.rs` can drive the reader pump and the heartbeat without standing up a + /// wasmtime store: both live in tasks this constructor spawns, and nothing else reaches them. + pub async fn connect(ws_url: &str, ack_timeout: Option) -> Result { // The shared helper opens the socket and completes the et-connect // handshake (with bounded retries), so the agent_id is known the moment // it returns -- no polling for ConnectAck afterwards. @@ -149,10 +149,20 @@ impl WsBackend { }) } - async fn current_state(&self) -> State { + /// The connection's state as the reader pump last left it. + pub async fn current_state(&self) -> State { *self.connection_state.lock().await } + /// Take the next inbound message, or `None` if none arrives within `timeout`. + /// + /// The guest-facing `recv` converts to WIT types on the way out; this is the same drain without that + /// step, so a test can assert on the `ServerMessage` the reader pump actually produced. + pub async fn next_message(&self, timeout: Duration) -> Option { + let mut rx = self.inbox.lock().await; + tokio::time::timeout(timeout, rx.recv()).await.ok().flatten() + } + async fn current_agent_id(&self) -> String { self.agent_id.lock().await.clone().unwrap_or_default() } diff --git a/services/ws-wasi-runner/tests/ws_backend.rs b/services/ws-wasi-runner/tests/ws_backend.rs new file mode 100644 index 00000000..06f5d1e1 --- /dev/null +++ b/services/ws-wasi-runner/tests/ws_backend.rs @@ -0,0 +1,106 @@ +//! Host websocket backend: the inbound binary-relay path and the heartbeat. +//! +//! `modules.rs` drives whole guests end to end, but no bundled guest ever receives a binary relay frame, and +//! none sits still long enough for the 5s heartbeat to tick. Both paths live in tasks `WsBackend::connect` +//! spawns, so neither is reachable by exercising the guest API -- which is why they were the two uncovered +//! regions in this file. These tests drive the backend directly against an in-process ws-server instead. +#![cfg(test)] +#![expect( + clippy::arithmetic_side_effects, + clippy::single_call_fn, + reason = "integration test: deadline arithmetic cannot overflow in a test's lifetime; step helpers" +)] + +use std::time::Duration; + +use edge_toolkit::ws::{ClientMessage, ServerMessage}; +use et_ws_wasi_runner::bindings::et::ws_wasi::ws::State; +use et_ws_wasi_runner::host::ws::WsBackend; +use futures_util::{SinkExt as _, StreamExt as _}; +use tokio_tungstenite::{connect_async, tungstenite}; + +/// Handshake budget for both the backend and the raw peer. +const ACK_TIMEOUT: Duration = Duration::from_secs(10); +/// How long to wait for a relayed frame to come back round through the hub. +const RELAY_TIMEOUT: Duration = Duration::from_secs(10); + +/// Connect a plain websocket client and complete et-connect, returning the registered socket. +/// +/// Deliberately not `WsBackend`: the point is to have a second agent the hub will relay *to*, driven with raw +/// frames, so the backend under test is the only thing being measured. +async fn connect_peer( + ws_url: &str, +) -> tokio_tungstenite::WebSocketStream> { + let (mut socket, _response) = connect_async(ws_url).await.unwrap(); + let connect = serde_json::to_string(&ClientMessage::Connect { agent_id: None }).unwrap(); + socket.send(tungstenite::Message::text(connect)).await.unwrap(); + + let deadline = tokio::time::Instant::now() + ACK_TIMEOUT; + while tokio::time::Instant::now() < deadline { + let remaining = deadline - tokio::time::Instant::now(); + let Ok(Some(Ok(frame))) = tokio::time::timeout(remaining, socket.next()).await else { + break; + }; + let tungstenite::Message::Text(text) = frame else { + continue; + }; + if matches!( + serde_json::from_str::(&text), + Ok(ServerMessage::ConnectAck { .. }) + ) { + return socket; + } + } + panic!("peer never received its et-connect-ack"); +} + +/// A binary frame from another agent reaches the guest inbox as `RelayBinary`. +/// +/// The hub forwards any frame it cannot parse as a known `ClientMessage` verbatim, so a raw binary payload +/// lands on the reader pump's `Message::Binary` arm -- the one that calls `from_binary_frame`. +#[tokio::test(flavor = "current_thread")] +async fn binary_frame_arrives_as_relay_binary() { + let server = et_ws_test_server::start(); + let backend = WsBackend::connect(&server.ws_url, Some(ACK_TIMEOUT)).await.unwrap(); + let mut peer = connect_peer(&server.ws_url).await; + + let payload = b"\x00\x01\x02 binary relay \xff".to_vec(); + peer.send(tungstenite::Message::binary(payload.clone())).await.unwrap(); + + // The ack the handshake already consumed is re-surfaced as the first inbox message, so skip past it. + let deadline = tokio::time::Instant::now() + RELAY_TIMEOUT; + while tokio::time::Instant::now() < deadline { + let remaining = deadline - tokio::time::Instant::now(); + let Some(message) = backend.next_message(remaining).await else { + break; + }; + if let ServerMessage::RelayBinary { content } = message { + assert_eq!( + content, payload, + "the relayed bytes must survive the round trip unchanged" + ); + return; + } + } + panic!("no RelayBinary reached the guest inbox"); +} + +/// The heartbeat keeps the connection open past the server's idle-close window. +/// +/// The server only bumps `last_activity` on inbound frames and closes an idle connection at 15s, so a backend +/// whose pinger never ticked is shut by the time this assertion runs. Staying `Connected` is therefore direct +/// evidence the pinger loop ran and its `Ping` was accepted as activity. +#[tokio::test(flavor = "current_thread")] +async fn heartbeat_keeps_the_connection_past_the_idle_close() { + let server = et_ws_test_server::start(); + let backend = WsBackend::connect(&server.ws_url, Some(ACK_TIMEOUT)).await.unwrap(); + + // Comfortably past the 15s idle close, with nothing else sent on the socket in the meantime. + tokio::time::sleep(Duration::from_secs(18)).await; + + assert_eq!( + backend.current_state().await, + State::Connected, + "an idle backend must be held open by its own heartbeat" + ); +} diff --git a/services/ws-wasm-agent/tests/client.rs b/services/ws-wasm-agent/tests/client.rs index d6568620..5805a877 100644 --- a/services/ws-wasm-agent/tests/client.rs +++ b/services/ws-wasm-agent/tests/client.rs @@ -6,6 +6,10 @@ //! `wasm-agent-cov` mise task builds instrumented to measure the agent's coverage. #![cfg(test)] #![cfg(target_arch = "wasm32")] +// `wasm_bindgen_test` expands to `#[coverage(off)]`, whose feature is still unstable, so the instrumented +// build needs the gate and every other build must not carry it. `coverage_nightly` is set only by the wasm +// coverage RUSTFLAGS, which run on nightly; rust-lang/rust#84605 is open, so no toolchain bump removes this. +#![cfg_attr(coverage_nightly, feature(coverage_attribute))] use std::cell::RefCell; use std::rc::Rc; diff --git a/services/ws-wasm-agent/tests/web.rs b/services/ws-wasm-agent/tests/web.rs index ac6346d8..f5f3f788 100644 --- a/services/ws-wasm-agent/tests/web.rs +++ b/services/ws-wasm-agent/tests/web.rs @@ -1,5 +1,6 @@ #![cfg(test)] #![cfg(target_arch = "wasm32")] +#![cfg_attr(coverage_nightly, feature(coverage_attribute))] use et_web::{describe_js_error, sleep_ms, websocket_url}; use et_ws_wasm_agent::{WsClient, WsClientConfig, wait_for_connected}; diff --git a/services/ws-web-runner/build.rs b/services/ws-web-runner/build.rs index 7f4b15a0..78f66d69 100644 --- a/services/ws-web-runner/build.rs +++ b/services/ws-web-runner/build.rs @@ -71,9 +71,10 @@ fn link_mingw_shim() { let locale_args = ["-c", "-O2", "-o", &locale_obj, "mingw-shim/msvc_crt_locale.c"]; run(std::process::Command::new(gcc.path()).args(locale_args)); - // The locale wrappers (setlocale, ...) are split into their own standalone object so Codacy can path-exclude - // just their write-once symbol caches; they intercept -lmsvcrt exactly as msvc_crt_locale.c does, so they too - // must be a link-line object rather than an archive member. They call ucrt_resolve_once from msvc_crt_locale.o. + // The locale wrappers (setlocale, ...) are split into their own standalone object so their write-once symbol + // caches sit alone in one path-excludable file; they intercept -lmsvcrt exactly as msvc_crt_locale.c does, so + // they too must be a link-line object rather than an archive member. They call ucrt_resolve_once from + // msvc_crt_locale.o. let locale_cache_obj = format!("{out_dir}/msvc_crt_locale_cache.o"); let locale_cache_args = [ "-c", diff --git a/services/ws-web-runner/mingw-shim/msvc_crt_alloc.c b/services/ws-web-runner/mingw-shim/msvc_crt_alloc.c index fc35a878..332c8603 100644 --- a/services/ws-web-runner/mingw-shim/msvc_crt_alloc.c +++ b/services/ws-web-runner/mingw-shim/msvc_crt_alloc.c @@ -4,8 +4,8 @@ * allocation functions MISRA 21.3 forbids -- you cannot implement an allocator without an allocator -- * so this code is isolated here precisely so the one analyzer that cannot suppress that rule per line (Codacy's * cxx / cppcheck, which offers only path-level excludes) can exclude just this file while the rest of the shim - * stays fully analyzed. It remains covered by DeepSource's clang-tidy and the repo's own clang-tidy / cpplint / - * flawfinder. Like msvc_crt_locale.c it links as a standalone object so _dupenv_s intercepts -lmsvcrt, and the + * stays fully analyzed. Every other analyzer the repo runs over C still reads it. + * Like msvc_crt_locale.c it links as a standalone object so _dupenv_s intercepts -lmsvcrt, and the * operator-new symbols resolve the msvc_crt_ops.s jumps in the shim archive. */ #include diff --git a/services/ws-web-runner/mingw-shim/msvc_crt_locale_cache.c b/services/ws-web-runner/mingw-shim/msvc_crt_locale_cache.c index f859b5b3..7099f331 100644 --- a/services/ws-web-runner/mingw-shim/msvc_crt_locale_cache.c +++ b/services/ws-web-runner/mingw-shim/msvc_crt_locale_cache.c @@ -4,11 +4,11 @@ * object on the link line, not an archive member -- a linker only extracts archive members left-to-right, so an * archived setlocale would lose to msvcrt's. Each wrapper caches its resolved ucrtbase target in a write-once * static populated race-free by ucrt_resolve_once. Those function-local statics are why this file is isolated: - * Codacy's "Local static variable" rule flags them for audit and, unlike DeepSource (suppressed inline with - * skipcq), offers only path-level excludes -- so .codacy.yaml excludes just this file while the rest of the shim, - * including the ucrt_resolve_once / ucrt_sym resolver these call, stays fully analyzed. It also stays covered by - * DeepSource's clang-tidy and the repo's own clang-tidy / cpplint / flawfinder. The caches are reviewed-safe: - * each is written exactly once, to a resolved function address. */ + * Codacy's "Local static variable" rule flags them for audit and, unlike the analyzers that take an inline + * skipcq, offers only path-level excludes -- so .codacy.yaml excludes just this file while the rest of the shim, + * including the ucrt_resolve_once / ucrt_sym resolver these call, stays fully analyzed. Every other analyzer the + * repo runs over C still reads it. The caches are reviewed-safe: each is written exactly once, to a resolved + * function address. */ // ucrt_resolve_once is defined in msvc_crt_locale.c; declared here so this standalone object links against it. void *ucrt_resolve_once(void *volatile *cache, const char *name); diff --git a/services/ws-web-runner/mingw-shim/msvc_crt_shim.c b/services/ws-web-runner/mingw-shim/msvc_crt_shim.c index d737193e..f2bde811 100644 --- a/services/ws-web-runner/mingw-shim/msvc_crt_shim.c +++ b/services/ws-web-runner/mingw-shim/msvc_crt_shim.c @@ -7,9 +7,9 @@ * the plain-named impls below -- MSVC x64 and mingw x64 share the Microsoft x64 calling convention. * These symbols must keep the CRT's exact reserved names (`_`/`__`-prefixed) and, for the mutable ones, * non-const storage, to match the archive's ABI -- renaming or const-ing them would break the link. Each - * therefore carries an inline clang-tidy `// NOLINT` for bugprone-reserved-identifier / cert-dcl37-c (plus - * cppcoreguidelines-avoid-non-const-global-variables on the globals) -- the narrowest scope, honored by both - * our clang-tidy and DeepSource's clang-tidy-based cxx (CXX-E2000 reserved identifier, CXX-W2009 non-const). */ + * therefore carries an inline `// NOLINT` for bugprone-reserved-identifier / cert-dcl37-c (plus + * cppcoreguidelines-avoid-non-const-global-variables on the globals) -- the narrowest scope, and honored by + * every analyzer that reports those, `CXX-E2000` (reserved identifier) and `CXX-W2009` (non-const) included. */ #include #include diff --git a/services/ws-web-runner/src/shims/xhr.js b/services/ws-web-runner/src/shims/xhr.js index 5d601cd4..412a9380 100644 --- a/services/ws-web-runner/src/shims/xhr.js +++ b/services/ws-web-runner/src/shims/xhr.js @@ -57,7 +57,7 @@ if (typeof globalThis.XMLHttpRequest === "undefined") { } } - // skipcq: JS-R1005 -- XHR shim; complexity 6 is within the repo's oxlint ceiling (eslint/complexity max 10) + // skipcq: JS-R1005 -- XHR shim; complexity 6 is within the repo's own ceiling of 10 send(body) { const base = globalThis.location?.href; const url = base ? new URL(this.#url, base).href : this.#url; diff --git a/utilities/cli/src/deployment_types/k3s.rs b/utilities/cli/src/deployment_types/k3s.rs index 7e2ff616..1e2c621f 100644 --- a/utilities/cli/src/deployment_types/k3s.rs +++ b/utilities/cli/src/deployment_types/k3s.rs @@ -62,7 +62,7 @@ const OPENOBSERVE_IMAGE: &str = "openobserve/openobserve:v0.91.5"; /// Service name of the collector, which is also its Deployment name and the DNS name its in-cluster URL resolves. /// /// Held as a constant so a URL cannot drift from the `Service` it addresses. Composing the URL from it rather -/// than writing the host into the literal also keeps `link-check` from reading an in-cluster DNS name as an +/// than writing the host into the literal also keeps link checking from reading an in-cluster DNS name as an /// external link it should be able to reach. The hub's equivalent lives beside the URL builder that needs it. const COLLECTOR_SERVICE: &str = "openobserve"; @@ -118,8 +118,8 @@ pub fn generate_k3s_deployment(cluster: &ClusterInput, output_dir: &Path) -> Res /// Serialise the documents every scenario emits, in apply order. /// /// Split from the caller so the per-runner documents append to a finished list: each serialisation is a -/// fallible step, and eight of them in one function put it past the cyclomatic-complexity ceiling Codacy -/// enforces without saying anything about how the deployment is shaped. +/// fallible step, and eight of them in one function put it past the repo's cyclomatic-complexity ceiling +/// without saying anything about how the deployment is shaped. fn fixed_documents(namespace: &str, cluster_name: &str, module_paths: &[String]) -> Result, CliError> { Ok(vec![ document(&namespace_object(namespace))?, @@ -133,7 +133,7 @@ fn fixed_documents(namespace: &str, cluster_name: &str, module_paths: &[String]) ]) } -/// Serialise one object, in the YAML style `dprint-check` expects of a committed file. +/// Serialise one object, in the YAML style the repo's formatters hold a committed file to. #[expect( clippy::unwrap_used, clippy::unwrap_in_result, @@ -330,8 +330,8 @@ fn volume_mount(name: &str, path: &str) -> VolumeMount { /// /// `path` is a container mount point in the manifest this generator emits, not a path anything in this process /// opens, so a caller passing `/tmp` is naming the containerised program's own temp directory rather than -/// creating a world-writable file on the host. `DeepSource`'s `RS-S1003` reads the literal as the latter, which -/// is why every call site passing `/tmp` carries a `skipcq` for that one rule. +/// creating a world-writable file on the host. `RS-S1003` reads the literal as the latter, which is why every +/// call site passing `/tmp` carries a `skipcq` for that one rule. fn scratch(name: &str, path: &str) -> (VolumeMount, Volume) { let volume = Volume { empty_dir: Some(EmptyDirVolumeSource::default()), diff --git a/utilities/cli/src/deployment_types/mise.rs b/utilities/cli/src/deployment_types/mise.rs index c11cba19..5491e77f 100644 --- a/utilities/cli/src/deployment_types/mise.rs +++ b/utilities/cli/src/deployment_types/mise.rs @@ -236,11 +236,10 @@ fn mise_env() -> Table { /// Build a task's `depends` array, sorted. /// -/// Sorted because `config/taplo.toml` sets `reorder_arrays = true`, so `taplo-fmt` sorts this array in the -/// committed file. Emitting it in the order the tasks happen to be assembled leaves the two permanently at odds: -/// the formatter sorts the generated file and the next `regen-verification` unsorts it, so `verification-check` -/// reports drift whichever ran last. The order carries no meaning to mise either -- `depends` is a set of -/// prerequisites it starts together, not a sequence. +/// The committed file is formatted with its arrays reordered, so emitting this one in the order the tasks happen +/// to be assembled leaves formatter and generator permanently at odds: each unsorts what the other sorted, and +/// the drift check reports whichever ran last. The order carries no meaning to mise either -- `depends` is a set +/// of prerequisites it starts together, not a sequence. fn mise_depends(depends: &[String]) -> Table { let mut sorted = depends.to_vec(); sorted.sort_unstable(); diff --git a/utilities/cli/src/deployment_types/scenario_image.rs b/utilities/cli/src/deployment_types/scenario_image.rs index 7ead271a..7e04c488 100644 --- a/utilities/cli/src/deployment_types/scenario_image.rs +++ b/utilities/cli/src/deployment_types/scenario_image.rs @@ -95,7 +95,7 @@ fn render_dockerfile( // Every module serves out of `pkg/`, whether that directory is committed (the JS shims) or produced by a // build (wasm-pack output, wheels). Probing for it makes this file depend on what happens to be built in the // working tree: CI checks out unbuilt Rust modules, found no `services/ws-modules/har1/pkg`, and emitted the - // module root instead -- so the committed output and the CI regeneration disagreed and verification-check + // module root instead -- so the committed output and the CI regeneration disagreed, and the drift check // failed. A module that genuinely has no `pkg/` now fails loudly at image build rather than silently // generating a different Dockerfile per machine. for (repo_path, docker_path) in repo_paths { diff --git a/utilities/cli/src/lib.rs b/utilities/cli/src/lib.rs index 7c518c27..af4ec86f 100644 --- a/utilities/cli/src/lib.rs +++ b/utilities/cli/src/lib.rs @@ -163,13 +163,9 @@ const PYODIDE_DOCKER_PATH: &str = "/app/node_modules/pyodide"; /// secret scanner duly found it. Holding it in one uncommitted file instead keeps the deployment reproducible /// (regenerating the scenario rewrites this file too) while leaving no credential in a tracked file to suppress. /// -/// Suppression was the previous answer and it did not hold. Inline markers only work where the scanner reads -/// them: gitleaks honours `gitleaks:allow` on the same line, so the trailing marker in `compose.yaml` worked -/// locally while Codacy's own scan ignored it, and the `mise.toml` marker sat on its own line -- forced there -/// because taplo realigns trailing comments and the drift check then failed either way round -- where gitleaks -/// never read it at all. Whether any of it mattered came down to the separator the password happened to draw: -/// gitleaks' `generic-api-key` and `hashicorp-tf-password` rules match a run of `[\w.=-]`, so a password joined -/// by `_` was reported while one containing `%` was not, and each new scenario was a coin toss. +/// An inline suppression marker is not an alternative here. Each scanner reads only its own marker syntax, some +/// read none at all from a committed file, and whether a given password is reported at all comes down to the +/// separator it happens to draw -- so the same marker holds for one scenario and not the next. pub const SECRETS_ENV_FILE: &str = "secrets.env"; #[derive(Debug, Clone)] diff --git a/utilities/int-gen/src/error.rs b/utilities/int-gen/src/error.rs index b410b4e7..8a303037 100644 --- a/utilities/int-gen/src/error.rs +++ b/utilities/int-gen/src/error.rs @@ -41,7 +41,7 @@ pub enum Error { #[error(transparent)] Syn(#[from] syn::Error), // `wit-parser` and the wasmtime bindgen return `anyhow::Result`; this variant is what lets `?` convert - // one. The anyhow-only-in-error-rs semgrep rule keeps the name confined to this line. + // one. A repo-wide rule keeps the name confined to this line. #[error(transparent)] Anyhow(#[from] anyhow::Error), #[error("zig codegen: {0}")] diff --git a/utilities/int-gen/src/lib.rs b/utilities/int-gen/src/lib.rs index 3d724077..c3cf4bdb 100644 --- a/utilities/int-gen/src/lib.rs +++ b/utilities/int-gen/src/lib.rs @@ -96,10 +96,10 @@ pub fn generate_core() -> Result<(), Error> { // logic lives in `asyncapi`. The returned `Value` is what we serialise // to ws.yaml below. let spec_value = asyncapi::build_spec()?; - // serde_yaml's emitter quotes/indents differently than dprint's - // `pretty_yaml` plugin -- pipe the output through `pretty_yaml` (the same - // engine dprint uses) so the committed YAML stays dprint-canonical and - // `dprint check` doesn't drift between regenerations. + // serde_yaml's emitter quotes/indents differently than the repo's YAML + // formatter -- pipe the output through `pretty_yaml`, the same engine that + // formatter runs on, so the committed YAML is already canonical and the + // format check doesn't drift between regenerations. let yaml = serde_yaml::to_string(&spec_value)?; // serde_yaml always emits well-formed YAML, so pretty_yaml's parse step // can't fail here -- the only error variant is a syntax error. @@ -184,7 +184,7 @@ pub fn generate_zig() -> Result<(), Error> { Ok(()) } -/// Write only when the contents differ, to keep `mise run check` quiet on no-op regenerations. +/// Write only when the contents differ, so a no-op regeneration leaves no diff behind. #[expect( clippy::print_stdout, reason = "et-int-gen is a CLI; `wrote ` per generated file is intended user-visible progress output" diff --git a/utilities/int-gen/src/wit/bindings.rs b/utilities/int-gen/src/wit/bindings.rs index 5bf96cd3..6f302b38 100644 --- a/utilities/int-gen/src/wit/bindings.rs +++ b/utilities/int-gen/src/wit/bindings.rs @@ -11,8 +11,8 @@ //! so a wasmtime bump that changes the generated code surfaces as a failing check rather than silently. //! //! WIT doc comments are copied through verbatim and land nested ~20 columns deep, so a comment in the WIT -//! that fits the editorconfig line length on its own can overflow once emitted here. That is why the world's -//! comments wrap well short of it; `editorconfig-check` on the emitted file is what enforces the result. +//! that fits the editorconfig line length on its own can overflow once emitted here, which is why the world's +//! comments wrap well short of it. //! //! prettyplease is the emitted file's only formatter, and `.rustfmt.toml` lists it under `ignore` for that //! reason: running rustfmt over it as well is what breaks. prettyplease's output fits `max_width`, but diff --git a/utilities/wasm-cov-wrapper/src/main.rs b/utilities/wasm-cov-wrapper/src/main.rs index 6a9bc8c9..5e579b27 100644 --- a/utilities/wasm-cov-wrapper/src/main.rs +++ b/utilities/wasm-cov-wrapper/src/main.rs @@ -12,7 +12,7 @@ //! subprocess spawn as a command-injection shape and cannot suppress it per line. That is a false positive here -- //! the args are cargo's own trusted rustc invocation, and forwarding argv to rustc is this file's entire purpose, //! so no code change removes it. The file is kept minimal and single-purpose so the path exclude is as narrow as -//! possible; it stays fully covered by clippy and DeepSource's Rust analyzer. +//! possible; every other analyzer the repo runs over Rust still reads it. #![expect( clippy::print_stderr, reason = "a build-tool wrapper reports its own startup failure to stderr" From ab14bf545bf62125dfa4f314952f5cd2b5a6be61 Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Tue, 15 Sep 2026 15:23:30 +0800 Subject: [PATCH 03/24] fixes --- .deepsource.toml | 34 ++++++------ .mise/config.toml | 32 ++++++----- .mise/osv-ids.awk | 35 ------------ .../ws-modules/face-detection/tests/web.rs | 2 +- services/ws-modules/har1/tests/web.rs | 2 +- .../ws-modules/pic-viewer/tests/show_image.rs | 2 +- services/ws-modules/zig-data1/src/util.cpp | 5 ++ .../ws-modules/zig-except1/src/exceptions.cpp | 7 ++- services/ws-wasi-runner/tests/ws_backend.rs | 55 ++++++++++++++++--- services/ws-wasm-agent/tests/client.rs | 9 +-- services/ws-wasm-agent/tests/web.rs | 2 +- 11 files changed, 101 insertions(+), 84 deletions(-) delete mode 100644 .mise/osv-ids.awk diff --git a/.deepsource.toml b/.deepsource.toml index 23e62e8e..a14052ba 100644 --- a/.deepsource.toml +++ b/.deepsource.toml @@ -26,16 +26,20 @@ name = "dart" enabled = true name = "python" -[[analyzers]] -enabled = true -name = "javascript" - -[analyzers.meta] # The browser modules and the ws-server `static/app.js` are ES modules (top-level `import`/`export`). # Without an es-modules parser the analyzer treats them as scripts and reports spurious JS-0833 errors # ("'import'/'export' may appear only with 'sourceType: module'"). -environment = ["browser", "nodejs"] -module_system = "es-modules" +# +# Written inline rather than as a `[analyzers.meta]` table, and that is the point rather than a style choice. +# TOML binds a `[analyzers.meta]` header to the last `[[analyzers]]` element declared before it, so three such +# headers in one file rely on the reader tracking which element each attaches to. Every JS file in the repo +# kept reporting JS-0833 while this said `es-modules`, which is what a setting that never reached the analyzer +# looks like -- and the two `[analyzers.meta]` blocks below it are the only candidates for clobbering it. An +# inline table cannot be reattached or overwritten: it is a key of the element it sits in. +[[analyzers]] +enabled = true +meta = { environment = ["browser", "nodejs"], module_system = "es-modules" } +name = "javascript" [[analyzers]] enabled = true @@ -49,23 +53,19 @@ name = "docker" enabled = true name = "cxx" -[[analyzers]] -enabled = true -name = "java" - -[analyzers.meta] # runtime_version is required, and its range stops at 21 while config.java.toml pins JDK 26. # DeepSource reports incorrect results when the value is unset or mismatched, so it tracks the newest runtime the # analyzer offers rather than the one java-data1 actually compiles against. -runtime_version = "21" - [[analyzers]] enabled = true -name = "kotlin" +meta = { runtime_version = "21" } +name = "java" -[analyzers.meta] # language_version stops at 2.1 while kotlin-data1's Gradle build pins Kotlin 2.4.10. # The module's source stays within the 2.1 dialect, so the older parser still reads it; a future file that reaches # for newer syntax would fail to parse instead. runtime_version is left at its default because a wasmJs-only # multiplatform target has no JVM runtime for the JVM-API rules to judge. -language_version = "2.1" +[[analyzers]] +enabled = true +meta = { language_version = "2.1" } +name = "kotlin" diff --git a/.mise/config.toml b/.mise/config.toml index a29a6ce0..770a778c 100644 --- a/.mise/config.toml +++ b/.mise/config.toml @@ -537,17 +537,10 @@ wasm_rustflags = "-C target-cpu=mvp -C target-feature=+mutable-globals,+sign-ext # Instrumentation flags (leaf var) shared by the wasm coverage builds. # The nightly -Z flag adds branch regions to the covmap so the lcov exports carry BRDA records. `a_` prefix so # mise's alphabetical [vars] render defines it before wasm_cov_rustflags uses it. -# -# `coverage_nightly` is cargo-llvm-cov's convention, set here by hand because these builds do not go through -# it. The wasm-bindgen-test macro expands to `#[coverage(off)]`, whose feature is still unstable (rust-lang -# /rust#84605 is open, so no nightly stabilises it), and the test crates gate the matching `feature(...)` on -# this cfg. Paired with its own --check-cfg so the cfg stays declared rather than tripping unexpected_cfgs. -a_wasm_cov_ccfg = "--check-cfg=cfg(coverage_nightly)" -a_wasm_cov_instr = "-Cinstrument-coverage -Zcoverage-options=branch --cfg=coverage_nightly" +a_wasm_cov_instr = "-Cinstrument-coverage -Zcoverage-options=branch" # RUSTFLAGS for the wasm coverage builds: instrument + emit LLVM IR, no profiler runtime (minicov provides it). # Single codegen unit + LTO off so each crate emits one .ll the wasm-cov task can gut and feed to llc/llvm-cov. -a_wasm_cov_tail = "-Ccodegen-units=1 -Clto=off -Zno-profiler-runtime" -wasm_cov_rustflags = "--emit=llvm-ir {{ vars.a_wasm_cov_instr }} {{ vars.a_wasm_cov_ccfg }} {{ vars.a_wasm_cov_tail }}" +wasm_cov_rustflags = "--emit=llvm-ir {{ vars.a_wasm_cov_instr }} -Ccodegen-units=1 -Clto=off -Zno-profiler-runtime" # Coverage fragments the wasm module build commands append unconditionally (empty unless ET_TEST_COVERAGE is set). # wasm_cov is an inline RUSTFLAGS env prefix; the nightly toolchain + conda clang come from the coverage [env]. # The *_feat vars turn on minicov: cargo `--features` for the WASI cdylibs, wasm-pack `-- --features` for browsers @@ -1555,20 +1548,29 @@ run = "osv-scanner {{ vars.osv_locks }} --config config/osv-scanner.toml" [tasks."gen:osv-scanner"] description = "Regenerate config/osv-scanner.toml from config/deny.toml's [advisories].ignore list" # osv-scanner and cargo-deny must ignore the same advisory IDs. -# config/deny.toml is the source of truth (it carries the per-ID rationale). goawk + coreutils only, so -# the `dependencies` workflow can run it (via mise) next to the audit binaries. Both RUSTSEC-YYYY-NNNN and +# config/deny.toml is the source of truth (it carries the per-ID rationale). Both RUSTSEC-YYYY-NNNN and # GHSA-xxxx-xxxx-xxxx ids are extracted, so a GHSA-only advisory (one RustSec hasn't assigned) still filters. # -# An entry's `expires` date rides along as `ignoreUntil`, so the entry stops being honoured the day it lapses. -# An id that appears only in prose (the GHSA ones, which have no entry of their own) carries no date and stays -# ignored indefinitely. +# rg + coreutils ONLY, and that is a hard constraint rather than a preference. +# The `dependencies` workflow does no `mise install`: it names its six tools outright in the install-mise +# step, so anything outside that list is simply absent. A goawk rewrite of this task looked tidier and died +# there with `goawk: command not found`, and goawk cannot join that list either -- it is a Go binary, absent +# from taiki-e/install-action's manifest and not a crate cargo-binstall could fetch. Keep to the tools the +# workflow already installs, or add the tool to the workflow first and prove it resolves. +# +# One pattern, matching an id and the review date beside it, because every ignore entry carries one: the +# advisories conftest policy fails the build for any that does not. The date becomes osv-scanner's native +# `ignoreUntil`, which it enforces itself, so a lapsed entry stops being honoured rather than silently +# outliving its review. run = """ { # backticks in the echoed line are literal markdown, not command substitution. # shellcheck disable=SC2016 echo '# AUTO-GENERATED from config/deny.toml by `mise run gen:osv-scanner`.' echo 'IgnoredVulns = [' - goawk -f .mise/osv-ids.awk config/deny.toml | coreutils sort -u + id_re='"(RUSTSEC-[0-9]{4}-[0-9]{4}|GHSA(?:-[0-9a-z]{4}){3})"' + date_re='reason = "expires ([0-9]{4}-[0-9]{2}-[0-9]{2})"' + rg -oN -r ' { id = "$1", ignoreUntil = $2 },' "$id_re, $date_re" config/deny.toml | coreutils sort -u echo ']' } >config/osv-scanner.toml """ diff --git a/.mise/osv-ids.awk b/.mise/osv-ids.awk deleted file mode 100644 index 6ea57a8f..00000000 --- a/.mise/osv-ids.awk +++ /dev/null @@ -1,35 +0,0 @@ -# Prints one `IgnoredVulns` row per advisory id found in config/deny.toml. -# Both RUSTSEC-YYYY-NNNN and GHSA-xxxx-xxxx-xxxx ids are extracted, so a GHSA-only advisory (one RustSec has -# not assigned) still filters. An entry's `expires` date rides along as an `ignoreUntil` value. -BEGIN { q = "\"" } -{ - # An id can appear more than once on a line, and in prose as well as in an entry, so scan the whole line. - # The expiry is read from the line as a whole: only a real ignore entry carries a `reason`, so a bare id - # mentioned in a comment keeps the undated form and stays ignored indefinitely. - rest = $0 - while (match(rest, /"(RUSTSEC-[0-9]{4}-[0-9]{4}|GHSA(-[0-9a-z]{4}){3})"/)) { - # Save this match's span before any other match() call, which would overwrite RSTART/RLENGTH. - # Advancing `rest` by the expiry match's span instead leaves the id still in the remainder, so the loop - # matches it again and never terminates -- goawk spins on the first dated entry rather than failing. - s = RSTART - l = RLENGTH - id = substr(rest, s + 1, l - 2) - seen[id] = 1 - if (match($0, /reason = "expires [0-9]{4}-[0-9]{2}-[0-9]{2}"/)) { - d = substr($0, RSTART, RLENGTH) - sub(/.*expires /, "", d) - sub(/"$/, "", d) - expiry[id] = d - } - rest = substr(rest, s + l) - } -} -END { - for (id in seen) { - if (id in expiry) { - print " { id = " q id q ", ignoreUntil = " expiry[id] " }," - } else { - print " { id = " q id q " }," - } - } -} diff --git a/services/ws-modules/face-detection/tests/web.rs b/services/ws-modules/face-detection/tests/web.rs index 62569a49..aec8df5e 100644 --- a/services/ws-modules/face-detection/tests/web.rs +++ b/services/ws-modules/face-detection/tests/web.rs @@ -1,6 +1,6 @@ #![cfg(test)] #![cfg(target_arch = "wasm32")] -#![cfg_attr(coverage_nightly, feature(coverage_attribute))] +#![cfg_attr(wasm_bindgen_unstable_test_coverage, feature(coverage_attribute))] use et_ws_face_detection::{init, is_running, run, stop}; use wasm_bindgen_test::*; diff --git a/services/ws-modules/har1/tests/web.rs b/services/ws-modules/har1/tests/web.rs index 8070dc04..f87f5477 100644 --- a/services/ws-modules/har1/tests/web.rs +++ b/services/ws-modules/har1/tests/web.rs @@ -1,6 +1,6 @@ #![cfg(test)] #![cfg(target_arch = "wasm32")] -#![cfg_attr(coverage_nightly, feature(coverage_attribute))] +#![cfg_attr(wasm_bindgen_unstable_test_coverage, feature(coverage_attribute))] use et_ws_har1::{init, run}; use wasm_bindgen_test::*; diff --git a/services/ws-modules/pic-viewer/tests/show_image.rs b/services/ws-modules/pic-viewer/tests/show_image.rs index e8eddee3..909c2991 100644 --- a/services/ws-modules/pic-viewer/tests/show_image.rs +++ b/services/ws-modules/pic-viewer/tests/show_image.rs @@ -7,7 +7,7 @@ #![cfg(test)] #![cfg(target_arch = "wasm32")] -#![cfg_attr(coverage_nightly, feature(coverage_attribute))] +#![cfg_attr(wasm_bindgen_unstable_test_coverage, feature(coverage_attribute))] use et_ws_pic_viewer::show_image; use wasm_bindgen::JsCast; diff --git a/services/ws-modules/zig-data1/src/util.cpp b/services/ws-modules/zig-data1/src/util.cpp index d7ce8126..3c43ae9b 100644 --- a/services/ws-modules/zig-data1/src/util.cpp +++ b/services/ws-modules/zig-data1/src/util.cpp @@ -3,7 +3,12 @@ // compiler-provided C headers only, no exceptions/RTTI (build.zig passes -fno-exceptions -fno-rtti), no operator // new, and no non-trivial static initializers. Language-level features -- templates, constexpr, namespaces -- // all work. +// The / these would normally prefer are libc++ headers, and this target links no libc++. +// Only / are compiler-provided (they sit in clang's own resource include dir), so taking +// the advice would fail the build outright rather than modernise it. +// skipcq: CXX-W2030 -- freestanding wasm32: no libc++, so the C++ spellings do not exist here #include +// skipcq: CXX-W2030 -- freestanding wasm32: no libc++, so the C++ spellings do not exist here #include namespace { diff --git a/services/ws-modules/zig-except1/src/exceptions.cpp b/services/ws-modules/zig-except1/src/exceptions.cpp index 380bccea..fa81077c 100644 --- a/services/ws-modules/zig-except1/src/exceptions.cpp +++ b/services/ws-modules/zig-except1/src/exceptions.cpp @@ -22,7 +22,12 @@ // (Zig has no C++ cleanup semantics and the exception would escape as a raw `WebAssembly.Exception`), so every // extern "C" entry point in an exception-enabled TU catches everything it can throw and translates the failure // to a status code at the boundary, as try_divide() does at the bottom of this file. +// The / these would normally prefer are libc++ headers, and this target links no libc++. +// Only / are compiler-provided (they sit in clang's own resource include dir), so taking +// the advice would fail the build outright rather than modernise it. +// skipcq: CXX-W2030 -- freestanding wasm32: no libc++, so the C++ spellings do not exist here #include +// skipcq: CXX-W2030 -- freestanding wasm32: no libc++, so the C++ spellings do not exist here #include namespace { @@ -74,7 +79,7 @@ void *__cxa_begin_catch(void *thrown) { return thrown; } // NOLINTNEXTLINE(bugprone-reserved-identifier, cert-dcl37-c, cert-dcl51-cpp) void __cxa_end_catch(void) { if (pending_dtor != nullptr) { - pending_dtor(exception_slot); + pending_dtor(static_cast(exception_slot)); pending_dtor = nullptr; } } diff --git a/services/ws-wasi-runner/tests/ws_backend.rs b/services/ws-wasi-runner/tests/ws_backend.rs index 06f5d1e1..748aca75 100644 --- a/services/ws-wasi-runner/tests/ws_backend.rs +++ b/services/ws-wasi-runner/tests/ws_backend.rs @@ -1,20 +1,21 @@ -//! Host websocket backend: the inbound binary-relay path and the heartbeat. +//! Host websocket backend: the binary relay in both directions, and the heartbeat. //! -//! `modules.rs` drives whole guests end to end, but no bundled guest ever receives a binary relay frame, and -//! none sits still long enough for the 5s heartbeat to tick. Both paths live in tasks `WsBackend::connect` -//! spawns, so neither is reachable by exercising the guest API -- which is why they were the two uncovered -//! regions in this file. These tests drive the backend directly against an in-process ws-server instead. +//! `modules.rs` drives whole guests end to end, but no bundled guest sends or receives a binary relay frame, +//! and none sits still long enough for the 5s heartbeat to tick -- so all three paths were uncovered. The +//! inbound and heartbeat ones live in tasks `WsBackend::connect` spawns and are unreachable through the guest +//! API, hence driving the backend directly here against an in-process ws-server. #![cfg(test)] #![expect( clippy::arithmetic_side_effects, - clippy::single_call_fn, - reason = "integration test: deadline arithmetic cannot overflow in a test's lifetime; step helpers" + reason = "integration test: the deadline arithmetic cannot overflow within a test's lifetime" )] use std::time::Duration; use edge_toolkit::ws::{ClientMessage, ServerMessage}; -use et_ws_wasi_runner::bindings::et::ws_wasi::ws::State; +use et_ws_wasi_runner::HostState; +use et_ws_wasi_runner::bindings::et::ws_messages::messages::{ClientMessage as WitClientMessage, RelayBinaryPayload}; +use et_ws_wasi_runner::bindings::et::ws_wasi::ws::{Host as _, State}; use et_ws_wasi_runner::host::ws::WsBackend; use futures_util::{SinkExt as _, StreamExt as _}; use tokio_tungstenite::{connect_async, tungstenite}; @@ -104,3 +105,41 @@ async fn heartbeat_keeps_the_connection_past_the_idle_close() { "an idle backend must be held open by its own heartbeat" ); } + +/// A guest-sent `relay-binary` leaves as a raw binary frame another agent receives verbatim. +/// +/// The outbound counterpart to the test above, and it goes through `HostState` rather than `WsBackend` because +/// the WIT-to-Rust conversion and the relay-vs-typed-JSON choice both live in `::send`. +/// Constructing the state needs no wasmtime store -- it is a plain struct of the URLs and the REST client. +#[tokio::test(flavor = "current_thread")] +async fn relay_binary_leaves_as_a_raw_binary_frame() { + let server = et_ws_test_server::start(); + let mut peer = connect_peer(&server.ws_url).await; + let mut state = HostState::new(&server.base_url, server.ws_url.clone(), Some(ACK_TIMEOUT), false); + state.connect().await.unwrap(); + + let payload = b"\x10\x20 outbound relay \xfe".to_vec(); + state + .send(WitClientMessage::RelayBinary(RelayBinaryPayload { + content: payload.clone(), + })) + .await + .unwrap(); + + let deadline = tokio::time::Instant::now() + RELAY_TIMEOUT; + while tokio::time::Instant::now() < deadline { + let remaining = deadline - tokio::time::Instant::now(); + let Ok(Some(Ok(frame))) = tokio::time::timeout(remaining, peer.next()).await else { + break; + }; + if let tungstenite::Message::Binary(bytes) = frame { + assert_eq!( + bytes.to_vec(), + payload, + "the relay must not wrap or re-encode the bytes" + ); + return; + } + } + panic!("the peer never received the relayed binary frame"); +} diff --git a/services/ws-wasm-agent/tests/client.rs b/services/ws-wasm-agent/tests/client.rs index 5805a877..2246344b 100644 --- a/services/ws-wasm-agent/tests/client.rs +++ b/services/ws-wasm-agent/tests/client.rs @@ -6,10 +6,11 @@ //! `wasm-agent-cov` mise task builds instrumented to measure the agent's coverage. #![cfg(test)] #![cfg(target_arch = "wasm32")] -// `wasm_bindgen_test` expands to `#[coverage(off)]`, whose feature is still unstable, so the instrumented -// build needs the gate and every other build must not carry it. `coverage_nightly` is set only by the wasm -// coverage RUSTFLAGS, which run on nightly; rust-lang/rust#84605 is open, so no toolchain bump removes this. -#![cfg_attr(coverage_nightly, feature(coverage_attribute))] +// `wasm_bindgen_test` expands to `#[coverage(off)]` under the cfg below, and that feature is still unstable +// (rust-lang/rust#84605 is open, so no toolchain bump removes this). Gating on the very cfg that makes the +// macro emit the attribute keeps cause and gate in step -- the coverage tasks set it in their own RUSTFLAGS, +// which overrides the target-specific coverage flags, so any cfg of our own would not reach this build. +#![cfg_attr(wasm_bindgen_unstable_test_coverage, feature(coverage_attribute))] use std::cell::RefCell; use std::rc::Rc; diff --git a/services/ws-wasm-agent/tests/web.rs b/services/ws-wasm-agent/tests/web.rs index f5f3f788..65f11757 100644 --- a/services/ws-wasm-agent/tests/web.rs +++ b/services/ws-wasm-agent/tests/web.rs @@ -1,6 +1,6 @@ #![cfg(test)] #![cfg(target_arch = "wasm32")] -#![cfg_attr(coverage_nightly, feature(coverage_attribute))] +#![cfg_attr(wasm_bindgen_unstable_test_coverage, feature(coverage_attribute))] use et_web::{describe_js_error, sleep_ms, websocket_url}; use et_ws_wasm_agent::{WsClient, WsClientConfig, wait_for_connected}; From 52b2a6a67087fab043c1ea8c520373f5ba2477a4 Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Wed, 16 Sep 2026 02:03:17 +0800 Subject: [PATCH 04/24] more fixes --- .deepsource.toml | 19 ++++++---- .semgrepignore | 1 + config/generated-trees.toml | 1 + config/semgrep/comment-summary-line.yaml | 1 + config/semgrep/no-check-tool-mentions.yaml | 1 + config/semgrep/no-python-outside-modules.yaml | 19 ++++++++++ config/semgrep/no-shell-scripts.yaml | 35 +++++++++++++++++++ services/ws-modules/zig-data1/src/util.c | 5 +++ 8 files changed, 75 insertions(+), 7 deletions(-) create mode 100644 .semgrepignore create mode 100644 config/semgrep/no-python-outside-modules.yaml diff --git a/.deepsource.toml b/.deepsource.toml index a14052ba..b2f60143 100644 --- a/.deepsource.toml +++ b/.deepsource.toml @@ -28,14 +28,19 @@ name = "python" # The browser modules and the ws-server `static/app.js` are ES modules (top-level `import`/`export`). # Without an es-modules parser the analyzer treats them as scripts and reports spurious JS-0833 errors -# ("'import'/'export' may appear only with 'sourceType: module'"). +# ("'import'/'export' may appear only with 'sourceType: module'"), which it still does despite this setting. # -# Written inline rather than as a `[analyzers.meta]` table, and that is the point rather than a style choice. -# TOML binds a `[analyzers.meta]` header to the last `[[analyzers]]` element declared before it, so three such -# headers in one file rely on the reader tracking which element each attaches to. Every JS file in the repo -# kept reporting JS-0833 while this said `es-modules`, which is what a setting that never reached the analyzer -# looks like -- and the two `[analyzers.meta]` blocks below it are the only candidates for clobbering it. An -# inline table cannot be reattached or overwritten: it is a key of the element it sits in. +# Ruled out, so nobody spends a fourth attempt on it: the settings reaching the analyzer at all. +# These were `[analyzers.meta]` table headers, three of them in this file, each binding to the last +# `[[analyzers]]` declared above it -- a shape a lenient parser could flatten so that only the final one +# survives, which would leave javascript with no module_system and explain the errors exactly. Rewriting all +# three as inline `meta` keys, which cannot be reattached or overwritten, changed nothing: run +# 4e3986bd-db2f-440a-84fc-9163ef240292 on commit ab14bf545bf62125dfa4f314952f5cd2b5a6be61 still failed. +# The inline form is kept only because it is unambiguous; it is not the fix. +# +# Also ruled out: the files themselves. The repo's own JS linter parses every one of them as an ES module +# with no parse error, and `module_system` is a documented key whose value here is one of its documented +# options -- so neither the source nor the spelling of this setting is what the analyzer is objecting to. [[analyzers]] enabled = true meta = { environment = ["browser", "nodejs"], module_system = "es-modules" } diff --git a/.semgrepignore b/.semgrepignore new file mode 100644 index 00000000..8188a635 --- /dev/null +++ b/.semgrepignore @@ -0,0 +1 @@ +# This file exists to switch the scanner's built-in ignore list off, not to add anything to it. diff --git a/config/generated-trees.toml b/config/generated-trees.toml index e98ea9aa..48f681fe 100644 --- a/config/generated-trees.toml +++ b/config/generated-trees.toml @@ -65,6 +65,7 @@ required_in = [ "config/semgrep/comment-summary-line.yaml", "config/semgrep/no-check-tool-mentions.yaml", "config/semgrep/no-non-ascii.yaml", + "config/semgrep/no-python-outside-modules.yaml", "config/semgrep/no-task-name-mentions.yaml", "config/semgrep/no-todo.yaml", "config/semgrep/no-type-comments.yaml", diff --git a/config/semgrep/comment-summary-line.yaml b/config/semgrep/comment-summary-line.yaml index cbe4db6b..c4db293d 100644 --- a/config/semgrep/comment-summary-line.yaml +++ b/config/semgrep/comment-summary-line.yaml @@ -15,6 +15,7 @@ rules: - "*.yaml" - ".dockerignore" - ".gitignore" + - ".semgrepignore" - "Dockerfile*" exclude: # Skip machine-emitted TOML/YAML (int-gen / regen output). diff --git a/config/semgrep/no-check-tool-mentions.yaml b/config/semgrep/no-check-tool-mentions.yaml index 63140441..5fe4ea1d 100644 --- a/config/semgrep/no-check-tool-mentions.yaml +++ b/config/semgrep/no-check-tool-mentions.yaml @@ -544,6 +544,7 @@ rules: exclude: - "/.mise/**" # Tool pin, lockfile entry, and the scan/fix tasks. - "/CLAUDE.md" # Documents which checks an agent runs for which file types. + - "/.semgrepignore" # Its own ignore file, whose comments explain what the tool's default skips. - "/.codacy.yaml" # Excludes this ruleset, which its own copy of the tool misparses. - "/.github/workflows/test.yaml" # Notes the Windows lane's handling of the tool. - "/Dockerfile.nanoserver" # Disables the tool for an image whose build does not need it. diff --git a/config/semgrep/no-python-outside-modules.yaml b/config/semgrep/no-python-outside-modules.yaml new file mode 100644 index 00000000..e817f6e8 --- /dev/null +++ b/config/semgrep/no-python-outside-modules.yaml @@ -0,0 +1,19 @@ +rules: + - id: no-python-outside-modules + languages: [generic] + paths: + include: + - "*.py" + exclude: + - "/services/ws-modules/**" # The Python modules, whose source is why the language is here at all. + - "/services/ws-pyo3-runner/python/**" # Guest programs the runner loads, module payloads in all but name. + - "/generated/**" # Generated clients, owned by the generator that emits them. + pattern-regex: \A + message: >- + Python outside the module trees is banned. It is a guest language in this repo, not a tooling language: + it is here so that a module can be written in it and run in a browser or a runner, and every such file + sits under `services/ws-modules/` beside the manifest that builds and packages it. A `.py` anywhere else + is a loose script by another name, carrying every problem one brings -- nothing invokes it, nothing + tests it, and nothing fails when it stops working. Short and simple belongs in a task body under + `.mise/`; anything more involved gets its own directory under `utilities/` with a `README.md`. + severity: ERROR diff --git a/config/semgrep/no-shell-scripts.yaml b/config/semgrep/no-shell-scripts.yaml index 9d60eaf1..291f425b 100644 --- a/config/semgrep/no-shell-scripts.yaml +++ b/config/semgrep/no-shell-scripts.yaml @@ -15,3 +15,38 @@ rules: beside the config that invokes it, named for the interpreter that reads it -- that file is formatted and linted as the shell it is, which a string embedded in a config never can be. severity: ERROR + + - id: no-windows-shell-scripts + languages: [generic] + paths: + include: + - "*.bat" + - "*.ps1" + exclude: + - "**/.venv/**" # Where `activate.bat` / `activate.ps1` are written by the venv tool, not by us. + pattern-regex: \A + message: >- + A `.bat` or `.ps1` file is banned. Windows is a first-tier platform here, which is precisely why its + shell gets no scripts of its own: a task body runs the same way on all five supported platforms, so + logic written once is logic every lane exercises, whereas a batch or PowerShell file is reachable from + one platform and silently untested on the other four. That is how a Windows-only path rots unnoticed + until it is the lane holding up a merge. Write the logic once in a task body under `.mise/`, or in its + own directory under `utilities/` when it has outgrown one, and let every platform run the same thing. + The `.venv/` exemption covers the activation scripts a virtualenv generates for itself, which are the + tool's output rather than anyone's source. + severity: ERROR + + - id: no-cmd-scripts + languages: [generic] + paths: + include: + - "*.cmd" + pattern-regex: \A + message: >- + A `.cmd` file is banned everywhere in this tree, with no exemption at all -- not in `.mise/`, and not + under a `.venv/` the way its sibling extensions are. The reasoning is the same one that bans `.bat` and + `.ps1`: a Windows-only shell file is exercised on one of five platforms and silently untested on the + other four. It gets no carve-out because nothing generates one -- a virtualenv writes `activate.bat` + and `activate.ps1` but never a `.cmd` -- so every `.cmd` in the tree is something a person wrote by + hand, and belongs in a task body under `.mise/` or its own directory under `utilities/` instead. + severity: ERROR diff --git a/services/ws-modules/zig-data1/src/util.c b/services/ws-modules/zig-data1/src/util.c index 5a700346..f2ee5ecc 100644 --- a/services/ws-modules/zig-data1/src/util.c +++ b/services/ws-modules/zig-data1/src/util.c @@ -1,4 +1,9 @@ +// The suggested / are not C headers at all, so this C++-only rule cannot apply here. +// This translation unit is C, and the wasm32-freestanding target links no libc++ either, so taking the advice +// would fail to compile rather than modernise anything. +// skipcq: CXX-W2030 -- C, not C++: does not exist in this language #include +// skipcq: CXX-W2030 -- C, not C++: does not exist in this language #include // Returns the sum of all bytes in buf, mod 256. From e8a38b44eba21f46001ec01cd1ddccdd3eeca4ea Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Wed, 16 Sep 2026 06:41:14 +0800 Subject: [PATCH 05/24] more fixes --- .deepsource.toml | 8 +++++++ .mise/config.coverage.toml | 19 ++++++++++++---- .mise/config.toml | 13 +++++++++++ .mise/wasm-ll-objs.sh | 15 ++++++++----- Cargo.toml | 6 +++++ config/jscpd-baseline.json | 22 +++++-------------- config/semgrep/no-check-tool-mentions.yaml | 2 -- .../dart-comm1/pkg/et_ws_dart_comm1.js | 1 + .../dart-data1/pkg/et_ws_dart_data1.js | 1 + .../dart-math1/pkg/et_ws_dart_math1.js | 1 + .../dotnet-data1/pkg/et_ws_dotnet_data1.js | 2 +- .../dotnet-math1/pkg/et_ws_dotnet_math1.js | 2 +- .../java-data1/pkg/et_ws_java_data1.js | 1 + .../java-math1/pkg/et_ws_java_math1.js | 1 + services/ws-modules/js-data1/src/index.js | 1 + .../ws-modules/js-math1/pkg/et_ws_js_math1.js | 1 + .../kotlin-data1/pkg/et_ws_kotlin_data1.js | 1 + .../kotlin-math1/pkg/et_ws_kotlin_math1.js | 1 + .../ws-modules/pydata1/pkg/et_ws_pydata1.js | 1 + .../ws-modules/pydemo1/pkg/et_ws_pydemo1.js | 1 + .../ws-modules/pyeye1/pkg/et_ws_pyeye1.js | 1 + .../ws-modules/pyface1/pkg/et_ws_pyface1.js | 1 + .../ws-modules/pymath1/pkg/et_ws_pymath1.js | 1 + .../pyspeech1/pkg/et_ws_pyspeech1.js | 1 + services/ws-modules/pywasm1/index.js | 1 + .../ws-modules/rcomm1/pkg/et_ws_rcomm1.js | 1 + .../ws-modules/rdata1/pkg/et_ws_rdata1.js | 1 + .../ws-modules/rmath1/pkg/et_ws_rmath1.js | 1 + .../zig-data1/pkg/et_ws_zig_data1.js | 1 + .../zig-except1/pkg/et_ws_zig_except1.js | 1 + .../zig-math1/pkg/et_ws_zig_math1.js | 1 + services/ws-server/static/app.js | 1 + services/ws-server/static/meters.js | 1 + 33 files changed, 84 insertions(+), 29 deletions(-) diff --git a/.deepsource.toml b/.deepsource.toml index b2f60143..734c6276 100644 --- a/.deepsource.toml +++ b/.deepsource.toml @@ -41,6 +41,14 @@ name = "python" # Also ruled out: the files themselves. The repo's own JS linter parses every one of them as an ES module # with no parse error, and `module_system` is a documented key whose value here is one of its documented # options -- so neither the source nor the spelling of this setting is what the analyzer is objecting to. +# +# Ruled out last, and the reason no spelling of the key below can ever be the fix: `es-modules` is the +# analyzer's own DOCUMENTED DEFAULT for `module_system`, so writing it out restates what was already in force +# and changes nothing by construction. The package boundary a parser would otherwise resolve a bare `.js` +# against says the same thing -- every module's `pkg/package.json` already carries `"type": "module"`. Both +# inputs declare an ES module, JS-0833 is reported regardless, and the finding is reported only for files a +# given diff touches, so it arrives two or three at a time rather than all at once. What remains is a per-file +# `// skipcq: JS-0833` above the first export, added as each shim next comes up in a review. [[analyzers]] enabled = true meta = { environment = ["browser", "nodejs"], module_system = "es-modules" } diff --git a/.mise/config.coverage.toml b/.mise/config.coverage.toml index 812e2cbe..7b24693a 100644 --- a/.mise/config.coverage.toml +++ b/.mise/config.coverage.toml @@ -201,11 +201,24 @@ arg "" help="Space-separated libs/ directory names that must be at 100% br # `--branch` flag: branch regions are baked into the covmap by the `-Zcoverage-options=branch` that task # appends, and every export carries them from there. With no profile data the underlying command fails on its # own, which is the right answer -- there is nothing to assert. +# +# The `show-env` eval is what points `report` at that data instead of at an empty directory. Under `show-env` +# the profiles land in the plain target dir, which is where the collecting task's cargo invocations put them; +# a bare `cargo llvm-cov report` -- which is what this is, running in its own process one CI step later -- +# defaults to `target/llvm-cov-target` instead and finds nothing: +# error: failed to merge profile data: not found *.profraw files in +# /home/runner/work/core/core/target/llvm-cov-target; this may occur if target directory is accidentally +# cleared, or running report subcommand without running any tests or binaries +# on commit 52b2a6a67087fab043c1ea8c520373f5ba2477a4 at +# https://github.com/edge-toolkit/core/actions/runs/35005110677/job/104502812372. The raw profiles are still +# on disk at that point -- the collecting task's own `report` leaves them alone -- so the only thing this +# process was missing is the environment that says where to look. [tasks.cargo-llvm-cov-branch-check] description = "Fail unless every native libs/ crate is at 100% branch coverage" run = """ coreutils mkdir -p target/cov report=target/cov/native-libs.json +eval "$(cargo llvm-cov show-env --sh)" cargo llvm-cov report --json --summary-only --output-path "$report" mise run _cov-branch-assert "$report" "{{ vars.native_cov_libs }}" """ @@ -439,8 +452,7 @@ trap - EXIT host="$(rustc +{{ vars.rust_nightly }} -vV | goawk '/^host:/ { print $2 }')" llbin="$(rustc +{{ vars.rust_nightly }} --print sysroot)/lib/rustlib/$host/bin" "$llbin/llvm-profdata" merge -sparse -o "$covdir/agent.profdata" "$covdir"/wasm-agent-*.profraw -task=wasm-agent-cov -. .mise/wasm-ll-objs.sh +. .mise/wasm-ll-objs.sh "$covdir" "$llbin" wasm-agent-cov "$llbin/llvm-cov" export --format=lcov --instr-profile "$covdir/agent.profdata" "${objs[@]}" >"$covdir/all.lcov" coreutils touch lcov.info goawk -v want="ws-wasm-agent/src/" -f .mise/lcov-keep-want.awk "$covdir/all.lcov" >>lcov.info @@ -494,8 +506,7 @@ cargo test -p et-ws-pic-viewer --features et-web/coverage --target wasm32-unknow host="$(rustc +{{ vars.rust_nightly }} -vV | goawk '/^host:/ { print $2 }')" llbin="$(rustc +{{ vars.rust_nightly }} --print sysroot)/lib/rustlib/$host/bin" "$llbin/llvm-profdata" merge -sparse -o "$covdir/pic-viewer.profdata" "$covdir"/pic-viewer-*.profraw -task=pic-viewer-cov -. .mise/wasm-ll-objs.sh +. .mise/wasm-ll-objs.sh "$covdir" "$llbin" pic-viewer-cov "$llbin/llvm-cov" export --format=lcov --instr-profile "$covdir/pic-viewer.profdata" "${objs[@]}" >"$covdir/all.lcov" coreutils touch lcov.info goawk -v want="ws-modules/pic-viewer/src/" -f .mise/lcov-keep-want.awk "$covdir/all.lcov" >>lcov.info diff --git a/.mise/config.toml b/.mise/config.toml index 770a778c..9f6b5e72 100644 --- a/.mise/config.toml +++ b/.mise/config.toml @@ -794,6 +794,7 @@ depends = [ "regal-check", "ryl-check", "semgrep-check", + "shellcheck-check", "shellcheck-mise-check", "shfmt-mise-check", "taplo-check", @@ -1178,6 +1179,18 @@ run = "regal lint -c config/regal.yaml config/conftest/policy/" description = "Auto-fix the regal violations that have an auto-fixer (e.g. opa-fmt)" run = "regal fix -c config/regal.yaml config/conftest/policy/" +[tasks.shellcheck-check] +description = "shellcheck the standalone shell fragments committed under .mise/" +# The sibling task below reads shell out of the TOML; this one covers the files that are already shell on disk. +# Nothing was linting them, so an external analyzer reported their findings first -- a `.sh` under `.mise/` is +# exactly as much shell as a task body is, and had none of the same coverage. +# No `--shell` override and no `--exclude`: each file declares its own interpreter through a shebang or a +# `shell=` directive, and the exclusions the sibling task carries exist for extracted bodies, whose +# Tera-masked names read as unassigned variables. Applying either here would hide real findings. +# `-x` follows what a fragment sources, resolved from the repo root, which is the base a task body's relative +# `source` path resolves against when it pulls one of these in. +run = 'shellcheck -x --source-path="{{ vars.config_root_fwd }}" .mise/*.sh' + [tasks.shellcheck-mise-check] description = "shellcheck every bash-shell mise task body across .mise/config*.toml" # Extract each multi-line bash-shell task body and shellcheck it in isolation. diff --git a/.mise/wasm-ll-objs.sh b/.mise/wasm-ll-objs.sh index a14ed2cf..787efd31 100644 --- a/.mise/wasm-ll-objs.sh +++ b/.mise/wasm-ll-objs.sh @@ -1,7 +1,11 @@ +# shellcheck shell=bash + # Turns a wasm-bindgen test build's instrumented .ll into the llvm-cov objects an export needs. # Sourced by the coverage tasks that instrument a browser module, which differ only in which module they -# build: the caller sets `task` (named in the failure message), `covdir` and `llbin` beforehand, and reads -# the populated `objs` array afterwards. +# build: the caller passes the coverage directory, the llvm bin directory, and its own task name (which the +# failure message reports) as arguments, and reads the populated `objs` array afterwards. They arrive as +# arguments rather than as variables the caller happens to have set, so that what this fragment needs is +# declared at the call site instead of being an unwritten agreement about names. # # Both build-dir layouts are searched. A plain `deps/*.ll` glob broke when nightly made # `-Z build-dir-new-layout` the default: per-crate intermediates moved to `/build///out/`, @@ -9,6 +13,9 @@ # "target/wasm32-unknown-unknown/debug/deps/*.ll" not found` on commit 12bfe419 at # https://github.com/edge-toolkit/core/actions/runs/30797697402/job/91634929015. `find` also avoids the # silent-nullglob trap the literal-pattern failure exposed. +covdir=$1 +llbin=$2 +task=$3 objs=() lls="$(find target/wasm32-unknown-unknown/debug -path '*/build/*' -name '*.ll' 2>/dev/null)" if [ -z "$lls" ]; then @@ -24,6 +31,4 @@ while IFS= read -r ll; do goawk -f .mise/llvm-cov-gut.awk "$ll" >"$covdir/$name.g.ll" "$llbin/llc" -filetype=obj -mtriple=x86_64-unknown-linux-gnu -o "$covdir/$name.o" "$covdir/$name.g.ll" objs+=("-object" "$covdir/$name.o") -done <- diff --git a/services/ws-modules/dart-comm1/pkg/et_ws_dart_comm1.js b/services/ws-modules/dart-comm1/pkg/et_ws_dart_comm1.js index 26192504..69fa484c 100644 --- a/services/ws-modules/dart-comm1/pkg/et_ws_dart_comm1.js +++ b/services/ws-modules/dart-comm1/pkg/et_ws_dart_comm1.js @@ -1,5 +1,6 @@ // et_ws_dart_comm1.js -- ES module shim for dart-comm1 +// skipcq: JS-0833 -- committed ES module; the analyzer's script-mode parse is a false positive export default async function init() { await new Promise((resolve, reject) => { const s = document.createElement("script"); diff --git a/services/ws-modules/dart-data1/pkg/et_ws_dart_data1.js b/services/ws-modules/dart-data1/pkg/et_ws_dart_data1.js index 8b449cea..62b96390 100644 --- a/services/ws-modules/dart-data1/pkg/et_ws_dart_data1.js +++ b/services/ws-modules/dart-data1/pkg/et_ws_dart_data1.js @@ -1,5 +1,6 @@ // et_ws_dart_data1.js -- ES module shim for dart-data1 +// skipcq: JS-0833 -- committed ES module; the analyzer's script-mode parse is a false positive export default async function init() { await new Promise((resolve, reject) => { const s = document.createElement("script"); diff --git a/services/ws-modules/dart-math1/pkg/et_ws_dart_math1.js b/services/ws-modules/dart-math1/pkg/et_ws_dart_math1.js index 205c1de2..4daaeba8 100644 --- a/services/ws-modules/dart-math1/pkg/et_ws_dart_math1.js +++ b/services/ws-modules/dart-math1/pkg/et_ws_dart_math1.js @@ -1,5 +1,6 @@ // et_ws_dart_math1.js -- ES module shim for dart-math1 +// skipcq: JS-0833 -- committed ES module; the analyzer's script-mode parse is a false positive export default async function init() { await new Promise((resolve, reject) => { const s = document.createElement("script"); diff --git a/services/ws-modules/dotnet-data1/pkg/et_ws_dotnet_data1.js b/services/ws-modules/dotnet-data1/pkg/et_ws_dotnet_data1.js index 07494161..0191d0e6 100644 --- a/services/ws-modules/dotnet-data1/pkg/et_ws_dotnet_data1.js +++ b/services/ws-modules/dotnet-data1/pkg/et_ws_dotnet_data1.js @@ -3,7 +3,7 @@ let exports = null; -// skipcq: JS-0833 -- committed .NET WASM ES-module shim; DeepSource's script-mode parse is a false positive +// skipcq: JS-0833 -- committed ES module; the analyzer's script-mode parse is a false positive export default async function init() { const { dotnet } = await import(new URL("dotnet.js", import.meta.url).href); const { getAssemblyExports, setModuleImports } = await dotnet.create(); diff --git a/services/ws-modules/dotnet-math1/pkg/et_ws_dotnet_math1.js b/services/ws-modules/dotnet-math1/pkg/et_ws_dotnet_math1.js index f8bb3cd2..0b9c4b35 100644 --- a/services/ws-modules/dotnet-math1/pkg/et_ws_dotnet_math1.js +++ b/services/ws-modules/dotnet-math1/pkg/et_ws_dotnet_math1.js @@ -7,7 +7,7 @@ let exports = null; -// skipcq: JS-0833 -- committed .NET WASM ES-module shim; DeepSource's script-mode parse is a false positive +// skipcq: JS-0833 -- committed ES module; the analyzer's script-mode parse is a false positive export default async function init() { const { dotnet } = await import(new URL("dotnet.js", import.meta.url).href); const { getAssemblyExports, setModuleImports } = await dotnet.create(); diff --git a/services/ws-modules/java-data1/pkg/et_ws_java_data1.js b/services/ws-modules/java-data1/pkg/et_ws_java_data1.js index 1b22aa0d..7b649497 100644 --- a/services/ws-modules/java-data1/pkg/et_ws_java_data1.js +++ b/services/ws-modules/java-data1/pkg/et_ws_java_data1.js @@ -3,6 +3,7 @@ let javaRun = null; +// skipcq: JS-0833 -- committed ES module; the analyzer's script-mode parse is a false positive export default async function init() { let ws = null, wsState = "disconnected", diff --git a/services/ws-modules/java-math1/pkg/et_ws_java_math1.js b/services/ws-modules/java-math1/pkg/et_ws_java_math1.js index bf332db8..35ddc432 100644 --- a/services/ws-modules/java-math1/pkg/et_ws_java_math1.js +++ b/services/ws-modules/java-math1/pkg/et_ws_java_math1.js @@ -8,6 +8,7 @@ let javaRun = null; +// skipcq: JS-0833 -- committed ES module; the analyzer's script-mode parse is a false positive export default async function init() { let ws = null, wsState = "disconnected", diff --git a/services/ws-modules/js-data1/src/index.js b/services/ws-modules/js-data1/src/index.js index 0e8adb21..cc4eb69e 100644 --- a/services/ws-modules/js-data1/src/index.js +++ b/services/ws-modules/js-data1/src/index.js @@ -11,6 +11,7 @@ // failure (runner exits non-zero). We throw only on the core round-trip failing, never on an S3 feature the // service is simply missing. +// skipcq: JS-0833 -- committed ES module; the analyzer's script-mode parse is a false positive import { GetObjectCommand, HeadObjectCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3"; const MODULE = "js-data1"; diff --git a/services/ws-modules/js-math1/pkg/et_ws_js_math1.js b/services/ws-modules/js-math1/pkg/et_ws_js_math1.js index 0751ec3a..e4a50579 100644 --- a/services/ws-modules/js-math1/pkg/et_ws_js_math1.js +++ b/services/ws-modules/js-math1/pkg/et_ws_js_math1.js @@ -67,6 +67,7 @@ function fedAvg(input) { return [weight, bias]; } +// skipcq: JS-0833 -- committed ES module; the analyzer's script-mode parse is a false positive export default async function init() {} export async function run() { diff --git a/services/ws-modules/kotlin-data1/pkg/et_ws_kotlin_data1.js b/services/ws-modules/kotlin-data1/pkg/et_ws_kotlin_data1.js index 7fa8e7a0..0ef6203d 100644 --- a/services/ws-modules/kotlin-data1/pkg/et_ws_kotlin_data1.js +++ b/services/ws-modules/kotlin-data1/pkg/et_ws_kotlin_data1.js @@ -1,6 +1,7 @@ // et_ws_kotlin_data1.js -- ES module shim for kotlin-data1 (Kotlin/Wasm, WasmGC) // Interface: default(), run() +// skipcq: JS-0833 -- committed ES module; the analyzer's script-mode parse is a false positive export default async function init() { let ws = null, wsState = "disconnected", diff --git a/services/ws-modules/kotlin-math1/pkg/et_ws_kotlin_math1.js b/services/ws-modules/kotlin-math1/pkg/et_ws_kotlin_math1.js index fb11736c..c406fb7f 100644 --- a/services/ws-modules/kotlin-math1/pkg/et_ws_kotlin_math1.js +++ b/services/ws-modules/kotlin-math1/pkg/et_ws_kotlin_math1.js @@ -7,6 +7,7 @@ // accessors below (WasmGC has no JSON parser of its own; the accessors keep the guest // dependency-free). +// skipcq: JS-0833 -- committed ES module; the analyzer's script-mode parse is a false positive export default async function init() { let ws = null, wsState = "disconnected", diff --git a/services/ws-modules/pydata1/pkg/et_ws_pydata1.js b/services/ws-modules/pydata1/pkg/et_ws_pydata1.js index da54a161..814efa40 100644 --- a/services/ws-modules/pydata1/pkg/et_ws_pydata1.js +++ b/services/ws-modules/pydata1/pkg/et_ws_pydata1.js @@ -36,6 +36,7 @@ function loadPyodideScript() { }); } +// skipcq: JS-0833 -- committed ES module; the analyzer's script-mode parse is a false positive export default async function init() { await loadPyodideScript(); // `mise install pyodide` extracts the full GitHub-release distribution diff --git a/services/ws-modules/pydemo1/pkg/et_ws_pydemo1.js b/services/ws-modules/pydemo1/pkg/et_ws_pydemo1.js index 7fd6a1f7..45e0e676 100644 --- a/services/ws-modules/pydemo1/pkg/et_ws_pydemo1.js +++ b/services/ws-modules/pydemo1/pkg/et_ws_pydemo1.js @@ -15,6 +15,7 @@ let runtime = null; let pythonScriptPromise = null; let pythonRuntimePromise = null; +// skipcq: JS-0833 -- committed ES module; the analyzer's script-mode parse is a false positive export default async function init() {} export const is_running = () => runtime !== null; diff --git a/services/ws-modules/pyeye1/pkg/et_ws_pyeye1.js b/services/ws-modules/pyeye1/pkg/et_ws_pyeye1.js index c1cdb345..eb6208cb 100644 --- a/services/ws-modules/pyeye1/pkg/et_ws_pyeye1.js +++ b/services/ws-modules/pyeye1/pkg/et_ws_pyeye1.js @@ -17,6 +17,7 @@ let pyodide; let py; let runtime = null; +// skipcq: JS-0833 -- committed ES module; the analyzer's script-mode parse is a false positive export default async function init() { if (!globalThis.loadPyodide) { await new Promise((resolve, reject) => { diff --git a/services/ws-modules/pyface1/pkg/et_ws_pyface1.js b/services/ws-modules/pyface1/pkg/et_ws_pyface1.js index eab6a487..991a142a 100644 --- a/services/ws-modules/pyface1/pkg/et_ws_pyface1.js +++ b/services/ws-modules/pyface1/pkg/et_ws_pyface1.js @@ -19,6 +19,7 @@ let runtime = null; let workCanvas = null; let tensorData = null; +// skipcq: JS-0833 -- committed ES module; the analyzer's script-mode parse is a false positive export default async function init() { if (!globalThis.loadPyodide) { await new Promise((resolve, reject) => { diff --git a/services/ws-modules/pymath1/pkg/et_ws_pymath1.js b/services/ws-modules/pymath1/pkg/et_ws_pymath1.js index 5fa7340e..6438658a 100644 --- a/services/ws-modules/pymath1/pkg/et_ws_pymath1.js +++ b/services/ws-modules/pymath1/pkg/et_ws_pymath1.js @@ -22,6 +22,7 @@ function loadPyodideScript() { }); } +// skipcq: JS-0833 -- committed ES module; the analyzer's script-mode parse is a false positive export default async function init() { await loadPyodideScript(); // The full Pyodide distribution is served at /modules/pyodide/, so the runtime resolves from this diff --git a/services/ws-modules/pyspeech1/pkg/et_ws_pyspeech1.js b/services/ws-modules/pyspeech1/pkg/et_ws_pyspeech1.js index 7b4d8170..58709089 100644 --- a/services/ws-modules/pyspeech1/pkg/et_ws_pyspeech1.js +++ b/services/ws-modules/pyspeech1/pkg/et_ws_pyspeech1.js @@ -6,6 +6,7 @@ let py; let cfg; let runtime = null; +// skipcq: JS-0833 -- committed ES module; the analyzer's script-mode parse is a false positive export default async function init() { if (!globalThis.loadPyodide) { await new Promise((resolve, reject) => { diff --git a/services/ws-modules/pywasm1/index.js b/services/ws-modules/pywasm1/index.js index a5e7a555..06df7e1e 100644 --- a/services/ws-modules/pywasm1/index.js +++ b/services/ws-modules/pywasm1/index.js @@ -1,3 +1,4 @@ +// skipcq: JS-0833 -- committed ES module; the analyzer's script-mode parse is a false positive import init, { pyExec } from "./rustpython_wasm.js"; export async function run() { diff --git a/services/ws-modules/rcomm1/pkg/et_ws_rcomm1.js b/services/ws-modules/rcomm1/pkg/et_ws_rcomm1.js index 7c3c774b..e5d68c52 100644 --- a/services/ws-modules/rcomm1/pkg/et_ws_rcomm1.js +++ b/services/ws-modules/rcomm1/pkg/et_ws_rcomm1.js @@ -11,6 +11,7 @@ const R_SOURCE_URL = "/modules/et-ws-rcomm1/module.R"; let webR = null; +// skipcq: JS-0833 -- committed ES module; the analyzer's script-mode parse is a false positive export default async function init() { const { WebR } = await import(`${WEBR_BASE_URL}webr.mjs`); webR = new WebR({ baseUrl: WEBR_BASE_URL }); diff --git a/services/ws-modules/rdata1/pkg/et_ws_rdata1.js b/services/ws-modules/rdata1/pkg/et_ws_rdata1.js index 6915f4ea..f440710b 100644 --- a/services/ws-modules/rdata1/pkg/et_ws_rdata1.js +++ b/services/ws-modules/rdata1/pkg/et_ws_rdata1.js @@ -11,6 +11,7 @@ const R_SOURCE_URL = "/modules/et-ws-rdata1/module.R"; let webR = null; +// skipcq: JS-0833 -- committed ES module; the analyzer's script-mode parse is a false positive export default async function init() { const { WebR } = await import(`${WEBR_BASE_URL}webr.mjs`); webR = new WebR({ baseUrl: WEBR_BASE_URL }); diff --git a/services/ws-modules/rmath1/pkg/et_ws_rmath1.js b/services/ws-modules/rmath1/pkg/et_ws_rmath1.js index dcab8842..7dddb021 100644 --- a/services/ws-modules/rmath1/pkg/et_ws_rmath1.js +++ b/services/ws-modules/rmath1/pkg/et_ws_rmath1.js @@ -12,6 +12,7 @@ const R_SOURCE_URL = "/modules/et-ws-rmath1/module.R"; let webR = null; +// skipcq: JS-0833 -- committed ES module; the analyzer's script-mode parse is a false positive export default async function init() { const { WebR } = await import(`${WEBR_BASE_URL}webr.mjs`); webR = new WebR({ baseUrl: WEBR_BASE_URL }); diff --git a/services/ws-modules/zig-data1/pkg/et_ws_zig_data1.js b/services/ws-modules/zig-data1/pkg/et_ws_zig_data1.js index f49c19ca..490657f6 100644 --- a/services/ws-modules/zig-data1/pkg/et_ws_zig_data1.js +++ b/services/ws-modules/zig-data1/pkg/et_ws_zig_data1.js @@ -10,6 +10,7 @@ // for other types) // Data area starts at byte offset 16. +// skipcq: JS-0833 -- committed ES module; the analyzer's script-mode parse is a false positive export default async function init() {} export async function run() { diff --git a/services/ws-modules/zig-except1/pkg/et_ws_zig_except1.js b/services/ws-modules/zig-except1/pkg/et_ws_zig_except1.js index eaa0c1b6..78c58091 100644 --- a/services/ws-modules/zig-except1/pkg/et_ws_zig_except1.js +++ b/services/ws-modules/zig-except1/pkg/et_ws_zig_except1.js @@ -8,6 +8,7 @@ // [3] aux length (unused by this module; kept for shim parity with zig-data1) // Data area starts at byte offset 16. +// skipcq: JS-0833 -- committed ES module; the analyzer's script-mode parse is a false positive export default async function init() {} export async function run() { diff --git a/services/ws-modules/zig-math1/pkg/et_ws_zig_math1.js b/services/ws-modules/zig-math1/pkg/et_ws_zig_math1.js index 61881a68..36347588 100644 --- a/services/ws-modules/zig-math1/pkg/et_ws_zig_math1.js +++ b/services/ws-modules/zig-math1/pkg/et_ws_zig_math1.js @@ -9,6 +9,7 @@ // [3] aux length (binary request body for rest_request) // Data area starts at byte offset 16. +// skipcq: JS-0833 -- committed ES module; the analyzer's script-mode parse is a false positive export default async function init() {} export async function run() { diff --git a/services/ws-server/static/app.js b/services/ws-server/static/app.js index 70f6b2b0..181e1757 100644 --- a/services/ws-server/static/app.js +++ b/services/ws-server/static/app.js @@ -1,3 +1,4 @@ +// skipcq: JS-0833 -- committed ES module; the analyzer's script-mode parse is a false positive import init, { initTracing, WsClient, WsClientConfig } from "/modules/et-ws-wasm-agent/et_ws_wasm_agent.js"; // Bump this string on every meaningful app.js edit. index.html loads this file via a plain, non-cache-busted diff --git a/services/ws-server/static/meters.js b/services/ws-server/static/meters.js index 3cce29e2..5a2bf495 100644 --- a/services/ws-server/static/meters.js +++ b/services/ws-server/static/meters.js @@ -11,6 +11,7 @@ // its bottom-right corner handle, and closed with the corner x button, which also stops the probe // and releases the WebGPU device. +// skipcq: JS-0833 -- committed ES module; the analyzer's script-mode parse is a false positive import Stats from "/modules/stats-gl/dist/main.js"; const PROBE_SHADER = ` From aec4133097f57ad406fb16ad5231fafb31641e09 Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Wed, 16 Sep 2026 09:05:01 +0800 Subject: [PATCH 06/24] add more tests --- .mise/config.coverage.toml | 22 ++- libs/edge-toolkit/src/config.rs | 14 +- libs/edge-toolkit/tests/mise_env.rs | 57 +++++++ libs/edge-toolkit/tests/npm_mod.rs | 20 +++ libs/edge-toolkit/tests/pipx_site_packages.rs | 151 ++++++++++++++++-- libs/edge-toolkit/tests/registry.rs | 31 +++- libs/et-otlp/src/lib.rs | 40 ++++- libs/et-otlp/tests/resource.rs | 62 +++++++ libs/path/tests/relative.rs | 43 +++++ libs/test-otlp/tests/metrics.rs | 20 ++- libs/ws-runner-common/Cargo.toml | 3 +- libs/ws-runner-common/src/lib.rs | 8 +- .../tests/fetch_main_field.rs | 26 +++ .../ws-runner-common/tests/register_frames.rs | 94 +++++++++++ 14 files changed, 562 insertions(+), 29 deletions(-) create mode 100644 libs/edge-toolkit/tests/mise_env.rs create mode 100644 libs/et-otlp/tests/resource.rs create mode 100644 libs/path/tests/relative.rs create mode 100644 libs/ws-runner-common/tests/register_frames.rs diff --git a/.mise/config.coverage.toml b/.mise/config.coverage.toml index 7b24693a..6fe4977a 100644 --- a/.mise/config.coverage.toml +++ b/.mise/config.coverage.toml @@ -117,18 +117,32 @@ coreutils mkdir -p "$ort_build_dir/examples" "$ort_build_dir/deps" # the top-level `cargo llvm-cov` does), so the nightly `-Zcoverage-options=branch` is appended to the wrapper # RUSTFLAGS directly; `report`'s lcov export emits the BRDA records on its own once the covmap carries branch # regions. On Linux continuous mode additionally needs relocatable counters -# (`-Cllvm-args=-runtime-counter-relocation`). The wrapper flags env is CARGO_ENCODED_RUSTFLAGS format (ASCII +# (`-Cllvm-args=-runtime-counter-relocation`). +# macOS is the exception and keeps the profile pattern show-env emits, because continuous mode there writes +# nothing back at all. The run completes and the report generates, but every counter in it reads zero: +# lines: 0/8677 hit +# branches: 0/954 hit +# from that lane's own artifact on commit 52b2a6a67087fab043c1ea8c520373f5ba2477a4 at +# https://github.com/edge-toolkit/core/actions/runs/35032214650/job/104593077352, and reproduced on a +# workstation by changing this one variable and nothing else -- the same crates and tests report real +# numbers under the merge-pool pattern and zeros under %c. The relocation flag above is what makes %c work +# on Linux and has no Mach-O counterpart to reach for. The carve-out costs exactly what %c was added for: +# the pyo3 runner that nextest SIGKILLs contributes nothing on macOS, so its async paths read 0% there and +# the Linux lane is the one that measures them. The wrapper flags env is CARGO_ENCODED_RUSTFLAGS format (ASCII # unit separator 0x1f between flags), not space-separated -- a space append glues the llvm-arg onto the last # existing flag and rustc rejects `--cfg=coverage_nightly -Cllvm-args=...` as one invalid --cfg value. goawk # generates the separator byte at run time: bash dollar-quote hex is not a valid escape in this TOML basic # string, and a TOML unicode escape renders as an invisible byte in the body, so it is constructed visibly. -cov_dir="$(coreutils dirname "$LLVM_PROFILE_FILE")" -export LLVM_PROFILE_FILE="$cov_dir/core-cont-%p%c.profraw" +host_os="$(coreutils uname -s)" +if [ "$host_os" != "Darwin" ]; then + cov_dir="$(coreutils dirname "$LLVM_PROFILE_FILE")" + export LLVM_PROFILE_FILE="$cov_dir/core-cont-%p%c.profraw" +fi us="$(goawk 'BEGIN { printf "%c", 31 }')" flags="${__CARGO_LLVM_COV_RUSTC_WRAPPER_RUSTFLAGS:-}" append_flag() { flags="${flags:+${flags}${us}}$1"; } append_flag "-Zcoverage-options=branch" -if [ "$(coreutils uname -s)" = "Linux" ]; then +if [ "$host_os" = "Linux" ]; then append_flag "-Cllvm-args=-runtime-counter-relocation" fi export __CARGO_LLVM_COV_RUSTC_WRAPPER_RUSTFLAGS="$flags" diff --git a/libs/edge-toolkit/src/config.rs b/libs/edge-toolkit/src/config.rs index b851d6e8..af638195 100644 --- a/libs/edge-toolkit/src/config.rs +++ b/libs/edge-toolkit/src/config.rs @@ -395,7 +395,19 @@ pub fn mise_python_site_packages() -> Vec { else { return Vec::new(); }; - let Ok(tools) = serde_json::from_slice::>(&output.stdout) else { + site_packages_from_tool_list(&output.stdout) +} + +/// The `site-packages` directories named by a `mise ls --current --json` payload. +/// +/// Split from [`mise_python_site_packages`] so the shape of the payload can be exercised without a `mise` to +/// produce it. Every failure here is a silent empty list -- an embedded interpreter simply starts with a bare +/// `sys.path` and the first `import` of a mise-managed package fails far from the cause -- so the payload +/// shapes that yield nothing are worth pinning: a body that is not JSON at all, a tool that is not a `pipx:` +/// one, and an install whose directory carries no venv. +#[must_use] +pub fn site_packages_from_tool_list(tool_list_json: &[u8]) -> Vec { + let Ok(tools) = serde_json::from_slice::>(tool_list_json) else { return Vec::new(); }; tools diff --git a/libs/edge-toolkit/tests/mise_env.rs b/libs/edge-toolkit/tests/mise_env.rs new file mode 100644 index 00000000..c8fca6d0 --- /dev/null +++ b/libs/edge-toolkit/tests/mise_env.rs @@ -0,0 +1,57 @@ +//! Covers every arm of `mise_env_includes`, the gate each guest-language test asks before it runs. +//! +//! The function decides whether a language's toolchain is loaded, and every caller treats a `false` as +//! "skip this work". That makes both its failure modes silent: a wrong `true` runs a test against a toolchain +//! that is not installed, and a wrong `false` turns a real test into a no-op that still reports as passing. +//! The unset case in particular has to answer `true` -- a bare `cargo test` sets no `MISE_ENV`, and answering +//! `false` there would quietly disable the guest-language suites for anyone not going through a task. +#![cfg(test)] + +use edge_toolkit::config::{Language, mise_env_includes}; +use et_test_helpers::temp_env; + +#[test] +fn an_unset_mise_env_includes_every_language() { + // No MISE_ENV at all: the read fails and the answer is an unconditional `true`, so a plain `cargo test` + // outside the task runner exercises every language rather than skipping them all. + temp_env::with_var_unset("MISE_ENV", || { + assert!(mise_env_includes(Language::Python)); + assert!(mise_env_includes(Language::Zig)); + }); +} + +#[test] +fn an_empty_mise_env_includes_nothing() { + // An empty value is a set-but-empty env list, which is not the same as unset: nothing is loaded, so every + // language must answer `false`. Splitting `""` on `,` yields one empty segment, which would match no + // language anyway -- the explicit emptiness check is what keeps that from depending on split's behaviour. + temp_env::with_var("MISE_ENV", Some(""), || { + assert!(!mise_env_includes(Language::Python)); + assert!(!mise_env_includes(Language::Rust)); + }); +} + +#[test] +fn a_populated_mise_env_includes_only_what_it_lists() { + // The everyday case, taking both arms of the membership test in one pass: a language in the list and one + // absent from it. Segments are trimmed, so a list written with spaces resolves the same way. + temp_env::with_var("MISE_ENV", Some("rust, python ,zig"), || { + assert!(mise_env_includes(Language::Python)); + assert!(mise_env_includes(Language::Rust)); + assert!(mise_env_includes(Language::Zig)); + assert!(!mise_env_includes(Language::Java)); + assert!(!mise_env_includes(Language::Dart)); + }); +} + +#[test] +fn a_prefix_of_a_language_name_is_not_a_match() { + // Segment equality, not substring: `r` must not satisfy `rust`, and `java` must not satisfy `js`. + // A substring test here would silently enable suites whose toolchain is absent. + temp_env::with_var("MISE_ENV", Some("r,java"), || { + assert!(mise_env_includes(Language::R)); + assert!(mise_env_includes(Language::Java)); + assert!(!mise_env_includes(Language::Rust)); + assert!(!mise_env_includes(Language::Js)); + }); +} diff --git a/libs/edge-toolkit/tests/npm_mod.rs b/libs/edge-toolkit/tests/npm_mod.rs index e2e2fd68..d64299fb 100644 --- a/libs/edge-toolkit/tests/npm_mod.rs +++ b/libs/edge-toolkit/tests/npm_mod.rs @@ -123,6 +123,26 @@ fn resolves_scoped_package_layout() { ); } +#[test] +fn aube_scan_skips_entries_that_do_not_carry_the_package() { + // A single `global-aube/` entry carrying the full `.aube/node_modules` scaffolding but a + // different package inside it. The scan enters the loop, finds the wanted package absent, and has to keep + // going rather than returning the enclosing directory -- returning it would hand the modules service a + // `node_modules` that does not contain the package it is about to serve, and every request would 404. + // + // Exactly one entry on purpose: `read_dir` yields entries in an order the filesystem chooses, so a fixture + // with a decoy *and* a match could satisfy itself on the match first and never take the skip path at all. + // `returns_none_when_neither_layout_has_the_package` below creates no `global-aube`, so it never enters + // this loop; this is the only fixture that walks it without finding anything. + let install = TempDir::new().unwrap(); + let decoy = install + .path() + .join("global-aube/decoy-hash/node_modules/.aube/node_modules"); + fs::create_dir_all(decoy.join("some-other-package")).unwrap(); + + assert!(find_npm_modules_path_in(install.path(), "onnxruntime-web").is_none()); +} + #[test] fn returns_none_when_neither_layout_has_the_package() { let install = TempDir::new().unwrap(); diff --git a/libs/edge-toolkit/tests/pipx_site_packages.rs b/libs/edge-toolkit/tests/pipx_site_packages.rs index f365291d..6a6cd41d 100644 --- a/libs/edge-toolkit/tests/pipx_site_packages.rs +++ b/libs/edge-toolkit/tests/pipx_site_packages.rs @@ -10,29 +10,158 @@ use edge_toolkit::config::find_site_packages_in; use fs_err as fs; use tempfile::TempDir; -#[test] -fn resolves_pipx_venv_layout() { - // /cowsay/lib/python3.13/site-packages -- the shape mise's pipx - // backend lays down; both `` and the python version are scanned, not - // assumed. +/// Build `/` and assert the resolver returns exactly that directory. +/// +/// The two layouts below differ only in the directory names between the install root and `site-packages`, +/// which is the whole point of the resolver: it scans the variable segments rather than assuming either +/// shape. Asserting them through one body keeps that the only difference on display. +fn resolves_venv_layout(venv: &str) { let install = TempDir::new().unwrap(); - let site_packages = install.path().join("cowsay/lib/python3.13/site-packages"); + let site_packages = install.path().join(venv); fs::create_dir_all(&site_packages).unwrap(); let found = find_site_packages_in(install.path()); assert_eq!(found.as_deref(), Some(site_packages.as_path())); } +#[test] +fn resolves_pipx_venv_layout() { + // The shape mise's pipx backend lays down; both `` and the python version are scanned, not assumed. + resolves_venv_layout("cowsay/lib/python3.13/site-packages"); +} + #[test] fn resolves_windows_pipx_venv_layout() { - // /cowsay/Lib/site-packages -- the shape uv (pipx backend) lays - // down on Windows: capital `Lib`, no python-version subdir. + // The shape uv (pipx backend) lays down on Windows: capital `Lib`, no python-version subdir. + resolves_venv_layout("cowsay/Lib/site-packages"); +} + +/// Probe availability and run the lookup with `path` as the whole of `PATH`, reporting both. +/// +/// Both are read under one `PATH` so the pair can be compared: the lookup answers with an empty list for two +/// quite different reasons -- mise was never found, or mise was found and its query failed -- and only the +/// availability flag distinguishes them. Asserting the list alone would let a test pass while exercising the +/// wrong path entirely. +fn availability_and_lookup(path: &str) -> (bool, Vec) { + et_test_helpers::temp_env::with_var("PATH", Some(path), || { + ( + edge_toolkit::config::mise_is_available(), + edge_toolkit::config::mise_python_site_packages(), + ) + }) +} + +#[test] +fn site_packages_lookup_is_empty_when_mise_is_missing() { + // `mise_python_site_packages` is what pre-populates the pyo3 runner's `sys.path`, and it must degrade to + // an empty list rather than panicking when there is no mise to ask. An empty PATH makes the availability + // probe's spawn fail the same way it would on a deployment that never installed mise, so the function + // returns before it tries to parse any tool list. + let (available, found) = availability_and_lookup(""); + assert!(!available, "an empty PATH must hide mise from the availability probe"); + assert!( + found.is_empty(), + "expected no site-packages paths when mise is unavailable, got {found:?}" + ); +} + +/// Write a stand-in `mise` into `dir` that answers `--version` and fails every other invocation. +/// +/// The availability probe and the tool-list call are two separate spawns of the same name, so the only way to +/// reach the "mise is here but the query failed" path is a command that distinguishes between them. +#[expect( + clippy::single_call_fn, + reason = "distinct fixture builder for the stand-in command; kept separate from the assertions it feeds" +)] +fn write_fake_mise_that_fails_its_queries(dir: &TempDir) { + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt as _; + + let script = dir.path().join("mise"); + fs::write( + &script, + "#!/bin/sh\nif [ \"$1\" = \"--version\" ]; then\n echo 2026.1.1\n exit 0\nfi\nexit 1\n", + ) + .unwrap(); + // Read the mode back and set the execute bits, rather than building a permissions value from + // scratch, so nothing here has to name the standard filesystem module the repo routes around. + let mut permissions = fs::metadata(&script).unwrap().permissions(); + permissions.set_mode(0o755); + fs::set_permissions(&script, permissions).unwrap(); + } + #[cfg(not(unix))] + { + // A batch stand-in, which Windows resolves through the system command processor: that is located + // from the system directory rather than from `PATH`, so it stays reachable even though the probe + // below hands the process a `PATH` containing only this directory. + fs::write( + dir.path().join("mise.bat"), + "@echo off\r\nif \"%1\"==\"--version\" (echo 2026.1.1& exit /b 0)\r\nexit /b 1\r\n", + ) + .unwrap(); + } +} + +#[test] +fn a_failing_tool_list_query_yields_no_paths() { + // mise resolves and reports a version, so the availability probe passes, but the tool-list query exits + // non-zero -- a broken config, an unreadable install directory, a version whose `ls` flags differ. The + // runner has to treat that as "no mise-managed packages" and carry on, because the alternative is an + // embedded interpreter that refuses to start on a machine where everything else works. + let dir = TempDir::new().unwrap(); + write_fake_mise_that_fails_its_queries(&dir); + + let (available, found) = availability_and_lookup(&dir.path().display().to_string()); + + // Asserted first, and separately: an empty list is also what a *missing* mise produces, so without + // pinning the stand-in as present this test would still pass if it were never found at all -- exercising + // the earlier bail-out and reporting success for a path it never reached. + assert!( + available, + "the stand-in must answer --version, or this asserts the wrong branch" + ); + assert!( + found.is_empty(), + "a failing `mise ls` must yield no paths, got {found:?}" + ); +} + +#[test] +fn a_tool_list_that_is_not_json_yields_no_paths() { + // `mise ls --current --json` answered with something unparsable -- an older mise, a wrapper that printed + // a warning first, a truncated pipe. The runner must come away with an empty `sys.path` addition rather + // than failing to start, so the interpreter still boots and only the mise-managed imports are missing. + assert!(edge_toolkit::config::site_packages_from_tool_list(b"mise: not a tool list").is_empty()); + assert!( + edge_toolkit::config::site_packages_from_tool_list(b"").is_empty(), + "an empty body is not valid JSON either" + ); +} + +#[test] +fn only_pipx_tools_with_a_venv_contribute_paths() { + // One `pipx:` tool whose install really carries a venv, one `pipx:` tool whose install does not, and one + // non-`pipx:` tool. Only the first contributes: the others are what a mixed tool set looks like, and + // letting either through would put a directory with no `site-packages` on the interpreter's path. let install = TempDir::new().unwrap(); - let site_packages = install.path().join("cowsay/Lib/site-packages"); + let with_venv = install.path().join("cowsay-install"); + let site_packages = with_venv.join("cowsay/lib/python3.13/site-packages"); fs::create_dir_all(&site_packages).unwrap(); + let without_venv = install.path().join("bare-install"); + fs::create_dir_all(&without_venv).unwrap(); - let found = find_site_packages_in(install.path()); - assert_eq!(found.as_deref(), Some(site_packages.as_path())); + let tool_list = serde_json::json!({ + "pipx:cowsay": [{ "active": true, "install_path": with_venv }], + "pipx:bare": [{ "active": true, "install_path": without_venv }], + "npm:some-node-tool": [{ "active": true, "install_path": with_venv }], + }); + let found = edge_toolkit::config::site_packages_from_tool_list(&serde_json::to_vec(&tool_list).unwrap()); + assert_eq!( + found, + vec![site_packages], + "expected only the pipx install that has a venv" + ); } #[test] diff --git a/libs/edge-toolkit/tests/registry.rs b/libs/edge-toolkit/tests/registry.rs index 69fbd2bd..c266dec6 100644 --- a/libs/edge-toolkit/tests/registry.rs +++ b/libs/edge-toolkit/tests/registry.rs @@ -6,7 +6,7 @@ use std::collections::BTreeMap; -use edge_toolkit::ws::AgentConnectionState; +use edge_toolkit::ws::{AgentConnectionState, ConnectStatus}; use edge_toolkit::ws_server::{AgentRecord, AgentRegistry}; use tempfile::tempdir; @@ -42,6 +42,35 @@ fn save_load_roundtrip_and_session_lookup() { assert_eq!(empty.agent_session(&agent_id), None); } +#[test] +fn unknown_ids_fall_through_to_the_assign_and_no_op_paths() { + let registry = AgentRegistry::::default(); + + // A requested id the registry has never seen is not a reconnect: the lookup misses, so the caller's + // `new_id` is inserted fresh and the status comes back Assigned. Getting this wrong would hand a + // reconnecting client someone else's identity, or resurrect an id the server has forgotten. + let (agent_id, status) = registry.connect_agent( + Some("never-registered".to_string()), + "agent-fresh".to_string(), + "127.0.0.1", + "sess-a".to_string(), + ); + assert_eq!(agent_id, "agent-fresh"); + assert_eq!(status, ConnectStatus::Assigned); + + // Disconnecting an id that was never registered is a no-op rather than an insert: the registry still + // holds exactly the one agent above, still Connected. + registry.mark_disconnected("never-registered"); + let summaries = registry.list_agents(); + assert_eq!( + summaries.len(), + 1, + "no record should have been created, got {summaries:?}" + ); + assert_eq!(summaries[0].agent_id, "agent-fresh"); + assert_eq!(summaries[0].state, AgentConnectionState::Connected); +} + #[test] fn agent_record_with_pending_builder_replaces_the_map() { let record = AgentRecord::::new(AgentConnectionState::Disconnected, None, None) diff --git a/libs/et-otlp/src/lib.rs b/libs/et-otlp/src/lib.rs index 547e05fe..ee8ab791 100644 --- a/libs/et-otlp/src/lib.rs +++ b/libs/et-otlp/src/lib.rs @@ -49,6 +49,36 @@ impl OtelHandles { } } +/// The HTTP headers every OTLP exporter is built with, carrying basic auth when the config supplies it. +/// +/// Split out of [`init`] so both shapes are reachable from a test. `init` installs a process-global +/// subscriber and can therefore run at most once per process, which leaves anything decided inside it +/// testable only in whichever configuration that single call happens to use -- and a collector reached +/// without its credentials rejects every export, silently, for the life of the process. +#[must_use] +pub fn exporter_headers(auth: Option<&edge_toolkit::auth::BasicAuth>) -> std::collections::HashMap { + let mut headers = std::collections::HashMap::new(); + if let Some(auth) = auth { + auth.add_basic_auth_header(&mut headers); + } + headers +} + +/// The resource attributes describing this service, given whatever the host's name resolved to. +/// +/// Takes the hostname rather than reading it, for the same reason as [`exporter_headers`] and one more: +/// the `None` case is a host whose name does not resolve or is not UTF-8, which no test can bring about +/// by asking the real machine. Passing it in is what makes the attribute's absence an asserted behaviour +/// instead of an assumption -- the resource must still carry the version, and simply omit the instance. +#[must_use] +pub fn service_descriptors(hostname: Option) -> Vec { + let mut descriptors = vec![KeyValue::new("service.version", env!("CARGO_PKG_VERSION").to_string())]; + if let Some(hostname) = hostname { + descriptors.push(KeyValue::new("service.instance", hostname)); + } + descriptors +} + /// Initialise the global tracing subscriber + `OTel` pipeline against `config`. /// /// Call exactly once per process; a second call returns an error from @@ -64,10 +94,7 @@ pub fn init(config: &OtlpConfig) -> Result Result String { + Component::CurDir.as_os_str().to_string_lossy().into_owned() +} + +#[test] +fn identical_paths_render_as_the_current_directory() { + let dir = Path::new("/workspace/services/ws-server"); + assert_eq!(relative_path_from(dir, dir), current_dir()); +} + +#[test] +fn paths_differing_only_in_normalisation_still_render_as_the_current_directory() { + // Interior `.` and `..` segments are normalised away before the comparison, so these two spellings of one + // directory take the same branch as the literally-identical pair above. + let from = Path::new("/workspace/services/ws-server"); + let target = Path::new("/workspace/services/modules/../ws-server"); + assert_eq!(relative_path_from(from, target), current_dir()); +} + +#[test] +fn a_target_below_the_base_still_renders_its_suffix() { + // The neighbouring case, kept here so the empty-parts branch is not asserted in isolation: a real + // relative path must still come back joined with forward slashes on every host. + let from = Path::new("/workspace"); + let target = Path::new("/workspace/services/ws-server"); + assert_eq!(relative_path_from(from, target), "services/ws-server"); +} diff --git a/libs/test-otlp/tests/metrics.rs b/libs/test-otlp/tests/metrics.rs index 9a07f5db..46aa4a8c 100644 --- a/libs/test-otlp/tests/metrics.rs +++ b/libs/test-otlp/tests/metrics.rs @@ -21,6 +21,18 @@ fn int_point(value: i64) -> NumberDataPoint { } } +/// A floating-point data point, which the flattener counts but must not add into the summed value. +#[expect( + clippy::single_call_fn, + reason = "the float twin of int_point above; kept beside it so the pair reads as one fixture vocabulary" +)] +fn double_point(value: f64) -> NumberDataPoint { + NumberDataPoint { + value: Some(number_data_point::Value::AsDouble(value)), + ..Default::default() + } +} + fn metric(name: &str, data: Option) -> Metric { Metric { name: name.to_owned(), @@ -60,8 +72,10 @@ fn sample_request() -> ExportMetricsServiceRequest { ), metric( "gauge.metric", + // One integer point and one float, so the summing walk takes both its arms: the float is counted + // among the data points but contributes nothing to the total. Some(Data::Gauge(Gauge { - data_points: vec![int_point(9)], + data_points: vec![int_point(9), double_point(2.5)], })), ), metric( @@ -142,6 +156,10 @@ async fn metrics_endpoint_decodes_json_protobuf_and_flattens_every_shape() { let gauge = flat.iter().find(|rec| rec.name == "gauge.metric").unwrap(); assert_eq!(gauge.value, 9, "Gauge sums its integer data points"); + assert_eq!( + gauge.data_points, 2, + "the float point is counted even though it adds nothing to the value" + ); let hist = flat.iter().find(|rec| rec.name == "hist.metric").unwrap(); assert_eq!(hist.value, 0, "Histogram contributes no summed value"); diff --git a/libs/ws-runner-common/Cargo.toml b/libs/ws-runner-common/Cargo.toml index 80589258..b39529fe 100644 --- a/libs/ws-runner-common/Cargo.toml +++ b/libs/ws-runner-common/Cargo.toml @@ -32,7 +32,8 @@ tracing.workspace = true et-rest-client.workspace = true et-test-helpers.workspace = true serde-env.workspace = true -tokio = { workspace = true, features = ["macros", "rt"] } +tokio = { workspace = true, features = ["macros", "net", "rt", "test-util"] } +tokio-tungstenite = { workspace = true, features = ["handshake"] } [lints] workspace = true diff --git a/libs/ws-runner-common/src/lib.rs b/libs/ws-runner-common/src/lib.rs index c8c01d67..78d83238 100644 --- a/libs/ws-runner-common/src/lib.rs +++ b/libs/ws-runner-common/src/lib.rs @@ -9,7 +9,7 @@ //! These were duplicated across the runner crates; one implementation here //! keeps them in sync with the server. -use std::time::{Duration, Instant, SystemTime}; +use std::time::{Duration, SystemTime}; use edge_toolkit::ws::{ClientMessage, ConnectStatus, ServerMessage}; use futures_util::{SinkExt as _, StreamExt as _}; @@ -329,7 +329,11 @@ async fn fetch_package_json_bytes( client: &et_rest_client::Client, module_name: &str, ) -> Result, BootstrapError> { - let start = Instant::now(); + // Tokio's clock rather than the standard one, so the window is the same two minutes in production and + // something a paused-time test can step through. The two are identical when time is running: this type + // reads the same monotonic source, and only a runtime that has explicitly paused it behaves differently. + // Without it the only way to reach the give-up arm below is to wait out the full `MODULE_WAIT`. + let start = tokio::time::Instant::now(); loop { match try_fetch_package_json(client, module_name).await { Ok(bytes) => return Ok(bytes), diff --git a/libs/ws-runner-common/tests/fetch_main_field.rs b/libs/ws-runner-common/tests/fetch_main_field.rs index 57e6145c..102184de 100644 --- a/libs/ws-runner-common/tests/fetch_main_field.rs +++ b/libs/ws-runner-common/tests/fetch_main_field.rs @@ -72,6 +72,32 @@ async fn fetch_main_field_retries_until_the_hub_serves_the_module() { assert_eq!(main_field_after(2).await, "index.js"); } +#[tokio::test(start_paused = true)] +async fn fetch_main_field_gives_up_once_the_wait_window_is_spent() { + let port = reserve_port(); + // A hub that never admits to having the module: the count of 404s to serve is larger than the window can + // ask for, so every attempt misses and the loop runs until it is out of time. That is a genuinely absent + // module rather than a slow one, and it has to be reported instead of retried forever -- a runner stuck + // here looks identical to one that is working. + // + // Paused time is what makes this affordable. The window is two minutes of retries half a second apart; + // the sleeps between attempts advance the clock instantly, so the test spends real time only on the + // request each attempt makes. + // + // The handle is dropped rather than joined: this hub never stops answering, so there is nothing to wait + // for. Each test runs in its own process, so the listener goes away with it. + let _hub = spawn_hub(port, usize::MAX, PACKAGE_JSON); + + let error = fetch_main_field(&client_for(port), "et-ws-never-served") + .await + .unwrap_err(); + + assert!( + matches!(&error, BootstrapError::Rest(_)), + "expected the last fetch failure to be reported, got: {error:?}" + ); +} + #[tokio::test] async fn fetch_main_field_rejects_a_package_json_without_a_main_field() { let port = reserve_port(); diff --git a/libs/ws-runner-common/tests/register_frames.rs b/libs/ws-runner-common/tests/register_frames.rs new file mode 100644 index 00000000..c4f1d2fb --- /dev/null +++ b/libs/ws-runner-common/tests/register_frames.rs @@ -0,0 +1,94 @@ +//! Covers what the registration handshake does with frames that are not the ack it is waiting for. +//! +//! `register_once` reads from the socket until an `et-connect-ack` arrives, and the two ways that read can go +//! wrong are both silent from the runner's point of view. A binary frame arriving first -- a broadcast from +//! another agent, which the hub forwards verbatim to every connected client -- must be stepped over rather +//! than ending the handshake; treating it as a failure would make registration fail whenever a peer happened +//! to be chatty. A socket that closes before acking must come back as `ConnectionClosed` rather than hanging +//! until the budget expires, so the retry loop can report what actually happened. +//! +//! Both cases need a hub that misbehaves on purpose, so each test stands one up rather than driving the real +//! server, which has no way to be asked for either shape. Accepting a connection is the half of the websocket +//! protocol the crate itself never uses -- the runner only ever dials out -- so the server side arrives as a +//! test-only dependency feature. +#![cfg(test)] + +use std::time::Duration; + +use edge_toolkit::ws::{ConnectStatus, ServerMessage}; +use et_ws_runner_common::{ConnectError, connect_and_register}; +use futures_util::{SinkExt as _, StreamExt as _}; +use tokio::net::TcpListener; +use tokio_tungstenite::tungstenite::Message; + +/// The ack frame a real hub answers `et-connect` with. +/// +/// Built through the wire enum rather than hand-written JSON, so a rename of the type tag breaks this test +/// instead of quietly making it test nothing. +#[expect( + clippy::single_call_fn, + reason = "the wire fixture for the stub hub; named so the handshake body reads as a sequence of frames" +)] +fn ack_frame(agent_id: &str) -> String { + serde_json::to_string(&ServerMessage::ConnectAck { + agent_id: agent_id.to_owned(), + status: ConnectStatus::Assigned, + }) + .unwrap() +} + +#[tokio::test] +async fn a_binary_frame_before_the_ack_is_stepped_over() { + let port = et_test_helpers::reserve_port(); + let listener = TcpListener::bind(("127.0.0.1", port)).await.unwrap(); + let hub = tokio::spawn(async move { + let (stream, _addr) = listener.accept().await.unwrap(); + let mut socket = tokio_tungstenite::accept_async(stream).await.unwrap(); + let _connect = socket.next().await.unwrap().unwrap(); + // A binary frame is the hub relaying another agent's payload; it carries no handshake meaning. + socket.send(Message::binary(vec![0_u8, 1, 2])).await.unwrap(); + socket.send(Message::text(ack_frame("agent-7"))).await.unwrap(); + // Hold the socket open until the client has taken the ack and returned it to the caller. + tokio::time::sleep(Duration::from_millis(250)).await; + }); + + let url = format!("ws://127.0.0.1:{port}/ws"); + // Unwrapped rather than matched: a failure here *is* the regression this test exists for -- the binary + // frame ended the handshake -- and the panic carries the error that says so. + let (_socket, agent_id, status) = connect_and_register(&url, None, Some(Duration::from_secs(5))) + .await + .unwrap(); + assert_eq!(agent_id, "agent-7"); + assert_eq!(status, ConnectStatus::Assigned); + hub.await.unwrap(); +} + +#[tokio::test] +async fn a_socket_closed_before_the_ack_reports_connection_closed() { + let port = et_test_helpers::reserve_port(); + let listener = TcpListener::bind(("127.0.0.1", port)).await.unwrap(); + let hub = tokio::spawn(async move { + // Every attempt gets the same treatment, because the caller retries: read the registration, then hang + // up without answering. A hub restarting mid-handshake looks exactly like this from the outside. + loop { + let Ok((stream, _addr)) = listener.accept().await else { + return; + }; + let Ok(mut socket) = tokio_tungstenite::accept_async(stream).await else { + continue; + }; + let _connect = socket.next().await; + let _closed = socket.close(None).await; + } + }); + + let url = format!("ws://127.0.0.1:{port}/ws"); + let err = connect_and_register(&url, None, Some(Duration::from_millis(400))) + .await + .unwrap_err(); + assert!( + matches!(err, ConnectError::ConnectionClosed), + "expected the closed-socket error rather than a timeout, got {err:?}" + ); + hub.abort(); +} From 94cb8d2724539794d1d05d0ecd9e488247d96e6f Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Wed, 16 Sep 2026 09:34:58 +0800 Subject: [PATCH 07/24] fix coverage --- .mise/config.toml | 6 +-- .mise/mise.lock | 3 +- .mise/mise.windows.lock | 2 +- Cargo.lock | 55 ++++++++++++++++--------- Cargo.toml | 25 ++++++----- config/conftest/policy/cargo/cargo.rego | 1 + 6 files changed, 56 insertions(+), 36 deletions(-) diff --git a/.mise/config.toml b/.mise/config.toml index 9f6b5e72..4b597151 100644 --- a/.mise/config.toml +++ b/.mise/config.toml @@ -435,10 +435,10 @@ url = "https://github.com/edge-toolkit/core/releases/download/augeas-v1/1.14.1-x # # The version is chosen by its LLVM, which has to match the `minicov` in [workspace.dependencies]: minicov # writes the guests' profraw and this toolchain's llvm-tools reads it, so a mismatch traps the guest with -# `wasm trap: out of bounds memory access` in `coverage::dump`. `nightly-2026-08-31` (rustc 908501772) carries -# LLVM 23.1.0, which is the profraw-11 format minicov 0.3.9 emits. Bump this and minicov together, never one +# `wasm trap: out of bounds memory access` in `coverage::dump`. `nightly-2026-08-05` (rustc 1ed2df61a) carries +# LLVM 22.1.8, which is the profraw-10 format minicov 0.3.8 emits. Bump this and minicov together, never one # alone, and check `rustc + --version --verbose` reports an LLVM the minicov release names. -rust_nightly = "nightly-2026-08-31" +rust_nightly = "nightly-2026-08-05" # Tier-`a_` prefix marks leaf values referenced by later vars. # a_* vars are referenced by later vars (face1_src), and mise renders [vars] in alphabetical order; `a_` sorts # before every non-prefixed var name so the reference resolves. Shared GitHub release tag for the HF-asset diff --git a/.mise/mise.lock b/.mise/mise.lock index bf7effb8..015a3788 100644 --- a/.mise/mise.lock +++ b/.mise/mise.lock @@ -1210,6 +1210,7 @@ checksum = "sha256:b51f0208fdff83515a787bd8ab9ac5865ed84dabb66d0c709957bb59793c6 url = "https://github.com/wasm-bindgen/wasm-bindgen/releases/download/0.2.128/wasm-bindgen-0.2.128-x86_64-unknown-linux-musl.tar.gz" url_api = "https://api.github.com/repos/wasm-bindgen/wasm-bindgen/releases/assets/545035799" provenance = "github-attestations" +provenance_verified = true [tools."github:wasm-bindgen/wasm-bindgen"."platforms.macos-arm64"] checksum = "sha256:67ba17f260977725c0b541b516dbb5153538140f079a900329fb6077661b47ab" @@ -1751,7 +1752,7 @@ url = "https://github.com/BurntSushi/ripgrep/releases/download/15.1.0/ripgrep-15 url_api = "https://api.github.com/repos/BurntSushi/ripgrep/releases/assets/307305871" [[tools.rust]] -version = "nightly-2026-08-31" +version = "nightly-2026-08-05" backend = "core:rust" [tools.rust.options] diff --git a/.mise/mise.windows.lock b/.mise/mise.windows.lock index be4ba1ae..d39a4061 100644 --- a/.mise/mise.windows.lock +++ b/.mise/mise.windows.lock @@ -1155,7 +1155,7 @@ checksum = "sha256:e04493f469fd3861e119a603700cfeafe8da0090aed39b547ad3975b21f48 url = "https://github.com/edge-toolkit/core/releases/download/gnupg-w32-v1/2.5.20_20260513-x86_64-pc-windows.tar.gz" [[tools.rust]] -version = "nightly-2026-08-31-x86_64-pc-windows-gnullvm" +version = "nightly-2026-08-05-x86_64-pc-windows-gnullvm" backend = "core:rust" [tools.rust.options] diff --git a/Cargo.lock b/Cargo.lock index 9ed74624..b029dc82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1313,6 +1313,12 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "castaway" version = "0.2.4" @@ -1599,16 +1605,6 @@ version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e8ccc4ea9f6acc32d102c0f6d471d11d913ad15f20c04de743374861fa1d414" -[[package]] -name = "console_error_panic_hook" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" -dependencies = [ - "cfg-if", - "wasm-bindgen", -] - [[package]] name = "const-hex" version = "1.19.1" @@ -7299,9 +7295,9 @@ dependencies = [ [[package]] name = "minicov" -version = "0.3.9" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3aa3aa12b448ac225b3102217d1ac5cc717908f02722926524b0599c933c7a0" +checksum = "4869b6a491569605d66d3952bcdf03df789e5b536e5f0cf7758a7f08a55ae24d" dependencies = [ "cc", "walkdir", @@ -7889,6 +7885,12 @@ dependencies = [ "prost-build", ] +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + [[package]] name = "opaque-debug" version = "0.3.1" @@ -11133,7 +11135,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.3", "once_cell", "rustix 1.1.4", "windows-sys 0.61.2", @@ -12274,30 +12276,43 @@ dependencies = [ [[package]] name = "wasm-bindgen-test" -version = "0.3.45" +version = "0.3.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d381749acb0943d357dcbd8f0b100640679883fcdeeef04def49daf8d33a5426" +checksum = "45863ef0bef521c12124eb39d9a38513c47db75f22e503c06beacd40afeb35db" dependencies = [ - "console_error_panic_hook", + "async-trait", + "cast", "js-sys", + "libm", "minicov", - "scoped-tls", + "nu-ansi-term", + "num-traits", + "oorandom", + "serde", + "serde_json", "wasm-bindgen", "wasm-bindgen-futures", "wasm-bindgen-test-macro", + "wasm-bindgen-test-shared", ] [[package]] name = "wasm-bindgen-test-macro" -version = "0.3.45" +version = "0.3.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c97b2ef2c8d627381e51c071c2ab328eac606d3f69dd82bcbca20a9e389d95f0" +checksum = "8c89dcab8b516b6b603baca9d550b7282d68fcc7f367e3956cff7ebf406a3f12" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.5", ] +[[package]] +name = "wasm-bindgen-test-shared" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f37b4f992cebe528ef34964ae69681ac0fe7080071e7298e46008f9d380302af" + [[package]] name = "wasm-compose" version = "0.252.0" diff --git a/Cargo.toml b/Cargo.toml index e38db431..b82be95b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -123,12 +123,21 @@ libc = "0.2" local-ip-address = "0.6" log = "0.4" # minicov's profiler runtime must match the LLVM of the toolchain that instruments the guests. -# 0.3.9 moved to profraw format 11 ("Sync with LLVM 23.1.0-rc3"), so `vars.rust_nightly` has to stay on a -# nightly carrying LLVM 23.1 or later. Pairing 0.3.9 with an LLVM 22 nightly makes capture_coverage walk -# value-profile records with the wrong layout, trapping inside every instrumented WASI guest with +# Held at exactly 0.3.8 (profraw format 10, LLVM 22) because wasm-bindgen-test 0.3.78 -- the only release that +# pairs with wasm-bindgen 0.2.128 -- pins `minicov = "=0.3.8"` under its coverage cfg (wasm-bindgen PR 5283), +# and cargo unifies every 0.3.x onto one copy. As a caret range the resolver satisfied that pin the other way +# round: it kept 0.3.9 for us and dropped wasm-bindgen-test to 0.3.45, whose test exports the 0.2.128 runner +# cannot see, so every browser coverage build reported `no tests to run!` and then failed on its missing +# `*.profraw` (`No such file or directory`). Seen on commit e8a38b44eba21f46001ec01cd1ddccdd3eeca4ea at +# https://github.com/edge-toolkit/core/actions/runs/35032214650/job/104593077391. +# +# `vars.rust_nightly` has to carry the LLVM this minicov targets. 0.3.9 moved to profraw format 11 ("Sync with +# LLVM 23.1.0-rc3"), and pairing either release with the other's LLVM makes capture_coverage walk value-profile +# records with the wrong layout, trapping inside every instrumented WASI guest with # `memory fault at wasm address 0xecec7ccd` / `wasm trap: out of bounds memory access` from -# `initializeValueProfRuntimeRecord`. Bump both together, never one alone. -minicov = "0.3" +# `initializeValueProfRuntimeRecord`. Bump both together, never one alone; the `=` lifts once wasm-bindgen-test +# accepts a newer minicov. +minicov = "=0.3.8" # Third-party OTLP mock collector, wrapped by et-test-otlp for the workspace's integration tests. # Serves the spec paths (/v1/traces, ...), so a consumer points `collector_url` at `http://host:port/v1` # for `et-otlp`'s `{collector_url}/traces` shape to land on them. @@ -405,12 +414,6 @@ ignore = [ # Unmaintained upstream at dajoha/color-print. # Pulled by deno_node via color-print. "color-print-proc-macro", - # Archived upstream at rustwasm/console_error_panic_hook. - # Pulled by wasm-bindgen-test, so it arrives with the test harness every browser module and the wasm agent - # already depend on rather than by a choice of ours, and only ever builds for wasm32 test targets. Archived - # as finished rather than abandoned: it installs a panic hook that forwards to `console.error`, which is a - # handful of lines against a web API that has not moved in a decade. - "console_error_panic_hook", # Archived upstream at matklad/countme. "countme", # Unmaintained upstream at denoland/deno_native_certs. diff --git a/config/conftest/policy/cargo/cargo.rego b/config/conftest/policy/cargo/cargo.rego index 90cab5fc..092ce025 100644 --- a/config/conftest/policy/cargo/cargo.rego +++ b/config/conftest/policy/cargo/cargo.rego @@ -232,6 +232,7 @@ deny contains msg if { # crate's own `package.version` -- the rule above enforces exactly that -- which is always a full triple. patch_pin_exception := { "deno_error": "exact pin: JsErrorBox must be the type deno_core holds, so a 0.7.2 would be a second copy", + "minicov": "exact pin: wasm-bindgen-test 0.3.78 requires =0.3.8, and cargo holds one 0.3.x copy for both", "ort": "exact pin on a prerelease, which has no two-part form; rc.11+ moved API wasmtime-wasi-nn calls", "wasmtime": "47.0.3 is the security floor; RUSTSEC-2026-0222 has no fix anywhere below it in the 47 line", "wasmtime-internal-wit-bindgen": "47.0.4 tracks the wasmtime release; the crate is internal and its API can move", From c6c4fce73dd25aa58754963867ccf9523caae1bb Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Wed, 16 Sep 2026 09:51:47 +0800 Subject: [PATCH 08/24] remove password --- .../actions/install-mise-tools/action.yaml | 5 -- Cargo.lock | 1 + libs/edge-toolkit/tests/pipx_site_packages.rs | 52 +++++++++---------- libs/et-otlp/Cargo.toml | 3 ++ libs/et-otlp/tests/resource.rs | 21 +++++++- 5 files changed, 49 insertions(+), 33 deletions(-) diff --git a/.github/actions/install-mise-tools/action.yaml b/.github/actions/install-mise-tools/action.yaml index 5010060b..0c5529b7 100644 --- a/.github/actions/install-mise-tools/action.yaml +++ b/.github/actions/install-mise-tools/action.yaml @@ -40,11 +40,6 @@ runs: using: composite steps: # Reclaim runner disk before anything is installed into it. - # Folded in here rather than written at each call site, where every caller had it immediately before this - # action anyway -- six workflows opening with the same checkout / free-disk / install-mise trio is a lot of - # copy-paste for one fixed order. The action dispatches per OS internally (jlumbroso on Linux, this repo's - # Windows reclaim on Windows, nothing on macOS), so it needs no guard here. A caller that wants the Windows - # reclaim's opt-in removals still calls free-disk-space-windows directly, as docker-windows.yaml does. - name: Free disk space uses: ./.github/actions/free-disk-space diff --git a/Cargo.lock b/Cargo.lock index b029dc82..2d2841f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4395,6 +4395,7 @@ name = "et-otlp" version = "0.1.0" dependencies = [ "edge-toolkit", + "fs-err", "hostname", "log", "opentelemetry", diff --git a/libs/edge-toolkit/tests/pipx_site_packages.rs b/libs/edge-toolkit/tests/pipx_site_packages.rs index 6a6cd41d..1aed739b 100644 --- a/libs/edge-toolkit/tests/pipx_site_packages.rs +++ b/libs/edge-toolkit/tests/pipx_site_packages.rs @@ -6,6 +6,7 @@ #![cfg(test)] +use command_error::CommandExt as _; use edge_toolkit::config::find_site_packages_in; use fs_err as fs; use tempfile::TempDir; @@ -65,42 +66,41 @@ fn site_packages_lookup_is_empty_when_mise_is_missing() { ); } -/// Write a stand-in `mise` into `dir` that answers `--version` and fails every other invocation. +/// Build a stand-in `mise` into `dir` that answers `--version` and fails every other invocation. /// /// The availability probe and the tool-list call are two separate spawns of the same name, so the only way to /// reach the "mise is here but the query failed" path is a command that distinguishes between them. +/// +/// A real executable, compiled here by `rustc`, rather than a script: the probe spawns the bare name `mise`, +/// which Windows resolves to `mise.exe` and nothing else, so a `mise.bat` on the same `PATH` is never found +/// and the probe reports mise absent -- the very branch this fixture exists to get past. That is how the +/// windows-11-arm lane failed on commit aec4133097f57ad406fb16ad5231fafb31641e09 with +/// `the stand-in must answer --version, or this asserts the wrong branch` +/// (). Compiling costs a second +/// and needs `rustc` on `PATH`, which anything running `cargo test` already has; the one shape then serves +/// every platform, with no shell or batch dialect to keep in step. #[expect( clippy::single_call_fn, reason = "distinct fixture builder for the stand-in command; kept separate from the assertions it feeds" )] fn write_fake_mise_that_fails_its_queries(dir: &TempDir) { - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt as _; - - let script = dir.path().join("mise"); - fs::write( - &script, - "#!/bin/sh\nif [ \"$1\" = \"--version\" ]; then\n echo 2026.1.1\n exit 0\nfi\nexit 1\n", - ) - .unwrap(); - // Read the mode back and set the execute bits, rather than building a permissions value from - // scratch, so nothing here has to name the standard filesystem module the repo routes around. - let mut permissions = fs::metadata(&script).unwrap().permissions(); - permissions.set_mode(0o755); - fs::set_permissions(&script, permissions).unwrap(); + const STAND_IN: &str = r#"fn main() { + if std::env::args().nth(1).as_deref() == Some("--version") { + println!("2026.1.1"); + } else { + std::process::exit(1); } - #[cfg(not(unix))] - { - // A batch stand-in, which Windows resolves through the system command processor: that is located - // from the system directory rather than from `PATH`, so it stays reachable even though the probe - // below hands the process a `PATH` containing only this directory. - fs::write( - dir.path().join("mise.bat"), - "@echo off\r\nif \"%1\"==\"--version\" (echo 2026.1.1& exit /b 0)\r\nexit /b 1\r\n", - ) +} +"#; + let source = dir.path().join("mise.rs"); + fs::write(&source, STAND_IN).unwrap(); + let exe = dir.path().join(format!("mise{}", std::env::consts::EXE_SUFFIX)); + let _compiled: std::process::Output = std::process::Command::new("rustc") + .args(["--edition", "2021", "-o"]) + .arg(&exe) + .arg(&source) + .output_checked() .unwrap(); - } } #[test] diff --git a/libs/et-otlp/Cargo.toml b/libs/et-otlp/Cargo.toml index a0a00e9d..2386cb45 100644 --- a/libs/et-otlp/Cargo.toml +++ b/libs/et-otlp/Cargo.toml @@ -23,5 +23,8 @@ tracing-log.workspace = true tracing-opentelemetry.workspace = true tracing-subscriber.workspace = true +[dev-dependencies] +fs-err.workspace = true + [lints] workspace = true diff --git a/libs/et-otlp/tests/resource.rs b/libs/et-otlp/tests/resource.rs index b24b279b..e47f6ccb 100644 --- a/libs/et-otlp/tests/resource.rs +++ b/libs/et-otlp/tests/resource.rs @@ -7,7 +7,9 @@ #![cfg(test)] use edge_toolkit::auth::BasicAuth; +use edge_toolkit::config::get_project_root; use et_otlp::{exporter_headers, service_descriptors}; +use fs_err as fs; use opentelemetry::Value; #[test] @@ -18,9 +20,24 @@ fn headers_carry_basic_auth_only_when_the_config_supplies_it() { "an unauthenticated config must add no headers at all" ); + // The credential comes from the committed dev-only env file the local collector starts from, so the + // header is built from the value that collector actually accepts and a rotation there needs no matching + // edit here. Read the way docker's `--env-file` reads it: everything after the `=` is the value, verbatim. + let env_file = get_project_root().join("config/o2.env"); + let env_text = fs::read_to_string(&env_file).unwrap(); + let env_value = |key: &str| -> String { + env_text + .lines() + .find_map(|line| line.strip_prefix(key)?.strip_prefix('=')) + .unwrap_or_else(|| panic!("no {key} in {}", env_file.display())) + .to_string() + }; + let user = env_value("ZO_ROOT_USER_EMAIL"); + let password = env_value("ZO_ROOT_USER_PASSWORD"); + // With auth: exactly one `authorization` header, and the value is the base64 of `user:password` // rather than either half in the clear. - let auth = BasicAuth::new("root@example.com".to_string(), "Complexpass#123".to_string().into()); + let auth = BasicAuth::new(user, password.clone().into()); let headers = exporter_headers(Some(&auth)); let authorization = &headers["authorization"]; assert!( @@ -28,7 +45,7 @@ fn headers_carry_basic_auth_only_when_the_config_supplies_it() { "expected an HTTP basic credential, got {authorization:?}" ); assert!( - !authorization.contains("Complexpass#123"), + !authorization.contains(&password), "the password must be encoded, not passed through in the clear" ); assert_eq!(headers.len(), 1, "no other headers should be added: {headers:?}"); From d32048c2e87fea4693efed25d1f12482cca28248 Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Wed, 16 Sep 2026 11:15:23 +0800 Subject: [PATCH 09/24] more fixes --- .github/workflows/test.yaml | 13 +++++++-- config/ast-grep/rules/no-std-env-var.yaml | 10 ++++--- libs/edge-toolkit/tests/pipx_site_packages.rs | 28 ++++++++++++++++++- libs/edge-toolkit/tests/registry.rs | 17 +++++++++++ .../ws-runner-common/tests/register_frames.rs | 9 ++++++ 5 files changed, 69 insertions(+), 8 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 35fcedd7..be04319b 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -254,13 +254,14 @@ jobs: # source-build vector and rustfs, neither of which completes. Naming tools individually sidesteps both, # so these lanes exercise the compiler and the self-contained tests where `default` cannot run at all. # macos/x64 gets the larger bound because its cargo_ws_excludes is empty, so it also builds - # et-ws-web-runner (deno -> rusty_v8) that the Windows lanes drop. + # et-ws-web-runner (deno -> rusty_v8) that the Windows lanes drop; it covers the sum of the step ceilings + # below (install 25 + lld 10 + build 45 + tests 20), so the build step's own limit is the one that binds. matrix: include: - os: windows-11-arm timeout: 45 - os: macos-26-intel - timeout: 75 + timeout: 100 # Job-level so it is already set when install-mise runs `mise trust` and reports the value. env: MISE_ENV: rust @@ -328,8 +329,14 @@ jobs: uses: ./.github/actions/remove-mise-shim # A build, not a check: codegen and linking are where a new host triple actually fails. + # The ceiling allows for the macos-26-intel runners' pace, which swings by more than two to one between + # runs of the same tree: the full build took 13 to 24 minutes across the branch's earlier runs, then a + # run that changed no compiled code was cut off at 30 with 798 of 1167 crates done and rustc still + # visibly working -- `The action 'Build workspace' has timed out after 30 minutes.` on commit + # c6c4fce73dd25aa58754963867ccf9523caae1bb at + # https://github.com/edge-toolkit/core/actions/runs/35045764412/job/104635143568. - name: Build workspace - timeout-minutes: 30 + timeout-minutes: 45 run: mise run cargo-build # Only the tests that need no server, module or network -- see the cargo-test-libs task for which and why. diff --git a/config/ast-grep/rules/no-std-env-var.yaml b/config/ast-grep/rules/no-std-env-var.yaml index 92f0e4a5..0e14509d 100644 --- a/config/ast-grep/rules/no-std-env-var.yaml +++ b/config/ast-grep/rules/no-std-env-var.yaml @@ -10,6 +10,8 @@ message: | services/ws-wasi-runner/tests/ sets one to opt the spawned runner into a macOS-only fast-exit path. - `MISE_*` (e.g. `MISE_ENV`) -- the mise tool's own state, not app config; reading these lets helpers like `mise_env_includes` reflect tooling behaviour without round-tripping through a struct. + - `PATH` -- the process's own executable search path, not app config; a test that shadows a tool reads it + only to hand it back with the stand-in's directory in front. rule: any: - pattern: std::env::var($ARG) @@ -19,10 +21,10 @@ rule: constraints: ARG: not: - # Allow a string literal or a const identifier with one of the exempt prefixes. - # That is a string literal ("RUST_LOG" / "ET_TEST_..." / "MISE_...") or a const identifier - # (RUST_LOG / ET_TEST_... / MISE_...). - regex: '^"?(RUST_|ET_TEST_|MISE_)' + # Allow a string literal or a const identifier with one of the exempt prefixes, or the exact name PATH. + # That is a string literal ("RUST_LOG" / "ET_TEST_..." / "MISE_..." / "PATH") or a const identifier + # (RUST_LOG / ET_TEST_... / MISE_... / PATH). + regex: '^"?(RUST_|ET_TEST_|MISE_|PATH"?$)' ignores: # The project-root helper reads CARGO_MANIFEST_DIR to locate the repo root from a build script. # That is a cargo build variable, not app config. diff --git a/libs/edge-toolkit/tests/pipx_site_packages.rs b/libs/edge-toolkit/tests/pipx_site_packages.rs index 1aed739b..4e320d5a 100644 --- a/libs/edge-toolkit/tests/pipx_site_packages.rs +++ b/libs/edge-toolkit/tests/pipx_site_packages.rs @@ -101,6 +101,32 @@ fn write_fake_mise_that_fails_its_queries(dir: &TempDir) { .arg(&source) .output_checked() .unwrap(); + // Run it once by path before anything depends on it, so a stand-in that compiled but cannot start fails + // here with its own error instead of surfacing later as "mise is absent". + let _probed: std::process::Output = std::process::Command::new(&exe) + .arg("--version") + .output_checked() + .unwrap(); +} + +/// `PATH` with `dir` in front of the current value, so a stand-in there shadows the real tool. +/// +/// In front of rather than instead of: the probe runs under this `PATH`, and an executable the toolchain has +/// just built may still resolve part of its runtime through it. With `PATH` cut down to the stand-in's own +/// directory, the x64 Windows lanes (whose Rust host is `x86_64-pc-windows-gnullvm`) compiled the stand-in +/// and then reported mise absent -- `the stand-in must answer --version, or this asserts the wrong branch` +/// on commit c6c4fce73dd25aa58754963867ccf9523caae1bb at +/// -- while the arm64 lane, +/// hosted on `aarch64-pc-windows-msvc`, found it. Search order still puts `dir` first, which is all the +/// shadowing needs. +#[expect( + clippy::single_call_fn, + reason = "distinct fixture step beside the stand-in builder; kept separate so the PATH shape is explained once" +)] +fn path_with_first(dir: &std::path::Path) -> String { + let rest = std::env::var_os("PATH").unwrap_or_default(); + let joined = std::env::join_paths(std::iter::once(dir.to_path_buf()).chain(std::env::split_paths(&rest))).unwrap(); + joined.to_string_lossy().into_owned() } #[test] @@ -112,7 +138,7 @@ fn a_failing_tool_list_query_yields_no_paths() { let dir = TempDir::new().unwrap(); write_fake_mise_that_fails_its_queries(&dir); - let (available, found) = availability_and_lookup(&dir.path().display().to_string()); + let (available, found) = availability_and_lookup(&path_with_first(dir.path())); // Asserted first, and separately: an empty list is also what a *missing* mise produces, so without // pinning the stand-in as present this test would still pass if it were never found at all -- exercising diff --git a/libs/edge-toolkit/tests/registry.rs b/libs/edge-toolkit/tests/registry.rs index c266dec6..27895981 100644 --- a/libs/edge-toolkit/tests/registry.rs +++ b/libs/edge-toolkit/tests/registry.rs @@ -69,6 +69,23 @@ fn unknown_ids_fall_through_to_the_assign_and_no_op_paths() { ); assert_eq!(summaries[0].agent_id, "agent-fresh"); assert_eq!(summaries[0].state, AgentConnectionState::Connected); + + // The known-id arm, asserted in this same `S = String` instantiation rather than left to the server tests + // that hit it for real. The branch gate scores a generic function by the instantiation that covers the most + // of it (llvm-cov merges an instantiation group's branch counts by taking the maximum, not the union), so + // the no-op arm above covered here and the update arm covered only under the server's session type read as + // one arm each, never both: `libs/edge-toolkit/src/ws_server.rs 9/10 branches` on commit + // c6c4fce73dd25aa58754963867ccf9523caae1bb at + // https://github.com/edge-toolkit/core/actions/runs/35045764477/job/104635181514, while the lcov export, + // which sums instantiations, showed every branch taken. + registry.mark_disconnected("agent-fresh"); + let summaries = registry.list_agents(); + assert_eq!(summaries[0].state, AgentConnectionState::Disconnected); + assert_eq!( + registry.agent_session("agent-fresh"), + None, + "disconnecting must drop the session handle" + ); } #[test] diff --git a/libs/ws-runner-common/tests/register_frames.rs b/libs/ws-runner-common/tests/register_frames.rs index c4f1d2fb..770488cc 100644 --- a/libs/ws-runner-common/tests/register_frames.rs +++ b/libs/ws-runner-common/tests/register_frames.rs @@ -79,6 +79,15 @@ async fn a_socket_closed_before_the_ack_reports_connection_closed() { }; let _connect = socket.next().await; let _closed = socket.close(None).await; + // Drain until the client's close reply has been read and the stream ends, so the socket is + // dropped only after the close handshake completes. Dropping it straight after sending the close + // frame leaves that reply unread in the receive buffer, which makes the kernel tear the connection + // down with a reset instead of a FIN; Linux still hands the client its end-of-stream, but Windows + // surfaces the reset first, so the client saw + // `WebSocket(Io(Os { code: 10053, kind: ConnectionAborted, ... }))` instead of `ConnectionClosed` + // on the windows-11-arm lane at commit c6c4fce73dd25aa58754963867ccf9523caae1bb + // (). + while let Some(Ok(_frame)) = socket.next().await {} } }); From 560b2cf919046f6b8a2e33b2652cc7eae68017b5 Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Wed, 16 Sep 2026 12:42:12 +0800 Subject: [PATCH 10/24] more coverage --- .github/workflows/coverage.yaml | 4 +- .mise/config.coverage.toml | 83 ++++++++++----- .mise/cov-branch-union.jq | 22 ++++ libs/web/src/lib.rs | 26 ++++- services/ws-wasm-agent/tests/web.rs | 156 +++++++++++++++++++++++++++- 5 files changed, 257 insertions(+), 34 deletions(-) create mode 100644 .mise/cov-branch-union.jq diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml index 6a666839..6fbb1ab2 100644 --- a/.github/workflows/coverage.yaml +++ b/.github/workflows/coverage.yaml @@ -162,7 +162,9 @@ jobs: timeout-minutes: 20 run: mise run pic-viewer-cov - # After all three merges, because it reads what they leave in target/wasi-cov. + # After all three merges, because it reads what they leave behind. + # That is the guests' profiles and objects under target/wasi-cov, and the two browser runs' under + # target/agent-cov and target/pic-viewer-cov. # The task is Linux-gated internally as well as here: this `if` keeps the step off the macOS lane at all, # and the gate inside it keeps a direct `mise run` on a workstation from passing on an empty report. - name: Check wasm libs/ branch coverage diff --git a/.mise/config.coverage.toml b/.mise/config.coverage.toml index 6fe4977a..27dfc887 100644 --- a/.mise/config.coverage.toml +++ b/.mise/config.coverage.toml @@ -35,9 +35,10 @@ # The libs/ crates each branch-coverage check is answerable for, as directory names under libs/. # The split is which pipeline carries the crate's coverage, not a judgement about how well tested it is. # The native set is everything cargo-llvm-cov's own run instruments and reports; the wasm set is the two -# crates that only ever compile to wasm, whose covmaps reach lcov.info through wasm-cov's .ll-gutting path -# and so can only be asserted where that path runs. A crate belongs to exactly one list: naming it in both -# would let a gap in one pipeline be answered by the other. +# crates that only ever compile to wasm, whose covmaps come out of the .ll-gutting path the three wasm-side +# tasks share (wasm-cov for the guests, wasm-agent-cov and pic-viewer-cov for the browser tests) and so can +# only be asserted where that path runs. A crate belongs to exactly one list: naming it in both would let a +# gap in one pipeline be answered by the other. native_cov_libs = "edge-toolkit et-otlp path test-helpers test-otlp ws-runner-common" wasm_cov_libs = "wasi-guest web" @@ -299,11 +300,18 @@ shell = "{{ vars.task_shell }}" # header). Windows never runs coverage at all. So on any other OS there is no data to judge and the check says # so and stops, rather than passing on an empty report. # -# One export over every module at once, not one per module. -# libs/web and libs/wasi-guest are linked into several guests, so each module's covmap holds its own partial -# view of the same source file. Exported separately those views would be judged separately and a branch -# covered by one guest would still read as missed in another; merging the profraw first makes the export the -# union, which is what "did anything exercise this branch" means. Same shape wasm-agent-cov already uses. +# One export per coverage source, folded into a union, rather than one export over everything. +# libs/web and libs/wasi-guest are linked into several guests and into the browser test binaries, so every +# source holds its own partial view of the same file, and "did anything exercise this branch" is the union of +# those views. The sources are what the three merge tasks leave behind: wasm-cov's per-guest profdata and gutted +# object under target/wasi-cov, and the agent's and pic-viewer's profdata plus every object their test binary +# linked under target/agent-cov and target/pic-viewer-cov. Each is exported on its own, with the branch regions a +# full `--format=text` export carries, and cov-branch-union.jq folds them into the summary shape +# _cov-branch-assert reads. A single export over every guest object against one merged profile was the first +# shape, and it reported libs/web as absent even though the same objects export it one at a time: +# `libs/web: no records in the report -- this run never exercised the crate` on commit +# d32048c2e87fea4693efed25d1f12482cca28248 at +# https://github.com/edge-toolkit/core/actions/runs/35051118134/job/104651501456. [tasks.wasm-cov-branch-check] description = "Linux-only: fail unless every wasm libs/ crate is at 100% branch coverage" run = """ @@ -312,30 +320,49 @@ if [ "$os" != "Linux" ]; then echo "wasm-cov-branch-check: the wasm coverage pipeline only runs on Linux; nothing to check on $os" exit 0 fi -covdir=target/wasi-cov -if [ -z "$(find "$covdir" -maxdepth 1 -name '*.profraw' -print -quit 2>/dev/null)" ]; then - echo "wasm-cov-branch-check: no $covdir/*.profraw -- run the instrumented wasm build and wasm-cov first" >&2 - exit 1 -fi -found="$(find "$covdir" -maxdepth 1 -name '*.o' -print 2>/dev/null | coreutils sort)" -if [ -z "$found" ]; then - echo "wasm-cov-branch-check: no $covdir/*.o -- wasm-cov has not gutted the guest .ll into objects yet" >&2 +host="$(rustc -vV | goawk '/^host:/ { print $2 }')" +bin="$(rustc +{{ vars.rust_nightly }} --print sysroot)/lib/rustlib/$host/bin" +exports=target/cov/wasm-branch +coreutils rm -rf "$exports" +coreutils mkdir -p "$exports" +export_branches() { + out=$1 + profdata=$2 + shift 2 + "$bin/llvm-cov" export --format=text --instr-profile "$profdata" "$@" >"$exports/$out.json" +} + +guests=target/wasi-cov +if [ -z "$(find "$guests" -maxdepth 1 -name '*.o' -print -quit 2>/dev/null)" ]; then + echo "wasm-cov-branch-check: no $guests/*.o -- run the instrumented wasm build and wasm-cov first" >&2 exit 1 fi -objs=() -while IFS= read -r obj; do - [ -n "$obj" ] || continue - objs+=("-object" "$obj") -done <&2 + exit 1 + fi + objs=() + while IFS= read -r obj; do + [ -n "$obj" ] || continue + objs+=("-object" "$obj") + done </dev/null | coreutils sort) OBJS -host="$(rustc -vV | goawk '/^host:/ { print $2 }')" -bin="$(rustc +{{ vars.rust_nightly }} --print sysroot)/lib/rustlib/$host/bin" -coreutils mkdir -p target/cov + export_branches "${run#* }" "$profdata" "${objs[@]}" +done + report=target/cov/wasm-libs.json -prof="$covdir/all.profdata" -"$bin/llvm-profdata" merge -sparse -o "$prof" "$covdir"/*.profraw -"$bin/llvm-cov" export --format=text --summary-only --instr-profile "$prof" "${objs[@]}" >"$report" +coreutils cat "$exports"/*.json | jaq -n -f .mise/cov-branch-union.jq >"$report" +echo "wasm-cov-branch-check: libs/ files in the union:" +jaq -r '.data[0].files[].filename | select(contains("/libs/"))' "$report" | coreutils sort mise run _cov-branch-assert "$report" "{{ vars.wasm_cov_libs }}" """ shell = "{{ vars.task_shell }}" diff --git a/.mise/cov-branch-union.jq b/.mise/cov-branch-union.jq new file mode 100644 index 00000000..94cb575f --- /dev/null +++ b/.mise/cov-branch-union.jq @@ -0,0 +1,22 @@ +# Folds several llvm-cov JSON exports into the one summary cov-branch-assert.jq reads. +# The exports (`--format=text`, with branches) arrive as `inputs`, one per coverage source: each WASI guest, the +# browser agent, pic-viewer. A source file that is linked into several of them -- libs/web, libs/wasi-guest -- +# shows up in each with only the outcomes that source happened to exercise, so the outcomes are unioned by +# region across every export: an arm is covered if anything, anywhere, took it, which is what "did anything +# exercise this branch" means. A file without branches still gets a 0/0 entry, so a crate that was compiled but +# has nothing to branch on reads as present rather than as never exercised. +# +# A branch entry is llvm-cov's [line_start, col_start, line_end, col_end, count, false_count, file_id, +# expansion_id, kind]; the first four name the region and the next two are its two outcomes. +[ inputs | .data[0].files[] | { filename, branches: (.branches // []) } ] +| group_by(.filename) +| map( + ([ .[].branches[] | { region: (.[0:4] | map(tostring) | join(":")), taken: (.[4] > 0), skipped: (.[5] > 0) } ] + | group_by(.region) + | map({ taken: (map(.taken) | any), skipped: (map(.skipped) | any) })) as $regions + | { filename: .[0].filename, + summary: { branches: { + count: ($regions | length * 2), + covered: ($regions | map((if .taken then 1 else 0 end) + (if .skipped then 1 else 0 end)) | add // 0) + } } }) +| { data: [ { files: . } ] } diff --git a/libs/web/src/lib.rs b/libs/web/src/lib.rs index 49eddb54..e6f88dd4 100644 --- a/libs/web/src/lib.rs +++ b/libs/web/src/lib.rs @@ -77,6 +77,19 @@ pub async fn request_sensor_permission(target: JsValue) -> Result Result<(), JsValue> { let window = web_sys::window().ok_or_else(|| JsValue::from_str("No window available"))?; + sleep_ms_on(&window, duration_ms).await +} + +/// Resolve after `duration_ms` milliseconds via `window`'s `setTimeout`, rejecting if that call throws. +/// +/// Takes the window rather than reading it, for the reason [`websocket_url_from_location`] takes the location: +/// the browser's own `setTimeout` never throws for a callback and a delay, so the rejection this returns when +/// it does can only be asserted against a window handed in. +#[expect( + clippy::future_not_send, + reason = "wasm_bindgen_futures::JsFuture is Rc-backed and never Send; runs in single-threaded browser WASM" +)] +pub async fn sleep_ms_on(window: &web_sys::Window, duration_ms: i32) -> Result<(), JsValue> { let promise = js_sys::Promise::new(&mut |resolve, reject| { let callback = Closure::once_into_js(move || { ignore(resolve.call0(&JsValue::NULL)); @@ -95,10 +108,19 @@ pub async fn sleep_ms(duration_ms: i32) -> Result<(), JsValue> { pub fn websocket_url() -> Result { let window = web_sys::window().ok_or_else(|| JsValue::from_str("No window available"))?; let location = js_sys::Reflect::get(window.as_ref(), &JsValue::from_str("location"))?; - let protocol = js_sys::Reflect::get(&location, &JsValue::from_str("protocol"))? + websocket_url_from_location(&location) +} + +/// The `/ws` endpoint URL for a page at `location`, upgrading to `wss:` when its protocol is `https:`. +/// +/// Takes the location rather than reading `window.location`: a test page is only ever served over plain +/// http, so the `wss:` upgrade can be asserted only against a location handed in. `location` needs just the +/// `protocol` and `host` properties, which is what makes a plain object stand in for the real one. +pub fn websocket_url_from_location(location: &JsValue) -> Result { + let protocol = js_sys::Reflect::get(location, &JsValue::from_str("protocol"))? .as_string() .ok_or_else(|| JsValue::from_str("window.location.protocol is unavailable"))?; - let host = js_sys::Reflect::get(&location, &JsValue::from_str("host"))? + let host = js_sys::Reflect::get(location, &JsValue::from_str("host"))? .as_string() .ok_or_else(|| JsValue::from_str("window.location.host is unavailable"))?; let ws_protocol = if protocol == "https:" { "wss:" } else { "ws:" }; diff --git a/services/ws-wasm-agent/tests/web.rs b/services/ws-wasm-agent/tests/web.rs index 65f11757..6f30a3ff 100644 --- a/services/ws-wasm-agent/tests/web.rs +++ b/services/ws-wasm-agent/tests/web.rs @@ -2,10 +2,13 @@ #![cfg(target_arch = "wasm32")] #![cfg_attr(wasm_bindgen_unstable_test_coverage, feature(coverage_attribute))] -use et_web::{describe_js_error, sleep_ms, websocket_url}; +use et_web::{ + SENSOR_PERMISSION_GRANTED, describe_js_error, get_media_devices, request_sensor_permission, sleep_ms, sleep_ms_on, + websocket_url, websocket_url_from_location, +}; use et_ws_wasm_agent::{WsClient, WsClientConfig, wait_for_connected}; -use js_sys::{Object, Reflect}; -use wasm_bindgen::JsValue; +use js_sys::{Function, Object, Reflect}; +use wasm_bindgen::{JsCast as _, JsValue}; use wasm_bindgen_test::*; wasm_bindgen_test_configure!(run_in_browser); @@ -104,3 +107,150 @@ fn describe_js_error_falls_back_to_debug_when_json_throws() { "the Debug fallback must still describe the error" ); } + +/// A fresh object carrying one property, standing in for a browser object the helper under test reads. +/// +/// The helpers reach their properties through `Reflect::get`, so a plain object with the right key is +/// indistinguishable from the real navigator, window or location -- which is what lets each refusal arm be +/// driven on demand instead of waiting for a browser that happens to lack the feature. +fn object_with(key: &str, value: &JsValue) -> Object { + let object = Object::new(); + let _set = Reflect::set(object.as_ref(), &JsValue::from_str(key), value) + .expect("setting a property on a fresh object cannot fail"); + object +} + +/// A `location` stand-in with just the two properties `websocket_url_from_location` reads. +fn location_with(protocol: &str, host: &str) -> JsValue { + let location = object_with("protocol", &JsValue::from_str(protocol)); + let _set = Reflect::set(location.as_ref(), &JsValue::from_str("host"), &JsValue::from_str(host)) + .expect("setting a property on a fresh object cannot fail"); + location.into() +} + +/// The endpoint follows the page's scheme: `wss:` behind https, plain `ws:` otherwise. +#[wasm_bindgen_test] +fn websocket_url_upgrades_to_wss_only_on_an_https_page() { + let secure = websocket_url_from_location(&location_with("https:", "edge.example:8443")) + .expect("a location with protocol and host yields a URL"); + assert_eq!(secure, "wss://edge.example:8443/ws"); + + let plain = websocket_url_from_location(&location_with("http:", "localhost:8080")) + .expect("a location with protocol and host yields a URL"); + assert_eq!(plain, "ws://localhost:8080/ws"); +} + +/// A location without a string `protocol` is refused rather than defaulted, so a broken page fails loudly. +#[wasm_bindgen_test] +fn websocket_url_refuses_a_location_without_a_protocol() { + let err = websocket_url_from_location(&Object::new().into()).unwrap_err(); + + assert_eq!( + err.as_string().as_deref(), + Some("window.location.protocol is unavailable") + ); +} + +/// Each of the three shapes `get_media_devices` refuses: the property missing, null, or not a `MediaDevices`. +/// +/// The first two are the insecure-context case (a page served over plain http from a non-local host), where +/// the browser leaves `navigator.mediaDevices` undefined; the third is a navigator whose property exists but +/// is not the real API object, which the cast catches. +#[wasm_bindgen_test] +fn get_media_devices_refuses_a_navigator_without_a_usable_media_devices() { + let unavailable = "navigator.mediaDevices is unavailable. Use https://... or http://localhost and allow access."; + let cases = [ + (Object::new(), unavailable), + (object_with("mediaDevices", &JsValue::NULL), unavailable), + ( + object_with("mediaDevices", Object::new().as_ref()), + "navigator.mediaDevices is not accessible in this browser", + ), + ]; + for (navigator, expected) in cases { + let navigator: web_sys::Navigator = navigator.unchecked_into(); + let err = get_media_devices(&navigator).unwrap_err(); + assert_eq!(err.as_string().as_deref(), Some(expected)); + } +} + +/// On a secure page the real navigator's `MediaDevices` is handed back as-is. +/// +/// The test page is served from a loopback origin, which the browser treats as secure, so the real navigator +/// carries the API object. +#[wasm_bindgen_test] +fn get_media_devices_returns_the_real_api_on_a_secure_page() { + let navigator = web_sys::window().expect("the test page has a window").navigator(); + + let devices = get_media_devices(&navigator).expect("a loopback page is a secure context"); + assert!(devices.is_instance_of::()); +} + +/// Every target that cannot be asked is treated as already granted. +/// +/// That is no target at all, or one without a callable `requestPermission` -- which is every browser except +/// iOS Safari, where the method exists. +#[wasm_bindgen_test] +async fn request_sensor_permission_is_granted_wherever_nothing_can_be_asked() { + let targets = [ + JsValue::NULL, + JsValue::UNDEFINED, + Object::new().into(), + object_with("requestPermission", &JsValue::NULL).into(), + ]; + for target in targets { + let outcome = request_sensor_permission(target) + .await + .expect("an unaskable target must not be an error"); + assert_eq!(outcome, SENSOR_PERMISSION_GRANTED); + } +} + +/// With a callable `requestPermission`, its promise decides the answer. +/// +/// A string answer is returned as-is, anything else reads as granted, and a property that is not callable is +/// an error rather than a silent grant. +#[wasm_bindgen_test] +async fn request_sensor_permission_asks_a_target_that_can_answer() { + let denies = object_with( + "requestPermission", + Function::new_no_args("return Promise.resolve('denied');").as_ref(), + ); + let outcome = request_sensor_permission(denies.into()) + .await + .expect("a resolving requestPermission is not an error"); + assert_eq!(outcome, "denied"); + + let answers_nonsense = object_with( + "requestPermission", + Function::new_no_args("return Promise.resolve(42);").as_ref(), + ); + let outcome = request_sensor_permission(answers_nonsense.into()) + .await + .expect("a non-string answer falls back to granted rather than failing"); + assert_eq!(outcome, SENSOR_PERMISSION_GRANTED); + + let not_callable = object_with("requestPermission", &JsValue::from_f64(1.0)); + let err = request_sensor_permission(not_callable.into()).await.unwrap_err(); + assert_eq!(err.as_string().as_deref(), Some("requestPermission is not callable")); +} + +/// A `setTimeout` that throws turns into a rejection of the sleep, carrying the thrown error. +/// +/// The browser's own `setTimeout` never throws for a callback and a delay, so a window stand-in whose +/// `setTimeout` does is the only way to see the rejection arm run. +#[wasm_bindgen_test] +async fn sleep_ms_on_rejects_when_set_timeout_throws() { + let refusing = object_with( + "setTimeout", + Function::new_no_args("throw new Error('no timers here');").as_ref(), + ); + let window: web_sys::Window = refusing.unchecked_into(); + + let err = sleep_ms_on(&window, 1).await.unwrap_err(); + assert!( + err.is_instance_of::(), + "the rejection must carry the thrown error, got {}", + describe_js_error(&err) + ); +} From aacb89e9bc870acfa9bf8bbbca0c771fafc03ccd Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Wed, 16 Sep 2026 17:15:50 +0800 Subject: [PATCH 11/24] tidy --- .deepsource.toml | 3 +- .../actions/install-mise-tools/action.yaml | 7 +- .github/actions/install-mise/action.yaml | 2 +- .github/actions/remove-mise-shim/action.yaml | 3 +- .github/workflows/check.yaml | 3 +- .github/workflows/docker-linux.yaml | 3 +- .github/workflows/test.yaml | 5 +- .mise/config.coverage.toml | 13 +- .mise/config.dotnet.toml | 8 +- .mise/config.java.toml | 3 +- .mise/config.js.toml | 6 +- .mise/config.kotlin.toml | 6 +- .mise/config.macos.toml | 6 +- .mise/config.python.toml | 2 +- .mise/config.toml | 42 +++--- .mise/config.windows.toml | 5 +- .mise/config.zig.toml | 6 +- .mise/task-shell.sh | 2 +- CLAUDE.md | 49 ++++-- Cargo.lock | 12 ++ Cargo.toml | 4 +- Dockerfile.nanoserver | 6 +- .../rules/gha-checkout-fetch-depth-1.yaml | 9 +- config/ast-grep/rules/no-std-env-var.yaml | 15 +- config/ast-grep/rules/no-string-new.yaml | 2 +- config/clang-tidy.yaml | 2 +- .../policy_pairing.rego} | 6 +- .../policy_pairing_test.rego} | 24 +-- config/lychee.toml | 5 +- config/oxlintrc.jsonc | 4 +- .../deepsource-codes-are-qualified.yaml | 18 +++ config/semgrep/no-check-tool-mentions.yaml | 18 ++- config/semgrep/no-type-comments.yaml | 2 +- libs/edge-toolkit/tests/pipx_site_packages.rs | 41 ++---- libs/edge-toolkit/tests/registry.rs | 2 +- libs/path/tests/find.rs | 2 +- libs/test-helpers/Cargo.toml | 2 + libs/test-helpers/src/lib.rs | 25 ++++ libs/test-helpers/tests/path_prefix.rs | 78 ++++++++++ libs/web/src/lib.rs | 4 +- .../ws-runner-common/tests/register_frames.rs | 3 +- ruff.toml | 4 +- services/storage/tests/s3_backend.rs | 4 +- services/ws-server/Dockerfile | 4 +- .../ws-web-runner/mingw-shim/msvc_crt_shim.c | 3 +- utilities/cli/src/deployment_types/k3s.rs | 4 +- utilities/cli/tests/scenario_runners.rs | 7 +- utilities/repo-check/Cargo.toml | 23 +++ utilities/repo-check/README.md | 22 +++ utilities/repo-check/src/commit_hashes.rs | 139 ++++++++++++++++++ utilities/repo-check/src/error.rs | 21 +++ utilities/repo-check/src/main.rs | 42 ++++++ 52 files changed, 576 insertions(+), 155 deletions(-) rename config/conftest/policy/{policy_tests/policy_tests.rego => policy_pairing/policy_pairing.rego} (94%) rename config/conftest/policy/{policy_tests/policy_tests_test.rego => policy_pairing/policy_pairing_test.rego} (79%) create mode 100644 config/semgrep/deepsource-codes-are-qualified.yaml create mode 100644 libs/test-helpers/tests/path_prefix.rs create mode 100644 utilities/repo-check/Cargo.toml create mode 100644 utilities/repo-check/README.md create mode 100644 utilities/repo-check/src/commit_hashes.rs create mode 100644 utilities/repo-check/src/error.rs create mode 100644 utilities/repo-check/src/main.rs diff --git a/.deepsource.toml b/.deepsource.toml index 734c6276..1844ba3a 100644 --- a/.deepsource.toml +++ b/.deepsource.toml @@ -35,7 +35,8 @@ name = "python" # `[[analyzers]]` declared above it -- a shape a lenient parser could flatten so that only the final one # survives, which would leave javascript with no module_system and explain the errors exactly. Rewriting all # three as inline `meta` keys, which cannot be reattached or overwritten, changed nothing: run -# 4e3986bd-db2f-440a-84fc-9163ef240292 on commit ab14bf545bf62125dfa4f314952f5cd2b5a6be61 still failed. +# 4e3986bd-db2f-440a-84fc-9163ef240292 on commit +# https://github.com/edge-toolkit/core/commit/ab14bf545bf62125dfa4f314952f5cd2b5a6be61 still failed. # The inline form is kept only because it is unambiguous; it is not the fix. # # Also ruled out: the files themselves. The repo's own JS linter parses every one of them as an ES module diff --git a/.github/actions/install-mise-tools/action.yaml b/.github/actions/install-mise-tools/action.yaml index 0c5529b7..d113d331 100644 --- a/.github/actions/install-mise-tools/action.yaml +++ b/.github/actions/install-mise-tools/action.yaml @@ -64,7 +64,8 @@ runs: # verification error ...: API error: GitHub API returned 403 Forbidden # "message": "API rate limit exceeded for installation. ..." # - # observed on commit 5c08a759202d166ac9fac48f5e54ec998d5fcb6e at + # observed on commit + # https://github.com/edge-toolkit/core/commit/5c08a759202d166ac9fac48f5e54ec998d5fcb6e at # https://github.com/edge-toolkit/core/actions/runs/31659062139/job/94319942583. The store is one OCI # image per platform, published exclusively by a maintainer running `mise run push-mise-tools` on a # machine of that platform -- CI only ever reads (no workflow holds packages:write), so a compromised @@ -76,7 +77,7 @@ runs: # Gated off by the `restore-store` input's default for now -- a store published by an older mise lays down # tool layouts the pinned mise counts as installed but cannot resolve, so `mise install` finds no gap to fill # and the tool never reaches PATH. On macOS that surfaced as `bash: wasm-pack: command not found` on commit - # 0e7273a1154db315a0f584077db4565ba66be603 at + # https://github.com/edge-toolkit/core/commit/0e7273a1154db315a0f584077db4565ba66be603 at # https://github.com/edge-toolkit/core/actions/runs/33727571518/job/100559962060, reproduced since against a # 2026.9.0 binary on an install tree 2026.8.5 resolves fine. Cold-installing everything reinstates the # rate-limit exposure described above, so this is a stopgap until the stores are republished. @@ -115,7 +116,7 @@ runs: # A failed first install has usually already written the shim, so clear it before the retry runs. # The trailing removal at the end of this action is too late to help here, and is skipped entirely once # the retry itself fails -- so without this the retry aborts in seconds. Observed on commit - # d801ec192814cf8de880c5935d83232222907975 at + # https://github.com/edge-toolkit/core/commit/d801ec192814cf8de880c5935d83232222907975 at # https://github.com/edge-toolkit/core/actions/runs/31652059090/job/94298363388, where the first install # died on a frippery.org (http:busybox) connect timeout. - name: Remove mise shim before the retry diff --git a/.github/actions/install-mise/action.yaml b/.github/actions/install-mise/action.yaml index 5b5704ee..136c8e81 100644 --- a/.github/actions/install-mise/action.yaml +++ b/.github/actions/install-mise/action.yaml @@ -46,7 +46,7 @@ runs: # ignores the variable outright past that, so every tool stops resolving at the same moment -- the mingw # lane reached 11215 characters and reported `dart: not found`, `uv: not found`, `cargo: not found`, # `rclone: not found` and `zig: not found` in one go, on commit - # eb5ade87e8bbf7c31b6e0e2c39fbaf0f4e2e01c2 at + # https://github.com/edge-toolkit/core/commit/eb5ade8f5b762191f805135f30cf58dce7d4ef83 at # https://github.com/edge-toolkit/core/actions/runs/33746317422/job/100619492734. Trimming the prefix to # `C:\i` reclaims ~43 characters per entry, which scales with the toolset -- so the lanes carrying the most # tools, the ones that overran first, save the most. config.windows.toml's mise_installs reads this same diff --git a/.github/actions/remove-mise-shim/action.yaml b/.github/actions/remove-mise-shim/action.yaml index aa1509cf..d36f4623 100644 --- a/.github/actions/remove-mise-shim/action.yaml +++ b/.github/actions/remove-mise-shim/action.yaml @@ -16,7 +16,8 @@ runs: # mise: recursive shim invocation detected for mise: /c/Users/runneradmin/AppData/Local/mise/shims/mise # # Removing just those two entries lets the PATH search fall through to the real binary while every other - # tool keeps the shim it needs. Observed on commit e8e44afc899591ac5cd9b5313e99b2f76a5a23cc at + # tool keeps the shim it needs. Observed on commit + # https://github.com/edge-toolkit/core/commit/e8e44afc899591ac5cd9b5313e99b2f76a5a23cc at # https://github.com/edge-toolkit/core/actions/runs/34430184394/job/102723879106, where test.yaml's # `minimal` job installed its toolchain and then could not run a single `mise run` afterwards. - name: Remove self-recursive mise shim on Windows diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index 3908622a..7f6c07e1 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -51,10 +51,11 @@ jobs: 'C:\i\http-busybox\1.37.0\ash.exe -euo pipefail {0}' || 'bash --noprofile --norc -euo pipefail {0}' }} steps: + # Full history here due to checks that use git log. - name: Checkout uses: actions/checkout@v7 with: - fetch-depth: 1 + fetch-depth: 0 persist-credentials: false - name: Install mise + tools diff --git a/.github/workflows/docker-linux.yaml b/.github/workflows/docker-linux.yaml index 9e299ee4..c7c1c4d0 100644 --- a/.github/workflows/docker-linux.yaml +++ b/.github/workflows/docker-linux.yaml @@ -151,7 +151,8 @@ jobs: # docker: Error response from daemon: pull access denied for et-test, repository does not exist # or may require 'docker login': denied: requested access to the resource is denied # - # on top of the real build error (observed on commit 5c08a759202d166ac9fac48f5e54ec998d5fcb6e at + # on top of the real build error (observed on commit + # https://github.com/edge-toolkit/core/commit/5c08a759202d166ac9fac48f5e54ec998d5fcb6e at # https://github.com/edge-toolkit/core/actions/runs/31659062139/job/94319942583), and handing # execution to whoever squats that name on Docker Hub if the pull ever resolved. `--pull=never` # keeps the registry unreachable even if the gate is edited away. diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index be04319b..edec0c4f 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -229,7 +229,8 @@ jobs: # # mise ERROR Failed to install tools: conda:m2-gnupg@2.2.41.1, core:java@26.0.1, core:rust@... # - # naming 18 tools, observed on commit 4488443a7f56398a3a27c4bca9713ba6900a755c at + # naming 18 tools, observed on commit + # https://github.com/edge-toolkit/core/commit/4488443a7f56398a3a27c4bca9713ba6900a755c at # https://github.com/edge-toolkit/core/actions/runs/34409246713/job/102659546843. The causes are upstream, # not ours: torch and cryptography publish no `win_arm64` wheel, msys2's conda channel is x86_64-only, # Adoptium's newest windows/aarch64 JDK is 23 against the pinned 26, and wasm-bindgen, openobserve, rustfs, @@ -333,7 +334,7 @@ jobs: # runs of the same tree: the full build took 13 to 24 minutes across the branch's earlier runs, then a # run that changed no compiled code was cut off at 30 with 798 of 1167 crates done and rustc still # visibly working -- `The action 'Build workspace' has timed out after 30 minutes.` on commit - # c6c4fce73dd25aa58754963867ccf9523caae1bb at + # https://github.com/edge-toolkit/core/commit/c6c4fce73dd25aa58754963867ccf9523caae1bb at # https://github.com/edge-toolkit/core/actions/runs/35045764412/job/104635143568. - name: Build workspace timeout-minutes: 45 diff --git a/.mise/config.coverage.toml b/.mise/config.coverage.toml index 27dfc887..96d3036f 100644 --- a/.mise/config.coverage.toml +++ b/.mise/config.coverage.toml @@ -55,7 +55,7 @@ RUSTUP_TOOLCHAIN = "{{ vars.rust_nightly }}" # called `Result::unwrap()` on an `Err` value: Output(OutputError { program: ".../target/debug/et-ws-wasi-runner", # status: ExitStatus(unix_wait_status(32512)), stdout_utf8: "", stderr_utf8: "", user_error: None }) # (32512 >> 8 == 127, the loader's "cannot start" code). Seen on commit -# b63888492c53cd403168ea24483cea53bde60899 at +# https://github.com/edge-toolkit/core/commit/b63888492c53cd403168ea24483cea53bde60899 at # https://github.com/edge-toolkit/core/actions/runs/30786563725/job/91601098246, in all four wasi-runner tests. # Pointing the loader straight at the install dir is layout-independent, so it holds however cargo lays the # build dir out. Scoped to this env because only the coverage lane runs binaries under nightly. Drop it together @@ -123,7 +123,8 @@ coreutils mkdir -p "$ort_build_dir/examples" "$ort_build_dir/deps" # nothing back at all. The run completes and the report generates, but every counter in it reads zero: # lines: 0/8677 hit # branches: 0/954 hit -# from that lane's own artifact on commit 52b2a6a67087fab043c1ea8c520373f5ba2477a4 at +# from that lane's own artifact on commit +# https://github.com/edge-toolkit/core/commit/52b2a6a67087fab043c1ea8c520373f5ba2477a4 at # https://github.com/edge-toolkit/core/actions/runs/35032214650/job/104593077352, and reproduced on a # workstation by changing this one variable and nothing else -- the same crates and tests report real # numbers under the merge-pool pattern and zeros under %c. The relocation flag above is what makes %c work @@ -224,7 +225,7 @@ arg "" help="Space-separated libs/ directory names that must be at 100% br # error: failed to merge profile data: not found *.profraw files in # /home/runner/work/core/core/target/llvm-cov-target; this may occur if target directory is accidentally # cleared, or running report subcommand without running any tests or binaries -# on commit 52b2a6a67087fab043c1ea8c520373f5ba2477a4 at +# on commit https://github.com/edge-toolkit/core/commit/52b2a6a67087fab043c1ea8c520373f5ba2477a4 at # https://github.com/edge-toolkit/core/actions/runs/35005110677/job/104502812372. The raw profiles are still # on disk at that point -- the collecting task's own `report` leaves them alone -- so the only thing this # process was missing is the environment that says where to look. @@ -272,7 +273,7 @@ for profraw in "$covdir"/*.profraw; do # profraw paired with the wrong module's covmap; (2) with 2+ matches, head exits after the first line and the # uutils find dies on the broken pipe (Rust ignores SIGPIPE) -- a panic on the Linux runners, exiting 101 with # its message discarded by the 2>/dev/null, so `pipefail` killed this task with no output at all. Observed on - # commit db374f051f72f7c285fe8267f5acc80a52cfcfd9 at + # commit https://github.com/edge-toolkit/core/commit/db374f051f72f7c285fe8267f5acc80a52cfcfd9 at # https://github.com/edge-toolkit/core/actions/runs/31994728610/job/95284251850; reproduced locally on macOS # as `find: stdout: Undefined error: 0` (EPIPE, exit 1) once the second match exists. ll="$(find target -path '*/release/build/*' \\( -name "$name.ll" -o -name "$name-*.ll" \\) -print -quit 2>/dev/null)" @@ -310,7 +311,7 @@ shell = "{{ vars.task_shell }}" # _cov-branch-assert reads. A single export over every guest object against one merged profile was the first # shape, and it reported libs/web as absent even though the same objects export it one at a time: # `libs/web: no records in the report -- this run never exercised the crate` on commit -# d32048c2e87fea4693efed25d1f12482cca28248 at +# https://github.com/edge-toolkit/core/commit/d32048c2e87fea4693efed25d1f12482cca28248 at # https://github.com/edge-toolkit/core/actions/runs/35051118134/job/104651501456. [tasks.wasm-cov-branch-check] description = "Linux-only: fail unless every wasm libs/ crate is at 100% branch coverage" @@ -456,7 +457,7 @@ driver="${CHROMEDRIVER:-$(mise which chromedriver)}" # stack, and follows the object rather than the profile. # cov-server crossed the threshold when it gained the storage service's object_store dependency tree (285 crates, # a 10.4 MB __llvm_covfun), which is why the coverage lane passed on commit d8d42f9c and failed from -# 6e80f4decd7d7d78ec6bd770d52105f4bd5db15a onward -- e.g. +# https://github.com/edge-toolkit/core/commit/6e80f4decd7d7d78ec6bd770d52105f4bd5db15a onward -- e.g. # https://github.com/edge-toolkit/core/actions/runs/30988267419/job/92248023234. # Dropping branch regions for this one binary keeps its line coverage and costs only its own BRDA records; every # other crate still gets branch coverage via `a_wasm_cov_instr` / the show-env wrapper. Restore the flag here diff --git a/.mise/config.dotnet.toml b/.mise/config.dotnet.toml index 619dd321..e9c439d0 100644 --- a/.mise/config.dotnet.toml +++ b/.mise/config.dotnet.toml @@ -25,7 +25,7 @@ dotnet_msbuild = "$$DOTNET_ROOT/sdk/10.0.300" # no PATH at all and the link dies with: # 'emcc' is not recognized as an internal or external command, # error MSB3073: The command "emcc "@...emcc-default.rsp" ..." exited with code 9009. -# Observed on commit 3c0d34021daa989a565aca1860ee41b638b90ff6 at +# Observed on commit https://github.com/edge-toolkit/core/commit/3c0d34021daa989a565aca1860ee41b638b90ff6 at # https://github.com/edge-toolkit/core/actions/runs/28678866448/job/85058000145 and reproduced locally by # padding PATH past 8191 chars. Overriding the MSBuild PATH *property* (global properties take precedence over # environment-derived ones) shortens only that composed tail: the Exec gets pack dirs + System32 (~650 chars) @@ -71,7 +71,8 @@ description = "Analyze C# sources (Roslynator)" # in every env var it passes to children -- DOTNET_ROOT arrives as `C:/Users/...`, a form hostfxr accepts # (dotnet-check works) but MSBuildLocator's SDK discovery does not, failing with: # Cannot choose MSBuild location automatically. Use option '-m, --msbuild-path' to specify MSBuild location -# Observed on commit ef55945932f5d417b8d205530f0868cff6d73ffa (locally; reproduced under `mise exec` by +# Observed on commit https://github.com/edge-toolkit/core/commit/ef55945932f5d417b8d205530f0868cff6d73ffa +# (locally; reproduced under `mise exec` by # setting a forward-slashed DOTNET_ROOT). The env var itself is fine as the `-m` argument -- mise's core # dotnet plugin exports DOTNET_ROOT on every OS, and the forward-slashed form ash delivers on Windows is # accepted there; only MSBuildLocator's own probing of it breaks. @@ -122,7 +123,8 @@ description = "Install the wasm-tools workload once, before any dotnet module bu # ILLink : error IL1034: Root assembly 'dotnet-data1, Version=1.0.0.0, Culture=neutral, # PublicKeyToken=null' does not have entry point. # error NETSDK1144: Optimizing assemblies for size failed. -# All three Windows lanes at once on commit 017a9aa4035a50db663ad6a800d1005051ca9a78, +# All three Windows lanes at once on commit +# https://github.com/edge-toolkit/core/commit/017a9aa4035a50db663ad6a800d1005051ca9a78, # https://github.com/edge-toolkit/core/actions/runs/32226380541/job/95986792705 -- serializing the install was # the fix for the race, guarding it was not part of that and only hid a broken workload state. run = "dotnet workload install wasm-tools --skip-manifest-update" diff --git a/.mise/config.java.toml b/.mise/config.java.toml index 9c89f857..a6f08a4a 100644 --- a/.mise/config.java.toml +++ b/.mise/config.java.toml @@ -16,7 +16,8 @@ maven = "3.9.16" # MISE_DATA_DIR overrides on either, and MISE_INSTALLS_DIR -- which CI sets to a short root -- relocates the # per-tool dirs out from under the data dir entirely, so it has to win where it is set. Missing that branch # pointed JAVA_HOME at a path no install ever wrote, and mvn.cmd rejected it with `The JAVA_HOME environment -# variable is not defined correctly` on commit 4e3ce2838f5f72717114b940ffed37c4e370e1a3 at +# variable is not defined correctly` on commit +# https://github.com/edge-toolkit/core/commit/4e3ce2838f5f72717114b940ffed37c4e370e1a3 at # https://github.com/edge-toolkit/core/actions/runs/33937604970/job/101228460727. # # Windows-only slash normalization: %LOCALAPPDATA% arrives in mise's tera context with forward slashes diff --git a/.mise/config.js.toml b/.mise/config.js.toml index 7754b96d..4cd443a6 100644 --- a/.mise/config.js.toml +++ b/.mise/config.js.toml @@ -104,7 +104,8 @@ ort_version="$(mise exec -- yq -p json -oy '.dependencies."onnxruntime-web"' "$p # Walking the install tree instead overruns Windows' 260-char MAX_PATH inside ort's own flatbuffers sources # (the deepest, `runtime-optimization-record-container-entry.js.map`, lands at 279 chars), and uutils find # then reports every unstattable entry as `find: : No such file or directory` -- on files that do -# exist -- and exits 1. Seen on commit 189935fddb306fa8e4dc36007fd73f6edf0ca2ee at +# exist -- and exits 1. Seen on commit +# https://github.com/edge-toolkit/core/commit/189935fddb306fa8e4dc36007fd73f6edf0ca2ee at # https://github.com/edge-toolkit/core/actions/runs/32195702185/job/95899138206 store="$install/node_modules/.mise/onnxruntime-web@$ort_version/node_modules/onnxruntime-web/dist" # The npm backend keeps pinned transitive deps in the .mise store above; a hoisted layout is the fallback. @@ -230,7 +231,8 @@ for tool in $tools; do # (the dependencies workflow). Installing what is already present is a no-op, so warm machines are # unaffected -- the failure without it is # `tool 'npm:@huggingface/transformers@latest' requires configured install dependency 'node@22', but its - # selected version is not installed`, on commit 0e7273a1154db315a0f584077db4565ba66be603 at + # selected version is not installed`, on commit + # https://github.com/edge-toolkit/core/commit/0e7273a1154db315a0f584077db4565ba66be603 at # https://github.com/edge-toolkit/core/actions/runs/33727571401/job/100559961260. mise install node mise install "$tool" diff --git a/.mise/config.kotlin.toml b/.mise/config.kotlin.toml index f294eaa4..1ed1c0f4 100644 --- a/.mise/config.kotlin.toml +++ b/.mise/config.kotlin.toml @@ -14,7 +14,7 @@ java = "26.0.1" # On Windows, mise creates no working gradle shim (see the gradle_bin var in config.windows.toml), and a bare # `gradle` under busybox bash resolves the extensionless Unix launcher, whose `JAVACMD="$JAVA_HOME/bin/java"` # (no `.exe`) check dies with "JAVA_HOME is set to an invalid directory" even though the JDK is present -- -# observed on commit 88825a7217aa80c04171a735fec8f7a550fb75d1 at +# observed on commit https://github.com/edge-toolkit/core/commit/88825a7217aa80c04171a735fec8f7a550fb75d1 at # https://github.com/edge-toolkit/core/actions/runs/31558277244/job/93995051606 (check:kotlin) and # https://github.com/edge-toolkit/core/actions/runs/31558277392/job/93995052197 (prefetch:kotlin). The # absolute path to `gradle.bat` sidesteps the PATH lookup; busybox's spawnve auto-wraps `.bat` with cmd.exe, @@ -46,10 +46,10 @@ shell = "{{ vars.task_shell }}" # > Could not set file mode 755 on '/root/.gradle/binaryen/binaryen-version_125' # # The loser alternates, which is why it reads as an unrelated flake: kotlin-data1 lost on the ubuntu:24.04 -# build at commit 0e7273a1154db315a0f584077db4565ba66be603 +# build at commit https://github.com/edge-toolkit/core/commit/0e7273a1154db315a0f584077db4565ba66be603 # (https://github.com/edge-toolkit/core/actions/runs/33727571318/job/100559961182), then kotlin-math1 lost on # fedora:42 and kotlin-data1 again on opensuse/leap:15.6, both at commit -# 6c6b6e6f0d123f131d3f41e188660151f6994f9e +# https://github.com/edge-toolkit/core/commit/6c6b6e6f0d123f131d3f41e188660151f6994f9e # (https://github.com/edge-toolkit/core/actions/runs/33847012202/job/100941026418 and # .../job/100941026442). `wait_for` rather than `depends`: math1 needs nothing kotlin-data1 produces, so this # must not pull that module into a build that only asked for this one -- it only defers when both are already diff --git a/.mise/config.macos.toml b/.mise/config.macos.toml index 100c79b1..244ec7b1 100644 --- a/.mise/config.macos.toml +++ b/.mise/config.macos.toml @@ -33,7 +33,7 @@ macosx_deployment_target = "{% if arch() == 'arm64' %}11.0{% else %}10.12{% endi # start at all, and nextest cannot even enumerate them -- # error: creating test list failed # dyld[94600]: Library not loaded: @rpath/libpython3.13.dylib -# on commit 8e1d10ba04c3baa27ba3ca6d29e9dd7b9d9d7b5a at +# on commit https://github.com/edge-toolkit/core/commit/e1363ae2ae2936c21df97d2ab0fe410857c860fa at # https://github.com/edge-toolkit/core/actions/runs/34925257378/job/104241837125 (and the coverage lane's # nightly build alongside it). A duplicate-rpath warning on one binary is the cheaper of the two. CARGO_TARGET_AARCH64_APPLE_DARWIN_RUSTFLAGS = "{{ vars.rpath_flag }} {{ vars.pylib_flag }}" @@ -59,8 +59,8 @@ _.path = ["/Library/Developer/CommandLineTools/usr/bin", "/bin", "/sbin", "/usr/ # reads it back from the Dockerfile in the checkout, so adding/removing a prereq there propagates here # automatically. Most apt names map cleanly to Xcode CLT binaries or macOS pre-installed tools; the case # patterns below adjust the few that don't (cert path, SDK headers via xcrun). -depends = ["_setup_all"] -description = "Preinstall: verify Xcode CLT + Dockerfile prereqs" +depends = ["_setup_all", "macos-sdk-check"] +description = "Preinstall: verify Xcode CLT + Dockerfile prereqs + the macOS SDK" run = """ xcode-select -p >/dev/null 2>&1 || { echo "preinstall: Xcode command-line tools not found." >&2 diff --git a/.mise/config.python.toml b/.mise/config.python.toml index ccb41dfe..ff40812b 100644 --- a/.mise/config.python.toml +++ b/.mise/config.python.toml @@ -39,7 +39,7 @@ ruff = "latest" # # Cannot find pyodide pip. Make a pyodide venv first? # -# on commit 5fb4626293685ee13b2da3b19686830ec5833cbb at +# on commit https://github.com/edge-toolkit/core/commit/5fb4626293685ee13b2da3b19686830ec5833cbb at # https://github.com/edge-toolkit/core/actions/runs/31864076908/job/94962233487, passing only on the retry # step once the full `mise install` had put real CPython on PATH. Nothing runs pyodide from PATH (the modules # service reads the install dir straight off disk), so point bin_path at a deliberately nonexistent subdir -- diff --git a/.mise/config.toml b/.mise/config.toml index 4b597151..e4babe0e 100644 --- a/.mise/config.toml +++ b/.mise/config.toml @@ -113,7 +113,7 @@ dprint = "latest" # v4.0.0 renamed every asset from `ec--` to `editorconfig-checker--`, and aqua's manifest # was not updated, so `latest` resolves to a version whose download it cannot find: # aqua:editorconfig-checker/editorconfig-checker@latest: no asset found: ec-windows-amd64.zip -# on commit 02d645a30b89850b5d27711e0dd8f61780dcdcb3 at +# on commit https://github.com/edge-toolkit/core/commit/02d645a30b89850b5d27711e0dd8f61780dcdcb3 at # https://github.com/edge-toolkit/core/actions/runs/33945730231/job/101251329874. The rename is not # Windows-specific -- every platform's asset moved -- so this holds every lane, not just that one. Drop the pin # once the aqua registry ships a version_overrides entry for 4.x. @@ -328,7 +328,7 @@ version = "08a1d6f" # # openobserve never bound its HTTP port :36657 # -# observed on commit 1f544590b473887015754f67c3af843f970d8dbc at +# observed on commit https://github.com/edge-toolkit/core/commit/1f544590b473887015754f67c3af843f970d8dbc at # https://github.com/edge-toolkit/core/actions/runs/31559765133/job/94267330524 (and the three sibling # old-glibc lanes of that run), while every glibc >= 2.39 lane passed. The musl build has no glibc floor. [tools."http:openobserve"] @@ -361,10 +361,10 @@ url = "https://downloads.openobserve.ai/releases/openobserve/v0.91.5/openobserve # attack. Enable the corresponding verification setting or update the lockfile. # # observed on macos-arm64 and windows-x64 with mise 2026.8.5 at commit -# 20b91714e47f52e7fcd898fc3df3dbf35bb53242. The http backend performs no provenance lookup, so nothing -# unverifiable lands in the lockfile. It also records no cross-platform checksums the way the github backend -# did: `mise lock` only hashes the asset for the platform it runs on, so each platform's checksum appears the -# first time the lockfile is refreshed there. +# https://github.com/edge-toolkit/core/commit/20b91714e47f52e7fcd898fc3df3dbf35bb53242. The http backend +# performs no provenance lookup, so nothing unverifiable lands in the lockfile. It also records no +# cross-platform checksums the way the github backend did: `mise lock` only hashes the asset for the platform +# it runs on, so each platform's checksum appears the first time the lockfile is refreshed there. # # Linux takes the static musl assets rather than the gnu ones aqua used to select, which import GLIBC_2.39 # symbols -- on every docker-linux matrix distro with glibc <= 2.38 the gnu build dies at exec and the @@ -372,7 +372,7 @@ url = "https://downloads.openobserve.ai/releases/openobserve/v0.91.5/openobserve # # rustfs did not start listening on port 33649 # -# observed on commit 1f544590b473887015754f67c3af843f970d8dbc at +# observed on commit https://github.com/edge-toolkit/core/commit/1f544590b473887015754f67c3af843f970d8dbc at # https://github.com/edge-toolkit/core/actions/runs/31559765133/job/94267330524 (and the three sibling # old-glibc lanes of that run), while every glibc >= 2.39 lane passed. The musl build has no glibc floor; # macOS and Windows have only the one build each. Direct upstream, per-version immutable URLs, so no checksum. @@ -788,10 +788,10 @@ depends = [ "kubeconform-check", "link-check", "ls-lint-check", - "macos-sdk-check", "mise-lock-check", "readme-mise-version-check", "regal-check", + "repo-check", "ryl-check", "semgrep-check", "shellcheck-check", @@ -1032,7 +1032,8 @@ env = { RUSTDOCFLAGS = "-Z unstable-options --check -D warnings" } # it, and the unwrapped symlink aborts on the first of those that does not exist: # thread 'main' panicked at .../ort-sys-2.0.0-rc.10/build.rs:129:66: # called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" } -# Seen on commit 9440a53b3f632307d5a41b654ff526981021759f in the macos + ubuntu check lanes, e.g. +# Seen on commit https://github.com/edge-toolkit/core/commit/9440a53b3f632307d5a41b654ff526981021759f in the +# macos + ubuntu check lanes, e.g. # https://github.com/edge-toolkit/core/actions/runs/30782416445/job/91589563319 -- and reproducible locally. # The feature cannot simply be switched off: wasmtime-wasi-nn declares `features = ["copy-dylibs"]` on its own # ort dependency, so feature unification keeps it enabled regardless of this workspace's ort entry. Creating the @@ -1101,7 +1102,7 @@ run = "conftest verify -p config/conftest/policy" description = "Check every conftest policy has a test file beside it (conftest)" run = """ find config/conftest/policy -name '*.rego' -print0 | - xargs -0 conftest test --combine --parser ignore --namespace policy_tests -p config/conftest/policy + xargs -0 conftest test --combine --parser ignore --namespace policy_pairing -p config/conftest/policy """ shell = "{{ vars.task_shell }}" @@ -1511,6 +1512,10 @@ trivy config --quiet --exit-code 1 --ignorefile config/trivyignore.yaml "$@" . """ shell = "{{ vars.task_shell }}" +[tasks.repo-check] +description = "Repository-wide checks that need to read git itself (commit hashes, ...)" +run = "cargo run -q -p et-repo-check" + [tasks.link-check] description = "Check that URLs in .md and .rs files are reachable (network)" # The .rs glob is scoped to the source dirs, not a bare `**/*.rs`. @@ -1914,17 +1919,18 @@ shell = "{{ vars.task_shell }}" # the `mise run` starts, and a `depends` install lands after that snapshot just as an in-body `mise install` # does -- so a cold runner still gets "bash: line 1: coreutils: command not found" (first seen with the in-body # form at https://github.com/edge-toolkit/core/actions/runs/31670112517/job/94352839713 on commit -# d8ad8cb99cf58a1e505cdbebc5b94f9893532b4f, then again through `depends` at -# https://github.com/edge-toolkit/core/actions/runs/31773896309/job/94685230924 on commit -# 0dd8cb009e2b81f9013d7a647092adf383391284). Every consumer therefore resolves these tools with `mise which` -# and calls them by absolute path, which needs no PATH entry at all. +# https://github.com/edge-toolkit/core/commit/d8ad8cb99cf58a1e505cdbebc5b94f9893532b4f, then again through +# `depends` at https://github.com/edge-toolkit/core/actions/runs/31773896309/job/94685230924 on commit +# https://github.com/edge-toolkit/core/commit/0dd8cb009e2b81f9013d7a647092adf383391284). Every consumer +# therefore resolves these tools with `mise which` and calls them by absolute path, which needs no PATH entry +# at all. # # Keep the tool specs unquoted. This body is a single line, so on Windows it runs under mise's default # `cmd /c` -- the repo's bash inline-shell default is set by `preinstall`, which has not run yet when CI # restores the store. cmd.exe does not strip quotes, so a quoted spec reaches mise verbatim and it tries to # clone a plugin from the literal name: `mise plugin:01mf02/jaq' clone https://github.com/01mf02/jaq'.git`, # ending in `Failed to install 'aqua:01mf02/jaq'@latest`, which stranded the Windows lane's restore on commit -# 0dd8cb009e2b81f9013d7a647092adf383391284 at +# https://github.com/edge-toolkit/core/commit/0dd8cb009e2b81f9013d7a647092adf383391284 at # https://github.com/edge-toolkit/core/actions/runs/31773896309/job/94685231006. [tasks._setup-store-tools] description = "Install the tools the mise-tools store tasks depend on (cold-checkout helper)" @@ -2344,11 +2350,7 @@ run = "cargo nextest run --config-file config/nextest.toml -p edge-toolkit -p et [tasks."test:rust"] # Rust tests. Always-loaded, so `test`'s glob always matches it. -# macos-sdk-check rides here as well as on check:rust so both GHA workflows get it without a workflow edit: -# `check` globs check:*, `test` globs test:*, and these two are the members that are always loaded. It is a -# host precondition rather than a test, but a broken SDK fails the compile it precedes with a far worse -# message, so naming it up front is worth the slight category stretch. -depends = ["cargo-test", "macos-sdk-check"] +depends = ["cargo-test"] description = "Run the Rust tests" [tasks.test] diff --git a/.mise/config.windows.toml b/.mise/config.windows.toml index b06d105f..95de701c 100644 --- a/.mise/config.windows.toml +++ b/.mise/config.windows.toml @@ -223,7 +223,8 @@ cargo_ws_excludes = "--exclude et-ws-web-runner" # big write blocks forever with no reader -- `mise run actionlint-check` then hangs with no output and an # `actionlint.exe` process pinned in tasklist. Present in every actionlint since the shellcheck rule landed # (v1.4.x; verified hanging on 1.7.7/1.7.11/1.7.12), masked on Linux/macOS by their 64KB pipe buffers, so -# those lanes keep full shellcheck coverage. Reproduced on commit ef55945932f5d417b8d205530f0868cff6d73ffa +# those lanes keep full shellcheck coverage. Reproduced on commit +# https://github.com/edge-toolkit/core/commit/ef55945932f5d417b8d205530f0868cff6d73ffa # with a synthetic 29KB run body (hangs) vs a 178B one (passes); a fake instantly-exiting shellcheck still # hangs it, proving shellcheck itself is uninvolved. Upstream fix is one line # (`cmd.Stdin = strings.NewReader(e.stdin)`); drop this override once a fixed actionlint releases. @@ -243,7 +244,7 @@ MISE_BASH_PATH = "{{ vars.winsh }}" # '"node"' is not recognized as an internal or external command, # operable program or batch file. # Error running tsgolint: "exit status: exit code: 1" -# Observed on commit 8cac021c75ebb69e8100a1ddb8d9b670e197fee4 at +# Observed on commit https://github.com/edge-toolkit/core/commit/8cac021c75ebb69e8100a1ddb8d9b670e197fee4 at # https://github.com/edge-toolkit/core/actions/runs/28857668889/job/85588159468 (check windows-latest lane). # oxlint consults this env var before its node_modules/.bin and PATH walks, and pointing it at the # per-platform exe nested in the npm package sidesteps node and cmd.exe entirely. Windows-only (this file) diff --git a/.mise/config.zig.toml b/.mise/config.zig.toml index 0f6acc7e..8cddd487 100644 --- a/.mise/config.zig.toml +++ b/.mise/config.zig.toml @@ -20,7 +20,8 @@ # Windows lane is long enough that cmd.exe discards it, leaving nothing to search: # 'clang-format' is not recognized as an internal or external command, # operable program or batch file. -# That took down the windows-latest check lane on commit 0dd8cb009e2b81f9013d7a647092adf383391284 at +# That took down the windows-latest check lane on commit +# https://github.com/edge-toolkit/core/commit/0dd8cb009e2b81f9013d7a647092adf383391284 at # https://github.com/edge-toolkit/core/actions/runs/31773896309/job/94685231006. An absolute path needs no # lookup, so the launcher runs whatever cmd.exe did with the PATH. pipx's Windows entry points are real # `.exe`s, so cpplint and flawfinder are unaffected. @@ -36,7 +37,8 @@ zig = "latest" # filters llvm-mingw out of the Windows toolset PATH. cc-rs builds then fall through to the GHA runner's # system clang (MSVC-default, no mingw sysroot) and every C-compiling build script dies with, e.g.: # aws-lc-sys@0.41.0: ...\aws-lc\tests\compiler_features_tests\c11.c:7:10: fatal error: 'stdlib.h' file not found -# which took down every Windows job on commit 60b631d4dea8d19f50386f1568bf6128c956795c at +# which took down every Windows job on commit +# https://github.com/edge-toolkit/core/commit/60b631d4dea8d19f50386f1568bf6128c956795c at # https://github.com/edge-toolkit/core/actions/runs/28995222108/job/86043285466 (check windows-latest lane). # The windows-x64 matching below mirrors the config.windows.toml entry -- keep them identical. [tools."github:mstorsjo/llvm-mingw"] diff --git a/.mise/task-shell.sh b/.mise/task-shell.sh index 47e5427b..1326d25f 100644 --- a/.mise/task-shell.sh +++ b/.mise/task-shell.sh @@ -9,7 +9,7 @@ # c:/i/http-busybox/1.37.0/ash: line 0: cargo: not found # # with dart, uv, rclone, zig, wasm-pack and coreutils all failing the same way in the same run, on commit -# 17056fad10ade2af7b2df3eced29f9ae77d20f3e at +# https://github.com/edge-toolkit/core/commit/17056fad10ade2af7b2df3eced29f9ae77d20f3e at # https://github.com/edge-toolkit/core/actions/runs/33877826343/job/101038940930. Upstream deleted the rewrite in # jdx/mise#12696, which sits in no release yet -- once it ships, this file becomes a no-op on every platform and # can go. Repointing MISE_BASH_PATH at an msys2 bash would also fix it, since that runtime renormalises the diff --git a/CLAUDE.md b/CLAUDE.md index e5ba6ad7..aa3ba9eb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -637,6 +637,25 @@ helper per test. Keep its dependency footprint small (currently just `port_check domain-specific gets its own test-support crate instead (e.g. `et-ws-test-server` for an in-process ws-server, or `et-test-otlp` for OTLP emit + capture-assertion support). +### Everything under `libs/` carries 100% test coverage + +A crate in `libs/` is shared infrastructure: every service and utility in the workspace builds on it, so a gap +there is a gap in everything downstream, and it is the place where a test costs least relative to what it +protects. The bar is therefore the whole of it -- every line and every branch, not an aggregate percentage that +lets one untested helper hide behind a well-covered neighbour. Services and utilities are held to no such +number; they are covered on their merits, which is exactly why the shared layer underneath has to be total. + +Write the tests with the code, not after it. A new function in `libs/` lands in the same change as the tests +that cover it, and a new branch in an existing one lands with the case that takes it. Finding the gap from a +red coverage lane instead means the change is already written, reviewed and pushed -- the most expensive moment +to discover that a branch was unreachable from the public API all along and the function needed a different +shape. A test that cannot fail is not coverage either: check a new test actually catches the bug it describes +by breaking the code under it once and watching it go red. + +Where a line genuinely cannot be reached -- a platform-gated arm, an error only the OS can produce -- restructure +so the unreachable part is as small as it can be, and say at the site why it is unreachable. A bare gap reads as +an oversight to the next person, who then spends the afternoon working out whether it is one. + ### NEVER skip, ignore, or platform-disable a test without explicit user approval A test that doesn't run is worse than no test: it reads as coverage while asserting nothing. Do not add `#[ignore]`, @@ -679,14 +698,15 @@ arriving a few seconds after torch's cold first import prints `cpu = _conversion_method_template(device=torch.device("cpu"))` line -- i.e. the spawned runner was still inside torch's import when the test's registration timeout expired. The ~117 MB `pipx:torch` package's first import on a cold runner is the slow step; a rerun passes because the import caches warm. Observed on commit -`6479913bdc288dd680fbe0520f63054e8c71fe6c` at +https://github.com/edge-toolkit/core/commit/6479913bdc288dd680fbe0520f63054e8c71fe6c at `https://github.com/edge-toolkit/core/actions/runs/28686533955/job/85080173283` (PR #70; the rerun passed and the PR merged), and on the `default (macos-latest, 45)` job -- same signature, unix-form `torch/lib/python3.13/site-packages/torch/_subclasses/functional_tensor.py:362` warning path -- on commit -`f2a85307a2415f4a50625627795e02c84f1c8e5d` at +https://github.com/edge-toolkit/core/commit/f2a85307a2415f4a50625627795e02c84f1c8e5d at `https://github.com/edge-toolkit/core/actions/runs/28865327221/job/85613919558` (PR #73), 20.68s into the test -run. Recurred at commit `1e323acb7fc433b66efed904b3b30ab3216dbf90` (PR #76) on three lanes of one run at once -- -the first Linux sighting, and the first time it took more than a single lane of a commit: build.yaml's +run. Recurred at commit https://github.com/edge-toolkit/core/commit/1e323acb7fc433b66efed904b3b30ab3216dbf90 +(PR #76) on three lanes of one run at once -- the first Linux sighting, and the first time it took more than a +single lane of a commit: build.yaml's `build (ubuntu:22.04)` (~15.76s) at `https://github.com/edge-toolkit/core/actions/runs/29034248895/job/86174973520`, and test.yaml's `override (mingw)` (~18.13s) and `override (msvc)` (~18.96s) at @@ -695,7 +715,8 @@ the first Linux sighting, and the first time it took more than a single lane of failures read as the cold torch import consistently overrunning the timeout rather than an occasional flake -- so the root-cause fix below is now due, not optional. -Recurred once more on the `override (mingw)` lane at commit `5998313315c491a6abffb7c1507e4adc4a4f3559`, +Recurred once more on the `override (mingw)` lane at commit +https://github.com/edge-toolkit/core/commit/5998313315c491a6abffb7c1507e4adc4a4f3559, `https://github.com/edge-toolkit/core/actions/runs/34187567425/job/101938939834`, failing at **122.2s** -- and that figure is the diagnosis. The test had two nested deadlines: a 2-minute `PEER_REGISTER_TIMEOUT` around the peer wait, inside a 3-minute `TORCH_EXCHANGE_BUDGET` around the whole exchange. The inner one decided every @@ -737,7 +758,7 @@ and the default `x86_64-pc-windows-gnullvm` both report `target_env = "gnu"`, an `all(windows, target_env = "gnu", not(target_abi = "llvm"))`; written without that last clause it silently takes the default Windows lane with it. -On commit `5998313315c491a6abffb7c1507e4adc4a4f3559` the same +On commit https://github.com/edge-toolkit/core/commit/5998313315c491a6abffb7c1507e4adc4a4f3559 the same workload passed on `gnullvm` (the default the Windows Dockerfiles and CI build) in 426s and on `msvc` in 443s, while `gnu` failed twice with the identical signature -- 644s at `https://github.com/edge-toolkit/core/actions/runs/34187567425/job/101938939834`, then 591s on a deliberate @@ -751,7 +772,8 @@ generated deployment. Every scenario whose trigger is `wasi-math1-sender` hits it, which is both of them. `pyo3-math1` was left ungated on the first pass because fail-fast had cancelled it before it ever ran on `gnu`; it then failed there -with the identical signature at 579s on commit `29dfe80a62ba7a27d8119c5b6332c3dbe2df815e` +with the identical signature at 579s on commit +https://github.com/edge-toolkit/core/commit/29dfe80a62ba7a27d8119c5b6332c3dbe2df815e (`https://github.com/edge-toolkit/core/actions/runs/34211905976/job/102014621776`) while passing on `gnullvm` in 472s and `msvc` in 458s. Its pyo3 twin registers and idles cleanly throughout -- what aborts is the wasi trigger, so the scenario fails for the reason above and not for anything to do with the pyo3 runner. @@ -769,7 +791,8 @@ rate-limited or time out. The captured signature is a tool install aborting on t usually preceded by several retried `mise WARN HTTP GET https://api.github.com/repos///releases...` lines. When it strands the install, the composite's retry step can then trip the separate busybox/Git-Bash `cygheap read copy failed` fork pathology and the job hits its action timeout instead of a clean error. Observed on -the `override (mingw)` job at commit `396aa98d24e4a945528cdbac33fbc61b66831e8a`, +the `override (mingw)` job at commit +https://github.com/edge-toolkit/core/commit/396aa98d24e4a945528cdbac33fbc61b66831e8a, `https://github.com/edge-toolkit/core/actions/runs/28698136445/job/85111320237` (a rerun of the same commit installed cleanly). This is an api.github.com rate-limit/transient-network flake, not a repo defect -- a `GITHUB_TOKEN` is already forwarded to raise the ceiling. If it becomes frequent rather than occasional, the @@ -800,7 +823,8 @@ exponential backoff (`retry_initial_backoff_secs=1`, doubling; `retry_max_durati `config/vector-otlp-relay.yaml`), so when its first attempts land in the gap between the mock being told to start and its listener actually accepting, the next retry can be tens of seconds out -- and at the original 30s poll ceiling that occasionally overran, so the span arrived just after the test gave up. Observed on the `default -(windows-latest)` lane at commit `71e70e56946abefcea6daf47a7745e5f8f186dad`, +(windows-latest)` lane at commit +https://github.com/edge-toolkit/core/commit/71e70e56946abefcea6daf47a7745e5f8f186dad, `https://github.com/edge-toolkit/core/actions/runs/28923326008/job/85829470834` (the failure is OS-agnostic -- any cold/contended runner, macOS included, can hit it). Fix applied: `wait_for_relayed_span` now polls ~120s (`Fixed::from_millis(250).take(480)`) instead of ~30s, which the poll exits the instant the span lands so the @@ -1053,7 +1077,8 @@ not as a repo-wide var-prefix. One transient class that hits the `cargo fetch` path specifically: libcurl's `[16] Error in the HTTP2 framing layer` during a `crates.io` download. Captured example: `https://github.com/edge-toolkit/core/actions/runs/27900771461/job/82560594632` -on commit `6e4c0030a830e4f8e6b15381cb5c2fbf481af704` -- `asyncapi-rust-codegen` download bailed with `curl failed` -> +on commit https://github.com/edge-toolkit/core/commit/6e4c0030a830e4f8e6b15381cb5c2fbf481af704 -- +`asyncapi-rust-codegen` download bailed with `curl failed` -> `[16] Error in the HTTP2 framing layer`. `CARGO_NET_RETRY` does **not** cover this -- the framing error surfaces from inside libcurl as a generic @@ -1211,8 +1236,8 @@ Work the gap in this order, and prefer the earliest step that applies: 1. **Find the local equivalent -- it usually already exists.** The linters here overlap the external services heavily, and the codes often correspond directly: DeepSource's `PYL-*` are pylint codes, which ruff - implements as `PL*` (`PYL-W0603` is ruff's `PLW0603`, `PYL-W0613` is ruff's `ARG`). If the rule is already - enabled, the gap is not the rule -- see step 2. + implements as `PL*` (`DeepSource PYL-W0603` is ruff's `PLW0603`, `DeepSource PYL-W0613` is ruff's `ARG`). + If the rule is already enabled, the gap is not the rule -- see step 2. 2. **Check whether an exemption is what hid it.** A path-glob carve-out (a `[lint.per-file-ignores]` entry, a `files:` allowlist, an `exclude_paths`) silences the rule for files that do not exist yet, so the local check stays quiet while the external analyzer flags each new file. Narrow it: replace the glob with per-site inline diff --git a/Cargo.lock b/Cargo.lock index 2d2841f8..38ac657e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4416,6 +4416,16 @@ dependencies = [ "tempfile", ] +[[package]] +name = "et-repo-check" +version = "0.1.0" +dependencies = [ + "command-error", + "fs-err", + "regex", + "thiserror 2.0.20", +] + [[package]] name = "et-rest-client" version = "0.1.0" @@ -4469,9 +4479,11 @@ name = "et-test-helpers" version = "0.1.0" dependencies = [ "command-error", + "fs-err", "port_check", "retry", "temp-env", + "tempfile", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index b82be95b..826bd5dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,7 @@ members = [ "utilities/int-gen", "utilities/cli", "utilities/onnx", + "utilities/repo-check", "utilities/wasm-cov-wrapper", "generated/rust-rest", ] @@ -128,7 +129,8 @@ log = "0.4" # and cargo unifies every 0.3.x onto one copy. As a caret range the resolver satisfied that pin the other way # round: it kept 0.3.9 for us and dropped wasm-bindgen-test to 0.3.45, whose test exports the 0.2.128 runner # cannot see, so every browser coverage build reported `no tests to run!` and then failed on its missing -# `*.profraw` (`No such file or directory`). Seen on commit e8a38b44eba21f46001ec01cd1ddccdd3eeca4ea at +# `*.profraw` (`No such file or directory`). Seen on commit +# https://github.com/edge-toolkit/core/commit/e8a38b44eba21f46001ec01cd1ddccdd3eeca4ea at # https://github.com/edge-toolkit/core/actions/runs/35032214650/job/104593077391. # # `vars.rust_nightly` has to carry the LLVM this minicov targets. 0.3.9 moved to profraw format 11 ("Sync with diff --git a/Dockerfile.nanoserver b/Dockerfile.nanoserver index 429f664a..3a26c38c 100644 --- a/Dockerfile.nanoserver +++ b/Dockerfile.nanoserver @@ -82,7 +82,7 @@ RUN curl -fsSL -o vc_redist.exe https://aka.ms/vs/17/release/vc_redist.x64.exe & # curl: (22) The requested URL returned error: 403 # # -- which failed `build (windows-2022, nanoserver)` at step 7 on commit -# 1f544590b473887015754f67c3af843f970d8dbc +# https://github.com/edge-toolkit/core/commit/1f544590b473887015754f67c3af843f970d8dbc # (https://github.com/edge-toolkit/core/actions/runs/31559765225/job/94252377147) # while the windows-2025 lane of that same run downloaded it fine. Per the # CLAUDE.md fetch-resilience rule, the fix for a blocked upstream is to serve @@ -231,7 +231,7 @@ ENV MISE_GPG_VERIFY=false # mise ERROR Failed to install aqua:cli/cli@latest: Lockfile requires github-attestations provenance for # aqua:cli/cli@2.97.0 but no verification was used. This may indicate a downgrade attack. Enable the # corresponding verification setting or update the lockfile. -# observed on commit 4f28cf1e3fa71ac195c8189d35b014823d2e54e6 at +# observed on commit https://github.com/edge-toolkit/core/commit/4f28cf1e3fa71ac195c8189d35b014823d2e54e6 at # https://github.com/edge-toolkit/core/actions/runs/31685098295/job/94399242373, where gh was simply the # first aqua tool to install -- every other one would have hit it in turn. `lockfile = false` makes every # lockfile read return None (mise's settings.lockfile_enabled gate), so no expectation reaches the installer. @@ -424,7 +424,7 @@ ARG MISE_DT_PY=pipx:componentize-py,pipx:datamodel-code-generator,pipx:openapi-p # mise ERROR Failed to install github:kucherenko/jscpd@5.1.1: GitHub artifact attestations verification # error for github:kucherenko/jscpd@5.1.1: Verification failed: Sigstore error: TUF error: Could not # determine cache directory -# on commit 5212a7a84adf4c9b9332ee882410d5e68155f692 at +# on commit https://github.com/edge-toolkit/core/commit/5212a7a84adf4c9b9332ee882410d5e68155f692 at # https://github.com/edge-toolkit/core/actions/runs/33950696515/job/101264776072. Dropping the tool rather # than the verification keeps attestation checking on everywhere it works. jscpd drives only jscpd-check and # its siblings, every one of them part of `check`, which this image never runs -- it stops at preinstall, diff --git a/config/ast-grep/rules/gha-checkout-fetch-depth-1.yaml b/config/ast-grep/rules/gha-checkout-fetch-depth-1.yaml index c2db2591..f0c6b4bc 100644 --- a/config/ast-grep/rules/gha-checkout-fetch-depth-1.yaml +++ b/config/ast-grep/rules/gha-checkout-fetch-depth-1.yaml @@ -2,13 +2,16 @@ id: gha-checkout-fetch-depth-1 language: yaml severity: error message: | - Every `actions/checkout@...` step in this repo must declare `fetch-depth: 1` in its `with:` block. + Every `actions/checkout@...` step in these workflows must declare `fetch-depth: 1` in its `with:` block. Explicit-beats-implicit (the action default is already 1, but pinning it at the call site keeps the value visible in review and ensures a future default change doesn't silently start cloning more), and the rule's primary purpose is to - prevent `fetch-depth: 0` from creeping back in -- no task in this repo reads git history, so cloning every commit just - burns runner minutes and disk. + prevent `fetch-depth: 0` from creeping back in -- nothing these lanes run reads the commit graph, so cloning every + commit just burns runner minutes and disk. The check lane is excluded here, and held to the opposite requirement, + because two of the checks it runs do read the commit graph and resolve nothing at depth 1. files: - .github/workflows/*.yaml +ignores: + - .github/workflows/check.yaml # Match every `uses: actions/checkout@...` whose surrounding step body lacks `fetch-depth: 1`. # The `regex` on the value field anchors at `actions/checkout@` so we don't false-fire on `uses:` lines for other # actions; the `@$V` portion of the action ref isn't a separable meta-var (it's part of a single plain_scalar token), diff --git a/config/ast-grep/rules/no-std-env-var.yaml b/config/ast-grep/rules/no-std-env-var.yaml index 0e14509d..7369ea80 100644 --- a/config/ast-grep/rules/no-std-env-var.yaml +++ b/config/ast-grep/rules/no-std-env-var.yaml @@ -10,8 +10,11 @@ message: | services/ws-wasi-runner/tests/ sets one to opt the spawned runner into a macOS-only fast-exit path. - `MISE_*` (e.g. `MISE_ENV`) -- the mise tool's own state, not app config; reading these lets helpers like `mise_env_includes` reflect tooling behaviour without round-tripping through a struct. - - `PATH` -- the process's own executable search path, not app config; a test that shadows a tool reads it - only to hand it back with the stand-in's directory in front. + + Every exemption above is a name prefix, which keeps it self-limiting: a variable has to be named into the + exemption to use it. A bare variable name would not be -- it would open that read to every file in the repo, + including production code that has a config struct to put it in. `PATH` was such an entry, and is now a + one-line `ast-grep-ignore` on the single test helper that shapes `PATH` for a stand-in. rule: any: - pattern: std::env::var($ARG) @@ -21,10 +24,10 @@ rule: constraints: ARG: not: - # Allow a string literal or a const identifier with one of the exempt prefixes, or the exact name PATH. - # That is a string literal ("RUST_LOG" / "ET_TEST_..." / "MISE_..." / "PATH") or a const identifier - # (RUST_LOG / ET_TEST_... / MISE_... / PATH). - regex: '^"?(RUST_|ET_TEST_|MISE_|PATH"?$)' + # Allow a string literal or a const identifier carrying one of the exempt prefixes. + # That is a string literal ("RUST_LOG" / "ET_TEST_..." / "MISE_...") or a const identifier + # (RUST_LOG / ET_TEST_... / MISE_...). Prefixes only -- see the message for why. + regex: '^"?(RUST_|ET_TEST_|MISE_)' ignores: # The project-root helper reads CARGO_MANIFEST_DIR to locate the repo root from a build script. # That is a cargo build variable, not app config. diff --git a/config/ast-grep/rules/no-string-new.yaml b/config/ast-grep/rules/no-string-new.yaml index fe1d4388..4082e112 100644 --- a/config/ast-grep/rules/no-string-new.yaml +++ b/config/ast-grep/rules/no-string-new.yaml @@ -2,7 +2,7 @@ id: no-string-new language: Rust severity: error message: | - `String::new()` is banned (`RS-W1079`): construct empty strings with `String::default()` instead, so + `String::new()` is banned (`DeepSource RS-W1079`): construct empty strings with `String::default()` instead, so the zero value routes through the `Default` impl like every other default-constructed value in the codebase. rule: pattern: String::new() diff --git a/config/clang-tidy.yaml b/config/clang-tidy.yaml index ea261b5c..80a5c339 100644 --- a/config/clang-tidy.yaml +++ b/config/clang-tidy.yaml @@ -2,6 +2,6 @@ # Applied via `mise run clang-tidy-check` (passed as --config-file=). # Bug-finding analysis only; opinionated readability/style checks are left off to avoid churn on the small C surface. # cppcoreguidelines-avoid-non-const-global-variables is pulled in specifically (not the whole cppcoreguidelines set) -# to mirror `CXX-W2009`, so the mingw-shim's ABI globals are caught here too, not only after a push. +# to mirror `DeepSource CXX-W2009`, so the mingw-shim's ABI globals are caught here too, not only after a push. Checks: "clang-analyzer-*,bugprone-*,performance-*,portability-*,cppcoreguidelines-avoid-non-const-global-variables" WarningsAsErrors: "*" diff --git a/config/conftest/policy/policy_tests/policy_tests.rego b/config/conftest/policy/policy_pairing/policy_pairing.rego similarity index 94% rename from config/conftest/policy/policy_tests/policy_tests.rego rename to config/conftest/policy/policy_pairing/policy_pairing.rego index b7174125..63e75318 100644 --- a/config/conftest/policy/policy_tests/policy_tests.rego +++ b/config/conftest/policy/policy_pairing/policy_pairing.rego @@ -1,12 +1,12 @@ # Every conftest policy carries an accompanying `_test.rego`, or an entry saying why it does not. -# Run with `--namespace policy_tests` over the policy tree itself, parsed with `ignore` so each file arrives as -# a path; only the paths are read here, never the contents. +# Run with `--namespace policy_pairing` over the policy tree itself, parsed with `ignore` so each file arrives +# as a path; only the paths are read here, never the contents. # # A policy rule that stops matching does not report anything -- it reports nothing, which is the same output as # a clean repo. The real `conftest-check-*` tasks only ever run over files that already comply, so they cannot # tell those two apart, and a rule can sit dead for months looking like a passing check. Feeding a known-bad # input is the only thing that distinguishes them, and a test file is where that input lives. -package policy_tests +package policy_pairing # conftest reports native separators when it walks a directory, so a Windows lane sees backslashes. normalised(path) := replace(path, "\\", "/") diff --git a/config/conftest/policy/policy_tests/policy_tests_test.rego b/config/conftest/policy/policy_pairing/policy_pairing_test.rego similarity index 79% rename from config/conftest/policy/policy_tests/policy_tests_test.rego rename to config/conftest/policy/policy_pairing/policy_pairing_test.rego index 23560f0a..d379998e 100644 --- a/config/conftest/policy/policy_tests/policy_tests_test.rego +++ b/config/conftest/policy/policy_pairing/policy_pairing_test.rego @@ -8,9 +8,9 @@ # A fixture holds a handful of paths rather than the whole policy tree, so every real `untested_policy` entry is # absent from it and the stale-entry rule reports each one. That noise is expected here, and is why these assert # on the message they are about rather than on a count. -package policy_tests_test +package policy_pairing_test -import data.policy_tests +import data.policy_pairing files(paths) := [entry | some p in paths; entry := {"path": p, "contents": [[]]}] @@ -22,7 +22,7 @@ names(msgs, fragment) if { demo_wanted := "add config/conftest/policy/demo/demo_test.rego" test_policy_with_its_test_is_accepted if { - msgs := policy_tests.deny with input as files([ + msgs := policy_pairing.deny with input as files([ "config/conftest/policy/demo/demo.rego", "config/conftest/policy/demo/demo_test.rego", ]) @@ -30,19 +30,19 @@ test_policy_with_its_test_is_accepted if { } test_policy_without_a_test_is_flagged if { - msgs := policy_tests.deny with input as files(["config/conftest/policy/demo/demo.rego"]) + msgs := policy_pairing.deny with input as files(["config/conftest/policy/demo/demo.rego"]) names(msgs, demo_wanted) } # A test file is not itself a policy, so it must not demand a test of its own. test_test_file_does_not_demand_its_own_test if { - msgs := policy_tests.deny with input as files(["config/conftest/policy/demo/demo_test.rego"]) + msgs := policy_pairing.deny with input as files(["config/conftest/policy/demo/demo_test.rego"]) not names(msgs, "demo_test_test.rego") } # Pairing is per policy, so another policy's test does not answer for this one. test_another_policy_test_does_not_count if { - msgs := policy_tests.deny with input as files([ + msgs := policy_pairing.deny with input as files([ "config/conftest/policy/demo/demo.rego", "config/conftest/policy/other/other_test.rego", ]) @@ -53,7 +53,7 @@ test_another_policy_test_does_not_count if { # Unnormalised, the `.rego` suffix still matches, so the policy is still demanded -- but its test is never # recognised as the answer, and the check goes red on that lane alone for files that are perfectly paired. test_windows_separators_still_pair_up if { - msgs := policy_tests.deny with input as files([ + msgs := policy_pairing.deny with input as files([ "config\\conftest\\policy\\demo\\demo.rego", "config\\conftest\\policy\\demo\\demo_test.rego", ]) @@ -62,18 +62,18 @@ test_windows_separators_still_pair_up if { # Anything that is not a .rego is none of this rule's business. test_non_rego_files_are_ignored if { - msgs := policy_tests.deny with input as files(["config/conftest/policy/demo/README.md"]) + msgs := policy_pairing.deny with input as files(["config/conftest/policy/demo/README.md"]) not names(msgs, "README") } # An exception excuses the policy it names, and only that one. test_excepted_policy_needs_no_test if { - msgs := policy_tests.deny with input as files(["config/conftest/policy/jscpd/jscpd.rego"]) + msgs := policy_pairing.deny with input as files(["config/conftest/policy/jscpd/jscpd.rego"]) not names(msgs, "add config/conftest/policy/jscpd/jscpd_test.rego") } test_exception_does_not_cover_other_policies if { - msgs := policy_tests.deny with input as files([ + msgs := policy_pairing.deny with input as files([ "config/conftest/policy/jscpd/jscpd.rego", "config/conftest/policy/demo/demo.rego", ]) @@ -82,7 +82,7 @@ test_exception_does_not_cover_other_policies if { # Writing the test is what retires the entry, so an entry that outlives its answer reports. test_excepted_policy_that_gained_a_test_reports_the_stale_entry if { - msgs := policy_tests.deny with input as files([ + msgs := policy_pairing.deny with input as files([ "config/conftest/policy/jscpd/jscpd.rego", "config/conftest/policy/jscpd/jscpd_test.rego", ]) @@ -90,6 +90,6 @@ test_excepted_policy_that_gained_a_test_reports_the_stale_entry if { } test_excepted_policy_that_was_deleted_reports_the_stale_entry if { - msgs := policy_tests.deny with input as files(["config/conftest/policy/demo/demo.rego"]) + msgs := policy_pairing.deny with input as files(["config/conftest/policy/demo/demo.rego"]) names(msgs, "names a policy that no longer exists") } diff --git a/config/lychee.toml b/config/lychee.toml index b24279fc..eddcec91 100644 --- a/config/lychee.toml +++ b/config/lychee.toml @@ -34,8 +34,9 @@ exclude_path = ["data", "generated", "node_modules", "target"] # was observed, fully expecting them to age out (GHA log retention is 3 months), and github.com serves those # pages inconsistently to anonymous clients even while they exist -- the check lane got # [404] https://github.com/edge-toolkit/core/actions/runs/28698136445/job/85111320237 (at 447:1) -# from CLAUDE.md's attestation-flake note on commit e401831a98197068f978ef760134caff882e2615 while the same -# URL returned 200 to a local curl minutes later. Checking them is all noise, no signal. +# from CLAUDE.md's attestation-flake note on commit +# https://github.com/edge-toolkit/core/commit/e401831a98197068f978ef760134caff882e2615 while the same URL +# returned 200 to a local curl minutes later. Checking them is all noise, no signal. # # The `file:` entry is the container-internal storage mount the compose generator emits as a `STORAGE_URL` # value; it names a path inside the ws-server image, so it is never resolvable from the host doing the check. diff --git a/config/oxlintrc.jsonc b/config/oxlintrc.jsonc index fbfd308a..1c9ea89a 100644 --- a/config/oxlintrc.jsonc +++ b/config/oxlintrc.jsonc @@ -28,9 +28,9 @@ // So it false-positives on every worker-side / main-side `worker.postMessage({...})` call we have. "unicorn/require-post-message-target-origin": "off", // Type-aware rule flagging `a && a.b` chains that an optional chain (`a?.b`) expresses more safely. - // The local equivalent of `JS-W1044`; needs `--type-aware` + oxlint-tsgolint + config/tsconfig.json. + // The local equivalent of `DeepSource JS-W1044`; needs `--type-aware` + oxlint-tsgolint + config/tsconfig.json. "typescript/prefer-optional-chain": "error", - // Cyclomatic-complexity ceiling -- the local twin of `JS-R1005`, keeping branchy code in check. + // Cyclomatic-complexity ceiling -- the local twin of `DeepSource JS-R1005`, keeping branchy code in check. // 10 is a practical ceiling: eslint's default of 20 is too loose, and that rule's medium=6 too strict here. "eslint/complexity": ["error", { "max": 10 }], }, diff --git a/config/semgrep/deepsource-codes-are-qualified.yaml b/config/semgrep/deepsource-codes-are-qualified.yaml new file mode 100644 index 00000000..d47ddf40 --- /dev/null +++ b/config/semgrep/deepsource-codes-are-qualified.yaml @@ -0,0 +1,18 @@ +rules: + - id: deepsource-code-must-name-its-analyzer + languages: [generic] + paths: + exclude: + - "/.deepsource.toml" # Its own config, where a code is already in that context. + - "/config/semgrep/deepsource-codes-are-qualified.yaml" # This rule, which spells the shape out. + # Two lookbehinds rather than one, because PCRE allows only fixed-width ones. + # The second exempts `skipcq: `, the pragma DeepSource itself parses: that grammar is the vendor's, not + # ours, and qualifying the code inside one would stop it suppressing anything. + pattern-regex: '(?- + Write a DeepSource finding as `DeepSource RS-W1079`, naming the analyzer in front of the code. A bare + code is unreadable to anyone who does not already know whose it is: `RS-`, `JS-` and `CXX-` look like + they could belong to any of the analyzers this repo runs, and a search for one turns up nothing unless + the searcher guesses the vendor first. The exception is a `skipcq:` pragma, whose grammar DeepSource + defines -- leave the code bare there, and let the surrounding comment carry the reason. + severity: ERROR diff --git a/config/semgrep/no-check-tool-mentions.yaml b/config/semgrep/no-check-tool-mentions.yaml index 85f6e5f3..43bdc0bb 100644 --- a/config/semgrep/no-check-tool-mentions.yaml +++ b/config/semgrep/no-check-tool-mentions.yaml @@ -59,6 +59,7 @@ rules: - "/config/conftest/policy/generated_trees/generated_trees.rego" # Enforces that mapping. - "/config/conftest/policy/generated_trees/generated_trees_test.rego" # Unit tests for it. - "/config/semgrep/no-*-mentions.yaml" # The mention bans, which cannot help but name what they ban. + - "/libs/test-helpers/src/lib.rs" # Carries a suppression pragma, which only works spelled out. pattern-regex: "(?i)ast-grep" message: >- Do not name the structural search tool outside its own ruleset and the configs that scope it. Pointing at @@ -212,11 +213,16 @@ rules: - "/.codacy.yaml" # Names it as the analyzer still covering what Codacy skips. - "/.github/workflows/coverage.yaml" # Reports coverage to it on CI. - "/config/semgrep/no-*-mentions.yaml" # The mention bans, which cannot help but name what they ban. - pattern-regex: "(?i)deepsource" + - "/config/semgrep/deepsource-codes-are-qualified.yaml" # Requires the name, so it has to write it. + # Unlike its siblings, this one permits the name where it qualifies one of the analyzer's codes. + # A bare code does not identify itself: `RS-`, `JS-` and `CXX-` could belong to any analyzer the repo + # runs, so the vendor in front is what makes the reference readable and searchable. The separate rule + # requiring that shape is what keeps this from being a way back in for prose about the tool. + pattern-regex: '(?i)deepsource(?! (RS|JS|CXX|CS|DOK|PYL|SCT|TYP)-[A-Z]{0,2}[0-9]{3,4}\b)' message: >- - Do not name this analyzer outside its own config and the pragmas it reads. A local rule mirroring one of - its findings names the code -- `RS-W1079`, `TYP-025`, `CXX-W2009` -- and stops there: the code identifies - the finding, survives a change of vendor, and is what a search will actually turn up. + Do not name this analyzer outside its own config, the pragmas it reads, and a reference to one of its + codes. Naming it in front of a code -- `DeepSource RS-W1079` -- is required, because the code alone does + not say whose it is. Naming it anywhere else describes the tool's reaction rather than the code. severity: ERROR - id: no-dprint-mentions @@ -319,8 +325,8 @@ rules: - "/config/jscpd.json" # Its own config. - "/config/jscpd-baseline.json" # The clone-fingerprint baseline it reads. - "/config/conftest/policy/jscpd/jscpd.rego" # Ratchets that baseline, and takes the tool as its package. - - "/config/conftest/policy/policy_tests/policy_tests.rego" # Names that policy path in its exception list. - - "/config/conftest/policy/policy_tests/policy_tests_test.rego" # Unit tests for it. + - "/config/conftest/policy/policy_pairing/policy_pairing.rego" # Names that policy path in its exception list. + - "/config/conftest/policy/policy_pairing/policy_pairing_test.rego" # Unit tests for it. - "/config/conftest/policy/generated_trees/generated_trees.rego" # Enforces the linter/tree mapping. - "/config/generated-trees.toml" # Maps each generated tree onto the exclusion key this tool needs. - "/config/semgrep/prefer-yaml-toml.yaml" # Allowlists its two files, which cannot be YAML or take comments. diff --git a/config/semgrep/no-type-comments.yaml b/config/semgrep/no-type-comments.yaml index 3ea19fe2..e6986109 100644 --- a/config/semgrep/no-type-comments.yaml +++ b/config/semgrep/no-type-comments.yaml @@ -18,6 +18,6 @@ rules: message: >- Legacy `# type:` comment. Use an inline annotation (`x: T = ...`, `def f(a: T) -> R:`) instead. The compiled Pyrefly type checker that backs `check:python` parses annotations but silently ignores legacy type comments, - so a broken one (an undefined name, a stray `--`) rots unseen -- exactly `TYP-025`. Annotations are + so a broken one (an undefined name, a stray `--`) rots unseen -- exactly `DeepSource TYP-025`. Annotations are the only form both tools check. `# type: ignore` (the suppression pragma) is exempt. severity: ERROR diff --git a/libs/edge-toolkit/tests/pipx_site_packages.rs b/libs/edge-toolkit/tests/pipx_site_packages.rs index 4e320d5a..e6a966f6 100644 --- a/libs/edge-toolkit/tests/pipx_site_packages.rs +++ b/libs/edge-toolkit/tests/pipx_site_packages.rs @@ -37,19 +37,17 @@ fn resolves_windows_pipx_venv_layout() { resolves_venv_layout("cowsay/Lib/site-packages"); } -/// Probe availability and run the lookup with `path` as the whole of `PATH`, reporting both. +/// Probe availability and run the lookup under whatever `PATH` the caller has arranged, reporting both. /// /// Both are read under one `PATH` so the pair can be compared: the lookup answers with an empty list for two /// quite different reasons -- mise was never found, or mise was found and its query failed -- and only the /// availability flag distinguishes them. Asserting the list alone would let a test pass while exercising the /// wrong path entirely. -fn availability_and_lookup(path: &str) -> (bool, Vec) { - et_test_helpers::temp_env::with_var("PATH", Some(path), || { - ( - edge_toolkit::config::mise_is_available(), - edge_toolkit::config::mise_python_site_packages(), - ) - }) +fn availability_and_lookup() -> (bool, Vec) { + ( + edge_toolkit::config::mise_is_available(), + edge_toolkit::config::mise_python_site_packages(), + ) } #[test] @@ -58,7 +56,7 @@ fn site_packages_lookup_is_empty_when_mise_is_missing() { // an empty list rather than panicking when there is no mise to ask. An empty PATH makes the availability // probe's spawn fail the same way it would on a deployment that never installed mise, so the function // returns before it tries to parse any tool list. - let (available, found) = availability_and_lookup(""); + let (available, found) = et_test_helpers::with_empty_path(availability_and_lookup); assert!(!available, "an empty PATH must hide mise from the availability probe"); assert!( found.is_empty(), @@ -74,7 +72,8 @@ fn site_packages_lookup_is_empty_when_mise_is_missing() { /// A real executable, compiled here by `rustc`, rather than a script: the probe spawns the bare name `mise`, /// which Windows resolves to `mise.exe` and nothing else, so a `mise.bat` on the same `PATH` is never found /// and the probe reports mise absent -- the very branch this fixture exists to get past. That is how the -/// windows-11-arm lane failed on commit aec4133097f57ad406fb16ad5231fafb31641e09 with +/// windows-11-arm lane failed on commit +/// with /// `the stand-in must answer --version, or this asserts the wrong branch` /// (). Compiling costs a second /// and needs `rustc` on `PATH`, which anything running `cargo test` already has; the one shape then serves @@ -109,26 +108,6 @@ fn write_fake_mise_that_fails_its_queries(dir: &TempDir) { .unwrap(); } -/// `PATH` with `dir` in front of the current value, so a stand-in there shadows the real tool. -/// -/// In front of rather than instead of: the probe runs under this `PATH`, and an executable the toolchain has -/// just built may still resolve part of its runtime through it. With `PATH` cut down to the stand-in's own -/// directory, the x64 Windows lanes (whose Rust host is `x86_64-pc-windows-gnullvm`) compiled the stand-in -/// and then reported mise absent -- `the stand-in must answer --version, or this asserts the wrong branch` -/// on commit c6c4fce73dd25aa58754963867ccf9523caae1bb at -/// -- while the arm64 lane, -/// hosted on `aarch64-pc-windows-msvc`, found it. Search order still puts `dir` first, which is all the -/// shadowing needs. -#[expect( - clippy::single_call_fn, - reason = "distinct fixture step beside the stand-in builder; kept separate so the PATH shape is explained once" -)] -fn path_with_first(dir: &std::path::Path) -> String { - let rest = std::env::var_os("PATH").unwrap_or_default(); - let joined = std::env::join_paths(std::iter::once(dir.to_path_buf()).chain(std::env::split_paths(&rest))).unwrap(); - joined.to_string_lossy().into_owned() -} - #[test] fn a_failing_tool_list_query_yields_no_paths() { // mise resolves and reports a version, so the availability probe passes, but the tool-list query exits @@ -138,7 +117,7 @@ fn a_failing_tool_list_query_yields_no_paths() { let dir = TempDir::new().unwrap(); write_fake_mise_that_fails_its_queries(&dir); - let (available, found) = availability_and_lookup(&path_with_first(dir.path())); + let (available, found) = et_test_helpers::with_path_prefix(dir.path(), availability_and_lookup); // Asserted first, and separately: an empty list is also what a *missing* mise produces, so without // pinning the stand-in as present this test would still pass if it were never found at all -- exercising diff --git a/libs/edge-toolkit/tests/registry.rs b/libs/edge-toolkit/tests/registry.rs index 27895981..b606a570 100644 --- a/libs/edge-toolkit/tests/registry.rs +++ b/libs/edge-toolkit/tests/registry.rs @@ -75,7 +75,7 @@ fn unknown_ids_fall_through_to_the_assign_and_no_op_paths() { // of it (llvm-cov merges an instantiation group's branch counts by taking the maximum, not the union), so // the no-op arm above covered here and the update arm covered only under the server's session type read as // one arm each, never both: `libs/edge-toolkit/src/ws_server.rs 9/10 branches` on commit - // c6c4fce73dd25aa58754963867ccf9523caae1bb at + // https://github.com/edge-toolkit/core/commit/c6c4fce73dd25aa58754963867ccf9523caae1bb at // https://github.com/edge-toolkit/core/actions/runs/35045764477/job/104635181514, while the lcov export, // which sums instantiations, showed every branch taken. registry.mark_disconnected("agent-fresh"); diff --git a/libs/path/tests/find.rs b/libs/path/tests/find.rs index 7e8da985..38abf5bc 100644 --- a/libs/path/tests/find.rs +++ b/libs/path/tests/find.rs @@ -56,7 +56,7 @@ fn repo_file_env_directives_name_the_var_the_file_and_both_rerun_triggers() { // stands rather than rewriting it to the platform separator, so on Windows the value is a mixed // `D:\checkout\services/ws-test-server/data/math1-input.json`, which every path API there accepts. Expecting // the separator to be rewritten failed on all three Windows lanes of commit - // 29dfe80a62ba7a27d8119c5b6332c3dbe2df815e with + // https://github.com/edge-toolkit/core/commit/29dfe80a62ba7a27d8119c5b6332c3dbe2df815e with // // unexpected rustc-env directive: // cargo:rustc-env=ET_PROBE_PATH=D:\a\core\core\services/ws-test-server/data/math1-input.json diff --git a/libs/test-helpers/Cargo.toml b/libs/test-helpers/Cargo.toml index b0596e6e..ba57c469 100644 --- a/libs/test-helpers/Cargo.toml +++ b/libs/test-helpers/Cargo.toml @@ -17,6 +17,8 @@ temp-env.workspace = true [dev-dependencies] command-error.workspace = true +fs-err.workspace = true +tempfile.workspace = true [lints] workspace = true diff --git a/libs/test-helpers/src/lib.rs b/libs/test-helpers/src/lib.rs index f0548a5b..18096071 100644 --- a/libs/test-helpers/src/lib.rs +++ b/libs/test-helpers/src/lib.rs @@ -35,6 +35,31 @@ where temp_env::with_var("PATH", Some(""), run) } +/// Run `run` with `dir` in front of `PATH`, so a stand-in there shadows the real tool. +/// +/// In front of rather than instead of: an executable the toolchain has just built may still resolve part of +/// its runtime through `PATH`. With `PATH` cut down to the stand-in's own directory, the x64 Windows lanes +/// (whose Rust host is `x86_64-pc-windows-gnullvm`) compiled a stand-in and then reported the real tool absent +/// -- `the stand-in must answer --version, or this asserts the wrong branch` on commit +/// at +/// -- while the arm64 lane, +/// hosted on `aarch64-pc-windows-msvc`, found it. Search order still puts `dir` first, which is all the +/// shadowing needs. +pub fn with_path_prefix(dir: &std::path::Path, run: Body) -> Out +where + Body: FnOnce() -> Out, +{ + // Reading the inherited `PATH` is the one thing this helper cannot avoid, and the reason it lives here + // rather than in each test: the value has to be handed back with `dir` in front, and nothing else carries + // the directories a freshly built stand-in needs to start. Suppressed on this line alone so the ban keeps + // applying to every other read in the crate, including a future one in this same function. + // ast-grep-ignore: no-std-env-var + let inherited = std::env::var_os("PATH").unwrap_or_default(); + let ordered = std::iter::once(dir.to_path_buf()).chain(std::env::split_paths(&inherited)); + let prefixed = std::env::join_paths(ordered).unwrap(); + temp_env::with_var("PATH", Some(&prefixed), run) +} + /// Reserve a free loopback TCP port, bound then released for the caller to claim. /// /// Same bind-`:0`-then-drop trick every test-support crate used to hand-roll; there is an inherent diff --git a/libs/test-helpers/tests/path_prefix.rs b/libs/test-helpers/tests/path_prefix.rs new file mode 100644 index 00000000..f392c737 --- /dev/null +++ b/libs/test-helpers/tests/path_prefix.rs @@ -0,0 +1,78 @@ +//! Covers `with_path_prefix`: a directory it puts in front of `PATH` resolves bare names, and only inside. +//! +//! Asserted by spawning rather than by inspecting the variable, because resolution is the whole contract -- +//! the helper exists so a stand-in shadows a real tool, and the OS's own program lookup is the only thing that +//! decides whether it does. Reading `PATH` back and matching on its text would assert the implementation. +#![cfg(test)] + +use std::process::Command; + +use command_error::CommandExt as _; +use fs_err as fs; +use tempfile::TempDir; + +/// Bare name the probe copy is invoked by, distinctive enough that nothing on a real `PATH` answers to it. +const PROBE_NAME: &str = "et-path-prefix-probe"; + +/// A directory holding a copy of the child-guard probe, named [`PROBE_NAME`]. +/// +/// Copied rather than compiled: the probe is already built for this crate's tests on every supported platform, +/// so reusing it keeps the fixture to a file copy and needs no toolchain at test time. The `.exe` suffix is +/// what makes the copy executable on Windows, where program lookup is extension-driven. +fn dir_holding_the_probe() -> TempDir { + let dir = TempDir::new().unwrap(); + let name = if cfg!(windows) { + format!("{PROBE_NAME}.exe") + } else { + PROBE_NAME.to_owned() + }; + let _bytes: u64 = fs::copy(env!("CARGO_BIN_EXE_child-guard-probe"), dir.path().join(name)).unwrap(); + dir +} + +/// Spawn the probe by bare name, reporting whether program lookup found it. +/// +/// `0` so a copy that is found exits immediately instead of holding the test open. +fn probe_resolves() -> bool { + Command::new(PROBE_NAME).arg("0").output().is_ok() +} + +#[test] +fn a_prefixed_directory_resolves_bare_names_inside_the_closure() { + let dir = dir_holding_the_probe(); + + assert!( + et_test_helpers::with_path_prefix(dir.path(), probe_resolves), + "a directory put in front of PATH must make its executables resolve by bare name" + ); +} + +#[test] +fn the_prefix_is_gone_once_the_closure_returns() { + let dir = dir_holding_the_probe(); + + // Asserted first so a failure here reads as "the prefix never worked" rather than "it was not restored": + // the check below passes trivially if the directory was never on PATH to begin with. + assert!( + et_test_helpers::with_path_prefix(dir.path(), probe_resolves), + "the probe must resolve inside the closure, or the restoration check below proves nothing" + ); + assert!( + !probe_resolves(), + "PATH must be restored when the closure returns, so the prefixed directory stops resolving" + ); +} + +#[test] +fn the_inherited_path_survives_behind_the_prefixed_directory() { + let dir = dir_holding_the_probe(); + + // Prefixing rather than replacing is the whole reason this helper exists: an executable the toolchain has + // just built may still resolve part of its runtime through the inherited PATH, and cutting PATH down to + // the stand-in's own directory left one unable to start on the x64 Windows lanes. A mise-managed tool is + // what proves the inherited entries are still there -- every sanctioned environment has them on PATH, so + // a failure here is a real regression rather than a machine that happens to lack the tool. + et_test_helpers::with_path_prefix(dir.path(), || { + let _ran: std::process::Output = Command::new("coreutils").arg("--version").output_checked().unwrap(); + }); +} diff --git a/libs/web/src/lib.rs b/libs/web/src/lib.rs index e6f88dd4..5f5cdc66 100644 --- a/libs/web/src/lib.rs +++ b/libs/web/src/lib.rs @@ -8,8 +8,8 @@ pub const SENSOR_PERMISSION_GRANTED: &str = "granted"; /// Discard `value`, marking a `Result` (or other `#[must_use]`) as intentionally ignored. /// -/// The workspace denies `let_underscore*` and `unused_results`, and `RS-E1021` flags `drop()` on a non-`Drop` -/// type (e.g. `Result`), so neither `let _ = expr` nor `drop(expr)` is available for discarding one. +/// The workspace denies `let_underscore*` and `unused_results`, and `DeepSource RS-E1021` flags `drop()` on a +/// non-`Drop` type (e.g. `Result`), so neither `let _ = expr` nor `drop(expr)` is available for discarding one. /// Passing the value here consumes it -- satisfying `must_use` / `unused_results` -- via neither. Intended for /// best-effort JS DOM calls in `()`-returning closures and event handlers where the error is deliberately dropped. pub fn ignore(_value: T) {} diff --git a/libs/ws-runner-common/tests/register_frames.rs b/libs/ws-runner-common/tests/register_frames.rs index 770488cc..fce3172f 100644 --- a/libs/ws-runner-common/tests/register_frames.rs +++ b/libs/ws-runner-common/tests/register_frames.rs @@ -85,7 +85,8 @@ async fn a_socket_closed_before_the_ack_reports_connection_closed() { // down with a reset instead of a FIN; Linux still hands the client its end-of-stream, but Windows // surfaces the reset first, so the client saw // `WebSocket(Io(Os { code: 10053, kind: ConnectionAborted, ... }))` instead of `ConnectionClosed` - // on the windows-11-arm lane at commit c6c4fce73dd25aa58754963867ccf9523caae1bb + // on the windows-11-arm lane at commit + // https://github.com/edge-toolkit/core/commit/c6c4fce73dd25aa58754963867ccf9523caae1bb // (). while let Some(Ok(_frame)) = socket.next().await {} } diff --git a/ruff.toml b/ruff.toml index f160c9d6..8049fc87 100644 --- a/ruff.toml +++ b/ruff.toml @@ -24,8 +24,8 @@ line-ending = "lf" [lint] # On top of the default rule set (E, F). Codes are kept alphabetically sorted. # "ARG" -- flake8-unused-arguments: unused function/method/lambda arguments. This is the local equivalent of -# `PYL-W0613`; ruff honours the underscore-prefix convention (`dummy-variable-rgx`), so a param kept -# only to satisfy a fixed callback signature (the pyo3-runner `init`/`on_text_frame` hooks) is silenced by +# `DeepSource PYL-W0613`; ruff honours the underscore-prefix convention (`dummy-variable-rgx`), so a param +# kept only to satisfy a fixed callback signature (the pyo3-runner `init`/`on_text_frame` hooks) is silenced by # renaming it `_send` / `_text` rather than deleting it. # "B" -- flake8-bugbear: likely bugs (mutable defaults, `zip` without `strict=`, loop-variable shadowing, ...). # "C4" -- flake8-comprehensions: unnecessary or unpythonic comprehensions and generator calls. diff --git a/services/storage/tests/s3_backend.rs b/services/storage/tests/s3_backend.rs index 337a8f08..2ffba34e 100644 --- a/services/storage/tests/s3_backend.rs +++ b/services/storage/tests/s3_backend.rs @@ -66,7 +66,7 @@ fn registry_with_agent(agent_id: &str) -> AgentRegistry<()> { /// Both output streams land in `log_path`, and every server-dependent assertion replays that file on failure. /// The beta server can fail entirely server-side -- a CI run answered the PUT below with a bare 500 -- and with /// its output discarded such a failure is undiagnosable from the test output alone. Observed on commit -/// d8ad8cb99cf58a1e505cdbebc5b94f9893532b4f at +/// at /// (build (ubuntu:26.04) rerun). #[expect( clippy::single_call_fn, @@ -130,7 +130,7 @@ fn aws_env(port: u16) -> Vec<(&'static str, Option)> { // Fixed upstream in rustfs PR https://github.com/rustfs/rustfs/pull/5663 (guarded dirs now share FILE_SHARE_WRITE // too; related issue https://github.com/rustfs/rustfs/issues/5419), merged 2026-08-03 -- AFTER the beta.12 tag // (2026-07-30), so no released rustfs contains it yet. Observed on our commit -// 59f4ab6368af5c6824dafe2e11c9c598f16f5334 at +// https://github.com/edge-toolkit/core/commit/59f4ab6368af5c6824dafe2e11c9c598f16f5334 at // https://github.com/edge-toolkit/core/actions/runs/30980282749/job/92223040435 (default (windows-latest, 120)). // Re-enable by dropping this attribute once the `rustfs` mise tool is bumped to a release that includes #5663. #[actix_rt::test] diff --git a/services/ws-server/Dockerfile b/services/ws-server/Dockerfile index 4b73f6e4..0a98ecbc 100644 --- a/services/ws-server/Dockerfile +++ b/services/ws-server/Dockerfile @@ -86,8 +86,8 @@ EOF # The browser client is built in its own layer, deliberately not folded into the cargo build above. # The two compile different targets from different inputs, so keeping them apart means editing the agent does # not invalidate the server's release build and vice versa. Three analyzers raise that split as the same -# consecutive-RUN anti-pattern under three different codes, so it takes three pragmas: `DOK-W1001` -# (`DOK-DL3059` is not a code anything here issues, so it suppressed nothing), and hadolint's DL3059 -- named +# consecutive-RUN anti-pattern under three different codes, so it takes three pragmas: `DeepSource DOK-W1001` +# (`DeepSource DOK-DL3059` is not a code it issues, so it suppressed nothing), and hadolint's DL3059 -- named # inline here rather than left to config/hadolint.yaml's ignore list, because one of the three runs its own # hadolint and reads neither that file nor a skipcq. # skipcq: DOK-W1001 diff --git a/services/ws-web-runner/mingw-shim/msvc_crt_shim.c b/services/ws-web-runner/mingw-shim/msvc_crt_shim.c index f2bde811..d439cdf9 100644 --- a/services/ws-web-runner/mingw-shim/msvc_crt_shim.c +++ b/services/ws-web-runner/mingw-shim/msvc_crt_shim.c @@ -9,7 +9,8 @@ * non-const storage, to match the archive's ABI -- renaming or const-ing them would break the link. Each * therefore carries an inline `// NOLINT` for bugprone-reserved-identifier / cert-dcl37-c (plus * cppcoreguidelines-avoid-non-const-global-variables on the globals) -- the narrowest scope, and honored by - * every analyzer that reports those, `CXX-E2000` (reserved identifier) and `CXX-W2009` (non-const) included. */ + * every analyzer that reports those, `DeepSource CXX-E2000` (reserved identifier) and `DeepSource CXX-W2009` + * (non-const) included. */ #include #include diff --git a/utilities/cli/src/deployment_types/k3s.rs b/utilities/cli/src/deployment_types/k3s.rs index 1e2c621f..983340e9 100644 --- a/utilities/cli/src/deployment_types/k3s.rs +++ b/utilities/cli/src/deployment_types/k3s.rs @@ -330,8 +330,8 @@ fn volume_mount(name: &str, path: &str) -> VolumeMount { /// /// `path` is a container mount point in the manifest this generator emits, not a path anything in this process /// opens, so a caller passing `/tmp` is naming the containerised program's own temp directory rather than -/// creating a world-writable file on the host. `RS-S1003` reads the literal as the latter, which is why every -/// call site passing `/tmp` carries a `skipcq` for that one rule. +/// creating a world-writable file on the host. `DeepSource RS-S1003` reads the literal as the latter, which is +/// why every call site passing `/tmp` carries a `skipcq` for that one rule. fn scratch(name: &str, path: &str) -> (VolumeMount, Volume) { let volume = Volume { empty_dir: Some(EmptyDirVolumeSource::default()), diff --git a/utilities/cli/tests/scenario_runners.rs b/utilities/cli/tests/scenario_runners.rs index c56c8d4b..b08e7f80 100644 --- a/utilities/cli/tests/scenario_runners.rs +++ b/utilities/cli/tests/scenario_runners.rs @@ -157,7 +157,7 @@ fn stored_model(storage_dir: &std::path::Path) -> Option<(f64, f64)> { // test drives runs `cargo run --quiet -p et-ws-web-runner`. Asking Windows to build the one crate CI has decided // to skip there is not a gap this test can close; that is the gnullvm rusty_v8 work, tracked separately. // -// Observed on commit 809600492822c600f14c19a35b1ff50bef687c23 at +// Observed on commit https://github.com/edge-toolkit/core/commit/809600492822c600f14c19a35b1ff50bef687c23 at // https://github.com/edge-toolkit/core/actions/runs/34108671291/job/101699520407 as // FAIL + LEAK [ 363.640s] (177/177) et-cli::scenario_runners math1_scenario_generated_runner_tasks_compute_the_model // no math1-output.json appeared in any storage bucket under C:\Users\RUNNER~1\AppData\Local\Temp\.tmpKcKuqX @@ -192,14 +192,15 @@ fn math1_scenario_generated_runner_tasks_compute_the_model() { // import -- an async host call awaited on a wasmtime fiber. The reading that fits is tokio's thread-local // runtime context not surviving the fiber stack switch under that target's TLS model. // -// It is the target env, not Windows. On commit 5998313315c491a6abffb7c1507e4adc4a4f3559 `wasi-math1` passed on +// It is the target env, not Windows. On commit +// https://github.com/edge-toolkit/core/commit/5998313315c491a6abffb7c1507e4adc4a4f3559 `wasi-math1` passed on // `gnullvm` (which `config.windows.toml` actually builds) in 426s and on `msvc` in 443s, while `gnu` failed // twice with the identical signature -- 644s at // https://github.com/edge-toolkit/core/actions/runs/34187567425/job/101938939834 and 591s on the re-run at // https://github.com/edge-toolkit/core/actions/runs/34187567425/job/101958844541 -- so it is reproducible // rather than a flake. `pyo3-math1` was left ungated at that point because fail-fast had cancelled it before // it ran on `gnu`; it then reproduced the same abort there at 579s on commit -// 29dfe80a62ba7a27d8119c5b6332c3dbe2df815e, +// https://github.com/edge-toolkit/core/commit/29dfe80a62ba7a27d8119c5b6332c3dbe2df815e, // https://github.com/edge-toolkit/core/actions/runs/34211905976/job/102014621776, having passed on `gnullvm` in // 472s and `msvc` in 458s. Its captured output puts the fault squarely on the trigger: the pyo3 twin registers // as an agent and idles to its own timeout, while the wasi trigger aborts as above. diff --git a/utilities/repo-check/Cargo.toml b/utilities/repo-check/Cargo.toml new file mode 100644 index 00000000..775a0932 --- /dev/null +++ b/utilities/repo-check/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "et-repo-check" +publish = true +description = "Repository-wide checks that need to read git itself" +version = "0.1.0" +edition.workspace = true +license.workspace = true +repository.workspace = true + +[[bin]] +doctest = false +name = "et-repo-check" +path = "src/main.rs" +test = false + +[dependencies] +command-error.workspace = true +fs-err.workspace = true +regex = { workspace = true, features = ["perf", "std", "unicode-perl"] } +thiserror.workspace = true + +[lints] +workspace = true diff --git a/utilities/repo-check/README.md b/utilities/repo-check/README.md new file mode 100644 index 00000000..23c55b20 --- /dev/null +++ b/utilities/repo-check/README.md @@ -0,0 +1,22 @@ +# et-repo-check + +Repository-wide checks that need to read git itself, rather than judge a file on its own. Anything one of the +repo's linters can express belongs there instead; this is for the questions they cannot answer, because the +answer lives in the object database or the commit graph rather than in any file. + +Run it with: + +```bash +mise run repo-check +``` + +It takes no arguments and reads only the checkout it runs in, so it needs no network and no credentials. It +does need real history: a shallow clone resolves almost nothing. + +## Checks + +- **commit-hashes** -- every full 40-character commit hash written into a tracked file must name a commit this + repository has. Each failure prints the file and line to edit, plus the web URL to write instead. + +To add another, put it in its own module with a `run(&[String]) -> anyhow::Result` returning its failure +count, and call it from `main`. diff --git a/utilities/repo-check/src/commit_hashes.rs b/utilities/repo-check/src/commit_hashes.rs new file mode 100644 index 00000000..1270409c --- /dev/null +++ b/utilities/repo-check/src/commit_hashes.rs @@ -0,0 +1,139 @@ +//! Rejects a commit hash written into a tracked file when this repository's history has no such commit. +//! +//! The repo's workaround rules ask every papered-over failure to record the commit it was observed on, so hashes +//! accumulate in comments across configs, Dockerfiles, workflows and prose. A hash is only evidence while it resolves: +//! once it does not, the reader cannot tell whether the note describes something real, and the pointer costs more than +//! it gives. +//! +//! A hash that fails here is usually not wrong. Pull requests merge by squash, so a commit written down while a branch +//! was in flight stops existing the moment it lands, even though GitHub still serves it forever. The fix is therefore +//! to rewrite the reference as the web URL this prints, which survives the squash, rather than to hunt for a +//! replacement hash. +//! +//! Membership is decided by `git rev-list --all`, which is reachability from a ref, and deliberately not by asking +//! whether the object exists. The two differ exactly where it matters: a workstation that once fetched a pull request +//! keeps those objects long after the branch is deleted, so `git cat-file` still answers `commit` for a hash that a +//! fresh clone cannot resolve at any depth. Judging by object presence therefore passes locally and fails on CI, which +//! is the wrong way round for a check whose whole job is to be trustworthy about what a reader will find. +//! +//! Two shapes are deliberately not checked. A hash already written as a `/commit/` URL is a reference to whatever +//! repository the URL names, which may not be this one. A hash written as a quoted string value is a pin some code +//! consumes -- an upstream revision, a gist revision -- and naming a foreign object is the whole point of it. Both +//! would otherwise fail here for being exactly what they are meant to be. + +use std::collections::{BTreeMap, HashSet}; +use std::process::Command; + +use command_error::CommandExt as _; +use fs_err as fs; +use regex::Regex; + +use crate::error::Error; + +/// Marks a hash already written as a web reference, which may name a repository other than this one. +const URL_MARKER: &str = "/commit/"; + +/// Where one hash was written, so a failure can name the line a reader has to edit. +struct Site { + path: String, + line: usize, +} + +/// Whether this hash is one of the two shapes that name something outside this repository on purpose. +#[expect( + clippy::single_call_fn, + reason = "distinct step of the scan; kept separate for readability" +)] +fn is_foreign(line: &str, start: usize, end: usize) -> bool { + let before = line.get(..start).unwrap_or_default(); + let after = line.get(end..).unwrap_or_default(); + before.ends_with(URL_MARKER) || (before.ends_with('"') && after.starts_with('"')) +} + +/// Every bare hash in the tracked set, mapped to the places it is written. +#[expect( + clippy::single_call_fn, + reason = "distinct step of the scan; kept separate for readability" +)] +fn collect(files: &[String]) -> Result>, Error> { + let pattern = Regex::new(r"\b[0-9a-f]{40}\b")?; + let mut found: BTreeMap> = BTreeMap::new(); + for path in files { + // A file that does not read as UTF-8 is binary, and carries no prose to check. + let Ok(text) = fs::read_to_string(path) else { + continue; + }; + for (offset, line) in text.lines().enumerate() { + for hit in pattern.find_iter(line) { + if is_foreign(line, hit.start(), hit.end()) { + continue; + } + let site = Site { + path: path.clone(), + line: offset.saturating_add(1), + }; + found.entry(hit.as_str().to_owned()).or_default().push(site); + } + } + } + Ok(found) +} + +/// Every commit on the permanent history, which is what "in the history" has to mean for this to be reproducible. +/// +/// `origin/main` rather than `--all`, because `--all` answers differently in each place it runs: a workstation +/// keeps remote-tracking refs for branches the remote deleted long ago, so it resolves hashes a fresh clone +/// cannot, and the check then passes locally and fails on CI. `origin/main` is the one ref every checkout agrees +/// on. It is also the honest bar: a hash that is only reachable from the branch in flight stops resolving the +/// moment that branch squash-merges, so accepting it now would just defer the failure. +#[expect( + clippy::single_call_fn, + reason = "distinct step of the scan; kept separate for readability" +)] +fn permanent_commits() -> Result, Error> { + let listing = Command::new("git").args(["rev-list", "origin/main"]).output_checked()?; + let text = String::from_utf8(listing.stdout)?; + Ok(text.lines().map(str::to_owned).collect()) +} + +/// The `owner/repo` this checkout pushes to, so the suggested replacement URL points at the right place. +#[expect( + clippy::single_call_fn, + reason = "distinct step of the report; kept separate for readability" +)] +fn origin_slug() -> Option { + let remote = Command::new("git") + .args(["remote", "get-url", "origin"]) + .output_checked() + .ok()?; + let url = String::from_utf8(remote.stdout).ok()?; + let trimmed = url.trim().trim_end_matches(".git"); + let tail = trimmed + .rsplit_once("github.com") + .map(|(_, rest)| rest.trim_start_matches([':', '/']))?; + Some(tail.to_owned()) +} + +/// Reports every unreachable hash and returns how many there were. +pub(crate) fn run(files: &[String]) -> Result { + let hashes = collect(files)?; + let known = permanent_commits()?; + let missing: Vec<&String> = hashes.keys().filter(|hash| !known.contains(*hash)).collect(); + if missing.is_empty() { + println!("repo-check commit-hashes: {} hashes, all reachable", hashes.len()); + return Ok(0); + } + let slug = origin_slug().unwrap_or_else(|| "/".to_owned()); + for hash in &missing { + for site in hashes.get(*hash).map(Vec::as_slice).unwrap_or_default() { + println!("{}:{}: no commit {hash} in this repository", site.path, site.line); + } + println!(" write it as https://github.com/{slug}/commit/{hash} if the commit is on a merged branch"); + } + println!( + "repo-check commit-hashes: {} of {} hashes are unreachable", + missing.len(), + hashes.len() + ); + Ok(missing.len()) +} diff --git a/utilities/repo-check/src/error.rs b/utilities/repo-check/src/error.rs new file mode 100644 index 00000000..670223ba --- /dev/null +++ b/utilities/repo-check/src/error.rs @@ -0,0 +1,21 @@ +//! The crate's error type and its foreign-error conversions. + +/// Errors raised by `et-repo-check`. +#[derive(Debug, thiserror::Error)] +#[expect( + clippy::exhaustive_enums, + clippy::error_impl_error, + reason = "one internal binary with no SemVer surface; every check returns this type and adds variants with itself" +)] +pub enum Error { + /// Carries the count so the process exits non-zero once, after every check has had its say. + #[error("repo-check: {0} problem(s) found")] + Failures(usize), + + #[error(transparent)] + Command(#[from] command_error::Error), + #[error(transparent)] + Regex(#[from] regex::Error), + #[error(transparent)] + Utf8(#[from] std::string::FromUtf8Error), +} diff --git a/utilities/repo-check/src/main.rs b/utilities/repo-check/src/main.rs new file mode 100644 index 00000000..8bcff8b8 --- /dev/null +++ b/utilities/repo-check/src/main.rs @@ -0,0 +1,42 @@ +//! Repository-wide checks that need to read git itself, which no file-at-a-time linter can do. +//! +//! The repo's linters each read one file and judge it alone. That covers almost everything this repo wants to +//! enforce, and a rule belongs there whenever it can be written there. What is left over is the small set of +//! questions whose answer lives in the object database or the commit graph rather than in any file, and this +//! binary is where those go. +//! +//! Every check runs rather than stopping at the first failure, so one run reports the whole picture instead of +//! revealing the next problem only after the previous one is fixed. + +use std::process::Command; + +use command_error::CommandExt as _; + +mod commit_hashes; +mod error; + +use self::error::Error; + +/// Every path git tracks, which is the same set the external analyzers see. +#[expect( + clippy::single_call_fn, + reason = "shared input every check reads; kept separate from any one of them" +)] +fn tracked_files() -> Result, Error> { + let listing = Command::new("git").args(["ls-files", "-z"]).output_checked()?; + let text = String::from_utf8(listing.stdout)?; + Ok(text + .split('\0') + .filter(|path| !path.is_empty()) + .map(str::to_owned) + .collect()) +} + +fn main() -> Result<(), Error> { + let files = tracked_files()?; + let failures = commit_hashes::run(&files)?; + if failures == 0 { + return Ok(()); + } + Err(Error::Failures(failures)) +} From cd5f74852125610e8ccf2075c054348114dadb22 Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Wed, 16 Sep 2026 19:41:27 +0800 Subject: [PATCH 12/24] fixes --- .../rules/gha-check-lane-fetch-depth-0.yaml | 27 +++++++++++++++++++ utilities/repo-check/src/commit_hashes.rs | 20 +++++++++++--- 2 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 config/ast-grep/rules/gha-check-lane-fetch-depth-0.yaml diff --git a/config/ast-grep/rules/gha-check-lane-fetch-depth-0.yaml b/config/ast-grep/rules/gha-check-lane-fetch-depth-0.yaml new file mode 100644 index 00000000..35ac5621 --- /dev/null +++ b/config/ast-grep/rules/gha-check-lane-fetch-depth-0.yaml @@ -0,0 +1,27 @@ +id: gha-check-lane-fetch-depth-0 +language: yaml +severity: error +message: | + The check lane's `actions/checkout@...` step must declare `fetch-depth: 0` in its `with:` block. + Two of the checks this lane runs read the commit graph rather than the worktree. At depth 1 the only commit that + exists is the tip, so every commit hash written into a tracked file resolves to nothing and the lane fails on a repo + that is perfectly fine -- while the branch-freshness check, having no `origin/main` to compare against, quietly skips + itself and reports success. The history is a few MiB, so fetching all of it costs close to nothing here. +files: + - .github/workflows/check.yaml +# Match every `uses: actions/checkout@...` whose surrounding step body lacks `fetch-depth: 0`. +# Same shape as the depth-1 rule this one inverts: the `@$V` portion of the action ref is not a separable meta-var +# (it is part of a single plain_scalar token), which is why the action is matched with a `pattern:` plus a +# `field-regex` rather than one pattern. +rule: + all: + - pattern: "uses: $X" + - has: + field: value + regex: "^actions/checkout@" + not: + inside: + kind: block_mapping + has: + pattern: "fetch-depth: 0" + stopBy: end diff --git a/utilities/repo-check/src/commit_hashes.rs b/utilities/repo-check/src/commit_hashes.rs index 1270409c..8ffec509 100644 --- a/utilities/repo-check/src/commit_hashes.rs +++ b/utilities/repo-check/src/commit_hashes.rs @@ -90,10 +90,21 @@ fn collect(files: &[String]) -> Result>, Error> { clippy::single_call_fn, reason = "distinct step of the scan; kept separate for readability" )] -fn permanent_commits() -> Result, Error> { +/// `None` where the ref is absent, which is a checkout this check cannot speak about rather than a failure. +/// The docker image builds copy the tree in and run the battery inside the container, where there is no remote +/// and so no `origin/main`; a hard error there would fail a lane over the shape of its checkout instead of over +/// anything in the repo. +fn permanent_commits() -> Result>, Error> { + if Command::new("git") + .args(["rev-parse", "--verify", "--quiet", "origin/main"]) + .output_checked() + .is_err() + { + return Ok(None); + } let listing = Command::new("git").args(["rev-list", "origin/main"]).output_checked()?; let text = String::from_utf8(listing.stdout)?; - Ok(text.lines().map(str::to_owned).collect()) + Ok(Some(text.lines().map(str::to_owned).collect())) } /// The `owner/repo` this checkout pushes to, so the suggested replacement URL points at the right place. @@ -117,7 +128,10 @@ fn origin_slug() -> Option { /// Reports every unreachable hash and returns how many there were. pub(crate) fn run(files: &[String]) -> Result { let hashes = collect(files)?; - let known = permanent_commits()?; + let Some(known) = permanent_commits()? else { + println!("repo-check commit-hashes: no origin/main to judge against in this checkout; skipping"); + return Ok(0); + }; let missing: Vec<&String> = hashes.keys().filter(|hash| !known.contains(*hash)).collect(); if missing.is_empty() { println!("repo-check commit-hashes: {} hashes, all reachable", hashes.len()); From 09c7e448317f23c60095cbdf90756eb9425741ea Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Wed, 16 Sep 2026 21:18:54 +0800 Subject: [PATCH 13/24] fix windows docker --- .mise/config.windows.toml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.mise/config.windows.toml b/.mise/config.windows.toml index 95de701c..e6ada795 100644 --- a/.mise/config.windows.toml +++ b/.mise/config.windows.toml @@ -154,6 +154,15 @@ py3_win_fwd = "{{ vars.a_mise_installs_fwd }}/python/3.13.14/python.exe" # ORT_LIB_LOCATION in config.toml [env] picks this on Windows. Version segment must match the # [tools] pin (conftest's version_drift enforces this). ort_loc_win = '{{ vars.mise_installs }}\github-microsoft-onnxruntime\1.22.0' +# The same install's DLL dir, prepended by the [env] `_.path` below so a binary linking ORT can start. +# ORT_LIB_LOCATION only tells the build where to link from; at load time Windows resolves onnxruntime.dll +# through the classic search, which reaches PATH but not the mise install dir. Until a test linked the +# et-ws-wasi-runner library nothing exercised that -- the runner binary is built but never started in the +# image -- so the servercore container aborted test enumeration outright: +# error: creating test list failed +# for `et-ws-wasi-runner::ws_backend`, command `...\ws_backend-.exe --list --format terse` +# aborted with code 0xc0000135: The specified module could not be found. (os error 126) +ort_bin_win = '{{ vars.ort_loc_win }}\lib' # llvm-mingw's bin dir (the github:mstorsjo/llvm-mingw install above). # The linker/dlltool [env] vars below point at absolute exes in here rather than bare names because mise's # cargo backend doesn't activate llvm-mingw onto the cargo: source-build subprocess's PATH: a bare `clang` @@ -284,6 +293,7 @@ _.path = [ "{{ vars.m2_gnupg_bin }}", "{{ vars.m2_make_bin }}", "{{ vars.maven_bin }}", + "{{ vars.ort_bin_win }}", "{{ vars.win_libclang }}", ] From a9817a998e4cf773be23eb944d8358e9ae0d70e6 Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Thu, 17 Sep 2026 00:58:06 +0800 Subject: [PATCH 14/24] debug --- utilities/cli/tests/scenario_runners.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/utilities/cli/tests/scenario_runners.rs b/utilities/cli/tests/scenario_runners.rs index b08e7f80..7c3ac0cf 100644 --- a/utilities/cli/tests/scenario_runners.rs +++ b/utilities/cli/tests/scenario_runners.rs @@ -296,15 +296,23 @@ fn run_scenario(scenario: &str, twin_task: &str, twin_crate: &str, trigger_crate let trigger_exited = trigger.guard.wait_for_exit(RUNNER_EXIT_TIMEOUT); let Some((weight, bias)) = model else { + // The two exit flags are reported here, not just asserted on the happy path below. + // A runner that writes nothing is the hard case to read: empty output alone cannot distinguish a + // process that died before it could log from one that started and sat idle, and the assertions that + // would have said which are never reached once this branch panics. panic!( concat!( "{}: no math1-output.json appeared in any storage bucket under {}\n", + "exited within RUNNER_EXIT_TIMEOUT: {}={}, math1-trigger={}\n", "--- {} stdout ---\n{}\n--- {} stderr ---\n{}\n", "--- math1-trigger stdout ---\n{}\n--- math1-trigger stderr ---\n{}" ), scenario, storage_dir.display(), twin_task, + twin_exited, + trigger_exited, + twin_task, captured(&twin.stdout), twin_task, captured(&twin.stderr), From c061181cc37d0d7816d2d3f2684e978f542330bb Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Thu, 17 Sep 2026 10:00:06 +0800 Subject: [PATCH 15/24] Windows fix --- .mise/config.coverage.toml | 6 +- .mise/config.maint.toml | 8 +- .mise/config.toml | 24 ++--- config/conftest/policy/mise/mise.rego | 48 ++++++++++ config/conftest/policy/mise/mise_test.rego | 52 ++++++++++ config/semgrep/no-split-url-in-comment.yaml | 33 +++++++ libs/test-helpers/src/lib.rs | 11 ++- libs/test-helpers/tests/child_guard.rs | 35 ++++++- services/ws-test-server/src/lib.rs | 85 +++++++++++++++- services/ws-test-server/tests/helpers.rs | 79 ++++++++++++++- utilities/cli/tests/scenario_runners.rs | 101 ++++++++++++++++---- 11 files changed, 442 insertions(+), 40 deletions(-) create mode 100644 config/semgrep/no-split-url-in-comment.yaml diff --git a/.mise/config.coverage.toml b/.mise/config.coverage.toml index 96d3036f..b85ba758 100644 --- a/.mise/config.coverage.toml +++ b/.mise/config.coverage.toml @@ -192,19 +192,19 @@ description = "Assert every named libs/ crate is at 100% branch coverage in an l hide = true # The whole assertion is one jaq query over llvm-cov's own JSON, with no parsing of our own. run = """ -report="$usage_report" +report="$USAGE_REPORT" if [ ! -f "$report" ]; then echo "cov-branch-assert: no coverage report at $report" >&2 echo "Run the coverage task that produces it first; a missing report is a failure, not a skip." >&2 exit 1 fi -shortfall="$(jaq -r --arg libs "$usage_libs" -f .mise/cov-branch-assert.jq "$report")" +shortfall="$(jaq -r --arg libs "$USAGE_LIBS" -f .mise/cov-branch-assert.jq "$report")" if [ -n "$shortfall" ]; then echo "cov-branch-assert: libs/ must be at 100% branch coverage, and is not:" >&2 echo "$shortfall" >&2 exit 1 fi -echo "cov-branch-assert: 100% branch coverage across $usage_libs" +echo "cov-branch-assert: 100% branch coverage across $USAGE_LIBS" """ shell = "{{ vars.task_shell }}" usage = """ diff --git a/.mise/config.maint.toml b/.mise/config.maint.toml index e9223b6a..6ff8d18b 100644 --- a/.mise/config.maint.toml +++ b/.mise/config.maint.toml @@ -738,19 +738,19 @@ per_batch=5 # `set --` then `$#` counts the array without the `${` + `#` pair, which mise's Tera pass reads as a comment. set -- "${crates[@]}" count=$# -start=$(((usage_batch - 1) * per_batch)) +start=$(((USAGE_BATCH - 1) * per_batch)) if [ "$start" -ge "$count" ]; then - echo "batch $usage_batch is past the end: $count crates, $per_batch per batch" >&2 + echo "batch $USAGE_BATCH is past the end: $count crates, $per_batch per batch" >&2 exit 1 fi args=(release publish) for crate in "${crates[@]:start:per_batch}"; do args+=(-p "$crate") done -if [ "${usage_execute:-}" = "true" ]; then +if [ "${USAGE_EXECUTE:-}" = "true" ]; then args+=(--execute) fi -echo "batch $usage_batch: ${crates[*]:start:per_batch}" +echo "batch $USAGE_BATCH: ${crates[*]:start:per_batch}" cargo "${args[@]}" """ shell = "{{ vars.task_shell }}" diff --git a/.mise/config.toml b/.mise/config.toml index e4babe0e..9357d9e2 100644 --- a/.mise/config.toml +++ b/.mise/config.toml @@ -983,18 +983,20 @@ description = "Reflow Rust comment prose to the 120-column width with parfit (in # table headers into the surrounding comment prose, so TOML, YAML and markdown stay hand-wrapped. The width matches # .rustfmt.toml's max_width. The skip regexes pass rustdoc `# Heading` lines and list items (`*`, `-`, `1.`) through # verbatim -- without them parfit folds `# Panics` into the sentence below it and a bulleted list into one paragraph. -# It also breaks a line after a hyphen, inside backticks included, and has no switch for that, so check the diff for -# comment lines ending in `-` and rejoin the word by hand. Given file paths, reflows just those; with none, every -# tracked Rust file except the generator-owned ones. The usage-spec is what carries the paths: mise appends bare task -# arguments to the body text, which the eval in task-shell.sh then rejects, whereas a `var` arg arrives space-joined -# in $usage_file for xargs to split. +# It also breaks a line after a hyphen, inside backticks and URLs included, and has no switch for that: it reflows +# a whole paragraph before re-breaking it, so a skip regex cannot protect a URL that starts mid-paragraph. Check +# the diff for comment lines ending in `-` and rejoin the word by hand; a split +# `https://github.com/edge-toolkit/core/...` leaves a dead link behind. +# Given file paths, reflows just those; with none, every tracked Rust file except the generator-owned ones. The +# usage-spec is what carries the paths: mise appends bare task arguments to the body text, which the eval in +# task-shell.sh then rejects, whereas a `var` arg arrives space-joined in $USAGE_FILE for xargs to split. run = """ heading='^\\s*//[/!]?\\s*#' item='^\\s*//[/!]?\\s*([-*]|[0-9]+\\.)\\s' -if [ -z "${usage_file:-}" ]; then +if [ -z "${USAGE_FILE:-}" ]; then git ls-files '*.rs' ':!generated/**' ':!services/ws-wasi-runner/src/bindings.rs' else - printf '%s\\n' "$usage_file" + printf '%s\\n' "$USAGE_FILE" fi | xargs -r parfit -w 120 -s "$heading" -s "$item" """ shell = "{{ vars.task_shell }}" @@ -1011,9 +1013,9 @@ run = "cargo clippy --keep-going --workspace --tests {{ vars.cargo_ws_excludes } alias = ["clippy-pkg"] description = "cargo clippy on a single workspace package (narrow inner-loop check)" # Same flags as cargo-clippy-check minus --workspace. -# Usage-spec captures the package name and exposes it as $usage_package; a missing arg fails up front with +# Usage-spec captures the package name and exposes it as $USAGE_PACKAGE; a missing arg fails up front with # mise's auto-generated usage message instead of clippy's generic "--package not found" error. -run = 'cargo clippy --keep-going --tests -p "$usage_package"' +run = 'cargo clippy --keep-going --tests -p "$USAGE_PACKAGE"' usage = 'arg "" help="Workspace package name (e.g. edge-toolkit)"' [tasks.cargo-doc-check] @@ -1970,7 +1972,7 @@ description = "Build the mise-tools OCI layout for the current toolset, relocata run = """ coreutils="$(mise which coreutils)" jaq="$(mise which jaq)" -out="${usage_out:?output directory required}" +out="${USAGE_OUT:?output directory required}" # Prefixed names carry their own backend; bare ones are registry short names to resolve. # `mise registry ` prints every backend for the name, preferred first, so take the first field. disabled="" @@ -2025,7 +2027,7 @@ case "$detected" in Windows*) def_plat=windows-x64 ;; *) echo "unrecognised platform '$detected'; pass one explicitly" >&2 && exit 1 ;; esac -plat="${usage_platform:-$def_plat}" +plat="${USAGE_PLATFORM:-$def_plat}" ref="ghcr.io/edge-toolkit/core/mise-tools/${plat}:latest" if [ "$plat" = "windows-x64" ]; then data="$LOCALAPPDATA/mise" diff --git a/config/conftest/policy/mise/mise.rego b/config/conftest/policy/mise/mise.rego index 4590a576..7220db6f 100644 --- a/config/conftest/policy/mise/mise.rego +++ b/config/conftest/policy/mise/mise.rego @@ -368,3 +368,51 @@ deny contains msg if { [file.path, required], ) } + +# Every arg or flag a task declares in its `usage` spec must be read by its `run` body. +# +# mise validates the CALLER's arguments against the spec -- an unknown arg or a missing required one is rejected up +# front -- but nothing checks the other side, so a task can declare an arg, accept it happily, and then ignore it. +# `parfit-fmt` did exactly that for months: it declared `arg "[file]..."` and guarded on `$usage_file`, a name mise +# has never set, so every invocation took the no-arguments branch and reflowed every tracked Rust file instead of the +# ones it was handed. Nothing failed; the blast radius was just silently the whole repo. +# +# The variable mise exports is `USAGE_` plus the declared name uppercased with `-` turned into `_`, so `--dry-run` +# arrives as `$USAGE_DRY_RUN`. Matching on the name alone (rather than the whole `${...}` form) keeps this true for +# a body that reads `$USAGE_OUT`, `"$USAGE_OUT"` or `${USAGE_OUT:?...}` alike. +usage_vars(spec) := vars if { + declarations := array.concat( + regex.find_all_string_submatch_n(`arg\s+"[<\[]([A-Za-z0-9_-]+)`, spec, -1), + regex.find_all_string_submatch_n(`flag\s+"--([A-Za-z0-9_-]+)`, spec, -1), + ) + vars := {var | + some declaration in declarations + var := sprintf("USAGE_%s", [upper(replace(declaration[1], "-", "_"))]) + } +} + +deny contains msg if { + some file in input + is_mise(file) + some name, task in file.contents.tasks + is_string(task.usage) + is_string(task.run) + some var in usage_vars(task.usage) + not contains(task.run, var) + msg := sprintf("%s: task %q declares a usage arg its run never reads as $%s", [file.path, name, var]) +} + +# The lowercase spelling of that variable is always a silent no-op. +# +# Called out separately from the rule above so the message names the actual mistake rather than reporting the arg as +# unused: every task in this repo was written against `$usage_`, which mise does not set and a `${...:-}` guard +# then reads as "no argument given". Flagged wherever it appears, including in a body whose declared args are read +# correctly elsewhere. +deny contains msg if { + some file in input + is_mise(file) + some name, task in file.contents.tasks + is_string(task.run) + regex.match(`\$\{?usage_[a-z]`, task.run) + msg := sprintf("%s: task %q reads $usage_* -- mise exports usage args uppercased, as $USAGE_*", [file.path, name]) +} diff --git a/config/conftest/policy/mise/mise_test.rego b/config/conftest/policy/mise/mise_test.rego index 7c1cea1d..41a947dc 100644 --- a/config/conftest/policy/mise/mise_test.rego +++ b/config/conftest/policy/mise/mise_test.rego @@ -111,3 +111,55 @@ test_multiline_run_with_the_strict_shell_is_accepted if { msgs := mise.deny with input as config(".mise/config.toml", task("build", body)) count(msgs) == 0 } + +# A declared arg the body never reads is the parfit-fmt failure mode: accepted, then silently ignored. +test_declared_arg_must_be_read_by_the_body if { + body := {"usage": `arg "[file]..." var=#true`, "run": "git ls-files '*.rs' | xargs -r parfit"} + msgs := mise.deny with input as config(".mise/config.toml", task("parfit-fmt", body)) + reports(msgs, "declares a usage arg its run never reads as $USAGE_FILE") +} + +test_declared_arg_that_is_read_is_accepted if { + body := {"usage": `arg "[file]..." var=#true`, "run": `echo "$USAGE_FILE" | xargs -r parfit`} + msgs := mise.deny with input as config(".mise/config.toml", task("parfit-fmt", body)) + count(msgs) == 0 +} + +# A required arg is declared `` rather than `[name]`, and reaches the body under the same rule. +test_required_arg_is_checked_too if { + body := {"usage": `arg "" help="Workspace package"`, "run": "cargo clippy -p \"$usage_package\""} + msgs := mise.deny with input as config(".mise/config.toml", task("clippy-pkg", body)) + reports(msgs, "declares a usage arg its run never reads as $USAGE_PACKAGE") +} + +# A flag's dashes become underscores, so `--dry-run` has to be read as `$USAGE_DRY_RUN`. +test_flag_dashes_become_underscores if { + body := {"usage": `flag "--dry-run" help="Print the plan"`, "run": "echo \"${USAGE_DRY_RUN:-}\""} + msgs := mise.deny with input as config(".mise/config.toml", task("release", body)) + count(msgs) == 0 +} + +test_flag_that_is_never_read_is_denied if { + body := {"usage": `flag "--execute" help="Actually publish"`, "run": "cargo release"} + msgs := mise.deny with input as config(".mise/config.toml", task("publish", body)) + reports(msgs, "declares a usage arg its run never reads as $USAGE_EXECUTE") +} + +# The lowercase spelling is reported on its own terms, so the message names the mistake rather than the symptom. +test_lowercase_usage_variable_is_denied if { + body := {"run": "out=\"${usage_out:?output directory required}\""} + msgs := mise.deny with input as config(".mise/config.toml", task("oci", body)) + reports(msgs, "reads $usage_* -- mise exports usage args uppercased") +} + +test_uppercase_usage_variable_is_not_denied if { + body := {"run": "out=\"${USAGE_OUT:?output directory required}\""} + msgs := mise.deny with input as config(".mise/config.toml", task("oci", body)) + count(msgs) == 0 +} + +# A task with no usage spec at all is none of this rule's business. +test_a_task_without_a_usage_spec_is_ignored if { + msgs := mise.deny with input as config(".mise/config.toml", task("plain", {"run": "cargo build"})) + count(msgs) == 0 +} diff --git a/config/semgrep/no-split-url-in-comment.yaml b/config/semgrep/no-split-url-in-comment.yaml new file mode 100644 index 00000000..2f6f107f --- /dev/null +++ b/config/semgrep/no-split-url-in-comment.yaml @@ -0,0 +1,33 @@ +rules: + - id: no-split-url-in-comment + languages: [generic] + paths: + include: + - "*.rs" + - "*.toml" + - "*.yaml" + - "*.rego" + - "*.py" + - "*.zig" + - "*.c" + - "*.h" + - "*.cs" + - "*.java" + - "Dockerfile*" + exclude: + # This rule's own pattern-regex and message necessarily contain the banned shape, so it can't scan itself. + - "/config/semgrep/no-split-url-in-comment.yaml" + # A comment line whose trailing token is an unfinished URL: the wrap landed inside the URL and broke the link. + # `parfit` (the Rust comment reflower) does this routinely -- it reflows a whole paragraph before re-breaking it, + # and breaks after a hyphen wherever one falls, so `https://github.com/edge-toolkit/core/...` comes back split + # after `edge-`. Nothing downstream complains; the link is simply dead, and the evidence a workaround comment + # exists to preserve goes with it. Anchored on a line ending in `-` because that is the shape the reflow leaves + # and a deliberately wrapped URL would not: a URL is never hand-broken mid-token. + # No autofix is viable here. Rejoining means deleting the newline AND the leading comment marker of the next + # line, then re-wrapping the paragraph, which is a multi-line restructure rather than one substitution. + pattern-regex: '(?m)^[^\n]*(?://|#)[^\n]*https?://[^\s]*-$' + message: >- + A URL is broken across two comment lines, which leaves a dead link. Rejoin it onto one line, even if that + line then runs past the usual width, and re-wrap the surrounding prose around it. A comment reflow that + split it cannot be told to keep a URL whole, so the repair is always by hand. + severity: ERROR diff --git a/libs/test-helpers/src/lib.rs b/libs/test-helpers/src/lib.rs index 18096071..b2ba3e26 100644 --- a/libs/test-helpers/src/lib.rs +++ b/libs/test-helpers/src/lib.rs @@ -102,6 +102,15 @@ impl ChildGuard { let _wait = self.child.wait(); } + /// Whether the child has already exited, without waiting on it and without killing it. + /// + /// The non-destructive counterpart to [`Self::wait_for_exit`], for a caller that is waiting on something else + /// -- a port, an agent registration -- and wants to stop early once the process it is waiting for has gone. An + /// unwaitable child (already reaped elsewhere) reads as exited, matching [`Self::wait_for_exit`]. + pub fn has_exited(&mut self) -> bool { + matches!(self.child.try_wait(), Ok(Some(_)) | Err(_)) + } + /// Wait up to `timeout` for the child to exit on its own, killing it if it overstays. /// /// Returns whether it exited by itself. Prefer this to [`Self::shutdown`] for a child that spawns its own @@ -125,7 +134,7 @@ impl ChildGuard { // The loop sleeps between polls, so a child that exits during that final sleep would otherwise be // reported as still running: `shutdown` below would reap it and this would return false, telling the // caller the process had to be killed when in fact it finished on its own. - if matches!(self.child.try_wait(), Ok(Some(_)) | Err(_)) { + if self.has_exited() { return true; } self.shutdown(); diff --git a/libs/test-helpers/tests/child_guard.rs b/libs/test-helpers/tests/child_guard.rs index 54c29952..3565544a 100644 --- a/libs/test-helpers/tests/child_guard.rs +++ b/libs/test-helpers/tests/child_guard.rs @@ -1,8 +1,9 @@ -//! Covers both outcomes of `ChildGuard::wait_for_exit` against a child whose lifetime the test picks. +//! Covers both outcomes of `ChildGuard::wait_for_exit` and of `has_exited` against a child whose lifetime the +//! test picks. #![cfg(test)] use std::process::Command; -use std::time::Duration; +use std::time::{Duration, Instant}; use command_error::CommandExt as _; use et_test_helpers::ChildGuard; @@ -38,3 +39,33 @@ fn wait_for_exit_kills_a_child_that_overstays() { "a probe asked to sleep for ten minutes should still be running when the timeout passes" ); } + +#[test] +fn has_exited_reports_a_child_that_has_ended() { + let mut guard = spawn_probe(0); + + // Polled rather than asserted once: the probe exits immediately, but "immediately" still means once the OS + // has got round to scheduling it, and a bare assertion would race that on a loaded runner. + let started = Instant::now(); + while started.elapsed() < Duration::from_secs(30) && !guard.has_exited() { + std::thread::sleep(Duration::from_millis(50)); + } + + assert!( + guard.has_exited(), + "a probe asked to sleep for 0ms should read as exited well inside the bound" + ); +} + +#[test] +fn has_exited_leaves_a_live_child_running() { + let mut guard = spawn_probe(600_000); + + assert!( + !guard.has_exited(), + "a probe asked to sleep for ten minutes has not exited" + ); + // Asked twice on purpose: the point of this helper over `wait_for_exit` is that it neither waits the child + // out nor kills it, so a second look must still find it running. + assert!(!guard.has_exited(), "has_exited must not itself end the child"); +} diff --git a/services/ws-test-server/src/lib.rs b/services/ws-test-server/src/lib.rs index f98a7dfe..eb916b4a 100644 --- a/services/ws-test-server/src/lib.rs +++ b/services/ws-test-server/src/lib.rs @@ -10,7 +10,7 @@ use std::time::Duration; use actix_web::{App, HttpServer, web}; -use edge_toolkit::ws::{ClientMessage, ServerMessage}; +use edge_toolkit::ws::{AgentConnectionState, AgentSummary, ClientMessage, ServerMessage}; use et_modules_service::{ModulesConfig, configure as configure_modules}; use et_storage_service::{StorageConfig, configure as configure_storage}; use et_ws_service::{AgentSession, WsAgentRegistry, WsConfig, configure as configure_ws}; @@ -89,6 +89,89 @@ pub fn start_on(port: u16) -> TestServer { panic!("test ws-server did not start within 5 seconds on port {port}"); } +/// How long one [`wait_for_connected_agents`] round reads replies before asking the hub again. +const ROSTER_POLL_INTERVAL: Duration = Duration::from_millis(250); + +/// Block until `count` agents other than the waiter itself are connected to the hub at `ws_url`. +/// +/// Returns the connected peer ids from the last roster the hub sent: at least `count` of them once the wait +/// succeeds, and whoever was present when the budget ran out otherwise, so a caller that gives up can report who +/// did come up rather than only that someone did not. +/// +/// Reading the roster means being an agent -- `et-list-agents` is a websocket request, not an HTTP route -- so the +/// waiter registers one of its own and filters itself out of every reply. Entries whose state is `Disconnected` +/// are filtered out too: the registry keeps listing an agent after its socket drops, so a runner that registered +/// and then exited would otherwise still read as up. +/// +/// Synchronous, and owns the runtime it needs, so a plain `#[test]` can gate on hub state without taking a tokio +/// dependency of its own. +#[must_use] +pub fn wait_for_connected_agents(ws_url: &str, count: usize, budget: Duration) -> Vec { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + runtime.block_on(poll_roster(ws_url, count, budget)) +} + +/// The async body of [`wait_for_connected_agents`], split out so the public helper can stay synchronous. +/// +/// One socket serves the whole wait: re-asking on a fresh connection each round would leave a trail of +/// disconnected waiter entries in the registry, and a stale one still marked connected would be counted as a peer +/// by the very filter that exists to exclude it. +#[expect( + clippy::single_call_fn, + reason = "async body of wait_for_connected_agents; separate so the public helper stays sync" +)] +async fn poll_roster(ws_url: &str, count: usize, budget: Duration) -> Vec { + let (mut socket, self_id) = connect_agent(ws_url).await; + let deadline = tokio::time::Instant::now() + budget; + let mut peers = Vec::new(); + let mut ask_again = tokio::time::Instant::now(); + while tokio::time::Instant::now() < deadline { + if tokio::time::Instant::now() >= ask_again { + let request = serde_json::to_string(&ClientMessage::ListAgents).unwrap(); + if socket.send(Message::text(request)).await.is_err() { + break; + } + ask_again = tokio::time::Instant::now() + ROSTER_POLL_INTERVAL; + } + let wait = deadline + .saturating_duration_since(tokio::time::Instant::now()) + .min(ROSTER_POLL_INTERVAL); + let Ok(frame) = tokio::time::timeout(wait, socket.next()).await else { + continue; + }; + match frame { + Some(Ok(Message::Text(text))) => { + let parsed = serde_json::from_str::(&text); + if let Ok(ServerMessage::ListAgentsResponse { agents }) = parsed { + peers = connected_peers(agents, &self_id); + if peers.len() >= count { + return peers; + } + } + } + Some(Ok(_)) => {} + Some(Err(_)) | None => break, + } + } + peers +} + +/// Reduce one roster reply to the ids of the connected agents that are not the waiter. +#[expect( + clippy::single_call_fn, + reason = "distinct filtering step; kept out of the poll loop for readability" +)] +fn connected_peers(agents: Vec, self_id: &str) -> Vec { + agents + .into_iter() + .filter(|agent| agent.agent_id != self_id && agent.state == AgentConnectionState::Connected) + .map(|agent| agent.agent_id) + .collect() +} + /// Open a ws connection to `ws_url` and drive `et-connect` through its ack. /// /// Returns `(stream, agent_id)` once the `et-connect-ack` has been observed. Lets a test drive the diff --git a/services/ws-test-server/tests/helpers.rs b/services/ws-test-server/tests/helpers.rs index 4b9693e7..563bd148 100644 --- a/services/ws-test-server/tests/helpers.rs +++ b/services/ws-test-server/tests/helpers.rs @@ -2,15 +2,90 @@ //! happy-path hub tests never reach: the ack-wait timeout, protocol-ack skipping, and control-frame skipping. Each test //! drives the helper against a tiny scripted ws server that emits an exact frame sequence, so the behaviour is //! deterministic rather than dependent on real-hub timing. +//! +//! `wait_for_connected_agents` is covered here too, but against a real hub rather than a scripted server, because +//! what it reports is the registry's own view of who is connected -- the thing a caller gates on, and one no +//! scripted frame sequence would prove. #![cfg(test)] +use std::thread::JoinHandle; +use std::time::{Duration, Instant}; + use edge_toolkit::ws::{ConnectStatus, ServerMessage}; -use et_ws_test_server::{connect_agent, next_payload}; -use futures_util::SinkExt as _; +use et_ws_test_server::{connect_agent, next_payload, start, wait_for_connected_agents}; +use futures_util::{SinkExt as _, StreamExt as _}; use tokio::net::TcpListener; use tokio_tungstenite::tungstenite::{Bytes, Message}; use tokio_tungstenite::{accept_async, connect_async}; +/// Budget for a wait that is expected to succeed; generous because only the failure paths are timing-sensitive. +const AMPLE: Duration = Duration::from_secs(10); + +/// Register one agent against `ws_url` on a thread of its own and keep its socket open for `hold`. +/// +/// The socket is polled throughout rather than merely held, so the hub's pings are answered and the agent stays +/// connected for as long as the test needs it. Joining the returned handle is what makes the agent go away. +fn peer_for(ws_url: &str, hold: Duration) -> JoinHandle<()> { + let url = ws_url.to_owned(); + std::thread::spawn(move || { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + runtime.block_on(async move { + let (mut socket, _agent_id) = connect_agent(&url).await; + // Elapsed-versus-budget rather than a computed deadline: comparing two `Duration`s needs no + // arithmetic on an `Instant`, which the workspace's restriction lints would otherwise object to. + let started = tokio::time::Instant::now(); + while started.elapsed() < hold { + let _frame = tokio::time::timeout(Duration::from_millis(100), socket.next()).await; + } + }); + }) +} + +#[test] +fn wait_for_connected_agents_sees_a_registered_peer() { + let server = start(); + let peer = peer_for(&server.ws_url, Duration::from_secs(3)); + + let seen = wait_for_connected_agents(&server.ws_url, 1, AMPLE); + + assert_eq!(seen.len(), 1, "expected the one connected peer, got {seen:?}"); + peer.join().unwrap(); +} + +#[test] +fn wait_for_connected_agents_gives_up_when_nobody_registers() { + let server = start(); + + // Nothing but the waiter itself ever connects, so this must run its budget out and report an empty roster + // rather than counting the agent it had to register in order to ask. + let seen = wait_for_connected_agents(&server.ws_url, 1, Duration::from_secs(2)); + + assert!(seen.is_empty(), "the waiter must not count itself, got {seen:?}"); +} + +#[test] +fn wait_for_connected_agents_ignores_a_peer_that_has_gone() { + let server = start(); + peer_for(&server.ws_url, Duration::from_millis(200)).join().unwrap(); + + // The registry keeps listing a departed agent -- only its state changes -- and the hub needs a moment to + // notice the closed socket, so poll until it has. Code that counted disconnected entries would never see an + // empty roster here, because each round leaves its own waiter behind, and would fail on the deadline instead. + let started = Instant::now(); + let mut seen = wait_for_connected_agents(&server.ws_url, 1, Duration::from_millis(500)); + while started.elapsed() < AMPLE && !seen.is_empty() { + seen = wait_for_connected_agents(&server.ws_url, 1, Duration::from_millis(500)); + } + + assert!( + seen.is_empty(), + "a departed peer must not count as connected, got {seen:?}" + ); +} + /// Start a ws server on a free port that accepts one connection, sends `frames` in order, then holds the socket open. /// /// With an empty `frames` it simply accepts and stays silent -- a server that never acks. diff --git a/utilities/cli/tests/scenario_runners.rs b/utilities/cli/tests/scenario_runners.rs index 7c3ac0cf..2513156f 100644 --- a/utilities/cli/tests/scenario_runners.rs +++ b/utilities/cli/tests/scenario_runners.rs @@ -37,12 +37,33 @@ use edge_toolkit::ports::Services; use et_test_helpers::{ChildGuard, drain_stderr, drain_stdout}; use fs_err as fs; -/// Wall-clock ceiling for the whole exchange once the hub is up. +/// Wall-clock ceiling for the exchange itself, measured from the moment the trigger is a registered agent. /// /// The sender broadcasts its pointer once a second across its manual-use window, and the twin stores its model on -/// the first one it sees, so this only has to outlast the runners' own startup. +/// the first one it sees, so this covers the twin's startup and one broadcast reaching it -- not the trigger's own +/// startup, which [`RUNNER_STARTUP_TIMEOUT`] absorbs before this clock starts. const EXCHANGE_TIMEOUT: Duration = Duration::from_secs(180); +/// Ceiling for a runner to get from `mise run` to a registered agent on the hub. +/// +/// Deliberately far larger than the exchange it precedes, because it measures something else entirely: `mise` +/// startup plus the generated task's `cargo run`, which on a cold CI container is minutes rather than seconds. On +/// the `build (windows-2025, servercore)` lane the trigger's first log line arrived 195s after it was spawned -- +/// and 83s after the twin had already exited, which is the failure this whole sequencing exists to prevent. +/// Observed on commit +/// at +/// . +/// +/// Costs nothing when the runners are quick: the wait returns the moment the roster shows them. +const RUNNER_STARTUP_TIMEOUT: Duration = Duration::from_secs(300); + +/// How much of [`RUNNER_STARTUP_TIMEOUT`] one roster wait consumes before the runner is checked for signs of life. +/// +/// The wait has to be re-entered to look at the process at all, and each re-entry registers a fresh waiter agent, +/// so this trades a handful of spent registry entries against how long a runner that died on its first breath +/// keeps the test sitting there. A runner that comes up normally is seen inside the first slice and costs one. +const REGISTRATION_SLICE: Duration = Duration::from_secs(15); + /// `RUNNER_TIMEOUT` handed to both runners so they exit on their own rather than running until killed. /// /// The generated tasks deliberately set no timeout -- a deployed cluster runs until it is stopped -- so this @@ -129,6 +150,43 @@ fn captured(buffer: &Arc>) -> String { .map_or_else(|poisoned| poisoned.into_inner().clone(), |guard| guard.clone()) } +/// Both of one runner's captured streams, labelled, for a failure message. +/// +/// A runner that writes nothing is the hard case to read: empty output alone cannot distinguish a process that died +/// before it could log from one that started and sat idle, so every failure path quotes both streams. +fn quoted(label: &str, runner: &Runner) -> String { + format!( + "--- {label} stdout ---\n{}\n--- {label} stderr ---\n{}", + captured(&runner.stdout), + captured(&runner.stderr) + ) +} + +/// Wait for `runner` to appear on the hub's roster, returning the peers seen or an empty vec if it never does. +/// +/// Gives up as soon as the runner's process has gone, rather than sitting out the whole startup budget waiting for +/// an agent that can no longer register -- a runner that fails during bootstrap (a module fetch 404, say) exits in +/// about a second, and the budget above is measured in minutes. +#[expect( + clippy::single_call_fn, + reason = "distinct step of the exchange; separate so the test body reads as hub, runners, exchange" +)] +fn wait_for_registration(ws_url: &str, runner: &mut Runner) -> Vec { + // Elapsed-versus-budget rather than a computed deadline: comparing two `Duration`s needs no arithmetic on an + // `Instant`, which the workspace's restriction lints would otherwise object to. + let started = Instant::now(); + while started.elapsed() < RUNNER_STARTUP_TIMEOUT { + let peers = et_ws_test_server::wait_for_connected_agents(ws_url, 1, REGISTRATION_SLICE); + if !peers.is_empty() { + return peers; + } + if runner.guard.has_exited() { + break; + } + } + Vec::new() +} + /// Return the first stored `math1-output.json` as `(weight, bias)`, or `None` until the twin has written one. #[expect( clippy::single_call_fn, @@ -274,9 +332,27 @@ fn run_scenario(scenario: &str, twin_task: &str, twin_crate: &str, trigger_crate let server = et_ws_test_server::start_on(Services::InsecureWebSocketServer.port()); let storage_dir = server.storage_dir.path(); - // Both runners come up together, exactly as `generated-scenario` starts them. - let mut twin = spawn_runner(scenario, twin_task); + // The trigger comes up first, and the twin only once the hub has actually seen it register. + // + // `generated-scenario` starts both at once and this test used to as well, which is what made it fail on the + // servercore lane: each runner's `RUNNER_TIMEOUT` starts when that runner starts, so two staggered startups + // give two disjoint lifetimes. The twin registered, idled out its 110s and exited, and the trigger's first log + // line arrived 83s after that -- it then broadcast its whole window to an empty hub, and the test read the + // result as a deployment that never stored anything. Waiting here ties the two lifetimes together at the point + // that matters: the sender re-broadcasts its pointer once a second across its manual-use window, so a twin + // spawned the moment the sender is live has that whole window to register and catch one. + // + // Starting the twin second also means its `cargo run` no longer races the trigger's for cargo's build lock, + // which is startup cost neither of them pays on a workstation and both were paying on CI. let mut trigger = spawn_runner(scenario, "math1-trigger"); + let registered = wait_for_registration(&server.ws_url, &mut trigger); + assert!( + !registered.is_empty(), + "math1-trigger never registered with the hub within {:?}\n{}", + RUNNER_STARTUP_TIMEOUT, + quoted("math1-trigger", &trigger) + ); + let mut twin = spawn_runner(scenario, twin_task); // Elapsed-versus-budget rather than a computed deadline: comparing two `Duration`s needs no arithmetic on // an `Instant`, which the workspace's restriction lints would otherwise object to. @@ -296,28 +372,21 @@ fn run_scenario(scenario: &str, twin_task: &str, twin_crate: &str, trigger_crate let trigger_exited = trigger.guard.wait_for_exit(RUNNER_EXIT_TIMEOUT); let Some((weight, bias)) = model else { - // The two exit flags are reported here, not just asserted on the happy path below. - // A runner that writes nothing is the hard case to read: empty output alone cannot distinguish a - // process that died before it could log from one that started and sat idle, and the assertions that - // would have said which are never reached once this branch panics. + // The two exit flags are reported here, not just asserted on the happy path below, because the assertions + // that would have said which runner misbehaved are never reached once this branch panics. panic!( concat!( "{}: no math1-output.json appeared in any storage bucket under {}\n", "exited within RUNNER_EXIT_TIMEOUT: {}={}, math1-trigger={}\n", - "--- {} stdout ---\n{}\n--- {} stderr ---\n{}\n", - "--- math1-trigger stdout ---\n{}\n--- math1-trigger stderr ---\n{}" + "{}\n{}" ), scenario, storage_dir.display(), twin_task, twin_exited, trigger_exited, - twin_task, - captured(&twin.stdout), - twin_task, - captured(&twin.stderr), - captured(&trigger.stdout), - captured(&trigger.stderr) + quoted(twin_task, &twin), + quoted("math1-trigger", &trigger) ); }; et_ws_test_server::math1::verify_math1_model(weight, bias).unwrap(); From fc07097f1b286ffa943966264340268414efe6ef Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Thu, 17 Sep 2026 11:13:27 +0800 Subject: [PATCH 16/24] rv to lower-case mise task args --- .mise/config.coverage.toml | 6 ++-- .mise/config.maint.toml | 8 ++--- .mise/config.toml | 14 ++++----- CLAUDE.md | 21 +++++++++++++ config/conftest/policy/mise/mise.rego | 34 +++++++++++++--------- config/conftest/policy/mise/mise_test.rego | 26 ++++++++--------- 6 files changed, 69 insertions(+), 40 deletions(-) diff --git a/.mise/config.coverage.toml b/.mise/config.coverage.toml index b85ba758..96d3036f 100644 --- a/.mise/config.coverage.toml +++ b/.mise/config.coverage.toml @@ -192,19 +192,19 @@ description = "Assert every named libs/ crate is at 100% branch coverage in an l hide = true # The whole assertion is one jaq query over llvm-cov's own JSON, with no parsing of our own. run = """ -report="$USAGE_REPORT" +report="$usage_report" if [ ! -f "$report" ]; then echo "cov-branch-assert: no coverage report at $report" >&2 echo "Run the coverage task that produces it first; a missing report is a failure, not a skip." >&2 exit 1 fi -shortfall="$(jaq -r --arg libs "$USAGE_LIBS" -f .mise/cov-branch-assert.jq "$report")" +shortfall="$(jaq -r --arg libs "$usage_libs" -f .mise/cov-branch-assert.jq "$report")" if [ -n "$shortfall" ]; then echo "cov-branch-assert: libs/ must be at 100% branch coverage, and is not:" >&2 echo "$shortfall" >&2 exit 1 fi -echo "cov-branch-assert: 100% branch coverage across $USAGE_LIBS" +echo "cov-branch-assert: 100% branch coverage across $usage_libs" """ shell = "{{ vars.task_shell }}" usage = """ diff --git a/.mise/config.maint.toml b/.mise/config.maint.toml index 6ff8d18b..e9223b6a 100644 --- a/.mise/config.maint.toml +++ b/.mise/config.maint.toml @@ -738,19 +738,19 @@ per_batch=5 # `set --` then `$#` counts the array without the `${` + `#` pair, which mise's Tera pass reads as a comment. set -- "${crates[@]}" count=$# -start=$(((USAGE_BATCH - 1) * per_batch)) +start=$(((usage_batch - 1) * per_batch)) if [ "$start" -ge "$count" ]; then - echo "batch $USAGE_BATCH is past the end: $count crates, $per_batch per batch" >&2 + echo "batch $usage_batch is past the end: $count crates, $per_batch per batch" >&2 exit 1 fi args=(release publish) for crate in "${crates[@]:start:per_batch}"; do args+=(-p "$crate") done -if [ "${USAGE_EXECUTE:-}" = "true" ]; then +if [ "${usage_execute:-}" = "true" ]; then args+=(--execute) fi -echo "batch $USAGE_BATCH: ${crates[*]:start:per_batch}" +echo "batch $usage_batch: ${crates[*]:start:per_batch}" cargo "${args[@]}" """ shell = "{{ vars.task_shell }}" diff --git a/.mise/config.toml b/.mise/config.toml index 9357d9e2..b81c9809 100644 --- a/.mise/config.toml +++ b/.mise/config.toml @@ -989,14 +989,14 @@ description = "Reflow Rust comment prose to the 120-column width with parfit (in # `https://github.com/edge-toolkit/core/...` leaves a dead link behind. # Given file paths, reflows just those; with none, every tracked Rust file except the generator-owned ones. The # usage-spec is what carries the paths: mise appends bare task arguments to the body text, which the eval in -# task-shell.sh then rejects, whereas a `var` arg arrives space-joined in $USAGE_FILE for xargs to split. +# task-shell.sh then rejects, whereas a `var` arg arrives space-joined in $usage_file for xargs to split. run = """ heading='^\\s*//[/!]?\\s*#' item='^\\s*//[/!]?\\s*([-*]|[0-9]+\\.)\\s' -if [ -z "${USAGE_FILE:-}" ]; then +if [ -z "${usage_file:-}" ]; then git ls-files '*.rs' ':!generated/**' ':!services/ws-wasi-runner/src/bindings.rs' else - printf '%s\\n' "$USAGE_FILE" + printf '%s\\n' "$usage_file" fi | xargs -r parfit -w 120 -s "$heading" -s "$item" """ shell = "{{ vars.task_shell }}" @@ -1013,9 +1013,9 @@ run = "cargo clippy --keep-going --workspace --tests {{ vars.cargo_ws_excludes } alias = ["clippy-pkg"] description = "cargo clippy on a single workspace package (narrow inner-loop check)" # Same flags as cargo-clippy-check minus --workspace. -# Usage-spec captures the package name and exposes it as $USAGE_PACKAGE; a missing arg fails up front with +# Usage-spec captures the package name and exposes it as $usage_package; a missing arg fails up front with # mise's auto-generated usage message instead of clippy's generic "--package not found" error. -run = 'cargo clippy --keep-going --tests -p "$USAGE_PACKAGE"' +run = 'cargo clippy --keep-going --tests -p "$usage_package"' usage = 'arg "" help="Workspace package name (e.g. edge-toolkit)"' [tasks.cargo-doc-check] @@ -1972,7 +1972,7 @@ description = "Build the mise-tools OCI layout for the current toolset, relocata run = """ coreutils="$(mise which coreutils)" jaq="$(mise which jaq)" -out="${USAGE_OUT:?output directory required}" +out="${usage_out:?output directory required}" # Prefixed names carry their own backend; bare ones are registry short names to resolve. # `mise registry ` prints every backend for the name, preferred first, so take the first field. disabled="" @@ -2027,7 +2027,7 @@ case "$detected" in Windows*) def_plat=windows-x64 ;; *) echo "unrecognised platform '$detected'; pass one explicitly" >&2 && exit 1 ;; esac -plat="${USAGE_PLATFORM:-$def_plat}" +plat="${usage_platform:-$def_plat}" ref="ghcr.io/edge-toolkit/core/mise-tools/${plat}:latest" if [ "$plat" = "windows-x64" ]; then data="$LOCALAPPDATA/mise" diff --git a/CLAUDE.md b/CLAUDE.md index aa3ba9eb..b90adfa0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1118,6 +1118,27 @@ expects to find inside the prebuilt archive, so an upstream rename makes it fall source build (which then flakes on crates.io) with no signal beyond a `bin is not found` line. Reach for install-action only when the prebuilt actually exists for the target triple _and_ the binary names still match. +## Revisit on Windows: why `parfit-fmt` read as needing an uppercase `usage` arg + +A task's `usage` args are read as `$usage_`, lowercased with `-` turned into `_`, and the mise conftest +policy holds every task to that. That is measured, not assumed: an `env` dump from inside a task body on mise +2026.9.1 macos-arm64 lists `usage_report` and `usage_libs` for a task declaring `` and ``, and no +`USAGE_*` name exists in the environment at all. + +The uppercase spelling was briefly standardised here on Windows evidence, and it survives on Windows because +environment names resolve case-insensitively there, so both spellings reach the one variable mise set. Everywhere +else `$USAGE_REPORT` is simply unset -- fatal under the task shell's `set -u`, and silently "no argument given" +behind a `${...:-}` guard. `cargo-clippy-check-pkg` spent that period linting nothing on Linux and macOS, passing +an empty `-p` to clippy, which reports `error: package name cannot be empty` only because the value reached a tool +that objected. + +What remains unexplained is the observation that started it. `parfit-fmt` declares `arg "[file]..."` and guarded +on `$usage_file`, and every invocation took the no-arguments branch and reflowed every tracked Rust file rather +than the ones it was handed. Casing cannot account for that on its own, since on Windows the lowercase read should +have resolved just as well as the uppercase one -- so a variadic arg may not be exported as a scalar at all. +Settle it on Windows before touching the convention again: dump the environment inside two task bodies, one with a +variadic arg and one with a plain arg, on the mise version the CI lanes install. + ## Linting Lint checks must be expressed through one of the repo's linters -- **never** as a bespoke shell script, whether a diff --git a/config/conftest/policy/mise/mise.rego b/config/conftest/policy/mise/mise.rego index 7220db6f..0f5d0aff 100644 --- a/config/conftest/policy/mise/mise.rego +++ b/config/conftest/policy/mise/mise.rego @@ -373,13 +373,20 @@ deny contains msg if { # # mise validates the CALLER's arguments against the spec -- an unknown arg or a missing required one is rejected up # front -- but nothing checks the other side, so a task can declare an arg, accept it happily, and then ignore it. -# `parfit-fmt` did exactly that for months: it declared `arg "[file]..."` and guarded on `$usage_file`, a name mise -# has never set, so every invocation took the no-arguments branch and reflowed every tracked Rust file instead of the -# ones it was handed. Nothing failed; the blast radius was just silently the whole repo. +# `parfit-fmt` did exactly that for months: it declared `arg "[file]..."` and guarded on a name that was not set, so +# every invocation took the no-arguments branch and reflowed every tracked Rust file instead of the ones it was +# handed. Nothing failed; the blast radius was just silently the whole repo. # -# The variable mise exports is `USAGE_` plus the declared name uppercased with `-` turned into `_`, so `--dry-run` -# arrives as `$USAGE_DRY_RUN`. Matching on the name alone (rather than the whole `${...}` form) keeps this true for -# a body that reads `$USAGE_OUT`, `"$USAGE_OUT"` or `${USAGE_OUT:?...}` alike. +# The variable mise exports is `usage_` plus the declared name lowercased with `-` turned into `_`, so `--dry-run` +# arrives as `$usage_dry_run`. Matching on the name alone (rather than the whole `${...}` form) keeps this true for +# a body that reads `$usage_out`, `"$usage_out"` or `${usage_out:?...}` alike. +# +# Measured rather than assumed, because the uppercase spelling reads as correct on one platform. +# An `env` dump from inside a task body on mise 2026.9.1 macos-arm64 lists the lowercase names and nothing else: an +# arg declared `` arrives as `usage_report`, with no `USAGE_REPORT` present at all. Windows resolves +# environment names case-insensitively, so there both spellings reach that same variable and the mistake is +# invisible -- which is why an uppercase convention can survive a Windows-only check. Lowercase is what mise sets, +# and is therefore the spelling that works on every platform. usage_vars(spec) := vars if { declarations := array.concat( regex.find_all_string_submatch_n(`arg\s+"[<\[]([A-Za-z0-9_-]+)`, spec, -1), @@ -387,7 +394,7 @@ usage_vars(spec) := vars if { ) vars := {var | some declaration in declarations - var := sprintf("USAGE_%s", [upper(replace(declaration[1], "-", "_"))]) + var := sprintf("usage_%s", [lower(replace(declaration[1], "-", "_"))]) } } @@ -402,17 +409,18 @@ deny contains msg if { msg := sprintf("%s: task %q declares a usage arg its run never reads as $%s", [file.path, name, var]) } -# The lowercase spelling of that variable is always a silent no-op. +# The uppercase spelling is a no-op everywhere except Windows, so it is rejected on sight. # # Called out separately from the rule above so the message names the actual mistake rather than reporting the arg as -# unused: every task in this repo was written against `$usage_`, which mise does not set and a `${...:-}` guard -# then reads as "no argument given". Flagged wherever it appears, including in a body whose declared args are read -# correctly elsewhere. +# unused. Unguarded it is an unset variable, which the task shell's `set -u` turns into an immediate failure; behind +# a `${...:-}` guard it is worse, because the task then runs to completion having silently taken the "no argument +# given" branch. Flagged wherever it appears, including in a body whose declared args are read correctly elsewhere, +# so the two spellings never get mixed within one task. deny contains msg if { some file in input is_mise(file) some name, task in file.contents.tasks is_string(task.run) - regex.match(`\$\{?usage_[a-z]`, task.run) - msg := sprintf("%s: task %q reads $usage_* -- mise exports usage args uppercased, as $USAGE_*", [file.path, name]) + regex.match(`\$\{?USAGE_[A-Z]`, task.run) + msg := sprintf("%s: task %q reads $USAGE_* -- mise exports usage args lowercased, as $usage_*", [file.path, name]) } diff --git a/config/conftest/policy/mise/mise_test.rego b/config/conftest/policy/mise/mise_test.rego index 41a947dc..a2b2cc4e 100644 --- a/config/conftest/policy/mise/mise_test.rego +++ b/config/conftest/policy/mise/mise_test.rego @@ -116,25 +116,25 @@ test_multiline_run_with_the_strict_shell_is_accepted if { test_declared_arg_must_be_read_by_the_body if { body := {"usage": `arg "[file]..." var=#true`, "run": "git ls-files '*.rs' | xargs -r parfit"} msgs := mise.deny with input as config(".mise/config.toml", task("parfit-fmt", body)) - reports(msgs, "declares a usage arg its run never reads as $USAGE_FILE") + reports(msgs, "declares a usage arg its run never reads as $usage_file") } test_declared_arg_that_is_read_is_accepted if { - body := {"usage": `arg "[file]..." var=#true`, "run": `echo "$USAGE_FILE" | xargs -r parfit`} + body := {"usage": `arg "[file]..." var=#true`, "run": `echo "$usage_file" | xargs -r parfit`} msgs := mise.deny with input as config(".mise/config.toml", task("parfit-fmt", body)) count(msgs) == 0 } # A required arg is declared `` rather than `[name]`, and reaches the body under the same rule. test_required_arg_is_checked_too if { - body := {"usage": `arg "" help="Workspace package"`, "run": "cargo clippy -p \"$usage_package\""} + body := {"usage": `arg "" help="Workspace package"`, "run": "cargo clippy -p \"$USAGE_PACKAGE\""} msgs := mise.deny with input as config(".mise/config.toml", task("clippy-pkg", body)) - reports(msgs, "declares a usage arg its run never reads as $USAGE_PACKAGE") + reports(msgs, "declares a usage arg its run never reads as $usage_package") } -# A flag's dashes become underscores, so `--dry-run` has to be read as `$USAGE_DRY_RUN`. +# A flag's dashes become underscores, so `--dry-run` has to be read as `$usage_dry_run`. test_flag_dashes_become_underscores if { - body := {"usage": `flag "--dry-run" help="Print the plan"`, "run": "echo \"${USAGE_DRY_RUN:-}\""} + body := {"usage": `flag "--dry-run" help="Print the plan"`, "run": "echo \"${usage_dry_run:-}\""} msgs := mise.deny with input as config(".mise/config.toml", task("release", body)) count(msgs) == 0 } @@ -142,18 +142,18 @@ test_flag_dashes_become_underscores if { test_flag_that_is_never_read_is_denied if { body := {"usage": `flag "--execute" help="Actually publish"`, "run": "cargo release"} msgs := mise.deny with input as config(".mise/config.toml", task("publish", body)) - reports(msgs, "declares a usage arg its run never reads as $USAGE_EXECUTE") + reports(msgs, "declares a usage arg its run never reads as $usage_execute") } -# The lowercase spelling is reported on its own terms, so the message names the mistake rather than the symptom. -test_lowercase_usage_variable_is_denied if { - body := {"run": "out=\"${usage_out:?output directory required}\""} +# The uppercase spelling is reported on its own terms, so the message names the mistake rather than the symptom. +test_uppercase_usage_variable_is_denied if { + body := {"run": "out=\"${USAGE_OUT:?output directory required}\""} msgs := mise.deny with input as config(".mise/config.toml", task("oci", body)) - reports(msgs, "reads $usage_* -- mise exports usage args uppercased") + reports(msgs, "reads $USAGE_* -- mise exports usage args lowercased") } -test_uppercase_usage_variable_is_not_denied if { - body := {"run": "out=\"${USAGE_OUT:?output directory required}\""} +test_lowercase_usage_variable_is_not_denied if { + body := {"run": "out=\"${usage_out:?output directory required}\""} msgs := mise.deny with input as config(".mise/config.toml", task("oci", body)) count(msgs) == 0 } From a60e9794d2b3b60e046854bacf909143f4d1bf42 Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Thu, 17 Sep 2026 12:52:00 +0800 Subject: [PATCH 17/24] disable k3s tests on windows docker --- .github/workflows/docker-windows.yaml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docker-windows.yaml b/.github/workflows/docker-windows.yaml index c8fdc1a4..c6ac2897 100644 --- a/.github/workflows/docker-windows.yaml +++ b/.github/workflows/docker-windows.yaml @@ -161,6 +161,17 @@ jobs: # modules), so the incremental caches land on the runner's C: drive. They are pure waste on a # single-shot CI compile and this lane has no disk to spare -- servercore ran out mid-link with # `There is not enough space on the disk. (os error 112)` even after the disk-free step left 43 GB. + # The k3s generator tests are skipped here, and only here. + # `not test(k3s)` names the two cases in et-cli::scenario_generation whose names carry it, leaving the + # deployment-type test that also writes a k3s.yaml in place, so the generator still runs on this lane. The + # skip is scoped to this step rather than gated in the source because it is this lane it applies to: the + # same tests run everywhere else, including the Windows host lanes, and were passing here too (0.270s and + # 0.301s on `build (windows-2022, servercore)`) when they were turned off, so nothing about them is known + # to be broken on Windows. Drop the filter to put them back. - name: Run cargo-test if: matrix.base == 'servercore' - run: docker run --rm --pull=never -e CARGO_INCREMENTAL=0 et-windows-test mise run cargo-test + run: | + args="--rm --pull=never -e CARGO_INCREMENTAL=0" + # $args is a word-split flag list by design; do not quote it. + # shellcheck disable=SC2086 + docker run $args et-windows-test mise run cargo-test -- -E 'not test(k3s)' From cf33e2c0f573da439a2ab2aae6359fc2e3ea1df0 Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Thu, 17 Sep 2026 14:26:11 +0800 Subject: [PATCH 18/24] add missing cov --- libs/test-helpers/tests/child_guard.rs | 40 ++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/libs/test-helpers/tests/child_guard.rs b/libs/test-helpers/tests/child_guard.rs index 3565544a..2839e943 100644 --- a/libs/test-helpers/tests/child_guard.rs +++ b/libs/test-helpers/tests/child_guard.rs @@ -18,6 +18,18 @@ fn spawn_probe(millis: u64) -> ChildGuard { ChildGuard::new(child) } +/// Poll until the probe has genuinely exited, giving up after ~30s. +/// +/// The probe exits immediately, but "immediately" still means once the OS has got round to scheduling it, and a +/// bare assertion would race that on a loaded runner. Returning quietly on the timeout rather than asserting +/// leaves the caller to say what it expected, so each failure reads as the thing that test was about. +fn wait_until_gone(guard: &mut ChildGuard) { + let started = Instant::now(); + while started.elapsed() < Duration::from_secs(30) && !guard.has_exited() { + std::thread::sleep(Duration::from_millis(50)); + } +} + #[test] fn wait_for_exit_reports_a_child_that_ends_on_its_own() { let mut guard = spawn_probe(0); @@ -44,12 +56,7 @@ fn wait_for_exit_kills_a_child_that_overstays() { fn has_exited_reports_a_child_that_has_ended() { let mut guard = spawn_probe(0); - // Polled rather than asserted once: the probe exits immediately, but "immediately" still means once the OS - // has got round to scheduling it, and a bare assertion would race that on a loaded runner. - let started = Instant::now(); - while started.elapsed() < Duration::from_secs(30) && !guard.has_exited() { - std::thread::sleep(Duration::from_millis(50)); - } + wait_until_gone(&mut guard); assert!( guard.has_exited(), @@ -57,6 +64,27 @@ fn has_exited_reports_a_child_that_has_ended() { ); } +/// A child that exited without the polling loop noticing is still reported as having exited on its own. +/// +/// The loop sleeps between polls, so a child can exit inside that gap and the loop run out of budget without +/// ever seeing it; the look after the loop is what catches that, and reporting it as a kill would tell the +/// caller the process had to be forced when it finished by itself. Racing a real sleep would be the obvious way +/// to arrive there and a flaky one -- a zero budget reaches the same branch every time, because the loop cannot +/// run at all and the look after it is the only thing left to decide the answer. +#[test] +fn wait_for_exit_credits_a_child_the_loop_never_saw_finish() { + let mut guard = spawn_probe(0); + + // Waited out first so the probe is genuinely gone before the zero-budget call, which otherwise reaches the + // same branch and answers "still running" simply because the OS had not scheduled the exit yet. + wait_until_gone(&mut guard); + + assert!( + guard.wait_for_exit(Duration::ZERO), + "a child that has already exited must read as exited even with no time left to look for it" + ); +} + #[test] fn has_exited_leaves_a_live_child_running() { let mut guard = spawn_probe(600_000); From d8f673487d12fcdabef02f4367a8e192f9d5be01 Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Thu, 17 Sep 2026 17:31:24 +0800 Subject: [PATCH 19/24] more intermittent failures in docker --- CLAUDE.md | 27 ++++++++++++ .../ws-wasi-runner/tests/vector_otlp_relay.rs | 21 ++++++++-- utilities/cli/tests/scenario_runners.rs | 41 ++++++++++++++++--- 3 files changed, 79 insertions(+), 10 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index b90adfa0..ce1e42c1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -799,6 +799,33 @@ installed cleanly). This is an api.github.com rate-limit/transient-network flake durable fix is to mirror the affected assets via the upstream-cache pattern (which fetches from our own release CDN, off the api.github.com attestation path) rather than adding a retry wrapper. +### Known intermittent CI failure: a long job dies with no log at all + +A `build` or `default` lane fails having produced **no log blob** -- `gh api .../jobs//logs` answers +`BlobNotFound`, the step that was running has a `null` conclusion rather than `failure`, and `Post Checkout` +never ran either. There is nothing to grep for, because nothing was written; the absence is the signature. + +It is the GitHub-hosted runner being reclaimed, not anything in the build. The one sighting that did retain its +log said so outright -- + + ##[error]The runner has received a shutdown signal. This can happen when the runner service is stopped, + or a manually started runner is canceled. + [cargo-test] ERROR sh exited with non-zero status: killed by SIGTERM + +-- on `default (ubuntu-24.04-arm, 50)` at +`https://github.com/edge-toolkit/core/actions/runs/35083902481/job/104754130780`. Three later sightings on +commit https://github.com/edge-toolkit/core/commit/cf33e2c0f573 lost their logs entirely: `build (fedora:42)` +at 71 minutes, `build (ubuntu:24.04)` at 80, and `build (debian:bookworm)` at 90, each well inside the 150-minute +job budget and each in a different run. + +Two things rule out a resource ceiling in our own build, which is the tempting reading. The lane **rotates** -- +fedora and ubuntu:24.04 both passed in the run that killed bookworm -- and the siblings sharing that runner image +and workload go green in the same run. A ceiling would hit the same heaviest lanes every time. Duration is the +only correlate: every sighting was a long job. + +So the remedy is `gh run rerun --job ` once the parent run completes, and there is nothing to fix. Do not +spend a diagnosis on it; check for the empty log first, and if the siblings passed, re-run and move on. + ### Fixed: vector_otlp_relay store-and-forward timing Root-caused and fixed on 2026-09-11 by capping the retry interval; kept here because the panic line is what a diff --git a/services/ws-wasi-runner/tests/vector_otlp_relay.rs b/services/ws-wasi-runner/tests/vector_otlp_relay.rs index c03cdd61..f3f41afa 100644 --- a/services/ws-wasi-runner/tests/vector_otlp_relay.rs +++ b/services/ws-wasi-runner/tests/vector_otlp_relay.rs @@ -34,10 +34,11 @@ const SPAN_NAME: &str = "relay-probe"; /// Redelivery ceiling, matching the `retry`-based poll this replaced. /// -/// Store-and-forward redelivery is inherently latent: Vector retries the initially-dead sink with an exponential -/// backoff (`retry_initial_backoff_secs=1`, doubling), so when its first attempts race the mock's listener coming -/// up, the next retry can land tens of seconds later. The old 30s ceiling intermittently timed that out on cold -/// CI runners; the wait returns the instant the span lands, so the wider ceiling costs nothing on the happy path. +/// Store-and-forward redelivery is inherently latent: Vector retries the initially-dead sink on a backoff, so +/// the span arrives some interval after the collector starts answering rather than at once. The old 30s ceiling +/// intermittently timed that out on cold CI runners; the wait returns the instant the span lands, so the wider +/// ceiling costs nothing on the happy path. What bounds the interval is the sink's `retry_max_duration_secs`, +/// which caps the gap between attempts -- so this ceiling should now be many attempts wide, not one or two. const RELAY_TIMEOUT: Duration = Duration::from_mins(2); #[test] @@ -103,6 +104,18 @@ fn vector_relays_buffered_otlp_after_backend_comes_online() { let runtime = Runtime::new().unwrap(); let mock = runtime.block_on(et_test_otlp::start_on(mock_port, Protocol::HttpBinary)); + // Wait for the listener before starting the clock on redelivery, so the two failures stay distinguishable. + // `start_on` returns once the server is constructed, which is not the same instant it accepts, and every + // retry that lands in that gap is one Vector spends against a closed port. Without this the test reports + // "store-and-forward failed" either way -- whether the relay is broken or the collector simply never came + // up -- and the second reading is the one the panic cannot say. A listener that is already accepting makes + // this return immediately, so the happy path pays nothing. + assert!( + wait_for_port(mock_port), + "the mock collector never accepted on :{mock_port}, so there was nothing for Vector to relay to\n{}", + stop_and_read(&mut vector, &log), + ); + // 5. The buffered span must now be forwarded, intact. let Some(relayed) = wait_for_relayed_span(&runtime, &mock) else { panic!( diff --git a/utilities/cli/tests/scenario_runners.rs b/utilities/cli/tests/scenario_runners.rs index 2513156f..aea99365 100644 --- a/utilities/cli/tests/scenario_runners.rs +++ b/utilities/cli/tests/scenario_runners.rs @@ -80,6 +80,13 @@ const RUNNER_TIMEOUT: &str = "110s"; /// on its own and then report it as a timeout. const RUNNER_EXIT_TIMEOUT: Duration = Duration::from_secs(150); +/// How long a failure message waits for a killed runner's drain threads to hand over what they read. +/// +/// Only ever spent on a path that is already failing, and only when both buffers are still empty, so a runner +/// that logged anything at all is quoted as soon as the thread stores it. Short because it covers a handoff +/// between two threads on the same machine, not any work the runner does. +const DRAIN_SETTLE: Duration = Duration::from_secs(2); + /// A spawned runner task, with both its output streams captured for the failure message. struct Runner { guard: ChildGuard, @@ -150,11 +157,33 @@ fn captured(buffer: &Arc>) -> String { .map_or_else(|poisoned| poisoned.into_inner().clone(), |guard| guard.clone()) } -/// Both of one runner's captured streams, labelled, for a failure message. +/// Both of one runner's captured streams, labelled, for a failure message, after ending it so they are filled. /// /// A runner that writes nothing is the hard case to read: empty output alone cannot distinguish a process that died -/// before it could log from one that started and sat idle, so every failure path quotes both streams. -fn quoted(label: &str, runner: &Runner) -> String { +/// before it could log from one that started and sat idle, so every failure path quotes both streams. That only +/// works on a runner that has finished. The drain threads hand their buffer over at EOF, so quoting one still +/// holding its pipes open reports two empty streams whatever it wrote -- which is how a hung trigger came to look +/// like a silent one across several CI rounds. Ending it first closes the pipes; the poll then covers the moment +/// between that and the drain thread storing what it read, and gives up rather than hanging if it stays empty, +/// since empty is a legitimate answer for a process that really did write nothing. +#[expect( + clippy::single_call_fn, + reason = "paired with quoted_now on purpose: inlining it invites a future path to quote a still-running runner" +)] +fn quoted(label: &str, runner: &mut Runner) -> String { + runner.guard.shutdown(); + let started = Instant::now(); + while started.elapsed() < DRAIN_SETTLE && captured(&runner.stdout).is_empty() && captured(&runner.stderr).is_empty() + { + std::thread::sleep(Duration::from_millis(50)); + } + quoted_now(label, runner) +} + +/// Format whatever the buffers hold right now, without touching the process. +/// +/// Split out for the paths that have already waited the runner out, which must not shut it down a second time. +fn quoted_now(label: &str, runner: &Runner) -> String { format!( "--- {label} stdout ---\n{}\n--- {label} stderr ---\n{}", captured(&runner.stdout), @@ -350,7 +379,7 @@ fn run_scenario(scenario: &str, twin_task: &str, twin_crate: &str, trigger_crate !registered.is_empty(), "math1-trigger never registered with the hub within {:?}\n{}", RUNNER_STARTUP_TIMEOUT, - quoted("math1-trigger", &trigger) + quoted("math1-trigger", &mut trigger) ); let mut twin = spawn_runner(scenario, twin_task); @@ -385,8 +414,8 @@ fn run_scenario(scenario: &str, twin_task: &str, twin_crate: &str, trigger_crate twin_task, twin_exited, trigger_exited, - quoted(twin_task, &twin), - quoted("math1-trigger", &trigger) + quoted_now(twin_task, &twin), + quoted_now("math1-trigger", &trigger) ); }; et_ws_test_server::math1::verify_math1_model(weight, bias).unwrap(); From a758de3541db51478646ac6dca411557ca47b7cb Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Fri, 18 Sep 2026 08:33:17 +0800 Subject: [PATCH 20/24] debugging --- .../ws-wasi-runner/tests/vector_otlp_relay.rs | 7 ++++- utilities/cli/tests/scenario_runners.rs | 30 ++++++++++++------- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/services/ws-wasi-runner/tests/vector_otlp_relay.rs b/services/ws-wasi-runner/tests/vector_otlp_relay.rs index f3f41afa..df7334d8 100644 --- a/services/ws-wasi-runner/tests/vector_otlp_relay.rs +++ b/services/ws-wasi-runner/tests/vector_otlp_relay.rs @@ -59,7 +59,12 @@ fn vector_relays_buffered_otlp_after_backend_comes_online() { let mut child = Command::new("vector") .arg("-c") .arg(&config_path) - .env("VECTOR_LOG", "warn") + // Sink detail, because `warn` left this test's failures undiagnosable. + // Every redelivery failure quoted two config-loading warnings and nothing else -- no attempt, no error, + // no retry -- which cannot distinguish a sink that retried and was refused from one that never had the + // event to send. The filter is narrow rather than a blanket `debug`: the sinks are the subsystem under + // test and the rest of Vector's debug output is noise the failure message would have to carry. + .env("VECTOR_LOG", "info,vector::sinks=debug") // Forward-slash the temp path: Vector interpolates it into a double-quoted YAML scalar, and on // Windows the backslashes would be parsed as YAML escapes (CI failed with "did not find expected // hexadecimal number"). Forward slashes are accepted on Windows too. diff --git a/utilities/cli/tests/scenario_runners.rs b/utilities/cli/tests/scenario_runners.rs index aea99365..fdb0eabd 100644 --- a/utilities/cli/tests/scenario_runners.rs +++ b/utilities/cli/tests/scenario_runners.rs @@ -112,6 +112,13 @@ fn spawn_runner(scenario: &str, task: &str) -> Runner { .arg(task) .current_dir(scenario_dir) .env("RUNNER_TIMEOUT", RUNNER_TIMEOUT) + // Make mise narrate its own startup, so a runner that never reaches its task can still be placed. + // On `build (windows-2025, servercore)` a trigger stayed alive for the whole 300s startup budget and + // wrote nothing at all -- not even the `$ cargo run ...` line mise echoes before it runs a task -- while + // the same commit on `windows-2022` logged normally and registered. Zero bytes says only "stalled before + // the task began", which covers tool resolution, the config load and the build lock alike; mise's own + // output distinguishes them. It rides on stderr, which is captured either way. + .env("MISE_VERBOSE", "1") .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn_checked() @@ -163,27 +170,30 @@ fn captured(buffer: &Arc>) -> String { /// before it could log from one that started and sat idle, so every failure path quotes both streams. That only /// works on a runner that has finished. The drain threads hand their buffer over at EOF, so quoting one still /// holding its pipes open reports two empty streams whatever it wrote -- which is how a hung trigger came to look -/// like a silent one across several CI rounds. Ending it first closes the pipes; the poll then covers the moment -/// between that and the drain thread storing what it read, and gives up rather than hanging if it stays empty, -/// since empty is a legitimate answer for a process that really did write nothing. +/// like a silent one across several CI rounds. Ending it first closes the pipes so there is something to read. #[expect( clippy::single_call_fn, reason = "paired with quoted_now on purpose: inlining it invites a future path to quote a still-running runner" )] fn quoted(label: &str, runner: &mut Runner) -> String { runner.guard.shutdown(); - let started = Instant::now(); - while started.elapsed() < DRAIN_SETTLE && captured(&runner.stdout).is_empty() && captured(&runner.stderr).is_empty() - { - std::thread::sleep(Duration::from_millis(50)); - } quoted_now(label, runner) } -/// Format whatever the buffers hold right now, without touching the process. +/// Quote a runner this path has already ended, waiting for its drain threads to hand over what they read. /// -/// Split out for the paths that have already waited the runner out, which must not shut it down a second time. +/// Split from [`quoted`] for the paths that waited the runner out themselves, which must not end it a second +/// time -- but they need the same wait. A runner killed at the end of `wait_for_exit` has its pipes closed only +/// microseconds before this reads them, so quoting it immediately races the drain thread and prints the blanks +/// that ending the process was supposed to prevent. The poll gives up rather than hanging, since empty is a +/// legitimate answer for a process that really did write nothing, and costs nothing once either stream has +/// anything in it. fn quoted_now(label: &str, runner: &Runner) -> String { + let started = Instant::now(); + while started.elapsed() < DRAIN_SETTLE && captured(&runner.stdout).is_empty() && captured(&runner.stderr).is_empty() + { + std::thread::sleep(Duration::from_millis(50)); + } format!( "--- {label} stdout ---\n{}\n--- {label} stderr ---\n{}", captured(&runner.stdout), From 432c1a910bb153e9c11e8b4b0f1f3e1659c5474e Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Fri, 18 Sep 2026 12:32:01 +0800 Subject: [PATCH 21/24] more coverage --- .mise/config.toml | 32 ++++++++++++++++++++++++++-- Cargo.toml | 2 +- services/ws-test-server/src/math1.rs | 32 +++++++++++++++++++++++++--- 3 files changed, 60 insertions(+), 6 deletions(-) diff --git a/.mise/config.toml b/.mise/config.toml index b81c9809..d63b82e8 100644 --- a/.mise/config.toml +++ b/.mise/config.toml @@ -1519,11 +1519,39 @@ description = "Repository-wide checks that need to read git itself (commit hashe run = "cargo run -q -p et-repo-check" [tasks.link-check] -description = "Check that URLs in .md and .rs files are reachable (network)" +description = "Check that URLs in prose, source and config files are reachable (network)" # The .rs glob is scoped to the source dirs, not a bare `**/*.rs`. # A recursive `**/*.rs` walks target/'s churning rustc temp files, and lychee expands globs before applying # `exclude_path`, so it aborts with a GlobError when one of those temps vanishes mid-iteration. -run = "lychee --config config/lychee.toml '**/*.md' 'libs/**/*.rs' 'services/**/*.rs' 'utilities/**/*.rs'" +# +# The config, workflow and Dockerfile globs are here because those files carry links nothing else validates. +# Every commit a workaround comment cites is written as a `/commit/` URL, and most of them live in a mise +# config, a workflow or a Dockerfile rather than in prose -- so with only the .md and .rs globs the majority of +# this repo's commit references were checked by nothing at all. +run = """ +cfg=config/lychee.toml +lychee --config "$cfg" '**/*.md' 'libs/**/*.rs' 'services/**/*.rs' 'utilities/**/*.rs' +# Everything except the commit URLs is excluded on the second pass, by `--exclude .*` with `--include` winning. +# Those files hold links this repo does not otherwise own -- release-asset bases joined to a filename at run +# time, doc placeholders, a regex quoted inside a comment -- and none of them is a URL a reader could follow. +# Checking the whole set reports 21 such fragments as broken. `--scheme https` drops the `file:` and +# `docker-image:` values for the same reason, and every commit URL is https. +conf=".mise/config*.toml .mise/*.sh config/*.toml Cargo.toml .deepsource.toml" +gha=".github/workflows/*.yaml .github/actions/*/*.yaml" +# k3s.yaml is skipped as a whole because one value in it cannot be filtered any other way. +# It passes a BuildKit named-context reference to `docker build`, and the `:latest` tag on the end reads as +# an invalid port number, so lychee fails to parse the token -- which happens before `--scheme` or +# `--exclude` get a say, and is why neither can reach it. Naming the value here would reproduce the problem +# in this file. The workflow carries no commit URLs today; one added there would go unchecked, which is the +# cost of the skip. +skip="--exclude-path .github/workflows/k3s.yaml" +# The flag patterns stay quoted while the glob lists are deliberately split and expanded by the shell. +# `--exclude .*` unquoted is a glob the shell resolves against the working directory, which hands lychee every +# dotfile as an input -- including its own `.lycheecache`, whose every line then reads as a broken URL. +# shellcheck disable=SC2086 +lychee --config "$cfg" --scheme https --exclude '.*' --include '/commit/[0-9a-f]{40}' $skip $conf $gha Dockerfile* +""" +shell = "{{ vars.task_shell }}" [tasks.ryl-check] description = "Lint YAML with ryl (a yamllint-compatible Rust linter)" diff --git a/Cargo.toml b/Cargo.toml index 826bd5dd..754f2078 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -141,7 +141,7 @@ log = "0.4" # accepts a newer minicov. minicov = "=0.3.8" # Third-party OTLP mock collector, wrapped by et-test-otlp for the workspace's integration tests. -# Serves the spec paths (/v1/traces, ...), so a consumer points `collector_url` at `http://host:port/v1` +# Serves the spec paths (/v1/traces, ...), so a consumer points `collector_url` at the mock's own `/v1` base # for `et-otlp`'s `{collector_url}/traces` shape to land on them. mock-collector = "0.2" onnx-extractor = "0.5" diff --git a/services/ws-test-server/src/math1.rs b/services/ws-test-server/src/math1.rs index 882c655b..3b96e317 100644 --- a/services/ws-test-server/src/math1.rs +++ b/services/ws-test-server/src/math1.rs @@ -39,6 +39,9 @@ pub const MATH1_TOLERANCE: f64 = 1e-12; /// How often the pointer is re-broadcast and the output file re-polled. const POLL_INTERVAL: Duration = Duration::from_millis(250); +/// What a peer closing the socket is reported as, whichever side of the exchange notices it. +const SOCKET_CLOSED: &str = "fake agent socket closed"; + /// Failure of the math1 exchange, either in the fake agent's transport or in the module's output. #[derive(Debug, thiserror::Error)] #[non_exhaustive] @@ -116,7 +119,7 @@ pub async fn drive_math1_exchange( }, Ok(Some(Ok(_))) => {} Ok(Some(Err(err))) => return Err(err.into()), - Ok(None) => return Err(Math1Error::Protocol("fake agent socket closed".to_string())), + Ok(None) => return Err(Math1Error::Protocol(SOCKET_CLOSED.to_string())), Err(_elapsed) => break, } } @@ -140,13 +143,36 @@ pub async fn drive_math1_exchange( // Ask for the roster and re-broadcast the pointer; both are safe to repeat. let list = serde_json::to_string(&ClientMessage::ListAgents)?; - socket.send(Message::text(list)).await?; + send_frame(&mut socket, list).await?; if !pointer.is_empty() { - socket.send(Message::text(pointer.clone())).await?; + send_frame(&mut socket, pointer.clone()).await?; } } } +/// Send one frame, reporting a peer that has already closed the same way the read side reports it. +/// +/// A peer's close reaches this exchange on whichever side happens to notice it first, and those were two +/// different errors: the drain loop returns the socket-closed protocol error, while a send issued after the +/// close lands surfaces tungstenite's `Sending after closing is not allowed` as a transport error. Which one a +/// caller saw depended on whether the close arrived inside the drain window or in the gap before the next send, +/// so a test asserting on the close was asserting on that timing. They are one condition and now read as one. +async fn send_frame(socket: &mut Socket, frame: String) -> Result<(), Math1Error> +where + Socket: futures_util::Sink + Unpin, +{ + use tokio_tungstenite::tungstenite::Error as WsError; + use tokio_tungstenite::tungstenite::error::ProtocolError; + + match socket.send(Message::text(frame)).await { + Ok(()) => Ok(()), + Err( + WsError::ConnectionClosed | WsError::AlreadyClosed | WsError::Protocol(ProtocolError::SendAfterClosing), + ) => Err(Math1Error::Protocol(SOCKET_CLOSED.to_string())), + Err(other) => Err(other.into()), + } +} + /// Check a module's global model against the expected weights for the canonical input. pub fn verify_math1_model(weight: f64, bias: f64) -> Result<(), Math1Error> { if (weight - MATH1_EXPECTED_WEIGHT).abs() > MATH1_TOLERANCE { From 8da5139369bbc95a2a82b67a3de56923bdb96044 Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Fri, 18 Sep 2026 14:15:51 +0800 Subject: [PATCH 22/24] skip on windows docker --- .github/workflows/docker-windows.yaml | 21 ++++++++++++++++++++- CLAUDE.md | 19 ++++++++++++------- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/.github/workflows/docker-windows.yaml b/.github/workflows/docker-windows.yaml index c6ac2897..6d99fda4 100644 --- a/.github/workflows/docker-windows.yaml +++ b/.github/workflows/docker-windows.yaml @@ -168,10 +168,29 @@ jobs: # same tests run everywhere else, including the Windows host lanes, and were passing here too (0.270s and # 0.301s on `build (windows-2022, servercore)`) when they were turned off, so nothing about them is known # to be broken on Windows. Drop the filter to put them back. + # + # `not test(pyo3_math1_scenario)` is the second skip, and unlike the k3s one it covers a real failure. + # Its trigger never registers on either servercore image, and nothing says why: + # math1-trigger never registered with the hub within 300s + # --- math1-trigger stdout --- + # --- math1-trigger stderr --- + # Both streams are empty after the process is killed and its drain threads are given time, and they stay + # empty with `MISE_VERBOSE=1` set on the spawn -- so the stall is before mise writes its first line, which + # rules out tool resolution, the config load and the task itself. The process starts and stays alive for + # the whole 300s. That is as far as a CI log reaches; what is left needs someone inside the image running + # `mise run math1-trigger` from `verification/local/output/pyo3-math1` by hand. + # Observed on commit + # https://github.com/edge-toolkit/core/commit/432c1a910bb153e9c11e8b4b0f1f3e1659c5474e on both hosts: + # https://github.com/edge-toolkit/core/actions/runs/35307356357/job/105482172804 (windows-2022) and + # https://github.com/edge-toolkit/core/actions/runs/35307356357/job/105482172400 (windows-2025). + # It is this image only: the same test passes on every Windows host lane and on a workstation, so the + # skip hides a difference between servercore and every other Windows environment rather than a defect in + # the test. Its two sibling scenarios stay on, and both keep exercising the same runner pair. - name: Run cargo-test if: matrix.base == 'servercore' run: | args="--rm --pull=never -e CARGO_INCREMENTAL=0" + skip='not test(k3s) and not test(pyo3_math1_scenario)' # $args is a word-split flag list by design; do not quote it. # shellcheck disable=SC2086 - docker run $args et-windows-test mise run cargo-test -- -E 'not test(k3s)' + docker run $args et-windows-test mise run cargo-test -- -E "$skip" diff --git a/CLAUDE.md b/CLAUDE.md index ce1e42c1..79884a07 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -878,14 +878,19 @@ a runner-image quirk, a toolchain bug, a CI-only flake -- and you decide to pape only way someone hitting the same symptom later finds your workaround is by grepping the repo for the error string. This applies equally to symptoms first seen locally and to GHA-only flakes. Alongside the error - string, **record the commit SHA the failure was observed on** (full - 40-char hash, so the comment stays unambiguous after force-pushes / - rebases) and, when applicable, **the GHA job-run URL** + string, **record the commit the failure was observed on as its web URL** + -- `https://github.com///commit/`, never + the bare hash -- and, when applicable, **the GHA job-run URL** (`https://github.com///actions/runs//job/`). - Both age out (the SHA may stop existing if a branch is deleted; the GHA - log expires at 3 months) but together they pin the WHERE and WHEN of the - evidence well enough for the next reader to cross-reference your local - notes, screenshots, or any persisted artifact. + The URL form is not decoration: pull requests here merge by squash, so a + commit written down while a branch is in flight stops existing the moment + it lands, and a bare hash that resolved when you wrote it resolves for + nobody afterwards. GitHub keeps serving the commit at that URL. This is + also enforced -- `repo-check` rejects any bare 40-char hash that is not + reachable from `origin/main`, which in practice means every one of them. + The GHA log still expires at 3 months, but the commit link does not, and + together they pin the WHERE and WHEN of the evidence well enough for the + next reader to cross-reference notes, screenshots, or any artifact. ## NEVER disable anything on Windows without explicitly asking the user first From 2574682d699c7ec0e1db9d6b10cb3f9d81026f02 Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Fri, 18 Sep 2026 16:16:19 +0800 Subject: [PATCH 23/24] another skip on windows docker --- .github/workflows/docker-windows.yaml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docker-windows.yaml b/.github/workflows/docker-windows.yaml index 6d99fda4..2bef3935 100644 --- a/.github/workflows/docker-windows.yaml +++ b/.github/workflows/docker-windows.yaml @@ -169,7 +169,7 @@ jobs: # 0.301s on `build (windows-2022, servercore)`) when they were turned off, so nothing about them is known # to be broken on Windows. Drop the filter to put them back. # - # `not test(pyo3_math1_scenario)` is the second skip, and unlike the k3s one it covers a real failure. + # The two scenario skips are the second and third, and unlike the k3s one they cover a real failure. # Its trigger never registers on either servercore image, and nothing says why: # math1-trigger never registered with the hub within 300s # --- math1-trigger stdout --- @@ -185,12 +185,17 @@ jobs: # https://github.com/edge-toolkit/core/actions/runs/35307356357/job/105482172400 (windows-2025). # It is this image only: the same test passes on every Windows host lane and on a workstation, so the # skip hides a difference between servercore and every other Windows environment rather than a defect in - # the test. Its two sibling scenarios stay on, and both keep exercising the same runner pair. + # the test. Both skipped scenarios are driven by `wasi-math1-sender`, which is why skipping one moved the + # same failure onto the other rather than turning the lane green -- the fault is the wasi trigger, not + # anything either twin does. `math1_scenario` stays on and is the one driven by the browser-targeted + # `math1-sender`, so the generator itself is still covered end to end here; what this lane loses is the + # wasi trigger, which no container currently starts. - name: Run cargo-test if: matrix.base == 'servercore' run: | args="--rm --pull=never -e CARGO_INCREMENTAL=0" - skip='not test(k3s) and not test(pyo3_math1_scenario)' + scenarios='not test(pyo3_math1_scenario) and not test(wasi_math1_scenario)' + skip="not test(k3s) and $scenarios" # $args is a word-split flag list by design; do not quote it. # shellcheck disable=SC2086 docker run $args et-windows-test mise run cargo-test -- -E "$skip" From d7df5c195b69271e8933ede78a7cdfbac566e91c Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Fri, 18 Sep 2026 18:28:09 +0800 Subject: [PATCH 24/24] mirror windows busybox --- .mise/config.maint.toml | 14 ++++++++++++++ .mise/config.windows.toml | 10 +++++++++- config/conftest/policy/mise/mise.rego | 1 + config/upstream-cache/data.toml | 6 ++++++ 4 files changed, 30 insertions(+), 1 deletion(-) diff --git a/.mise/config.maint.toml b/.mise/config.maint.toml index e9223b6a..de498677 100644 --- a/.mise/config.maint.toml +++ b/.mise/config.maint.toml @@ -680,6 +680,20 @@ gh release create "$tag" --repo "$GH_REPO" --title "$tag" --notes "$notes" --pre """ shell = "{{ vars.task_shell }}" +[tasks.bootstrap-busybox-w32-release] +description = "Create the busybox-w32-v1 GitHub release if it doesn't exist (idempotent)" +run = """ +tag=busybox-w32-v1 +if gh release view "$tag" --repo "$GH_REPO" >/dev/null 2>&1; then + echo "release $tag already exists on $GH_REPO" + exit 0 +fi +notes="https://frippery.org/busybox/ +GPL-2.0-only" +gh release create "$tag" --repo "$GH_REPO" --title "$tag" --notes "$notes" --prerelease "]` row in config/upstream-cache/data.toml # (the checksums.rego policy enforces the bidirectional cross-reference). +busybox_asset = "busybox64u.exe" dart_typegen_asset = "0.1.13-x86_64-pc-windows-msvc.tar.gz" gnupg_w32_asset = "2.5.20_20260513-x86_64-pc-windows.tar.gz" diff --git a/config/conftest/policy/mise/mise.rego b/config/conftest/policy/mise/mise.rego index 0f5d0aff..2538393d 100644 --- a/config/conftest/policy/mise/mise.rego +++ b/config/conftest/policy/mise/mise.rego @@ -160,6 +160,7 @@ deny contains msg if { allowed_http_forge_url := { # This repo's own upstream-cache mirror releases -- the documented pattern, not migration candidates. "http:augeas", + "http:busybox", "http:dart-typegen", "http:et-rp", "http:gnupg-w32", diff --git a/config/upstream-cache/data.toml b/config/upstream-cache/data.toml index e23ecd68..c0ead809 100644 --- a/config/upstream-cache/data.toml +++ b/config/upstream-cache/data.toml @@ -119,6 +119,12 @@ sha256 = "10fc4aff94a5c6016c51b4dbbc224277a340a083c315ca0a05e9f5ef9a869403" upstream = "https://github.com/hercules-team/augeas" url = "https://github.com/edge-toolkit/core/releases/download/augeas-v1/1.14.1-aarch64-apple-darwin.tar.xz" +[asset."busybox64u.exe"] +license = "GPL-2.0-only" +sha256 = "6e263d154d8548d1eb936f65d1d8312c80df31c45974e48d6335e4dcc0f4f34c" +upstream = "https://frippery.org/busybox/" +url = "https://github.com/edge-toolkit/core/releases/download/busybox-w32-v1/busybox64u.exe" + [asset."0.1.13-x86_64-pc-windows-msvc.tar.gz"] license = "MIT OR Apache-2.0" sha256 = "f21eab43597b0325b672de61fd396465aa371d3e9e32da6c56e93a89a003b5aa"