mirror of
https://github.com/fancive/obsidian-parallel-reader.git
synced 2026-07-22 06:53:43 +00:00
chore: add e2e contract gate
Change-Id: Idf5c36b17a5b816e354d07f1e28ff4011e54f32e
This commit is contained in:
parent
3bdd6d67c6
commit
970a7bc364
8 changed files with 200 additions and 0 deletions
15
.e2e/README.md
Normal file
15
.e2e/README.md
Normal 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.
|
||||
29
.e2e/cases/npm-test-risk-map.json
Normal file
29
.e2e/cases/npm-test-risk-map.json
Normal 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
14
.e2e/config.yaml
Normal 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
15
.e2e/gate.sh
Executable 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
22
.e2e/hook.sh
Executable 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
24
.e2e/run.sh
Executable 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"
|
||||
76
.e2e/scripts/write-artifact.mjs
Normal file
76
.e2e/scripts/write-artifact.mjs
Normal 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
5
.gitignore
vendored
|
|
@ -20,3 +20,8 @@ main.js.map
|
|||
|
||||
# Agent state
|
||||
.agent/
|
||||
|
||||
# E2E contract runtime artifacts
|
||||
.e2e/artifact.json
|
||||
.e2e/artifact.json.sig
|
||||
.e2e/results/
|
||||
|
|
|
|||
Loading…
Reference in a new issue