@mmerterden/multi-agent-pipeline 12.7.0 → 12.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +126 -0
- package/install/_common.mjs +48 -0
- package/install/_dev-only-files.mjs +125 -5
- package/install/claude.mjs +14 -8
- package/install/copilot.mjs +5 -8
- package/package.json +17 -2
- package/pipeline/commands/multi-agent/analysis/SKILL.md +1 -1
- package/pipeline/lib/credential-store.sh +20 -0
- package/pipeline/multi-agent-refs/_account-picker.md +1 -1
- package/pipeline/multi-agent-refs/_dev-context.md +1 -1
- package/pipeline/multi-agent-refs/_repo-picker.md +1 -1
- package/pipeline/multi-agent-refs/phases/operations.md +28 -0
- package/pipeline/multi-agent-refs/phases/phase-0-init.md +1 -0
- package/pipeline/multi-agent-refs/phases/phase-2-planning.md +1 -1
- package/pipeline/multi-agent-refs/phases/phase-3-dev.md +1 -2
- package/pipeline/multi-agent-refs/phases/phase-4-review.md +49 -4
- package/pipeline/schemas/prefs.schema.json +6 -0
- package/pipeline/scripts/_smoke-root.sh +61 -0
- package/pipeline/scripts/audit-log.sh +25 -0
- package/pipeline/scripts/smoke-cross-cli-behavior.sh +21 -7
- package/pipeline/eval/golden-tasks/01-ios-bugfix-darkmode/expected/phase-1-analysis.json +0 -25
- package/pipeline/eval/golden-tasks/01-ios-bugfix-darkmode/expected/phase-2-plan.json +0 -30
- package/pipeline/eval/golden-tasks/01-ios-bugfix-darkmode/expected/phase-4-review.json +0 -20
- package/pipeline/eval/golden-tasks/01-ios-bugfix-darkmode/expected/phase-4-triage.json +0 -15
- package/pipeline/eval/golden-tasks/01-ios-bugfix-darkmode/metadata.json +0 -14
- package/pipeline/eval/golden-tasks/01-ios-bugfix-darkmode/task.json +0 -12
- package/pipeline/eval/golden-tasks/02-android-feature-compose/expected/phase-1-analysis.json +0 -29
- package/pipeline/eval/golden-tasks/02-android-feature-compose/expected/phase-2-plan.json +0 -43
- package/pipeline/eval/golden-tasks/02-android-feature-compose/expected/phase-4-review.json +0 -35
- package/pipeline/eval/golden-tasks/02-android-feature-compose/expected/phase-4-triage.json +0 -35
- package/pipeline/eval/golden-tasks/02-android-feature-compose/metadata.json +0 -14
- package/pipeline/eval/golden-tasks/02-android-feature-compose/task.json +0 -12
- package/pipeline/eval/golden-tasks/03-backend-python-ratelimit/expected/phase-1-analysis.json +0 -29
- package/pipeline/eval/golden-tasks/03-backend-python-ratelimit/expected/phase-2-plan.json +0 -42
- package/pipeline/eval/golden-tasks/03-backend-python-ratelimit/expected/phase-4-review.json +0 -20
- package/pipeline/eval/golden-tasks/03-backend-python-ratelimit/expected/phase-4-triage.json +0 -15
- package/pipeline/eval/golden-tasks/03-backend-python-ratelimit/metadata.json +0 -14
- package/pipeline/eval/golden-tasks/03-backend-python-ratelimit/task.json +0 -12
- package/pipeline/eval/golden-tasks/04-frontend-next-hydration/expected/phase-1-analysis.json +0 -29
- package/pipeline/eval/golden-tasks/04-frontend-next-hydration/expected/phase-2-plan.json +0 -40
- package/pipeline/eval/golden-tasks/04-frontend-next-hydration/expected/phase-4-review.json +0 -20
- package/pipeline/eval/golden-tasks/04-frontend-next-hydration/expected/phase-4-triage.json +0 -15
- package/pipeline/eval/golden-tasks/04-frontend-next-hydration/metadata.json +0 -14
- package/pipeline/eval/golden-tasks/04-frontend-next-hydration/task.json +0 -12
- package/pipeline/eval/golden-tasks/05-ios-security-keychain/expected/phase-1-analysis.json +0 -29
- package/pipeline/eval/golden-tasks/05-ios-security-keychain/expected/phase-2-plan.json +0 -42
- package/pipeline/eval/golden-tasks/05-ios-security-keychain/expected/phase-4-review.json +0 -28
- package/pipeline/eval/golden-tasks/05-ios-security-keychain/expected/phase-4-triage.json +0 -27
- package/pipeline/eval/golden-tasks/05-ios-security-keychain/metadata.json +0 -14
- package/pipeline/eval/golden-tasks/05-ios-security-keychain/task.json +0 -12
- package/pipeline/eval/golden-tasks/06-android-refactor-usecase/expected/phase-1-analysis.json +0 -29
- package/pipeline/eval/golden-tasks/06-android-refactor-usecase/expected/phase-2-plan.json +0 -41
- package/pipeline/eval/golden-tasks/06-android-refactor-usecase/expected/phase-4-review.json +0 -12
- package/pipeline/eval/golden-tasks/06-android-refactor-usecase/expected/phase-4-triage.json +0 -6
- package/pipeline/eval/golden-tasks/06-android-refactor-usecase/metadata.json +0 -14
- package/pipeline/eval/golden-tasks/06-android-refactor-usecase/task.json +0 -12
- package/pipeline/eval/golden-tasks/07-backend-node-idempotency/expected/phase-1-analysis.json +0 -29
- package/pipeline/eval/golden-tasks/07-backend-node-idempotency/expected/phase-2-plan.json +0 -42
- package/pipeline/eval/golden-tasks/07-backend-node-idempotency/expected/phase-4-review.json +0 -28
- package/pipeline/eval/golden-tasks/07-backend-node-idempotency/expected/phase-4-triage.json +0 -27
- package/pipeline/eval/golden-tasks/07-backend-node-idempotency/metadata.json +0 -14
- package/pipeline/eval/golden-tasks/07-backend-node-idempotency/task.json +0 -12
- package/pipeline/eval/golden-tasks/08-ios-auth-consensus-unverified/expected/phase-1-analysis.json +0 -25
- package/pipeline/eval/golden-tasks/08-ios-auth-consensus-unverified/expected/phase-2-plan.json +0 -31
- package/pipeline/eval/golden-tasks/08-ios-auth-consensus-unverified/expected/phase-4-review.json +0 -12
- package/pipeline/eval/golden-tasks/08-ios-auth-consensus-unverified/expected/phase-4-triage.json +0 -18
- package/pipeline/eval/golden-tasks/08-ios-auth-consensus-unverified/metadata.json +0 -14
- package/pipeline/eval/golden-tasks/08-ios-auth-consensus-unverified/task.json +0 -12
- package/pipeline/eval/golden-tasks/README.md +0 -65
- package/pipeline/eval/intent-cases.json +0 -40
- package/pipeline/eval/run-metrics-fixture.json +0 -93
- package/pipeline/eval/triage/01-empty-findings/expected.json +0 -6
- package/pipeline/eval/triage/01-empty-findings/input.json +0 -5
- package/pipeline/eval/triage/01-empty-findings/notes.md +0 -7
- package/pipeline/eval/triage/02-real-blocker/expected.json +0 -15
- package/pipeline/eval/triage/02-real-blocker/input.json +0 -14
- package/pipeline/eval/triage/02-real-blocker/notes.md +0 -7
- package/pipeline/eval/triage/03-out-of-scope-defer/expected.json +0 -18
- package/pipeline/eval/triage/03-out-of-scope-defer/input.json +0 -14
- package/pipeline/eval/triage/03-out-of-scope-defer/notes.md +0 -10
- package/pipeline/eval/triage/04-false-positive-reject/expected.json +0 -18
- package/pipeline/eval/triage/04-false-positive-reject/input.json +0 -14
- package/pipeline/eval/triage/04-false-positive-reject/notes.md +0 -10
- package/pipeline/eval/triage/05-mixed-classification/expected.json +0 -43
- package/pipeline/eval/triage/05-mixed-classification/input.json +0 -38
- package/pipeline/eval/triage/05-mixed-classification/notes.md +0 -17
- package/pipeline/eval/triage/06-severity-mismatch/expected.json +0 -15
- package/pipeline/eval/triage/06-severity-mismatch/input.json +0 -14
- package/pipeline/eval/triage/06-severity-mismatch/notes.md +0 -9
- package/pipeline/eval/triage/07-duplicate-reviewers/expected.json +0 -27
- package/pipeline/eval/triage/07-duplicate-reviewers/input.json +0 -22
- package/pipeline/eval/triage/07-duplicate-reviewers/notes.md +0 -9
- package/pipeline/eval/triage/08-style-misclassified/expected.json +0 -18
- package/pipeline/eval/triage/08-style-misclassified/input.json +0 -14
- package/pipeline/eval/triage/08-style-misclassified/notes.md +0 -9
- package/pipeline/eval/triage/09-cascading-finding/expected.json +0 -23
- package/pipeline/eval/triage/09-cascading-finding/input.json +0 -22
- package/pipeline/eval/triage/09-cascading-finding/notes.md +0 -9
- package/pipeline/eval/triage/10-deferred-crossref/expected.json +0 -18
- package/pipeline/eval/triage/10-deferred-crossref/input.json +0 -14
- package/pipeline/eval/triage/10-deferred-crossref/notes.md +0 -9
- package/pipeline/eval/triage/11-vercel-token-leak-blocker/expected.json +0 -27
- package/pipeline/eval/triage/11-vercel-token-leak-blocker/input.json +0 -22
- package/pipeline/eval/triage/11-vercel-token-leak-blocker/notes.md +0 -14
- package/pipeline/eval/triage/README.md +0 -54
- package/pipeline/scripts/benchmark-phase-0.sh +0 -128
- package/pipeline/scripts/check-md-links.mjs +0 -88
- package/pipeline/scripts/eval-golden-tasks-live.mjs +0 -302
- package/pipeline/scripts/eval-golden-tasks.mjs +0 -224
- package/pipeline/scripts/eval-intent.mjs +0 -107
- package/pipeline/scripts/eval-mine-corpus.mjs +0 -211
- package/pipeline/scripts/eval-triage.mjs +0 -171
- package/pipeline/scripts/fixtures/diff-risk-android.diff +0 -40
- package/pipeline/scripts/fixtures/diff-risk-ios.diff +0 -48
- package/pipeline/scripts/fixtures/diff-risk-test-removal.diff +0 -40
- package/pipeline/scripts/fixtures/install-layout.tsv +0 -19
- package/pipeline/scripts/fixtures/pack-expected-count.txt +0 -1
- package/pipeline/scripts/fixtures/test-gap-node.diff +0 -30
- package/pipeline/scripts/fixtures/test-gap-python.diff +0 -32
- package/pipeline/scripts/lint-mcp-refs.mjs +0 -218
- package/pipeline/scripts/lint-skills.mjs +0 -154
- package/pipeline/scripts/run-smokes.mjs +0 -130
- package/pipeline/scripts/scorecard.mjs +0 -258
- package/pipeline/scripts/smoke-add-detail.sh +0 -137
- package/pipeline/scripts/smoke-agent-guard.sh +0 -74
- package/pipeline/scripts/smoke-agent-log-cost.sh +0 -262
- package/pipeline/scripts/smoke-agent-model-routing.sh +0 -87
- package/pipeline/scripts/smoke-ask-choice.sh +0 -42
- package/pipeline/scripts/smoke-autopilot-circuit-breaker.sh +0 -36
- package/pipeline/scripts/smoke-bitbucket-contract.sh +0 -255
- package/pipeline/scripts/smoke-changelog-version.sh +0 -47
- package/pipeline/scripts/smoke-channels-approval-gate.sh +0 -60
- package/pipeline/scripts/smoke-channels-flow.sh +0 -130
- package/pipeline/scripts/smoke-ci-workflows.sh +0 -88
- package/pipeline/scripts/smoke-clarify.sh +0 -148
- package/pipeline/scripts/smoke-command-inventory.sh +0 -81
- package/pipeline/scripts/smoke-commands-skills-parity.sh +0 -87
- package/pipeline/scripts/smoke-community-gates.sh +0 -75
- package/pipeline/scripts/smoke-compliance-skills.sh +0 -119
- package/pipeline/scripts/smoke-config-hygiene.sh +0 -58
- package/pipeline/scripts/smoke-context-budget.sh +0 -72
- package/pipeline/scripts/smoke-cost-budget.sh +0 -70
- package/pipeline/scripts/smoke-cost-summary.sh +0 -139
- package/pipeline/scripts/smoke-cross-phase-cohesion.sh +0 -128
- package/pipeline/scripts/smoke-description-tr.sh +0 -82
- package/pipeline/scripts/smoke-dev-critic.sh +0 -144
- package/pipeline/scripts/smoke-diff-explain.sh +0 -147
- package/pipeline/scripts/smoke-diff-risk.sh +0 -190
- package/pipeline/scripts/smoke-dynamic-skill-loading.sh +0 -160
- package/pipeline/scripts/smoke-eval-live.sh +0 -136
- package/pipeline/scripts/smoke-evidence-gate.sh +0 -93
- package/pipeline/scripts/smoke-extract-conventions.sh +0 -163
- package/pipeline/scripts/smoke-fetchers-offline.sh +0 -448
- package/pipeline/scripts/smoke-figma-dispatch.sh +0 -112
- package/pipeline/scripts/smoke-gate-hooks.sh +0 -74
- package/pipeline/scripts/smoke-gc-tmp.sh +0 -130
- package/pipeline/scripts/smoke-gc-worktrees.sh +0 -125
- package/pipeline/scripts/smoke-generate-issue.sh +0 -120
- package/pipeline/scripts/smoke-handoff-contract.sh +0 -92
- package/pipeline/scripts/smoke-identity-isolation.sh +0 -70
- package/pipeline/scripts/smoke-install-layout.sh +0 -248
- package/pipeline/scripts/smoke-intent-guard.sh +0 -86
- package/pipeline/scripts/smoke-issue-comment-template.sh +0 -86
- package/pipeline/scripts/smoke-issue-jira-triad.sh +0 -120
- package/pipeline/scripts/smoke-keychain.sh +0 -158
- package/pipeline/scripts/smoke-language-axis.sh +0 -109
- package/pipeline/scripts/smoke-learning-curve.sh +0 -61
- package/pipeline/scripts/smoke-learnings-ledger.sh +0 -86
- package/pipeline/scripts/smoke-lib-scripts.sh +0 -448
- package/pipeline/scripts/smoke-mcp-gate.sh +0 -68
- package/pipeline/scripts/smoke-md-links.sh +0 -8
- package/pipeline/scripts/smoke-md2confluence.sh +0 -126
- package/pipeline/scripts/smoke-metrics-cache-ratio.sh +0 -72
- package/pipeline/scripts/smoke-migrate-state.sh +0 -102
- package/pipeline/scripts/smoke-mode-dispatch-drift.sh +0 -161
- package/pipeline/scripts/smoke-model-fallback.sh +0 -89
- package/pipeline/scripts/smoke-multi-repo-integration.sh +0 -116
- package/pipeline/scripts/smoke-multi-repo-worktree.sh +0 -61
- package/pipeline/scripts/smoke-no-mcp-in-dev-phases.sh +0 -194
- package/pipeline/scripts/smoke-no-token-prompt.sh +0 -85
- package/pipeline/scripts/smoke-own-punctuation.sh +0 -103
- package/pipeline/scripts/smoke-pack-contents.sh +0 -140
- package/pipeline/scripts/smoke-pat-audit.sh +0 -128
- package/pipeline/scripts/smoke-per-repo-memory.sh +0 -156
- package/pipeline/scripts/smoke-phase-0-multi-repo.sh +0 -170
- package/pipeline/scripts/smoke-phase-6-multi.sh +0 -79
- package/pipeline/scripts/smoke-phase-banner.sh +0 -101
- package/pipeline/scripts/smoke-phase-tracker.sh +0 -324
- package/pipeline/scripts/smoke-phase0-bridge-contract.sh +0 -241
- package/pipeline/scripts/smoke-phase4-gates.sh +0 -45
- package/pipeline/scripts/smoke-phase4-triage.sh +0 -229
- package/pipeline/scripts/smoke-plan-approval-gate.sh +0 -71
- package/pipeline/scripts/smoke-plan-safety.sh +0 -139
- package/pipeline/scripts/smoke-plan-todos.sh +0 -196
- package/pipeline/scripts/smoke-pr-review-actions.sh +0 -152
- package/pipeline/scripts/smoke-pre-commit.sh +0 -170
- package/pipeline/scripts/smoke-pref-migration.sh +0 -226
- package/pipeline/scripts/smoke-prefs-language.sh +0 -134
- package/pipeline/scripts/smoke-progress-contract.sh +0 -127
- package/pipeline/scripts/smoke-prune-logs.sh +0 -137
- package/pipeline/scripts/smoke-purge.sh +0 -138
- package/pipeline/scripts/smoke-push-retry.sh +0 -75
- package/pipeline/scripts/smoke-repo-map.sh +0 -300
- package/pipeline/scripts/smoke-review-readiness.sh +0 -92
- package/pipeline/scripts/smoke-review-watch.sh +0 -146
- package/pipeline/scripts/smoke-routines.sh +0 -84
- package/pipeline/scripts/smoke-run-aggregator.sh +0 -216
- package/pipeline/scripts/smoke-run-metrics.sh +0 -50
- package/pipeline/scripts/smoke-search.sh +0 -187
- package/pipeline/scripts/smoke-shadow-git.sh +0 -224
- package/pipeline/scripts/smoke-skill-authoring.sh +0 -137
- package/pipeline/scripts/smoke-skill-language.sh +0 -83
- package/pipeline/scripts/smoke-skill-manifest.sh +0 -138
- package/pipeline/scripts/smoke-skill-scan.sh +0 -198
- package/pipeline/scripts/smoke-source-parity.sh +0 -85
- package/pipeline/scripts/smoke-subagent-validators.sh +0 -108
- package/pipeline/scripts/smoke-sync-parity.sh +0 -92
- package/pipeline/scripts/smoke-tasklist-ordering.sh +0 -112
- package/pipeline/scripts/smoke-telemetry.sh +0 -147
- package/pipeline/scripts/smoke-test-gap.sh +0 -183
- package/pipeline/scripts/smoke-token-budget.sh +0 -67
- package/pipeline/scripts/smoke-token-preflight.sh +0 -82
- package/pipeline/scripts/smoke-tracker-contract.sh +0 -191
- package/pipeline/scripts/smoke-tracker-tokens-invocation.sh +0 -73
- package/pipeline/scripts/smoke-triage-memory.sh +0 -174
- package/pipeline/scripts/smoke-update-check.sh +0 -135
- package/pipeline/scripts/smoke-url-enrichment.sh +0 -70
- package/pipeline/scripts/smoke-validate-analysis-doc.sh +0 -161
- package/pipeline/scripts/smoke-validator-contradiction.sh +0 -67
- package/pipeline/scripts/smoke-validator-gates.sh +0 -164
- package/pipeline/scripts/smoke-vercel-deploy-redact.sh +0 -129
- package/pipeline/scripts/smoke-verify-by-test.sh +0 -148
- package/pipeline/scripts/smoke-wiki-integration.sh +0 -122
- package/pipeline/scripts/smoke-work-summary.sh +0 -163
- package/pipeline/scripts/smoke-workflow-audit.sh +0 -101
- package/pipeline/scripts/smoke-worktree-path-convention.sh +0 -86
- package/pipeline/scripts/smoke-wrapper-preservation.sh +0 -68
- package/pipeline/scripts/smoke-write-state.sh +0 -159
- package/pipeline/scripts/sync-parity-check.sh +0 -135
- package/pipeline/scripts/test-gap-rules/android.json +0 -25
- package/pipeline/scripts/test-gap-rules/ios.json +0 -34
- package/pipeline/scripts/test-gap-rules/node.json +0 -29
- package/pipeline/scripts/test-gap-rules/python.json +0 -25
- package/pipeline/scripts/validate-schemas.mjs +0 -88
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# smoke-work-summary.sh - v7.1.0
|
|
3
|
-
#
|
|
4
|
-
# Verifies the Phase 7 Work Summary renderer contract:
|
|
5
|
-
# 1. Missing state + no flags → exit 2 (caller should skip section)
|
|
6
|
-
# 2. Full state fixture → renders header, scope, review, phases
|
|
7
|
-
# 3. Scope section marks ✅ for done tasks and ⏳ for pending
|
|
8
|
-
# 4. Review outcome suppressed when all buckets empty
|
|
9
|
-
# 5. Phase tick strip uses correct marks (completed ✅ · in_progress ▶ · skipped ⏭)
|
|
10
|
-
# 6. Post-hoc invocation with --branch + --base-branch (no state file) still renders header
|
|
11
|
-
# 7. prefs schema exposes reportContent.workSummary toggle
|
|
12
|
-
# 8. channels.md documents the Work Summary option
|
|
13
|
-
#
|
|
14
|
-
# Exit 0 all pass, 1 any failure.
|
|
15
|
-
|
|
16
|
-
set -euo pipefail
|
|
17
|
-
|
|
18
|
-
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
|
19
|
-
RENDER="$ROOT/pipeline/scripts/render-work-summary.sh"
|
|
20
|
-
SCHEMA="$ROOT/pipeline/schemas/prefs.schema.json"
|
|
21
|
-
CHANNELS_DOC="$ROOT/pipeline/commands/multi-agent/channels/SKILL.md"
|
|
22
|
-
|
|
23
|
-
pass=0; fail=0; failures=()
|
|
24
|
-
record_pass() { pass=$((pass + 1)); printf ' \033[0;32mPASS\033[0m %s\n' "$1"; }
|
|
25
|
-
record_fail() { fail=$((fail + 1)); failures+=("$1"); printf ' \033[0;31mFAIL\033[0m %s\n' "$1"; }
|
|
26
|
-
|
|
27
|
-
printf '→ smoke-work-summary (v7.1.0): Phase 7 Work Summary renderer contract\n'
|
|
28
|
-
|
|
29
|
-
# --- 1: missing state → exit 2 ---
|
|
30
|
-
tmp=$(mktemp -d); pushd "$tmp" >/dev/null
|
|
31
|
-
set +e
|
|
32
|
-
bash "$RENDER" GHOST-1 >/dev/null 2>&1
|
|
33
|
-
rc=$?
|
|
34
|
-
set -e
|
|
35
|
-
if [ "$rc" = "2" ]; then
|
|
36
|
-
record_pass "exit 2 when no state + no flags"
|
|
37
|
-
else
|
|
38
|
-
record_fail "expected exit 2 for empty state (got $rc)"
|
|
39
|
-
fi
|
|
40
|
-
popd >/dev/null; rm -rf "$tmp"
|
|
41
|
-
|
|
42
|
-
# --- 2-5: full fixture ---
|
|
43
|
-
tmp=$(mktemp -d); pushd "$tmp" >/dev/null
|
|
44
|
-
mkdir -p .worktrees/PROJ-2001
|
|
45
|
-
cat > .worktrees/PROJ-2001/agent-state.json <<'JSON'
|
|
46
|
-
{"taskId":"PROJ-2001","branch":"feature/PROJ-2001-checkout-v2","baseBranch":"develop","prNumber":"#256",
|
|
47
|
-
"planTodos":[
|
|
48
|
-
{"id":"T1","title":"Add checkout acceptance tests","status":"done"},
|
|
49
|
-
{"id":"T2","title":"Wire CheckoutViewModel","status":"done"},
|
|
50
|
-
{"id":"T3","title":"Optimize cart recomposition","status":"deferred"}
|
|
51
|
-
],
|
|
52
|
-
"reviewConsensus":{
|
|
53
|
-
"accepted":[{"severity":"blocking","file":"ViewModel.swift","line":42,"issue":"race","fix":"lock","reviewer":"opus"}],
|
|
54
|
-
"deferred":[],
|
|
55
|
-
"rejected":[{"finding":{"severity":"suggestion","file":"x.swift","line":0,"issue":"style","reviewer":"sonnet"},"reason":"style-only"}],
|
|
56
|
-
"approved":false
|
|
57
|
-
}}
|
|
58
|
-
JSON
|
|
59
|
-
cat > .worktrees/PROJ-2001/phase-tracker.json <<'JSON'
|
|
60
|
-
{"taskId":"PROJ-2001","phases":{
|
|
61
|
-
"0":{"status":"completed"},"1":{"status":"completed"},"2":{"status":"completed"},
|
|
62
|
-
"3":{"status":"completed"},"4":{"status":"completed"},"5":{"status":"skipped"},
|
|
63
|
-
"6":{"status":"completed"},"7":{"status":"in_progress"}
|
|
64
|
-
}}
|
|
65
|
-
JSON
|
|
66
|
-
out=$(bash "$RENDER" PROJ-2001)
|
|
67
|
-
popd >/dev/null; rm -rf "$tmp"
|
|
68
|
-
|
|
69
|
-
if grep -q '^### Work Summary' <<< "$out" && \
|
|
70
|
-
grep -q 'PROJ-2001' <<< "$out" && \
|
|
71
|
-
grep -q 'feature/PROJ-2001-checkout-v2' <<< "$out" && \
|
|
72
|
-
grep -q '#256' <<< "$out"; then
|
|
73
|
-
record_pass "renders task header with jira+branch+base+PR"
|
|
74
|
-
else
|
|
75
|
-
record_fail "task header missing fields"
|
|
76
|
-
fi
|
|
77
|
-
|
|
78
|
-
if grep -q '✅ T1' <<< "$out" && grep -q '✅ T2' <<< "$out"; then
|
|
79
|
-
record_pass "scope marks done tasks with ✅"
|
|
80
|
-
else
|
|
81
|
-
record_fail "done tasks not marked ✅ (output: $(echo "$out" | head -20))"
|
|
82
|
-
fi
|
|
83
|
-
|
|
84
|
-
# deferred is not "done" - should get ⏳
|
|
85
|
-
if grep -qE '⏳ T3' <<< "$out"; then
|
|
86
|
-
record_pass "scope marks non-done tasks with ⏳"
|
|
87
|
-
else
|
|
88
|
-
record_fail "non-done tasks not marked ⏳"
|
|
89
|
-
fi
|
|
90
|
-
|
|
91
|
-
if grep -q '1 accepted · 0 deferred · 1 rejected · approved=false' <<< "$out"; then
|
|
92
|
-
record_pass "review outcome line matches triage counts"
|
|
93
|
-
else
|
|
94
|
-
record_fail "review outcome line wrong (expected '1 accepted · 0 deferred · 1 rejected · approved=false')"
|
|
95
|
-
fi
|
|
96
|
-
|
|
97
|
-
if grep -q '0 Init ✅' <<< "$out" && \
|
|
98
|
-
grep -q '5 Test ⏭' <<< "$out" && \
|
|
99
|
-
grep -q '7 Report ▶' <<< "$out"; then
|
|
100
|
-
record_pass "phase tick strip renders correct marks"
|
|
101
|
-
else
|
|
102
|
-
record_fail "phase tick marks incorrect"
|
|
103
|
-
fi
|
|
104
|
-
|
|
105
|
-
# --- 4b: review outcome suppressed when empty ---
|
|
106
|
-
tmp=$(mktemp -d); pushd "$tmp" >/dev/null
|
|
107
|
-
mkdir -p .worktrees/PROJ-2002
|
|
108
|
-
cat > .worktrees/PROJ-2002/agent-state.json <<'JSON'
|
|
109
|
-
{"taskId":"PROJ-2002","branch":"fix/PROJ-2002","baseBranch":"main",
|
|
110
|
-
"planTodos":[{"id":"T1","title":"tiny fix","status":"done"}],
|
|
111
|
-
"reviewConsensus":{"accepted":[],"deferred":[],"rejected":[],"approved":true}}
|
|
112
|
-
JSON
|
|
113
|
-
out=$(bash "$RENDER" PROJ-2002)
|
|
114
|
-
popd >/dev/null; rm -rf "$tmp"
|
|
115
|
-
|
|
116
|
-
if ! grep -q '#### Review outcome' <<< "$out"; then
|
|
117
|
-
record_pass "review outcome hidden when all buckets empty"
|
|
118
|
-
else
|
|
119
|
-
record_fail "review outcome should be hidden when empty"
|
|
120
|
-
fi
|
|
121
|
-
|
|
122
|
-
# --- 6: post-hoc flags, no state ---
|
|
123
|
-
tmp=$(mktemp -d); pushd "$tmp" >/dev/null
|
|
124
|
-
out=$(bash "$RENDER" POST-HOC-1 --branch "bugfix/post-hoc" --base-branch develop 2>&1 || true)
|
|
125
|
-
popd >/dev/null; rm -rf "$tmp"
|
|
126
|
-
if grep -q '^### Work Summary' <<< "$out" && grep -q 'POST-HOC-1' <<< "$out"; then
|
|
127
|
-
record_pass "post-hoc flags render header without state"
|
|
128
|
-
else
|
|
129
|
-
record_fail "post-hoc invocation should still render header"
|
|
130
|
-
fi
|
|
131
|
-
|
|
132
|
-
# --- 7: prefs schema ---
|
|
133
|
-
if jq -e '.properties.global.properties.reportContent.properties.workSummary.default == false' "$SCHEMA" >/dev/null 2>&1; then
|
|
134
|
-
record_pass "prefs schema exposes reportContent.workSummary (default false)"
|
|
135
|
-
else
|
|
136
|
-
record_fail "prefs schema missing reportContent.workSummary"
|
|
137
|
-
fi
|
|
138
|
-
|
|
139
|
-
# --- 8: channels.md docs ---
|
|
140
|
-
if grep -q '### Work Summary' "$CHANNELS_DOC" && \
|
|
141
|
-
grep -q 'Yapılan iş özeti' "$CHANNELS_DOC" && \
|
|
142
|
-
grep -q 'Work summary generation' "$CHANNELS_DOC"; then
|
|
143
|
-
record_pass "channels.md documents Work Summary content option + menu row + generation spec"
|
|
144
|
-
else
|
|
145
|
-
record_fail "channels.md missing Work Summary docs"
|
|
146
|
-
fi
|
|
147
|
-
|
|
148
|
-
# --- 9: v7.1.1 preview-first flow documented ---
|
|
149
|
-
if grep -q 'Step 3a' "$CHANNELS_DOC" && \
|
|
150
|
-
grep -q 'summary-first' "$CHANNELS_DOC" && \
|
|
151
|
-
grep -q 'auto-tick' "$CHANNELS_DOC"; then
|
|
152
|
-
record_pass "channels.md documents v7.1.1 preview-before-select flow"
|
|
153
|
-
else
|
|
154
|
-
record_fail "channels.md missing Step 3a summary-first flow docs"
|
|
155
|
-
fi
|
|
156
|
-
|
|
157
|
-
printf '\n══ work-summary smoke: %d passed, %d failed ══\n' "$pass" "$fail"
|
|
158
|
-
if [ "$fail" -gt 0 ]; then
|
|
159
|
-
printf '\nFailures:\n'
|
|
160
|
-
for m in "${failures[@]}"; do printf ' - %s\n' "$m"; done
|
|
161
|
-
exit 1
|
|
162
|
-
fi
|
|
163
|
-
exit 0
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
#
|
|
3
|
-
# smoke-workflow-audit.sh - security + correctness audit of
|
|
4
|
-
# `.github/workflows/` with the two dedicated external linters:
|
|
5
|
-
#
|
|
6
|
-
# - zizmor GitHub Actions security auditor (template injection,
|
|
7
|
-
# credential persistence, cache poisoning, unpinned actions,
|
|
8
|
-
# excessive permissions, ...). Runs offline, no network.
|
|
9
|
-
# - actionlint workflow correctness linter (expression syntax, matrix
|
|
10
|
-
# typos, shellcheck of run: blocks, ...).
|
|
11
|
-
#
|
|
12
|
-
# Any finding from either tool fails the smoke.
|
|
13
|
-
#
|
|
14
|
-
# WHY THIS IS STRICT BY DEFAULT
|
|
15
|
-
#
|
|
16
|
-
# Until now a missing binary produced a SKIP and the smoke exited 0, so it
|
|
17
|
-
# reported "0 passed, 0 failed, 2 skipped" on the dev machine AND on the CI
|
|
18
|
-
# runner - neither had the tools. ci-lite.yml justified not installing them by
|
|
19
|
-
# saying to run the smoke locally "where the tools exist", but they did not
|
|
20
|
-
# exist there either. The audit had therefore never run anywhere, while
|
|
21
|
-
# appearing as a green step. When it was finally run it found nine real issues,
|
|
22
|
-
# one of them high severity (a dependency cache restored into the job that
|
|
23
|
-
# signs and publishes the release tarball).
|
|
24
|
-
#
|
|
25
|
-
# So the failure mode is inverted: absent tooling is a failure, and skipping is
|
|
26
|
-
# something you opt into deliberately and locally.
|
|
27
|
-
#
|
|
28
|
-
# brew install zizmor actionlint # macOS
|
|
29
|
-
# pipx install zizmor && go install github.com/rhysd/actionlint/cmd/actionlint@latest
|
|
30
|
-
#
|
|
31
|
-
# WORKFLOW_AUDIT_ALLOW_MISSING=1 downgrades a missing binary back to a SKIP for
|
|
32
|
-
# a local run. It is ignored when CI is set: CI installs both, pinned, and a
|
|
33
|
-
# runner that cannot must fail loudly rather than pass quietly.
|
|
34
|
-
#
|
|
35
|
-
# Structural baseline checks that need no external binary live in
|
|
36
|
-
# smoke-ci-workflows.sh - this smoke is the deeper, tool-backed layer.
|
|
37
|
-
#
|
|
38
|
-
# Exit codes: 0=pass, 1=findings or missing tooling, 2=workflows missing
|
|
39
|
-
|
|
40
|
-
set -uo pipefail
|
|
41
|
-
|
|
42
|
-
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
|
43
|
-
WF_DIR="$REPO_ROOT/.github/workflows"
|
|
44
|
-
|
|
45
|
-
[ -d "$WF_DIR" ] || { echo "FAIL: .github/workflows/ missing"; exit 2; }
|
|
46
|
-
|
|
47
|
-
# In CI the escape hatch does not apply, whatever the environment says.
|
|
48
|
-
ALLOW_MISSING="${WORKFLOW_AUDIT_ALLOW_MISSING:-0}"
|
|
49
|
-
[ -n "${CI:-}" ] && ALLOW_MISSING=0
|
|
50
|
-
|
|
51
|
-
PASS=0
|
|
52
|
-
FAIL=0
|
|
53
|
-
SKIPPED=0
|
|
54
|
-
pass() { PASS=$((PASS + 1)); echo " ✓ $1"; }
|
|
55
|
-
fail() { FAIL=$((FAIL + 1)); echo " ✗ $1"; }
|
|
56
|
-
skip() { SKIPPED=$((SKIPPED + 1)); echo " ↷ SKIP: $1"; }
|
|
57
|
-
|
|
58
|
-
# A tool that is not installed is a failure unless a local run opted out.
|
|
59
|
-
missing_tool() {
|
|
60
|
-
local tool="$1" install="$2"
|
|
61
|
-
if [ "$ALLOW_MISSING" = "1" ]; then
|
|
62
|
-
skip "$tool not on PATH, WORKFLOW_AUDIT_ALLOW_MISSING=1 ($install)"
|
|
63
|
-
else
|
|
64
|
-
fail "$tool not on PATH - the audit cannot run ($install)"
|
|
65
|
-
fi
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
# ──────────────────────────────────────────────────────────────────────────
|
|
69
|
-
echo "→ 1. zizmor (security audit, offline)"
|
|
70
|
-
if command -v zizmor >/dev/null 2>&1; then
|
|
71
|
-
# Flag names shifted across zizmor releases (--offline vs --no-online-audits);
|
|
72
|
-
# probe --help so the smoke works with whichever is installed.
|
|
73
|
-
HELP=$(zizmor --help 2>&1 || true)
|
|
74
|
-
OFFLINE_FLAG="--offline"
|
|
75
|
-
echo "$HELP" | grep -q -- '--offline' || OFFLINE_FLAG="--no-online-audits"
|
|
76
|
-
if OUT=$(zizmor "$WF_DIR" --min-severity medium "$OFFLINE_FLAG" 2>&1); then
|
|
77
|
-
pass "zizmor: no findings at medium+ severity"
|
|
78
|
-
else
|
|
79
|
-
fail "zizmor reported findings (medium+ severity)"
|
|
80
|
-
echo "$OUT" | sed 's/^/ /'
|
|
81
|
-
fi
|
|
82
|
-
else
|
|
83
|
-
missing_tool zizmor "brew install zizmor"
|
|
84
|
-
fi
|
|
85
|
-
|
|
86
|
-
# ──────────────────────────────────────────────────────────────────────────
|
|
87
|
-
echo "→ 2. actionlint (workflow correctness)"
|
|
88
|
-
if command -v actionlint >/dev/null 2>&1; then
|
|
89
|
-
if OUT=$(cd "$REPO_ROOT" && actionlint 2>&1); then
|
|
90
|
-
pass "actionlint: no findings"
|
|
91
|
-
else
|
|
92
|
-
fail "actionlint reported findings"
|
|
93
|
-
echo "$OUT" | sed 's/^/ /'
|
|
94
|
-
fi
|
|
95
|
-
else
|
|
96
|
-
missing_tool actionlint "brew install actionlint"
|
|
97
|
-
fi
|
|
98
|
-
|
|
99
|
-
echo ""
|
|
100
|
-
echo "══ smoke-workflow-audit: $PASS passed, $FAIL failed, $SKIPPED skipped ══"
|
|
101
|
-
[ "$FAIL" -gt 0 ] && exit 1 || exit 0
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
#
|
|
3
|
-
# smoke-worktree-path-convention.sh
|
|
4
|
-
#
|
|
5
|
-
# Asserts the pipeline never creates worktrees outside `{projectRoot}/{worktreeBasePath}/{taskId}`.
|
|
6
|
-
# Per `feedback_worktree_path_convention.md`:
|
|
7
|
-
# - Worktree is ALWAYS under the project root, NEVER under $HOME directly.
|
|
8
|
-
# - Default basePath is `.worktrees`, override via `prefs.global.worktreeBasePath`.
|
|
9
|
-
# - Multi-repo tasks: each repo gets its own independent worktree under that repo's root.
|
|
10
|
-
#
|
|
11
|
-
# This is a static-analysis lint, not a runtime probe. It greps the pipeline
|
|
12
|
-
# source for forbidden worktree paths in shell scripts, markdown contracts,
|
|
13
|
-
# and orchestrator command files. A behavioural check (live `git worktree list`)
|
|
14
|
-
# is out of scope - that would require a real run.
|
|
15
|
-
|
|
16
|
-
set -euo pipefail
|
|
17
|
-
|
|
18
|
-
SELF_DIR=$(cd "$(dirname "$0")" && pwd)
|
|
19
|
-
PIPELINE_ROOT=$(cd "$SELF_DIR/.." && pwd)
|
|
20
|
-
|
|
21
|
-
cd "$PIPELINE_ROOT"
|
|
22
|
-
|
|
23
|
-
FAIL=0
|
|
24
|
-
|
|
25
|
-
# Forbidden patterns: any worktree path that lives under $HOME directly,
|
|
26
|
-
# or uses repo-name--taskId style outside a project root.
|
|
27
|
-
declare -a FORBIDDEN=(
|
|
28
|
-
'\$HOME/\.worktrees' # $HOME/.worktrees/foo - wrong (must be under projectRoot)
|
|
29
|
-
'~/\.worktrees' # ~/.worktrees/foo - same
|
|
30
|
-
'\$HOME/[a-zA-Z0-9_-]+--[a-zA-Z0-9_-]+' # $HOME/repo--taskid pattern
|
|
31
|
-
'~/[a-zA-Z0-9_-]+--[a-zA-Z0-9_-]+' # ~/repo--taskid pattern
|
|
32
|
-
'git worktree add.*\$HOME/[^.]' # `git worktree add $HOME/something`
|
|
33
|
-
'git worktree add.*~/[^/]' # `git worktree add ~/something`
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
# In-scope files: shell + markdown phase contracts + orchestrator skills.
|
|
37
|
-
TARGETS=(
|
|
38
|
-
"scripts/*.sh"
|
|
39
|
-
"scripts/*.mjs"
|
|
40
|
-
"lib/*.sh"
|
|
41
|
-
"commands/multi-agent/SKILL.md"
|
|
42
|
-
"commands/multi-agent/*.md"
|
|
43
|
-
"multi-agent-refs/*.md"
|
|
44
|
-
"multi-agent-refs/phases/*.md"
|
|
45
|
-
"skills/shared/core/multi-agent/SKILL.md"
|
|
46
|
-
)
|
|
47
|
-
|
|
48
|
-
# Allowlist file paths - reference docs that mention the wrong pattern only
|
|
49
|
-
# to teach the rule are tagged with `# allow: worktree-bad-example` annotation
|
|
50
|
-
# on the offending line.
|
|
51
|
-
for pattern in "${FORBIDDEN[@]}"; do
|
|
52
|
-
for glob in "${TARGETS[@]}"; do
|
|
53
|
-
while IFS= read -r match; do
|
|
54
|
-
[ -z "$match" ] && continue
|
|
55
|
-
file="${match%%:*}"
|
|
56
|
-
# Allow lines that are rule-explanation examples (annotated).
|
|
57
|
-
if echo "$match" | grep -q '# allow: worktree-bad-example'; then continue; fi
|
|
58
|
-
# Allow this very smoke gate's own forbidden-pattern array.
|
|
59
|
-
case "$file" in
|
|
60
|
-
*/smoke-worktree-path-convention.sh) continue ;;
|
|
61
|
-
*/feedback_worktree_path_convention.md) continue ;;
|
|
62
|
-
esac
|
|
63
|
-
echo "FAIL [smoke-worktree-path-convention]: forbidden worktree path in $file"
|
|
64
|
-
echo " $match"
|
|
65
|
-
FAIL=$((FAIL + 1))
|
|
66
|
-
done < <(grep -rEn "$pattern" "pipeline/$glob" 2>/dev/null || true)
|
|
67
|
-
done
|
|
68
|
-
done
|
|
69
|
-
|
|
70
|
-
# Positive assertion: phase-0-init.md MUST mention the convention text.
|
|
71
|
-
PHASE0="multi-agent-refs/phases/phase-0-init.md"
|
|
72
|
-
if [ -f "pipeline/$PHASE0" ]; then
|
|
73
|
-
if ! grep -qE 'projectRoot.*worktreeBasePath|\.worktrees/.*taskId' "pipeline/$PHASE0"; then
|
|
74
|
-
echo "FAIL [smoke-worktree-path-convention]: phase-0-init.md must document {projectRoot}/{worktreeBasePath}/{taskId} convention"
|
|
75
|
-
FAIL=$((FAIL + 1))
|
|
76
|
-
fi
|
|
77
|
-
fi
|
|
78
|
-
|
|
79
|
-
if [ "$FAIL" -gt 0 ]; then
|
|
80
|
-
echo ""
|
|
81
|
-
echo "smoke-worktree-path-convention: $FAIL violation(s)."
|
|
82
|
-
echo "Worktree path contract: {projectRoot}/{worktreeBasePath default .worktrees}/{taskId}. Never under \$HOME directly. See feedback_worktree_path_convention.md."
|
|
83
|
-
exit 1
|
|
84
|
-
fi
|
|
85
|
-
|
|
86
|
-
echo "smoke-worktree-path-convention: ok"
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# smoke-wrapper-preservation.sh - local-only alias wrappers must survive install/update.
|
|
3
|
-
#
|
|
4
|
-
# Local-only wrappers (frontmatter `local-only: true`) live ONLY under
|
|
5
|
-
# ~/.claude/commands/multi-agent/ (never in pipeline/, never synced). install
|
|
6
|
-
# wipes that namespace before copying from pipeline; without the snapshot+restore
|
|
7
|
-
# carve-out in install/claude.mjs installCommands, wrappers are destroyed on every
|
|
8
|
-
# run. This asserts they survive a second install.
|
|
9
|
-
|
|
10
|
-
set -uo pipefail
|
|
11
|
-
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
12
|
-
ROOT="$(cd "$HERE/../.." && pwd)"
|
|
13
|
-
INSTALL_JS="$ROOT/install.js"
|
|
14
|
-
|
|
15
|
-
PASS=0; FAIL=0
|
|
16
|
-
pass() { PASS=$((PASS+1)); echo " ✓ $1"; }
|
|
17
|
-
fail() { FAIL=$((FAIL+1)); echo " ✗ $1"; }
|
|
18
|
-
|
|
19
|
-
TMP="$(mktemp -d)"
|
|
20
|
-
trap 'rm -rf "$TMP"' EXIT
|
|
21
|
-
|
|
22
|
-
echo "→ 1. first install lays down the namespace"
|
|
23
|
-
if HOME="$TMP" node "$INSTALL_JS" --claude >/dev/null 2>&1; then
|
|
24
|
-
pass "install --claude (1st) exited 0"
|
|
25
|
-
else
|
|
26
|
-
fail "install --claude (1st) failed"
|
|
27
|
-
echo "══ wrapper-preservation smoke: $PASS passed, $FAIL failed ══"; exit 1
|
|
28
|
-
fi
|
|
29
|
-
|
|
30
|
-
WRAP_DIR="$TMP/.claude/commands/multi-agent/zz-wrapper-probe"
|
|
31
|
-
mkdir -p "$WRAP_DIR"
|
|
32
|
-
cat > "$WRAP_DIR/SKILL.md" <<'EOF'
|
|
33
|
-
---
|
|
34
|
-
description: "probe local-only wrapper"
|
|
35
|
-
allowed-tools: Skill
|
|
36
|
-
local-only: true
|
|
37
|
-
---
|
|
38
|
-
Use the Skill tool to invoke `some-plugin:some-skill`.
|
|
39
|
-
EOF
|
|
40
|
-
[ -f "$WRAP_DIR/SKILL.md" ] && pass "seeded a local-only wrapper" || fail "could not seed wrapper"
|
|
41
|
-
|
|
42
|
-
echo ""
|
|
43
|
-
echo "→ 2. second install must PRESERVE the wrapper"
|
|
44
|
-
if HOME="$TMP" node "$INSTALL_JS" --claude >/dev/null 2>&1; then
|
|
45
|
-
pass "install --claude (2nd) exited 0"
|
|
46
|
-
else
|
|
47
|
-
fail "install --claude (2nd) failed"
|
|
48
|
-
fi
|
|
49
|
-
if [ -f "$WRAP_DIR/SKILL.md" ] && grep -q '^local-only: true' "$WRAP_DIR/SKILL.md"; then
|
|
50
|
-
pass "local-only wrapper survived re-install"
|
|
51
|
-
else
|
|
52
|
-
fail "local-only wrapper was DESTROYED by re-install (carve-out regressed)"
|
|
53
|
-
fi
|
|
54
|
-
|
|
55
|
-
echo ""
|
|
56
|
-
echo "→ 3. a normal (non-local-only) stray dir is still wiped"
|
|
57
|
-
STRAY="$TMP/.claude/commands/multi-agent/zz-stray-probe"
|
|
58
|
-
mkdir -p "$STRAY"; printf -- '---\ndescription: "stray"\n---\n' > "$STRAY/SKILL.md"
|
|
59
|
-
HOME="$TMP" node "$INSTALL_JS" --claude >/dev/null 2>&1
|
|
60
|
-
if [ -f "$STRAY/SKILL.md" ]; then
|
|
61
|
-
fail "non-local-only stray survived (wipe too weak)"
|
|
62
|
-
else
|
|
63
|
-
pass "non-local-only stray correctly wiped"
|
|
64
|
-
fi
|
|
65
|
-
|
|
66
|
-
echo ""
|
|
67
|
-
echo "══ wrapper-preservation smoke: $PASS passed, $FAIL failed ══"
|
|
68
|
-
[ "$FAIL" -eq 0 ]
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# smoke-write-state.sh - verify write-state.mjs atomic write + merge + lock behavior.
|
|
3
|
-
|
|
4
|
-
set -uo pipefail
|
|
5
|
-
|
|
6
|
-
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
7
|
-
WRITER="$SCRIPT_DIR/write-state.mjs"
|
|
8
|
-
|
|
9
|
-
if [ ! -f "$WRITER" ]; then
|
|
10
|
-
echo "FAIL: write-state.mjs not found" >&2
|
|
11
|
-
exit 1
|
|
12
|
-
fi
|
|
13
|
-
|
|
14
|
-
PASS=0
|
|
15
|
-
FAIL=0
|
|
16
|
-
pass() { PASS=$((PASS+1)); echo " ✓ $1"; }
|
|
17
|
-
fail() { FAIL=$((FAIL+1)); echo " ✗ $1"; }
|
|
18
|
-
|
|
19
|
-
TMPDIR=$(mktemp -d) || { echo "FAIL: mktemp" >&2; exit 1; }
|
|
20
|
-
trap 'rm -rf "$TMPDIR"' EXIT
|
|
21
|
-
|
|
22
|
-
STATE="$TMPDIR/state.json"
|
|
23
|
-
|
|
24
|
-
# 1. Initial write creates file
|
|
25
|
-
echo '{"currentPhase":0,"status":"in_progress"}' | node "$WRITER" "$STATE"
|
|
26
|
-
if [ -f "$STATE" ] && grep -q '"currentPhase": 0' "$STATE"; then
|
|
27
|
-
pass "initial write creates file"
|
|
28
|
-
else
|
|
29
|
-
fail "initial write missing/malformed"
|
|
30
|
-
fi
|
|
31
|
-
|
|
32
|
-
# 2. Merge preserves existing fields
|
|
33
|
-
echo '{"currentPhase":3}' | node "$WRITER" "$STATE"
|
|
34
|
-
if grep -q '"currentPhase": 3' "$STATE" && grep -q '"status": "in_progress"' "$STATE"; then
|
|
35
|
-
pass "merge preserves untouched fields"
|
|
36
|
-
else
|
|
37
|
-
fail "merge dropped fields"
|
|
38
|
-
fi
|
|
39
|
-
|
|
40
|
-
# 3. Nested merge
|
|
41
|
-
echo '{"phases":{"3":{"status":"done"}}}' | node "$WRITER" "$STATE"
|
|
42
|
-
echo '{"phases":{"4":{"status":"in_progress"}}}' | node "$WRITER" "$STATE"
|
|
43
|
-
if grep -q '"3"' "$STATE" && grep -q '"4"' "$STATE"; then
|
|
44
|
-
pass "nested merge preserves sibling keys"
|
|
45
|
-
else
|
|
46
|
-
fail "nested merge overwrote sibling keys"
|
|
47
|
-
fi
|
|
48
|
-
|
|
49
|
-
# 4. --replace overwrites completely
|
|
50
|
-
echo '{"currentPhase":7}' | node "$WRITER" --replace "$STATE"
|
|
51
|
-
if ! grep -q '"status"' "$STATE" && grep -q '"currentPhase": 7' "$STATE"; then
|
|
52
|
-
pass "--replace overwrites full state"
|
|
53
|
-
else
|
|
54
|
-
fail "--replace did not fully overwrite"
|
|
55
|
-
fi
|
|
56
|
-
|
|
57
|
-
# 5. Invalid JSON exits 1
|
|
58
|
-
echo 'not-json' | node "$WRITER" "$TMPDIR/other.json" 2>/dev/null
|
|
59
|
-
if [ $? -eq 1 ]; then
|
|
60
|
-
pass "invalid JSON exits 1"
|
|
61
|
-
else
|
|
62
|
-
fail "invalid JSON did not exit 1"
|
|
63
|
-
fi
|
|
64
|
-
|
|
65
|
-
# 6. Empty stdin exits 1
|
|
66
|
-
: | node "$WRITER" "$TMPDIR/empty.json" 2>/dev/null
|
|
67
|
-
if [ $? -eq 1 ]; then
|
|
68
|
-
pass "empty stdin exits 1"
|
|
69
|
-
else
|
|
70
|
-
fail "empty stdin did not exit 1"
|
|
71
|
-
fi
|
|
72
|
-
|
|
73
|
-
# 7. Missing path arg exits 3
|
|
74
|
-
echo '{}' | node "$WRITER" 2>/dev/null
|
|
75
|
-
if [ $? -eq 3 ]; then
|
|
76
|
-
pass "missing path exits 3"
|
|
77
|
-
else
|
|
78
|
-
fail "missing path did not exit 3"
|
|
79
|
-
fi
|
|
80
|
-
|
|
81
|
-
# 8. Atomic - no .tmp leftover after success
|
|
82
|
-
if ls "$TMPDIR"/.state.json.*.tmp 2>/dev/null | head -1 | grep -q .; then
|
|
83
|
-
fail "leftover .tmp file after success"
|
|
84
|
-
else
|
|
85
|
-
pass "no .tmp leftover after successful write"
|
|
86
|
-
fi
|
|
87
|
-
|
|
88
|
-
# 9. No .lock leftover after success
|
|
89
|
-
if [ -f "$STATE.lock" ]; then
|
|
90
|
-
fail "leftover .lock file after success"
|
|
91
|
-
else
|
|
92
|
-
pass "no .lock leftover after successful write"
|
|
93
|
-
fi
|
|
94
|
-
|
|
95
|
-
# 10. Concurrent writers don't corrupt state
|
|
96
|
-
#
|
|
97
|
-
# Was 5 writers, which lost an update roughly one run in five on a loaded
|
|
98
|
-
# machine and never on an idle one, so it read as a flaky test rather than the
|
|
99
|
-
# data-loss bug it was. 20 writers puts enough writers in the acquire window to
|
|
100
|
-
# make a regression show up on an idle machine too.
|
|
101
|
-
STATE2="$TMPDIR/concurrent.json"
|
|
102
|
-
WRITERS=20
|
|
103
|
-
echo '{"counter":0}' | node "$WRITER" "$STATE2"
|
|
104
|
-
for i in $(seq 1 "$WRITERS"); do
|
|
105
|
-
echo "{\"w$i\":$i}" | node "$WRITER" "$STATE2" &
|
|
106
|
-
done
|
|
107
|
-
wait
|
|
108
|
-
MISSING=0
|
|
109
|
-
for i in $(seq 1 "$WRITERS"); do
|
|
110
|
-
grep -q "\"w$i\": $i" "$STATE2" || MISSING=$((MISSING+1))
|
|
111
|
-
done
|
|
112
|
-
if [ "$MISSING" -eq 0 ]; then
|
|
113
|
-
pass "$WRITERS concurrent writers all persisted"
|
|
114
|
-
else
|
|
115
|
-
fail "$MISSING of $WRITERS concurrent writes lost"
|
|
116
|
-
fi
|
|
117
|
-
|
|
118
|
-
# 11. A lock with no readable PID must not be treated as stale
|
|
119
|
-
#
|
|
120
|
-
# This is the exact shape of the bug above. The lock used to be created empty
|
|
121
|
-
# and filled on the next line, so a writer that arrived in between read an
|
|
122
|
-
# unparseable PID, called the lock stale, and deleted it while its owner was
|
|
123
|
-
# still working. Both then wrote and one update vanished.
|
|
124
|
-
#
|
|
125
|
-
# An empty lock now has to block until the staleness window elapses, so a
|
|
126
|
-
# writer given a 1s acquire budget must give up with exit 2 rather than help
|
|
127
|
-
# itself to the lock.
|
|
128
|
-
STATE3="$TMPDIR/emptylock.json"
|
|
129
|
-
echo '{"seed":1}' | node "$WRITER" "$STATE3"
|
|
130
|
-
: > "$STATE3.lock"
|
|
131
|
-
LOCK_RC=0
|
|
132
|
-
echo '{"stolen":true}' | WRITE_STATE_LOCK_TIMEOUT_MS=1000 WRITE_STATE_LOCK_STALE_MS=30000 \
|
|
133
|
-
node "$WRITER" "$STATE3" >/dev/null 2>&1 || LOCK_RC=$?
|
|
134
|
-
if [ "$LOCK_RC" -eq 2 ] && ! grep -q '"stolen"' "$STATE3"; then
|
|
135
|
-
pass "empty lock is not stolen (exit 2, state untouched)"
|
|
136
|
-
else
|
|
137
|
-
fail "empty lock was treated as stale (exit $LOCK_RC) - the live-lock race is back"
|
|
138
|
-
fi
|
|
139
|
-
rm -f "$STATE3.lock"
|
|
140
|
-
|
|
141
|
-
# 12. A lock older than the staleness window is still reclaimed
|
|
142
|
-
#
|
|
143
|
-
# The counterpart to 11: refusing to reclaim an unreadable PID must not mean a
|
|
144
|
-
# genuinely abandoned lock blocks forever.
|
|
145
|
-
STATE4="$TMPDIR/stalelock.json"
|
|
146
|
-
echo '{"seed":1}' | node "$WRITER" "$STATE4"
|
|
147
|
-
: > "$STATE4.lock"
|
|
148
|
-
STALE_RC=0
|
|
149
|
-
echo '{"recovered":true}' | WRITE_STATE_LOCK_TIMEOUT_MS=3000 WRITE_STATE_LOCK_STALE_MS=1 \
|
|
150
|
-
node "$WRITER" "$STATE4" >/dev/null 2>&1 || STALE_RC=$?
|
|
151
|
-
if [ "$STALE_RC" -eq 0 ] && grep -q '"recovered"' "$STATE4"; then
|
|
152
|
-
pass "abandoned lock past the staleness window is reclaimed"
|
|
153
|
-
else
|
|
154
|
-
fail "stale lock not reclaimed (exit $STALE_RC) - writers would wedge"
|
|
155
|
-
fi
|
|
156
|
-
|
|
157
|
-
echo ""
|
|
158
|
-
echo "══ write-state smoke: $PASS passed, $FAIL failed ══"
|
|
159
|
-
[ "$FAIL" -eq 0 ] || exit 1
|