chore: add e2e contract gate

Change-Id: Idf5c36b17a5b816e354d07f1e28ff4011e54f32e
This commit is contained in:
wujunchen 2026-04-29 14:08:38 +08:00
parent 3bdd6d67c6
commit 970a7bc364
8 changed files with 200 additions and 0 deletions

15
.e2e/README.md Normal file
View file

@ -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.

View file

@ -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"]
}
]
}

14
.e2e/config.yaml Normal file
View file

@ -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

15
.e2e/gate.sh Executable file
View file

@ -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 "$@"

22
.e2e/hook.sh Executable file
View file

@ -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

24
.e2e/run.sh Executable file
View file

@ -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"

View file

@ -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');

5
.gitignore vendored
View file

@ -20,3 +20,8 @@ main.js.map
# Agent state
.agent/
# E2E contract runtime artifacts
.e2e/artifact.json
.e2e/artifact.json.sig
.e2e/results/