From 470ec1e0afc3ba60eff2123c410ba92b59b7fa57 Mon Sep 17 00:00:00 2001 From: Reiase Date: Tue, 8 Sep 2026 12:44:22 +0800 Subject: [PATCH 1/6] chore: route CI through just and consolidate recipes Make justfile the single source for local and CI build/test entrypoints, collapse overlapping gate/dev/cases/build helpers, and stop invoking raw cargo/uv commands from the main CI workflow. Co-authored-by: Cursor --- .github/actions/setup-build-env/action.yml | 2 - .github/workflows/ci.yml | 135 +++++------ .github/workflows/wheel.yml | 2 +- justfile | 264 +++++++++------------ 4 files changed, 180 insertions(+), 223 deletions(-) diff --git a/.github/actions/setup-build-env/action.yml b/.github/actions/setup-build-env/action.yml index 4e89bbaf..e5f7210d 100644 --- a/.github/actions/setup-build-env/action.yml +++ b/.github/actions/setup-build-env/action.yml @@ -12,8 +12,6 @@ inputs: components: description: "Rust components (comma-separated)" required: false - # rust-src keeps the opt-in `just sanitize` (-Z build-std) path usable in - # CI without changing the normal build commands. default: "rustfmt, clippy, rust-src" install-dioxus: description: "Install the pinned Dioxus CLI and wasm target" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 73c83ee9..3aec4b88 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,9 +32,7 @@ jobs: components: "rustfmt" - name: Rust formatting - run: | - cargo fmt --all -- --check - cargo fmt --manifest-path pchronicle-web/Cargo.toml -- --check + run: just fmt-check-rust rust-clippy-workspace: name: Clippy / Workspace @@ -79,22 +77,42 @@ jobs: - name: pChronicle storage feature contracts run: just clippy-pchronicle-features - python-lint: - name: Python Lint + rust-clippy-pchronicle-web: + name: Clippy / pChronicle Web runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - name: Setup Build Environment + uses: ./.github/actions/setup-build-env with: - python-version: "3.12" + install-nextest: "false" + install-zig: "false" + components: "clippy" + + - uses: Swatinem/rust-cache@v2 + with: + shared-key: ci-clippy-pchronicle-web + workspaces: pchronicle-web -> target + + - name: pChronicle Web Clippy + run: just clippy-pchronicle-web + + python-lint: + name: Python Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v6 + - name: Setup Build Environment + uses: ./.github/actions/setup-build-env with: - enable-cache: true + install-nextest: "false" + install-zig: "false" + components: "" - name: Ruff - run: uvx ruff check persisting/ + run: just lint-py rust-test: name: Rust Test / ${{ matrix.platform }} / ${{ matrix.shard }} @@ -107,12 +125,12 @@ jobs: os: ubuntu-latest shard: core packages: >- - -p persisting-agentctl - -p persisting-events - -p persisting-overlay-core - -p persisting-overlayfs - -p persisting-overlaynet - -p persisting-replay + persisting-agentctl + persisting-events + persisting-overlay-core + persisting-overlayfs + persisting-overlaynet + persisting-replay event_control: true prepare_pvisor: false prepare_runtime: false @@ -123,7 +141,7 @@ jobs: - platform: Linux os: ubuntu-latest shard: gateway - packages: -p persisting-gateway + packages: persisting-gateway event_control: false prepare_pvisor: false prepare_runtime: false @@ -134,7 +152,7 @@ jobs: - platform: Linux os: ubuntu-latest shard: pvisor - packages: -p persisting-pvisor + packages: persisting-pvisor event_control: false prepare_pvisor: true prepare_runtime: false @@ -145,7 +163,7 @@ jobs: - platform: Linux os: ubuntu-latest shard: ppilot - packages: -p persisting-ppilot + packages: persisting-ppilot event_control: false prepare_pvisor: false prepare_runtime: true @@ -157,8 +175,8 @@ jobs: os: ubuntu-latest shard: pchronicle packages: >- - -p persisting-pchronicle - -p persisting-pchronicle-cli + persisting-pchronicle + persisting-pchronicle-cli event_control: false prepare_pvisor: false prepare_runtime: false @@ -170,15 +188,15 @@ jobs: os: macos-latest shard: agent-runtime packages: >- - -p persisting-agentctl - -p persisting-events - -p persisting-gateway - -p persisting-overlay-core - -p persisting-overlayfs - -p persisting-overlaynet - -p persisting-ppilot - -p persisting-pvisor - -p persisting-replay + persisting-agentctl + persisting-events + persisting-gateway + persisting-overlay-core + persisting-overlayfs + persisting-overlaynet + persisting-ppilot + persisting-pvisor + persisting-replay event_control: false prepare_pvisor: false prepare_runtime: true @@ -189,7 +207,7 @@ jobs: - platform: macOS os: macos-latest shard: pvisor - packages: -p persisting-pvisor + packages: persisting-pvisor event_control: false prepare_pvisor: true prepare_runtime: false @@ -201,8 +219,8 @@ jobs: os: macos-latest shard: pchronicle packages: >- - -p persisting-pchronicle - -p persisting-pchronicle-cli + persisting-pchronicle + persisting-pchronicle-cli event_control: false prepare_pvisor: false prepare_runtime: false @@ -234,8 +252,7 @@ jobs: - name: Build Agent runtime component set if: matrix.prepare_runtime run: | - just pvisor debug - just build-components debug pchronicle-ppilot + just build-agent-runtime debug echo "${GITHUB_WORKSPACE}/target/debug" >> "${GITHUB_PATH}" - name: Run ${{ matrix.shard }} tests @@ -243,15 +260,11 @@ jobs: # GitHub-hosted Linux runners may disable unprivileged user # namespaces. The dedicated isolation job remains strict. PERSISTING_TEST_ALLOW_NO_USERNS: "1" - run: cargo nextest run --locked ${{ matrix.packages }} + run: just ci-nextest ${{ matrix.packages }} - name: Test shared event control contract if: matrix.event_control - run: cargo nextest run -p persisting-events --features control --locked - - - name: Build pChronicle CLI - if: matrix.smoke_pchronicle - run: cargo build -p persisting-pchronicle-cli --bin pchronicle --locked + run: just test-events-control - name: Smoke pVisor CLI if: matrix.smoke_pvisor @@ -259,13 +272,11 @@ jobs: - name: Smoke pPilot CLI if: matrix.smoke_ppilot - run: | - ./target/debug/ppilot run --help - ./target/debug/ppilot produce --help + run: just smoke-ppilot-cli - name: Smoke pChronicle CLI if: matrix.smoke_pchronicle - run: ./target/debug/pchronicle query --help + run: just smoke-pchronicle-cli rust-proptest: name: Rust Proptest / pChronicle @@ -307,11 +318,8 @@ jobs: shared-key: ci-pchronicle-web workspaces: pchronicle-web -> target - - name: Build Web assets - run: python3 scripts/packaging/stage_wheel_binaries.py --web-only - - - name: Test Web crate - run: cargo nextest run --manifest-path pchronicle-web/Cargo.toml --locked + - name: Build and test Web crate + run: just test-pchronicle-web pvisor-isolation-regression: name: pVisor Isolation Regression / Linux @@ -404,7 +412,7 @@ jobs: AWS_ENDPOINT: http://127.0.0.1:9000 AWS_ALLOW_HTTP: "true" AWS_VIRTUAL_HOSTED_STYLE_REQUEST: "false" - run: cargo nextest run -p persisting-pchronicle --test s3_storage --locked --run-ignored all + run: just test-pchronicle-s3 - name: Show MinIO logs after failure if: failure() @@ -420,19 +428,18 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - - uses: astral-sh/setup-uv@v6 + - name: Setup Build Environment + uses: ./.github/actions/setup-build-env with: - enable-cache: true + install-nextest: "false" + install-zig: "false" + components: "" - name: Sync Python deps - run: uv sync --extra all --extra dev + run: just py-dev - name: Pytest - run: uv run pytest tests/ -q + run: just test-py pvisor-cases: name: pVisor case checklist @@ -451,13 +458,9 @@ jobs: with: shared-key: ci-pvisor-cases - - name: Build pVisor - run: cargo build --release -p persisting-pvisor --locked - - name: Run documented cases run: | - python3 scripts/run-pvisor-cases.py \ - --pvisor ./target/release/pvisor \ + just cases pvisor -- \ --case A01,A02,A03,A04,A05,A06 \ --case B01,B02,B03,B04 \ --case C02,C03,C04 \ @@ -466,8 +469,7 @@ jobs: --case G01,G02,G03,G06 \ --case H01,H02 \ --case I01,I02,I03 \ - --case J03 \ - --report target/pvisor-case-report.md + --case J03 - name: Upload case report if: always() @@ -528,6 +530,7 @@ jobs: - format - rust-clippy-workspace - rust-clippy-pchronicle + - rust-clippy-pchronicle-web - python-lint - rust-test - rust-proptest diff --git a/.github/workflows/wheel.yml b/.github/workflows/wheel.yml index 3ac7b594..fe1580be 100644 --- a/.github/workflows/wheel.yml +++ b/.github/workflows/wheel.yml @@ -36,7 +36,7 @@ jobs: - uses: Swatinem/rust-cache@v2 - name: Build pChronicle Web assets - run: python3 scripts/packaging/stage_wheel_binaries.py --web-only + run: just chronicle-web-build - name: Apply nightly local version (PEP 440) if: inputs.local_version != '' diff --git a/justfile b/justfile index faab4426..c4cc44a8 100644 --- a/justfile +++ b/justfile @@ -22,7 +22,7 @@ default: @just --list --unsorted @echo "" @echo "常用:" - @echo " just gate # 提交前(fmt + lint + test-rust)" + @echo " just dev # 提交前(fmt + lint + test-rust)" @echo " just test [package] # 日常功能测试(可指定 Cargo 包)" @echo " just proptest pchronicle # pChronicle 全量 Proptest 回归" @echo " just ci # CI 近似全量" @@ -36,8 +36,6 @@ default: @echo " just benchmark-gateway # Gateway 转发与持久化黑盒压测" @echo " just benchmark-gateway-replay # 回放 examples/data 并生成人工 review bundle" @echo " just build-wheel # 打 release wheel → dist/" - @echo " just build-analysis # nightly 构建分析(按需)" - @echo " just sanitize # nightly Sanitizer 测试(按需)" @echo " just docs-serve # 本地文档" # ── 测试套件导航 ────────────────────────────────────────────────────────────── @@ -51,11 +49,8 @@ test-list: Persisting 测试入口 门禁 / Rust(just) - just gate 提交前:fmt + lint + test-rust - just dev 日常:fmt + clippy + check-quick + just dev 提交前:fmt + lint + test-rust just ci CI 近似 - just build-analysis nightly 构建瓶颈分析(按需) - just sanitize nightly Sanitizer 测试(按需) just test [package] 日常功能测试;可指定 Cargo 包 just proptest pchronicle pChronicle 全量性质测试回归 just capture-test / test-py 其他定向测试入口 @@ -65,6 +60,7 @@ test-list: just gateway-fuzz-formats / gateway-fuzz-forwarding just gateway-fuzz-storage / gateway-fuzz-network just cases pvisor|pchronicle|pchronicle-cluster + just cases pvisor -- --run-unavailable --keep 组件示例 just examples-pvisor 全部 pVisor 场景 @@ -273,40 +269,26 @@ chronicle-binary profile="debug": chronicle-web-build set -euo pipefail profile="{{ profile }}" case "$profile" in - debug) - binary="{{ repo }}/target/debug/pchronicle" - just build-components debug pchronicle - ;; - release) - binary="{{ repo }}/target/release/pchronicle" - just build-components release pchronicle - ;; + debug|release) ;; *) echo "unsupported pChronicle profile: $profile (expected debug or release)" >&2 exit 2 ;; esac - + just build-components "$profile" pchronicle + binary="{{ repo }}/target/$profile/pchronicle" test -x "$binary" "$binary" serve --help >/dev/null printf 'Built pChronicle test binary: %s\n' "$binary" printf 'Run: %s serve --warehouse %s\n' "$binary" "{{ repo }}/data" +# Thin forward to `build-components` for the full product CLI set. +[group('build')] build profile="debug": - #!/usr/bin/env bash - set -euo pipefail - profile="{{ profile }}" - case "$profile" in - debug|release) ;; - *) - echo "unsupported build profile: $profile (expected debug or release)" >&2 - exit 2 - ;; - esac - just build-components "$profile" all + just build-components "{{ profile }}" all -# Build one product component, the benchmark binary, or the complete runtime component set. -# This is the single Cargo build entry point used by local recipes and CI. +# Single Cargo build entry for product CLIs. Any set that produces `pvisor` +# also applies the macOS Hypervisor entitlement when running on Darwin. [group('build')] build-components profile="debug" components="all": #!/usr/bin/env bash @@ -318,10 +300,13 @@ build-components profile="debug" components="all": release) cargo_profile=release ;; *) echo "unsupported build profile: $profile (expected debug or release)" >&2; exit 2 ;; esac + + built_pvisor=0 case "$components" in all|runtime) cargo build --profile "$cargo_profile" --locked \ {{ component_pchronicle }} {{ component_pvisor }} {{ component_ppilot }} + built_pvisor=1 ;; pchronicle) cargo build --profile "$cargo_profile" --locked {{ component_pchronicle }} @@ -334,9 +319,11 @@ build-components profile="debug" components="all": pvisor-pchronicle) cargo build --profile "$cargo_profile" --locked \ {{ component_pvisor }} {{ component_pchronicle }} + built_pvisor=1 ;; pvisor) cargo build --profile "$cargo_profile" --locked {{ component_pvisor }} + built_pvisor=1 ;; ppilot) cargo build --profile "$cargo_profile" --locked {{ component_ppilot }} @@ -346,86 +333,44 @@ build-components profile="debug" components="all": {{ component_pchronicle }} {{ component_ppilot }} ;; *) - echo "unsupported component set: $components (all|runtime|pchronicle|pchronicle-benchmark|pvisor|pvisor-pchronicle|ppilot|pchronicle-ppilot)" >&2 + echo "unsupported component set: $components (all|pchronicle|pchronicle-benchmark|pvisor|pvisor-pchronicle|ppilot|pchronicle-ppilot)" >&2 exit 2 ;; esac -# Collect detailed Cargo build metrics without enabling the overhead for every -# normal build. Reports are persisted under CARGO_HOME/log and can be queried -# with `just build-analysis-report`. -[group('diagnostics')] -build-analysis package="persisting-pvisor" target_dir="target/build-analysis": - #!/usr/bin/env bash - set -euo pipefail - cargo +nightly -Zbuild-analysis \ - --config unstable.build-analysis=true \ - --config build.analysis.enabled=true \ - build --locked --timings --target-dir "{{ target_dir }}" -p "{{ package }}" - echo "Build analysis recorded. Run: just build-analysis-report" - -# Query metrics collected by build-analysis. `report` may be sessions, timings, -# or rebuilds; timings/rebuilds use the most recent session by default. -[group('diagnostics')] -build-analysis-report report="sessions": - #!/usr/bin/env bash - set -euo pipefail - case "{{ report }}" in - sessions|timings|rebuilds) ;; - *) echo "unsupported report: {{ report }} (sessions|timings|rebuilds)" >&2; exit 2 ;; - esac - cargo +nightly -Zbuild-analysis report "{{ report }}" + if [[ "$built_pvisor" -eq 1 ]]; then + just _sign-pvisor "$profile" + fi -# Run one crate's tests with a nightly sanitizer. Sanitizers require LLVM -# instrumentation and a rebuilt standard library, and are substantially slower. -[group('diagnostics')] -sanitize sanitizer="address" package="persisting-agentctl": - #!/usr/bin/env bash - set -euo pipefail - case "{{ sanitizer }}" in - address|leak|thread|undefined) ;; - *) echo "unsupported sanitizer: {{ sanitizer }} (address|leak|thread|undefined)" >&2; exit 2 ;; - esac - host="$$(rustup run nightly rustc -vV | sed -n 's/^host: //p')" - [[ -n "$$host" ]] || { echo "unable to determine nightly target; install nightly rustc" >&2; exit 1; } - export RUSTFLAGS="$${RUSTFLAGS:+$$RUSTFLAGS }-Zsanitizer={{ sanitizer }}" - cargo +nightly -Zbuild-std \ - test --locked --target "$$host" --target-dir "target/sanitizer/{{ sanitizer }}" \ - -p "{{ package }}" --lib - -# Build pVisor and add the Hypervisor entitlement required by macOS/HVF. -# Usage: `just pvisor` (release) or `just pvisor debug`. -[group('build')] -pvisor profile="release": +# macOS HVF entitlement for the pVisor binary produced by `build-components`. +[private] +_sign-pvisor profile: #!/usr/bin/env bash set -euo pipefail profile="{{ profile }}" case "$profile" in - release) - binary="{{ repo }}/target/release/pvisor" - just build-components release pvisor - ;; - debug) - binary="{{ repo }}/target/debug/pvisor" - just build-components debug pvisor - ;; - *) - echo "unsupported pVisor profile: $profile (expected release or debug)" >&2 - exit 2 - ;; + debug|release) ;; + *) echo "unsupported pVisor profile: $profile (expected debug or release)" >&2; exit 2 ;; esac - - if [[ "$(uname -s)" == "Darwin" ]]; then - entitlements="{{ repo }}/crates/persisting-pvisor/macos-hypervisor.entitlements" - command -v codesign >/dev/null - codesign --force --sign - --entitlements "$entitlements" "$binary" - codesign --verify --strict --verbose=2 "$binary" - codesign -d --entitlements :- "$binary" 2>&1 \ - | grep -q 'com.apple.security.hypervisor' - echo "Built and signed pVisor: $binary" - else - echo "Built pVisor: $binary" + binary="{{ repo }}/target/$profile/pvisor" + test -x "$binary" + if [[ "$(uname -s)" != "Darwin" ]]; then + echo "Built pVisor: $binary" + exit 0 fi + entitlements="{{ repo }}/crates/persisting-pvisor/macos-hypervisor.entitlements" + command -v codesign >/dev/null + codesign --force --sign - --entitlements "$entitlements" "$binary" + codesign --verify --strict --verbose=2 "$binary" + codesign -d --entitlements :- "$binary" 2>&1 \ + | grep -q 'com.apple.security.hypervisor' + echo "Built and signed pVisor: $binary" + +# Build (and on macOS, sign) pVisor. Thin forward to `build-components`. +# Usage: `just pvisor` (release) or `just pvisor debug`. +[group('build')] +pvisor profile="release": + just build-components "{{ profile }}" pvisor # Install the three product CLIs. install-cli: @@ -528,8 +473,9 @@ fix-py: fmt-py style: fmt-check lint @echo "✅ format + lint OK" -# fmt + lint + Rust 测试(提交前) -gate: +# fmt + lint + Rust 测试(日常 / 提交前) +[group('test')] +dev: just fmt just lint just test-rust @@ -538,30 +484,37 @@ gate: ci-lint: just fmt-check-rust just lint-rust - uvx ruff check persisting/ - -# 日常开发快捷路径。完整 workspace/all-targets lint 仍由 gate/CI 负责;这里 -# 只检查日常会改动的运行时 crate,并用无存储 feature 的 pChronicle 核心检查 -# 保持增量反馈快速。 -dev: - just fmt - cargo clippy \ - -p persisting-agentctl \ - -p persisting-events \ - -p persisting-gateway \ - -p persisting-ppilot \ - -p persisting-pvisor \ - --lib --bins --locked -- -D warnings - just check-quick + just lint-py # CI 近似:功能门禁 + Proptest 回归 + 构建 ci: - just gate + just dev just proptest pchronicle just build +# Build the Agent runtime binaries that pPilot integration tests resolve from PATH. +[group('build')] +build-agent-runtime profile="debug": + just pvisor "{{ profile }}" + just build-components "{{ profile }}" pchronicle-ppilot + # ── Rust 测试 ───────────────────────────────────────────────────────────────── +# CI shard helper: `just ci-nextest persisting-gateway persisting-events …` +[group('test')] +ci-nextest *packages: + #!/usr/bin/env bash + set -euo pipefail + if [[ "$#" -eq 0 ]]; then + echo "usage: just ci-nextest ..." >&2 + exit 2 + fi + args=() + for pkg in "$@"; do + args+=(-p "$pkg") + done + cargo nextest run --locked "${args[@]}" + # 单 crate:pchronicle | pchronicle-cli | agentctl | capture | ppilot | pvisor | dlcapt test-crate crate: #!/usr/bin/env bash @@ -603,22 +556,6 @@ test-rust package="": test-pvisor: cargo nextest run -p persisting-pvisor --locked -# Execute the bash blocks embedded in the pVisor Markdown case checklist. -# VM/container cases are skipped unless their host prerequisites are supplied; -# use --strict-skips through the script when a fully provisioned matrix is -# required. -[group('test')] -test-pvisor-cases: - cargo build --release -p persisting-pvisor --locked - python3 scripts/run-pvisor-cases.py --report target/pvisor-case-report.md - -# Execute every documented case even when VM/container resources are absent; -# missing prerequisites become real case failures for diagnosis. -[group('test')] -test-pvisor-cases-all: - cargo build --release -p persisting-pvisor --locked - python3 scripts/run-pvisor-cases.py --run-unavailable --keep --report target/pvisor-case-report.md - # Mandatory pVisor ↔ pChronicle capture bridge feature profile. [group('test')] test-pvisor-lance: @@ -638,6 +575,31 @@ smoke-pvisor-cli: target/debug/pvisor status --help >/dev/null target/debug/pvisor review --help >/dev/null +[group('test')] +smoke-ppilot-cli: + target/debug/ppilot run --help >/dev/null + target/debug/ppilot produce --help >/dev/null + +[group('test')] +smoke-pchronicle-cli: + just build-components debug pchronicle + target/debug/pchronicle query --help >/dev/null + +# Shared EventRecord control-plane feature contract used by the Linux core shard. +[group('test')] +test-events-control: + cargo nextest run -p persisting-events --features control --locked + +# Separate Cargo workspace covering the Dioxus trajectory workbench. +[group('test')] +test-pchronicle-web: chronicle-web-build + cargo nextest run --manifest-path pchronicle-web/Cargo.toml --locked + +# Real S3/MinIO contract (ignored by default; requires PCHRONICLE_S3_TEST_URI). +[group('test')] +test-pchronicle-s3: + cargo nextest run -p persisting-pchronicle --test s3_storage --locked --run-ignored all + test-capture-claude: cargo nextest run -p persisting-gateway --test capture_apps_claude --locked @@ -747,39 +709,33 @@ check-quick: capture-test: just test-crate capture -# Execute pChronicle single-machine/self-service cases. -[group('test')] -test-pchronicle-cases: - cargo build --release -p persisting-pchronicle-cli --locked - python3 scripts/run-pchronicle-cases.py --document docs/src/pchronicle/reference/cases-self.md --pchronicle target/release/pchronicle --report target/pchronicle-self-case-report.md - -# List and execute pChronicle platform/Catalog cases. Server lifecycle cases are -# reported as MANUAL unless explicitly selected with PCHRONICLE_CASE_MODE. -[group('test')] -test-pchronicle-cases-platform: - cargo build --release -p persisting-pchronicle-cli --locked - python3 scripts/run-pchronicle-cases.py --document docs/src/pchronicle/reference/cases-platform.md --pchronicle target/release/pchronicle --report target/pchronicle-platform-case-report.md - # Run documented integration cases by component. -# Examples: just cases pvisor | pchronicle | pchronicle-cluster -cases target: +# Examples: +# just cases pvisor +# just cases pchronicle +# just cases pchronicle-cluster +# Extra args are forwarded to the underlying runner after `--`, e.g. +# just cases pvisor -- --run-unavailable --keep +# just cases pvisor -- --case A01,A02 --case B01 +[group('test')] +cases target *args: #!/usr/bin/env bash set -euo pipefail case "{{target}}" in pvisor) - cargo build --release -p persisting-pvisor --locked - python3 scripts/run-pvisor-cases.py --report target/pvisor-case-report.md + just pvisor release + python3 scripts/run-pvisor-cases.py --report target/pvisor-case-report.md {{args}} ;; pchronicle) - cargo build --release -p persisting-pchronicle-cli --locked - python3 scripts/run-pchronicle-cases.py --document docs/src/pchronicle/reference/cases-self.md --pchronicle target/release/pchronicle --report target/pchronicle-self-case-report.md + just build-components release pchronicle + python3 scripts/run-pchronicle-cases.py --document docs/src/zh/pchronicle/reference/cases-self.md --pchronicle target/release/pchronicle --report target/pchronicle-self-case-report.md {{args}} ;; pchronicle-cluster) - cargo build --release -p persisting-pchronicle-cli --locked - python3 scripts/run-pchronicle-cases.py --document docs/src/pchronicle/reference/cases-platform.md --pchronicle target/release/pchronicle --report target/pchronicle-platform-case-report.md + just build-components release pchronicle + python3 scripts/run-pchronicle-cases.py --document docs/src/zh/pchronicle/reference/cases-platform.md --pchronicle target/release/pchronicle --report target/pchronicle-platform-case-report.md {{args}} ;; *) - echo "usage: just cases pvisor|pchronicle|pchronicle-cluster" >&2 + echo "usage: just cases pvisor|pchronicle|pchronicle-cluster [-- runner-args...]" >&2 exit 2 ;; esac From eca5278e1c9d57f063bc1a747794e045cb12cdf4 Mon Sep 17 00:00:00 2001 From: Reiase Date: Tue, 8 Sep 2026 12:47:46 +0800 Subject: [PATCH 2/6] fix: pass ci-nextest packages via just interpolation Variadic recipe arguments are not exposed as $@ in shebang recipes on the pinned just version, so package names never reached nextest. Co-authored-by: Cursor --- justfile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/justfile b/justfile index c4cc44a8..af4ef4fb 100644 --- a/justfile +++ b/justfile @@ -505,12 +505,14 @@ build-agent-runtime profile="debug": ci-nextest *packages: #!/usr/bin/env bash set -euo pipefail - if [[ "$#" -eq 0 ]]; then + # Variadic recipe args are interpolated by just; they are not passed as $@. + packages=({{ packages }}) + if [[ "${#packages[@]}" -eq 0 ]]; then echo "usage: just ci-nextest ..." >&2 exit 2 fi args=() - for pkg in "$@"; do + for pkg in "${packages[@]}"; do args+=(-p "$pkg") done cargo nextest run --locked "${args[@]}" From b9b3843a6ee51a034b1836efefdaef97fb32a410 Mon Sep 17 00:00:00 2001 From: Reiase Date: Tue, 8 Sep 2026 12:49:14 +0800 Subject: [PATCH 3/6] fix: require ci-nextest packages via just +variadic Use +packages so missing args fail in just itself, and expand package names through {{ packages }} instead of shebang $@. Co-authored-by: Cursor --- justfile | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/justfile b/justfile index af4ef4fb..a4d2b570 100644 --- a/justfile +++ b/justfile @@ -501,18 +501,13 @@ build-agent-runtime profile="debug": # ── Rust 测试 ───────────────────────────────────────────────────────────────── # CI shard helper: `just ci-nextest persisting-gateway persisting-events …` +# Variadic args are interpolated by just (not passed as shebang $@). [group('test')] -ci-nextest *packages: +ci-nextest +packages: #!/usr/bin/env bash set -euo pipefail - # Variadic recipe args are interpolated by just; they are not passed as $@. - packages=({{ packages }}) - if [[ "${#packages[@]}" -eq 0 ]]; then - echo "usage: just ci-nextest ..." >&2 - exit 2 - fi args=() - for pkg in "${packages[@]}"; do + for pkg in {{ packages }}; do args+=(-p "$pkg") done cargo nextest run --locked "${args[@]}" From 96a6f320a1833e5250262fe5fd7b67a3de429696 Mon Sep 17 00:00:00 2001 From: Reiase Date: Tue, 8 Sep 2026 12:57:00 +0800 Subject: [PATCH 4/6] fix: strip cases -- separator and absolutize pchronicle just forwards a leading `--` into recipe args, which argparse rejects. Also resolve pchronicle to an absolute path so temp-cwd case bodies can find the binary. Co-authored-by: Cursor --- .github/workflows/ci.yml | 2 +- justfile | 22 +++++++++++++------- scripts/run-pchronicle-cases.py | 37 +++++++++++++++++++++++++++++---- 3 files changed, 48 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3aec4b88..8e937b4f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -460,7 +460,7 @@ jobs: - name: Run documented cases run: | - just cases pvisor -- \ + just cases pvisor \ --case A01,A02,A03,A04,A05,A06 \ --case B01,B02,B03,B04 \ --case C02,C03,C04 \ diff --git a/justfile b/justfile index a4d2b570..01db9eee 100644 --- a/justfile +++ b/justfile @@ -60,7 +60,7 @@ test-list: just gateway-fuzz-formats / gateway-fuzz-forwarding just gateway-fuzz-storage / gateway-fuzz-network just cases pvisor|pchronicle|pchronicle-cluster - just cases pvisor -- --run-unavailable --keep + just cases pvisor --run-unavailable --keep 组件示例 just examples-pvisor 全部 pVisor 场景 @@ -711,28 +711,34 @@ capture-test: # just cases pvisor # just cases pchronicle # just cases pchronicle-cluster -# Extra args are forwarded to the underlying runner after `--`, e.g. -# just cases pvisor -- --run-unavailable --keep -# just cases pvisor -- --case A01,A02 --case B01 +# Extra runner flags can be passed directly, e.g. +# just cases pvisor --run-unavailable --keep +# just cases pvisor --case A01,A02 --case B01 [group('test')] cases target *args: #!/usr/bin/env bash set -euo pipefail + # Variadic args are interpolated by just. Callers may also pass a leading + # `--` to stop just flag parsing; strip it before forwarding to the runner. + args=({{ args }}) + if [[ "${#args[@]}" -gt 0 && "${args[0]}" == "--" ]]; then + args=("${args[@]:1}") + fi case "{{target}}" in pvisor) just pvisor release - python3 scripts/run-pvisor-cases.py --report target/pvisor-case-report.md {{args}} + python3 scripts/run-pvisor-cases.py --report target/pvisor-case-report.md "${args[@]}" ;; pchronicle) just build-components release pchronicle - python3 scripts/run-pchronicle-cases.py --document docs/src/zh/pchronicle/reference/cases-self.md --pchronicle target/release/pchronicle --report target/pchronicle-self-case-report.md {{args}} + python3 scripts/run-pchronicle-cases.py --document docs/src/zh/pchronicle/reference/cases-self.md --pchronicle "{{ repo }}/target/release/pchronicle" --report target/pchronicle-self-case-report.md "${args[@]}" ;; pchronicle-cluster) just build-components release pchronicle - python3 scripts/run-pchronicle-cases.py --document docs/src/zh/pchronicle/reference/cases-platform.md --pchronicle target/release/pchronicle --report target/pchronicle-platform-case-report.md {{args}} + python3 scripts/run-pchronicle-cases.py --document docs/src/zh/pchronicle/reference/cases-platform.md --pchronicle "{{ repo }}/target/release/pchronicle" --report target/pchronicle-platform-case-report.md "${args[@]}" ;; *) - echo "usage: just cases pvisor|pchronicle|pchronicle-cluster [-- runner-args...]" >&2 + echo "usage: just cases pvisor|pchronicle|pchronicle-cluster [runner-args...]" >&2 exit 2 ;; esac diff --git a/scripts/run-pchronicle-cases.py b/scripts/run-pchronicle-cases.py index fc681119..cec66b8d 100755 --- a/scripts/run-pchronicle-cases.py +++ b/scripts/run-pchronicle-cases.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """Run executable bash examples embedded in pChronicle cases documents.""" from __future__ import annotations -import argparse, datetime as dt, os, re, subprocess, tempfile, time +import argparse, datetime as dt, os, re, shutil, subprocess, tempfile, time from pathlib import Path CASE_RE=re.compile(r'^##\s+([SP]\d{2}):?\s*(.*)$') @@ -21,20 +21,49 @@ def parse(path): if code: out.append((ident,title,code)) return out +def resolve_pchronicle(value: str) -> str: + """Resolve to an absolute executable path. + + Case bodies run with cwd set to a temporary workspace, so relative paths + like ``target/release/pchronicle`` must be absolutized against the repo + checkout before substitution. + """ + candidate = Path(value).expanduser() + search = [] + if candidate.is_absolute(): + search.append(candidate) + else: + search.append(Path.cwd() / candidate) + search.append(candidate) + for path in search: + if path.is_file() and os.access(path, os.X_OK): + return str(path.resolve()) + which = shutil.which(value) + if which: + return which + for relative in ("target/release/pchronicle", "target/debug/pchronicle"): + path = Path.cwd() / relative + if path.is_file() and os.access(path, os.X_OK): + return str(path.resolve()) + raise SystemExit( + f"pchronicle not found: {value!r}; build it or pass --pchronicle /absolute/path/to/pchronicle" + ) + def main(): ap=argparse.ArgumentParser(); ap.add_argument('--document',type=Path,required=True); ap.add_argument('--pchronicle',default='pchronicle'); ap.add_argument('--case',default=''); ap.add_argument('--list',action='store_true'); ap.add_argument('--keep',action='store_true'); ap.add_argument('--report',type=Path); ap.add_argument('--timeout',type=int,default=120); a=ap.parse_args() cases=parse(a.document) if a.list: for x in cases: print(f'{x[0]}\t{x[1]}') return 0 + pchronicle = resolve_pchronicle(a.pchronicle) wanted={x for x in a.case.split(',') if x}; cases=[c for c in cases if not wanted or c[0] in wanted] results=[] for ident,title,code in cases: # Long-running server examples are documentation smoke cases; execute them only explicitly. if re.search(r'(^|\n)\s*pchronicle serve\s',code) and '--case' not in os.environ.get('PCHRONICLE_CASE_MODE',''): results.append((ident,'MANUAL','server command requires a running client')); print(f'{ident} MANUAL {title}'); continue - root=Path(tempfile.mkdtemp(prefix=f'pchronicle-{ident.lower()}-')); env=os.environ.copy(); env['PCHRONICLE_CASE_WORKSPACE']=str(root); env['PCHRONICLE_BIN']=a.pchronicle - code=code.replace('pchronicle',a.pchronicle) + root=Path(tempfile.mkdtemp(prefix=f'pchronicle-{ident.lower()}-')); env=os.environ.copy(); env['PCHRONICLE_CASE_WORKSPACE']=str(root); env['PCHRONICLE_BIN']=pchronicle + code=code.replace('pchronicle',pchronicle) started=time.monotonic() try: p=subprocess.run(['bash','-euo','pipefail','-c',code],cwd=root,env=env,text=True,capture_output=True,timeout=a.timeout) @@ -43,7 +72,7 @@ def main(): results.append((ident,status,detail)); print(f'{ident} {status} {title}') if detail: print(detail[-2000:]) if not a.keep: - import shutil; shutil.rmtree(root,ignore_errors=True) + shutil.rmtree(root,ignore_errors=True) if a.report: a.report.parent.mkdir(parents=True,exist_ok=True); now=dt.datetime.now(dt.timezone.utc).isoformat(); lines=[f'# pChronicle cases\n\nGenerated {now}.\n','| Case | Status | Detail |','|---|---|---|'] lines += [f'| {i} | {s} | {d.replace(chr(10)," ")[:500]} |' for i,s,d in results]; a.report.write_text('\n'.join(lines)+'\n',encoding='utf-8') From 9a7d67156fc922376e6484fb0545fb7f8ebca343 Mon Sep 17 00:00:00 2001 From: Reiase Date: Tue, 8 Sep 2026 13:00:18 +0800 Subject: [PATCH 5/6] fix: make pchronicle cases executable against current CLI Seed cases from examples/data via PCHRONICLE_CASE_FIXTURES, update import and export syntax, skip serve/S3 without an explicit mode, and harden the just cases recipe against empty-args set -u failures on macOS bash. Co-authored-by: Cursor --- .../src/en/pchronicle/reference/cases-self.md | 43 +++-- .../src/zh/pchronicle/reference/cases-self.md | 19 +- justfile | 16 +- scripts/run-pchronicle-cases.py | 176 +++++++++++++----- 4 files changed, 179 insertions(+), 75 deletions(-) diff --git a/docs/src/en/pchronicle/reference/cases-self.md b/docs/src/en/pchronicle/reference/cases-self.md index 47bcb4c7..c3d88006 100644 --- a/docs/src/en/pchronicle/reference/cases-self.md +++ b/docs/src/en/pchronicle/reference/cases-self.md @@ -1,65 +1,72 @@ -# pChronicle 单机与自助使用场景 +# pChronicle single-machine and self-service cases -本文覆盖不依赖 Catalog Server 的基础工作流。每个案例都可以在一台开发机上独立执行,Dataset 可以是本地目录或对象存储 URI。 +These cases cover workflows that do not depend on a Catalog Server. Each case can run on a developer machine against a local directory or an object-store URI. -## 准备 +## Setup + +`just cases pchronicle` sets `PCHRONICLE_CASE_FIXTURES` to the repository `examples/data` tree. When running by hand, export that variable first: ```bash +export PCHRONICLE_CASE_FIXTURES=/path/to/Persisting/examples/data mkdir -p /tmp/pchronicle-cases cd /tmp/pchronicle-cases -pchronicle onboard ``` -## S01:浏览本地 Dataset +## S01: Browse a local Dataset ```bash +pchronicle import --from "$PCHRONICLE_CASE_FIXTURES/atif/support-ticket.json" --to ./trajectory-data --mode create pchronicle list ./trajectory-data pchronicle stats ./trajectory-data ``` -预期:命令列出 Dataset 中的 runs、steps 和 tool calls;空 Dataset 返回明确的空结果。 +Expected: the commands list runs, steps, and tool calls in the Dataset. -## S02:执行 SQL 查询 +## S02: Run a SQL query ```bash +pchronicle import --from "$PCHRONICLE_CASE_FIXTURES/atif/support-ticket.json" --to ./trajectory-data --mode create pchronicle query ./trajectory-data \ --sql 'SELECT COUNT(*) AS runs FROM dataset.runs' ``` -预期:查询成功并返回确定的 runs 数量。 +Expected: the query succeeds and returns a definite run count. -## S03:运行内建分析 +## S03: Run a built-in analysis ```bash +pchronicle import --from "$PCHRONICLE_CASE_FIXTURES/atif/support-ticket.json" --to ./trajectory-data --mode create pchronicle stats overview ./trajectory-data ``` -预期:输出运行数、步骤数、工具调用数和时间范围。 +Expected: output includes run, step, and tool-call counts plus a time range. -## S04:导入和导出 +## S04: Import and export ```bash -pchronicle import input.jsonl --output ./trajectory-data -pchronicle export ./trajectory-data --output output.jsonl +pchronicle import --from "$PCHRONICLE_CASE_FIXTURES/atif/support-ticket.json" --to ./trajectory-data --mode create +pchronicle export --from ./trajectory-data --to ./output.atif.json --output-format atif +test -s ./output.atif.json ``` -预期:导出内容可以再次导入,记录的 ID 和事件顺序保持一致。 +Expected: the export file is non-empty and can be imported again. -## S05:本地 Warehouse +## S05: Local Warehouse ```bash pchronicle serve ./trajectory-data --listen 127.0.0.1:8081 ``` -预期:Web UI、`/api/query/tables`、`/api/catalog` 和 Explorer API 可用;未启用 Catalog 时不需要用户凭据。 +Expected: the Web UI, `/api/query/tables`, `/api/catalog`, and Explorer APIs are available; no user credentials are required when Catalog is disabled. -## S06:对象存储 Dataset +## S06: Object-store Dataset ```bash export AWS_ENDPOINT_URL_S3=http://127.0.0.1:9000 export AWS_ACCESS_KEY_ID=rustfsadmin export AWS_SECRET_ACCESS_KEY=rustfsadmin +export AWS_REGION=us-east-1 pchronicle list s3://bucket/trajectory ``` -预期:pChronicle 通过 S3 兼容接口发现并查询 Dataset。endpoint 和凭据不会写入 Dataset URI。 +Expected: pChronicle discovers and queries the Dataset through an S3-compatible endpoint. Endpoint and credentials are not written into the Dataset URI. Automated runs skip this case by default; set `PCHRONICLE_CASE_MODE=s3` with a reachable endpoint to execute it. diff --git a/docs/src/zh/pchronicle/reference/cases-self.md b/docs/src/zh/pchronicle/reference/cases-self.md index 47bcb4c7..6d273728 100644 --- a/docs/src/zh/pchronicle/reference/cases-self.md +++ b/docs/src/zh/pchronicle/reference/cases-self.md @@ -4,24 +4,28 @@ ## 准备 +`just cases pchronicle` 会把 `PCHRONICLE_CASE_FIXTURES` 指到仓库内的 `examples/data`。手工执行时先导出该变量: + ```bash +export PCHRONICLE_CASE_FIXTURES=/path/to/Persisting/examples/data mkdir -p /tmp/pchronicle-cases cd /tmp/pchronicle-cases -pchronicle onboard ``` ## S01:浏览本地 Dataset ```bash +pchronicle import --from "$PCHRONICLE_CASE_FIXTURES/atif/support-ticket.json" --to ./trajectory-data --mode create pchronicle list ./trajectory-data pchronicle stats ./trajectory-data ``` -预期:命令列出 Dataset 中的 runs、steps 和 tool calls;空 Dataset 返回明确的空结果。 +预期:命令列出 Dataset 中的 runs、steps 和 tool calls。 ## S02:执行 SQL 查询 ```bash +pchronicle import --from "$PCHRONICLE_CASE_FIXTURES/atif/support-ticket.json" --to ./trajectory-data --mode create pchronicle query ./trajectory-data \ --sql 'SELECT COUNT(*) AS runs FROM dataset.runs' ``` @@ -31,6 +35,7 @@ pchronicle query ./trajectory-data \ ## S03:运行内建分析 ```bash +pchronicle import --from "$PCHRONICLE_CASE_FIXTURES/atif/support-ticket.json" --to ./trajectory-data --mode create pchronicle stats overview ./trajectory-data ``` @@ -39,11 +44,12 @@ pchronicle stats overview ./trajectory-data ## S04:导入和导出 ```bash -pchronicle import input.jsonl --output ./trajectory-data -pchronicle export ./trajectory-data --output output.jsonl +pchronicle import --from "$PCHRONICLE_CASE_FIXTURES/atif/support-ticket.json" --to ./trajectory-data --mode create +pchronicle export --from ./trajectory-data --to ./output.atif.json --output-format atif +test -s ./output.atif.json ``` -预期:导出内容可以再次导入,记录的 ID 和事件顺序保持一致。 +预期:导出文件非空,且可再次导入。 ## S05:本地 Warehouse @@ -59,7 +65,8 @@ pchronicle serve ./trajectory-data --listen 127.0.0.1:8081 export AWS_ENDPOINT_URL_S3=http://127.0.0.1:9000 export AWS_ACCESS_KEY_ID=rustfsadmin export AWS_SECRET_ACCESS_KEY=rustfsadmin +export AWS_REGION=us-east-1 pchronicle list s3://bucket/trajectory ``` -预期:pChronicle 通过 S3 兼容接口发现并查询 Dataset。endpoint 和凭据不会写入 Dataset URI。 +预期:pChronicle 通过 S3 兼容接口发现并查询 Dataset。endpoint 和凭据不会写入 Dataset URI。默认自动化运行会跳过本案例;设置 `PCHRONICLE_CASE_MODE=s3` 且提供可达 endpoint 后再执行。 diff --git a/justfile b/justfile index 01db9eee..11128cca 100644 --- a/justfile +++ b/justfile @@ -718,24 +718,24 @@ capture-test: cases target *args: #!/usr/bin/env bash set -euo pipefail - # Variadic args are interpolated by just. Callers may also pass a leading - # `--` to stop just flag parsing; strip it before forwarding to the runner. - args=({{ args }}) - if [[ "${#args[@]}" -gt 0 && "${args[0]}" == "--" ]]; then - args=("${args[@]:1}") + # Variadic args are interpolated by just (not shebang $@). A leading `--` + # may be present when callers stop just flag parsing; strip it. + set -- {{ args }} + if [[ "${1:-}" == "--" ]]; then + shift fi case "{{target}}" in pvisor) just pvisor release - python3 scripts/run-pvisor-cases.py --report target/pvisor-case-report.md "${args[@]}" + python3 scripts/run-pvisor-cases.py --report target/pvisor-case-report.md "$@" ;; pchronicle) just build-components release pchronicle - python3 scripts/run-pchronicle-cases.py --document docs/src/zh/pchronicle/reference/cases-self.md --pchronicle "{{ repo }}/target/release/pchronicle" --report target/pchronicle-self-case-report.md "${args[@]}" + python3 scripts/run-pchronicle-cases.py --document docs/src/zh/pchronicle/reference/cases-self.md --pchronicle "{{ repo }}/target/release/pchronicle" --report target/pchronicle-self-case-report.md "$@" ;; pchronicle-cluster) just build-components release pchronicle - python3 scripts/run-pchronicle-cases.py --document docs/src/zh/pchronicle/reference/cases-platform.md --pchronicle "{{ repo }}/target/release/pchronicle" --report target/pchronicle-platform-case-report.md "${args[@]}" + python3 scripts/run-pchronicle-cases.py --document docs/src/zh/pchronicle/reference/cases-platform.md --pchronicle "{{ repo }}/target/release/pchronicle" --report target/pchronicle-platform-case-report.md "$@" ;; *) echo "usage: just cases pvisor|pchronicle|pchronicle-cluster [runner-args...]" >&2 diff --git a/scripts/run-pchronicle-cases.py b/scripts/run-pchronicle-cases.py index cec66b8d..2be452c0 100755 --- a/scripts/run-pchronicle-cases.py +++ b/scripts/run-pchronicle-cases.py @@ -1,26 +1,49 @@ #!/usr/bin/env python3 """Run executable bash examples embedded in pChronicle cases documents.""" from __future__ import annotations -import argparse, datetime as dt, os, re, shutil, subprocess, tempfile, time + +import argparse +import datetime as dt +import os +import re +import shutil +import subprocess +import tempfile +import time from pathlib import Path -CASE_RE=re.compile(r'^##\s+([SP]\d{2}):?\s*(.*)$') - -def parse(path): - lines=path.read_text(encoding='utf-8').splitlines(); out=[]; i=0 - while i str: """Resolve to an absolute executable path. @@ -49,32 +72,99 @@ def resolve_pchronicle(value: str) -> str: f"pchronicle not found: {value!r}; build it or pass --pchronicle /absolute/path/to/pchronicle" ) -def main(): - ap=argparse.ArgumentParser(); ap.add_argument('--document',type=Path,required=True); ap.add_argument('--pchronicle',default='pchronicle'); ap.add_argument('--case',default=''); ap.add_argument('--list',action='store_true'); ap.add_argument('--keep',action='store_true'); ap.add_argument('--report',type=Path); ap.add_argument('--timeout',type=int,default=120); a=ap.parse_args() - cases=parse(a.document) - if a.list: - for x in cases: print(f'{x[0]}\t{x[1]}') + +def should_skip_manual(code: str) -> str | None: + mode = os.environ.get("PCHRONICLE_CASE_MODE", "") + if re.search(r"(^|\n)\s*pchronicle serve\s", code) and "serve" not in mode.split(","): + return "server command requires a running client" + if re.search(r"\bs3://", code) and "s3" not in mode.split(","): + return "object-store case requires PCHRONICLE_CASE_MODE=s3 and a reachable endpoint" + return None + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--document", type=Path, required=True) + parser.add_argument("--pchronicle", default="pchronicle") + parser.add_argument("--case", default="") + parser.add_argument("--list", action="store_true") + parser.add_argument("--keep", action="store_true") + parser.add_argument("--report", type=Path) + parser.add_argument("--timeout", type=int, default=120) + parser.add_argument( + "--fixtures", + type=Path, + default=DEFAULT_FIXTURES, + help="Directory exposed to cases as PCHRONICLE_CASE_FIXTURES", + ) + args = parser.parse_args() + + cases = parse(args.document) + if args.list: + for ident, title, _ in cases: + print(f"{ident}\t{title}") return 0 - pchronicle = resolve_pchronicle(a.pchronicle) - wanted={x for x in a.case.split(',') if x}; cases=[c for c in cases if not wanted or c[0] in wanted] - results=[] - for ident,title,code in cases: - # Long-running server examples are documentation smoke cases; execute them only explicitly. - if re.search(r'(^|\n)\s*pchronicle serve\s',code) and '--case' not in os.environ.get('PCHRONICLE_CASE_MODE',''): - results.append((ident,'MANUAL','server command requires a running client')); print(f'{ident} MANUAL {title}'); continue - root=Path(tempfile.mkdtemp(prefix=f'pchronicle-{ident.lower()}-')); env=os.environ.copy(); env['PCHRONICLE_CASE_WORKSPACE']=str(root); env['PCHRONICLE_BIN']=pchronicle - code=code.replace('pchronicle',pchronicle) - started=time.monotonic() + + pchronicle = resolve_pchronicle(args.pchronicle) + fixtures = args.fixtures.expanduser().resolve() + if not fixtures.is_dir(): + raise SystemExit(f"fixtures directory not found: {fixtures}") + + wanted = {item for item in args.case.split(",") if item} + cases = [case for case in cases if not wanted or case[0] in wanted] + results = [] + + for ident, title, code in cases: + reason = should_skip_manual(code) + if reason is not None: + results.append((ident, "MANUAL", reason)) + print(f"{ident} MANUAL {title}") + continue + + root = Path(tempfile.mkdtemp(prefix=f"pchronicle-{ident.lower()}-")) + env = os.environ.copy() + env["PCHRONICLE_CASE_WORKSPACE"] = str(root) + env["PCHRONICLE_CASE_FIXTURES"] = str(fixtures) + env["PCHRONICLE_BIN"] = pchronicle + rendered = code.replace("pchronicle", pchronicle) try: - p=subprocess.run(['bash','-euo','pipefail','-c',code],cwd=root,env=env,text=True,capture_output=True,timeout=a.timeout) - ok=p.returncode==0; status='PASS' if ok else 'FAIL'; detail=(p.stdout+p.stderr).strip() - except subprocess.TimeoutExpired as e: status='FAIL'; detail=f'timeout after {a.timeout}s\n{e.stdout or ""}' - results.append((ident,status,detail)); print(f'{ident} {status} {title}') - if detail: print(detail[-2000:]) - if not a.keep: - shutil.rmtree(root,ignore_errors=True) - if a.report: - a.report.parent.mkdir(parents=True,exist_ok=True); now=dt.datetime.now(dt.timezone.utc).isoformat(); lines=[f'# pChronicle cases\n\nGenerated {now}.\n','| Case | Status | Detail |','|---|---|---|'] - lines += [f'| {i} | {s} | {d.replace(chr(10)," ")[:500]} |' for i,s,d in results]; a.report.write_text('\n'.join(lines)+'\n',encoding='utf-8') - return 1 if any(s=='FAIL' for _,s,_ in results) else 0 -if __name__=='__main__': raise SystemExit(main()) + completed = subprocess.run( + ["bash", "-euo", "pipefail", "-c", rendered], + cwd=root, + env=env, + text=True, + capture_output=True, + timeout=args.timeout, + ) + ok = completed.returncode == 0 + status = "PASS" if ok else "FAIL" + detail = (completed.stdout + completed.stderr).strip() + except subprocess.TimeoutExpired as exc: + status = "FAIL" + detail = f"timeout after {args.timeout}s\n{exc.stdout or ''}" + results.append((ident, status, detail)) + print(f"{ident} {status} {title}") + if detail: + print(detail[-2000:]) + if not args.keep: + shutil.rmtree(root, ignore_errors=True) + + if args.report: + args.report.parent.mkdir(parents=True, exist_ok=True) + now = dt.datetime.now(dt.timezone.utc).isoformat() + lines = [ + f"# pChronicle cases\n\nGenerated {now}.\n", + "| Case | Status | Detail |", + "|---|---|---|", + ] + lines += [ + f"| {ident} | {status} | {detail.replace(chr(10), ' ')[:500]} |" + for ident, status, detail in results + ] + args.report.write_text("\n".join(lines) + "\n", encoding="utf-8") + + return 1 if any(status == "FAIL" for _, status, _ in results) else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 3227e35b918d44b91d14bdb7ad735a8ccbae7f59 Mon Sep 17 00:00:00 2001 From: Reiase Date: Tue, 8 Sep 2026 13:05:25 +0800 Subject: [PATCH 6/6] fix: skip platform Directory cases without live Catalog P05 was executed with placeholder credentials against catalog:// and failed. Treat catalog pins, placeholder AKs, and RustFS-only setup as MANUAL unless PCHRONICLE_CASE_MODE opts in. Co-authored-by: Cursor --- .../en/pchronicle/reference/cases-platform.md | 4 ++++ .../zh/pchronicle/reference/cases-platform.md | 6 +++-- scripts/run-pchronicle-cases.py | 22 ++++++++++++++----- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/docs/src/en/pchronicle/reference/cases-platform.md b/docs/src/en/pchronicle/reference/cases-platform.md index 61cf9ef8..24545a4b 100644 --- a/docs/src/en/pchronicle/reference/cases-platform.md +++ b/docs/src/en/pchronicle/reference/cases-platform.md @@ -63,6 +63,8 @@ pchronicle query @team/prod \ ``` Expected: an authorized user can query `prod`; unknown datasets fail closed. +Automated runs skip this case by default; set `PCHRONICLE_CASE_MODE=catalog` +after completing P01–P04 against a live Directory. ## P06: Revoke a library grant @@ -87,6 +89,8 @@ export PCHRONICLE_RUSTFS_BUCKET=pchronicle-cases Then run the RustFS regression coverage for Dataset writes, Snapshot discovery (including `chronicle.manifest` when present), SQL, Explorer, and refresh. +Automated runs skip this case by default; set `PCHRONICLE_CASE_MODE=rustfs` +with a reachable endpoint to execute it. Platform checks: diff --git a/docs/src/zh/pchronicle/reference/cases-platform.md b/docs/src/zh/pchronicle/reference/cases-platform.md index 573b40d2..26408762 100644 --- a/docs/src/zh/pchronicle/reference/cases-platform.md +++ b/docs/src/zh/pchronicle/reference/cases-platform.md @@ -59,7 +59,8 @@ pchronicle query @team/prod \ --sql 'SELECT COUNT(*) AS runs FROM dataset.runs' ``` -预期:授权用户可查询 `prod`;未知 Dataset 失败关闭。 +预期:授权用户可查询 `prod`;未知 Dataset 失败关闭。默认自动化跳过;设置 +`PCHRONICLE_CASE_MODE=catalog` 并先完成本文 P01–P04 后再执行。 ## P06:撤销 library 授权 @@ -83,7 +84,8 @@ export PCHRONICLE_RUSTFS_BUCKET=pchronicle-cases ``` 然后跑 RustFS 回归,覆盖 Dataset 写入、Snapshot discovery(含 -`chronicle.manifest`)、SQL、Explorer 与 refresh。 +`chronicle.manifest`)、SQL、Explorer 与 refresh。默认自动化跳过;设置 +`PCHRONICLE_CASE_MODE=rustfs` 且 endpoint 可达后再执行。 平台验收重点: diff --git a/scripts/run-pchronicle-cases.py b/scripts/run-pchronicle-cases.py index 2be452c0..58a70594 100755 --- a/scripts/run-pchronicle-cases.py +++ b/scripts/run-pchronicle-cases.py @@ -74,11 +74,23 @@ def resolve_pchronicle(value: str) -> str: def should_skip_manual(code: str) -> str | None: - mode = os.environ.get("PCHRONICLE_CASE_MODE", "") - if re.search(r"(^|\n)\s*pchronicle serve\s", code) and "serve" not in mode.split(","): - return "server command requires a running client" - if re.search(r"\bs3://", code) and "s3" not in mode.split(","): - return "object-store case requires PCHRONICLE_CASE_MODE=s3 and a reachable endpoint" + mode = { + item.strip() + for item in os.environ.get("PCHRONICLE_CASE_MODE", "").split(",") + if item.strip() + } + if re.search(r"(^|\n)\s*pchronicle serve\b", code) and not mode.intersection( + {"serve", "catalog"} + ): + return "serve/Directory admin commands require PCHRONICLE_CASE_MODE=serve|catalog" + if re.search(r"\bcatalog://", code) and "catalog" not in mode: + return "Directory pin requires a running Catalog serve; set PCHRONICLE_CASE_MODE=catalog" + if re.search(r"\b(USER_AK|USER_SK|BACKEND_AK|BACKEND_SK)\b", code) and "catalog" not in mode: + return "case uses placeholder Directory credentials" + if re.search(r"\bs3://", code) and not mode.intersection({"s3", "catalog"}): + return "object-store case requires PCHRONICLE_CASE_MODE=s3|catalog and a reachable endpoint" + if re.search(r"\bPCHRONICLE_RUSTFS_", code) and "rustfs" not in mode: + return "RustFS regression requires PCHRONICLE_CASE_MODE=rustfs and a live endpoint" return None