From 970a7bc3642419f77017f7272ff09460475cdfa1 Mon Sep 17 00:00:00 2001 From: wujunchen Date: Wed, 29 Apr 2026 14:08:38 +0800 Subject: [PATCH] chore: add e2e contract gate Change-Id: Idf5c36b17a5b816e354d07f1e28ff4011e54f32e --- .e2e/README.md | 15 ++++++ .e2e/cases/npm-test-risk-map.json | 29 ++++++++++++ .e2e/config.yaml | 14 ++++++ .e2e/gate.sh | 15 ++++++ .e2e/hook.sh | 22 +++++++++ .e2e/run.sh | 24 ++++++++++ .e2e/scripts/write-artifact.mjs | 76 +++++++++++++++++++++++++++++++ .gitignore | 5 ++ 8 files changed, 200 insertions(+) create mode 100644 .e2e/README.md create mode 100644 .e2e/cases/npm-test-risk-map.json create mode 100644 .e2e/config.yaml create mode 100755 .e2e/gate.sh create mode 100755 .e2e/hook.sh create mode 100755 .e2e/run.sh create mode 100644 .e2e/scripts/write-artifact.mjs diff --git a/.e2e/README.md b/.e2e/README.md new file mode 100644 index 0000000..959c3c8 --- /dev/null +++ b/.e2e/README.md @@ -0,0 +1,15 @@ +# E2E Contract + +This repository uses `.e2e/run.sh` as the project-local runtime entry point. +The runner clears stale artifacts, executes the real `npm test` gate, records +`.e2e/results/npm-test.log`, and writes `.e2e/artifact.json` in CTRF format. + +Run the host-neutral gate with: + +```bash +E2E_CONTRACT_VALIDATOR_PYTHONPATH=/Users/wujunchen/dev/github.com/fancive/claude-code-addons/scripts \ +bash .e2e/gate.sh --json +``` + +Generated artifacts under `.e2e/artifact.json` and `.e2e/results/` are runtime +evidence and are intentionally ignored by git. diff --git a/.e2e/cases/npm-test-risk-map.json b/.e2e/cases/npm-test-risk-map.json new file mode 100644 index 0000000..a56cf62 --- /dev/null +++ b/.e2e/cases/npm-test-risk-map.json @@ -0,0 +1,29 @@ +{ + "tests": [ + { + "name": "build and typecheck gate", + "risk_tags": ["boundary_io", "wiring", "regression"], + "source_files": ["package.json", "esbuild.config.mjs", "tsconfig.json"] + }, + { + "name": "provider protocol and schema contracts", + "risk_tags": ["contract", "failure_path", "regression"], + "source_files": ["tests/providers.test.js", "tests/schema.test.js", "tests/direct-providers.test.js"] + }, + { + "name": "cache, markdown, and vault data integrity", + "risk_tags": ["data_integrity", "boundary_io", "regression"], + "source_files": ["tests/cache.test.js", "tests/direct-cache.test.js", "tests/vault-batch.test.js"] + }, + { + "name": "batch generation failure and cancellation paths", + "risk_tags": ["failure_path", "data_integrity", "regression"], + "source_files": ["tests/plugin-batch.test.js", "tests/direct-batch.test.js"] + }, + { + "name": "plugin module wiring and export surface", + "risk_tags": ["wiring", "contract", "regression"], + "source_files": ["tests/architecture.test.js", "tests/test-exports.test.js"] + } + ] +} diff --git a/.e2e/config.yaml b/.e2e/config.yaml new file mode 100644 index 0000000..3e14e1e --- /dev/null +++ b/.e2e/config.yaml @@ -0,0 +1,14 @@ +schema_version: "2.0" + +required_risk_tags: + - boundary_io + - wiring + - failure_path + - data_integrity + - contract + - regression + +preconditions: [] + +provenance: + cosign_required: false diff --git a/.e2e/gate.sh b/.e2e/gate.sh new file mode 100755 index 0000000..64ecc48 --- /dev/null +++ b/.e2e/gate.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$0")/.." + +if ! python -c 'import e2e_contract_validator' >/dev/null 2>&1; then + if [ -n "${E2E_CONTRACT_VALIDATOR_PYTHONPATH:-}" ]; then + export PYTHONPATH="${E2E_CONTRACT_VALIDATOR_PYTHONPATH}${PYTHONPATH:+:$PYTHONPATH}" + else + echo "e2e_contract_validator is not importable; set E2E_CONTRACT_VALIDATOR_PYTHONPATH or install it" >&2 + exit 3 + fi +fi + +python -m e2e_contract_validator gate --project . --json "$@" diff --git a/.e2e/hook.sh b/.e2e/hook.sh new file mode 100755 index 0000000..5c24387 --- /dev/null +++ b/.e2e/hook.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +set -euo pipefail + +[ "${STOP_HOOK_ACTIVE:-}" = "true" ] && exit 0 +export STOP_HOOK_ACTIVE=true + +OUT="$(mktemp)" +set +e +bash .e2e/gate.sh --json >"$OUT" +CODE=$? +set -e + +if [ "$CODE" -ne 0 ]; then + cat "$OUT" >&2 + rm -f "$OUT" + echo '{"decision":"block","reason":"e2e contract gate failed"}' + exit 2 +fi + +cat "$OUT" >&2 +rm -f "$OUT" +exit 0 diff --git a/.e2e/run.sh b/.e2e/run.sh new file mode 100755 index 0000000..7c20136 --- /dev/null +++ b/.e2e/run.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$0")/.." + +ARTIFACT=".e2e/artifact.json" +RESULTS_DIR=".e2e/results" +LOG="$RESULTS_DIR/npm-test.log" +START_MS="$(node -e 'process.stdout.write(String(Date.now()))')" + +rm -f "$ARTIFACT" +mkdir -p "$RESULTS_DIR" + +set +e +npm test >"$LOG" 2>&1 +RUN_EXIT=$? +set -e + +END_MS="$(node -e 'process.stdout.write(String(Date.now()))')" +LOG_SHA="$(shasum -a 256 "$LOG" | awk '{print $1}')" +export RUN_EXIT START_MS END_MS LOG LOG_SHA + +node .e2e/scripts/write-artifact.mjs +exit "$RUN_EXIT" diff --git a/.e2e/scripts/write-artifact.mjs b/.e2e/scripts/write-artifact.mjs new file mode 100644 index 0000000..646ee7a --- /dev/null +++ b/.e2e/scripts/write-artifact.mjs @@ -0,0 +1,76 @@ +import fs from 'fs'; + +const riskMap = JSON.parse(fs.readFileSync('.e2e/cases/npm-test-risk-map.json', 'utf8')); +const runExit = Number(process.env.RUN_EXIT || 1); +const startMs = Number(process.env.START_MS || Date.now()); +const endMs = Number(process.env.END_MS || startMs); +const durationMs = Math.max(0, endMs - startMs); +const logPath = String(process.env.LOG || '.e2e/results/npm-test.log').replace(/^\.e2e\//, ''); +const logSha = String(process.env.LOG_SHA || ''); + +const command = { + command: 'npm test', + exit_code: runExit, +}; + +const tests = + runExit === 0 + ? riskMap.tests.map((test) => ({ + name: test.name, + status: 'passed', + duration: durationMs / riskMap.tests.length, + tags: test.risk_tags.map((tag) => `risk:${tag}`), + extra: { + e2e_contract: { + command_recorded: command, + expected_exit_code: 0, + evidence: { + log_path: logPath, + log_sha256: logSha, + }, + source_files: test.source_files, + }, + }, + })) + : [ + { + name: 'npm test', + status: 'failed', + duration: durationMs, + tags: [], + extra: { + e2e_contract: { + command_recorded: command, + expected_exit_code: 0, + evidence: { + log_path: logPath, + log_sha256: logSha, + }, + }, + }, + }, + ]; + +const summary = { + tests: tests.length, + passed: tests.filter((test) => test.status === 'passed').length, + failed: tests.filter((test) => test.status === 'failed').length, + pending: 0, + skipped: 0, + other: 0, +}; + +const artifact = { + reportFormat: 'CTRF', + specVersion: '0.0.0', + results: { + tool: { + name: 'npm', + version: 'test', + }, + summary, + tests, + }, +}; + +fs.writeFileSync('.e2e/artifact.json', JSON.stringify(artifact, null, 2) + '\n'); diff --git a/.gitignore b/.gitignore index cbae2e8..9408df7 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,8 @@ main.js.map # Agent state .agent/ + +# E2E contract runtime artifacts +.e2e/artifact.json +.e2e/artifact.json.sig +.e2e/results/