Skip to content

Commit dfc16fa

Browse files
AlexStocksOmX
andcommitted
test(storage): harden exact-ref compatibility matrix
Make every migration retry prove its phase-aware interrupted layout and historical-binary readability before resume. Drive Base-v1 rejection and two-instance Head-v2 pairing negatives through the complete snapshot install API, then verify live and reopened storage against exact manifest identities. Constraint: keep temporary authority, archive mutation, worktree, build, and cleanup targets under realpath-validated Linux temp roots while disabling shared compiler wrappers Rejected: resume-first fault tests, permissive committed phases, current low-level checkpoint fixtures as compatibility evidence, one-instance pairing coverage, and hard-coded cleanup process counts Confidence: high Scope-risk: compatibility coverage adds stricter disposable runner work; production storage and CI workflow semantics are unchanged Directive: Option C Storage Task 7 specification follow-up Tested: five mutation red gates; Linux and PowerShell CLI negatives; TMPDIR worktree and symlink rejection; generated Rust compilation; exact Base/Vector-v1/Head Linux matrix with 11 gates and 30 migration phases in 1009 seconds; cleanup verified three worktrees removed and zero lingering temp-root processes Not-tested: full matrix through the PowerShell wrapper; shellcheck unavailable; CI workflow wiring remains deferred Related: #422 Co-authored-by: OmX <omx@oh-my-codex.dev> Signed-off-by: Xin.Zh <alexstocks@foxmail.com>
1 parent a00520e commit dfc16fa

3 files changed

Lines changed: 998 additions & 47 deletions

File tree

docs/superpowers/plans/2026-08-07-vector-set-storage-recovery.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -448,16 +448,22 @@
448448
- Base 写入 String、Hash、ZSet、TTL 并生成 v1 snapshot。
449449
- Vector-v1 写入 String、Hash、ZSet、TTL、Vector meta/member,并记录每实例 v1 manifest 的 incarnation/generation。
450450
- Head 对两种 source profile 的复制目录逐 phase 注入失败,每次检查目录、journal、CF、manifest 和用户数据。
451+
- 30 个 fault case 在 retry/resume 和 phase 计数前逐项验证 phase-aware live/shadow/backup 布局、Root/Instance digest 与历史 binary 权威副本读回;普通完成态严格为 `Committed`,仅显式关闭窗口后为 `RollbackWindowClosed`
451452
- 受控 rollback 后必须分别使用 Base binary 或 Vector-v1 binary 真实 reopen/read,不使用 Head parser 推断旧格式可读。
453+
- exact Base v1 archive 的 unknown CF、Vector CF、Vector meta 三个变体均通过 Head `install_snapshot` 完整入口在 pause 前拒绝,并验证 target authority 未变。
454+
- exact Head v2 使用两个实例真实 build/install/关闭/reopen/read;交换 0/1 pairing、篡改 instance 1 digest/incarnation、篡改 Root digest 均通过完整 install 入口拒绝。
455+
- runner 拒绝位于任一 Git worktree 内(含 symlink realpath)的 `TMPDIR`,禁用共享 sccache/wrapper,并检查无引用临时根的存活进程。
452456
- cleanup 移除临时 worktree、build 输出和 server 进程,并在退出前检查无遗留。
453457

454458
- [x] **步骤 3:Linux 验收**
455459

456460
```powershell
461+
wsl bash -lc 'cd /mnt/d/test/github/kiwi/.worktrees/wp8-storage-recovery && bash -n scripts/test-vector-storage-compat.sh && python3 tests/compat/vector_storage_fixture.py emit-rust --kind head-storage | rustfmt --edition 2024 --emit stdout >/dev/null'
462+
.\scripts\test-vector-storage-compat.ps1 --help
457463
wsl bash -lc 'cd /mnt/d/test/github/kiwi/.worktrees/wp8-storage-recovery && ./scripts/test-vector-storage-compat.sh --base-ref 688d905fec31b54aec76f36676f55efd8b5cfa17 --vector-v1-ref 733888fc90ad8ef039947e87b08d7500a405954a --head-ref "$(git rev-parse HEAD)"'
458464
```
459465

460-
实际结果:11 个 gate、30 个 migration phase 全部执行并通过;cleanup 移除 3 个临时 worktree、全部 build 输出且无 runner 进程遗留
466+
实际结果:针对 interrupted-state、Base snapshot reopen、完整 install、两实例 pairing、process cleanup 的 5 个变异均红灯;修复后静态/CLI 门禁通过。完整 Linux 矩阵 11 个 gate、30 个 migration phase(Base 16、Vector-v1 14)全部执行并通过,耗时 1009 秒;cleanup 移除 3 个临时 worktree、全部 build 输出,`tracked_processes=0``lingering_temp_processes=0`
461467

462468
## 工作流最终门禁
463469

scripts/test-vector-storage-compat.sh

Lines changed: 131 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,16 @@ die() {
2222
exit 1
2323
}
2424

25+
if (($# == 1)) && [[ $1 == --help || $1 == -h ]]; then
26+
usage
27+
exit 0
28+
fi
29+
for argument in "$@"; do
30+
if [[ $argument == --help || $argument == -h ]]; then
31+
die "--help must be used as the only argument"
32+
fi
33+
done
34+
2535
BASE_REF=""
2636
VECTOR_V1_REF=""
2737
HEAD_REF=""
@@ -31,10 +41,6 @@ HEAD_SEEN=0
3141

3242
while (($# > 0)); do
3343
case "$1" in
34-
--help|-h)
35-
usage
36-
exit 0
37-
;;
3844
--base-ref)
3945
((BASE_SEEN == 0)) || die "duplicate --base-ref"
4046
(($# >= 2)) || die "--base-ref requires a value"
@@ -77,18 +83,16 @@ FIXTURE_TOOL="$REPO_ROOT/tests/compat/vector_storage_fixture.py"
7783
[[ -f $FIXTURE_TOOL ]] || die "fixture tool is missing: $FIXTURE_TOOL"
7884

7985
for tool in awk bash basename c++ cargo cat cc cp date dirname find git grep mkdir mktemp \
80-
python3 realpath rm sed sha256sum stat wc; do
86+
pgrep python3 realpath rm sed sha256sum stat wc; do
8187
command -v "$tool" >/dev/null 2>&1 || die "required Linux tool is missing: $tool"
8288
done
8389

84-
if command -v sccache >/dev/null 2>&1; then
85-
export RUSTC_WRAPPER=sccache
86-
export CC="sccache cc"
87-
export CXX="sccache c++"
88-
printf 'BUILD_CACHE sccache=%s\n' "$(command -v sccache)"
89-
else
90-
printf 'BUILD_CACHE sccache=unavailable\n'
91-
fi
90+
unset RUSTC_WRAPPER RUSTC_WORKSPACE_WRAPPER CARGO_BUILD_RUSTC_WRAPPER
91+
unset SCCACHE_DIR SCCACHE_CACHE_SIZE SCCACHE_ENDPOINT SCCACHE_BUCKET SCCACHE_REGION
92+
unset SCCACHE_S3_USE_SSL SCCACHE_REDIS SCCACHE_MEMCACHED
93+
export CC="$(command -v cc)"
94+
export CXX="$(command -v c++)"
95+
printf 'BUILD_CACHE mode=disabled cc=%s cxx=%s\n' "$CC" "$CXX"
9296

9397
if [[ -d $REPO_ROOT/.git ]]; then
9498
COMMON_GIT_DIR=$(realpath -- "$REPO_ROOT/.git")
@@ -111,6 +115,25 @@ else
111115
fi
112116
[[ -d $COMMON_GIT_DIR ]] || die "common Git directory is missing: $COMMON_GIT_DIR"
113117

118+
path_is_within() {
119+
local candidate=$1
120+
local authority=$2
121+
[[ $candidate == "$authority" || $candidate == "$authority"/* ]]
122+
}
123+
124+
declare -a REGISTERED_WORKTREE_ROOTS=()
125+
while IFS= read -r record; do
126+
[[ $record == worktree\ * ]] || continue
127+
worktree_root=${record#worktree }
128+
if [[ $worktree_root =~ ^[A-Za-z]:[/\\] ]]; then
129+
command -v wslpath >/dev/null 2>&1 || die "wslpath is required for Windows worktree paths"
130+
worktree_root=$(wslpath -u "$worktree_root")
131+
fi
132+
worktree_root=$(realpath -- "$worktree_root")
133+
REGISTERED_WORKTREE_ROOTS+=("$worktree_root")
134+
done < <(git --git-dir="$COMMON_GIT_DIR" worktree list --porcelain)
135+
((${#REGISTERED_WORKTREE_ROOTS[@]} > 0)) || die "Git reported zero registered worktrees"
136+
114137
resolve_exact_commit() {
115138
git --git-dir="$COMMON_GIT_DIR" rev-parse --verify --end-of-options "$1^{commit}"
116139
}
@@ -122,13 +145,25 @@ HEAD_SHA=$(resolve_exact_commit "$HEAD_REF") || die "cannot resolve Head commit:
122145
[[ $VECTOR_V1_SHA == "$EXPECTED_VECTOR_V1_REF" ]] || die "Vector-v1 resolves to $VECTOR_V1_SHA, expected $EXPECTED_VECTOR_V1_REF"
123146
[[ $HEAD_SHA == "${HEAD_REF,,}" ]] || die "Head input must name the exact resolved commit: input=$HEAD_REF resolved=$HEAD_SHA"
124147

125-
TMP_BASE=$(realpath -- "${TMPDIR:-/tmp}")
148+
TMP_CANDIDATE=${TMPDIR:-/tmp}
149+
[[ -d $TMP_CANDIDATE ]] || die "TMPDIR is not an existing directory: $TMP_CANDIDATE"
150+
TMP_BASE=$(realpath -- "$TMP_CANDIDATE")
151+
for worktree_root in "${REGISTERED_WORKTREE_ROOTS[@]}"; do
152+
path_is_within "$TMP_BASE" "$worktree_root" && \
153+
die "TMPDIR must be outside every Git worktree: tmp=$TMP_BASE worktree=$worktree_root"
154+
done
155+
path_is_within "$TMP_BASE" "$REPO_ROOT" && die "TMPDIR must be outside REPO_ROOT: $TMP_BASE"
126156
TEMP_ROOT=$(mktemp -d "$TMP_BASE/kiwi-vector-storage-compat.XXXXXX")
127157
TEMP_ROOT=$(realpath -- "$TEMP_ROOT")
128158
[[ $(dirname -- "$TEMP_ROOT") == "$TMP_BASE" ]] || die "temporary root escaped TMPDIR: $TEMP_ROOT"
129159
[[ $(basename -- "$TEMP_ROOT") == kiwi-vector-storage-compat.* ]] || die "unexpected temporary root basename: $TEMP_ROOT"
160+
for worktree_root in "${REGISTERED_WORKTREE_ROOTS[@]}"; do
161+
path_is_within "$TEMP_ROOT" "$worktree_root" && \
162+
die "temporary root is inside a Git worktree: temp=$TEMP_ROOT worktree=$worktree_root"
163+
done
130164

131165
declare -a WORKTREES=()
166+
declare -a BACKGROUND_PIDS=()
132167
declare -a EXECUTED_GATES=()
133168
declare -A GATE_SET=()
134169
ACTIVE_GATE=""
@@ -153,11 +188,26 @@ safe_remove() {
153188
cleanup() {
154189
local status=$?
155190
local cleanup_failed=0
191+
local -a lingering_temp_pids=()
156192
trap - EXIT INT TERM
157193
set +e
158194
if [[ -n $ACTIVE_GATE ]]; then
159195
printf 'GATE %s FAIL duration=%ss\n' "$ACTIVE_GATE" "$(( $(date +%s) - ACTIVE_GATE_STARTED ))" >&2
160196
fi
197+
for pid in "${BACKGROUND_PIDS[@]}"; do
198+
if kill -0 "$pid" 2>/dev/null; then
199+
printf 'CLEANUP FAIL runner_background_pid_alive=%s\n' "$pid" >&2
200+
cleanup_failed=1
201+
fi
202+
done
203+
while IFS= read -r pid; do
204+
[[ -n $pid && $pid != $$ && $pid != $PPID ]] || continue
205+
lingering_temp_pids+=("$pid")
206+
done < <(pgrep -f -- "$TEMP_ROOT" 2>/dev/null || true)
207+
if ((${#lingering_temp_pids[@]} > 0)); then
208+
printf 'CLEANUP FAIL temp_root_processes=%s\n' "${lingering_temp_pids[*]}" >&2
209+
cleanup_failed=1
210+
fi
161211
for ((index=${#WORKTREES[@]} - 1; index >= 0; index--)); do
162212
worktree=${WORKTREES[$index]}
163213
assert_under_temp "$worktree" || cleanup_failed=1
@@ -184,7 +234,8 @@ cleanup() {
184234
if ((cleanup_failed != 0)); then
185235
status=1
186236
else
187-
printf 'CLEANUP PASS temp_root_removed=true worktrees_removed=%d tracked_processes=0\n' "${#WORKTREES[@]}"
237+
printf 'CLEANUP PASS temp_root_removed=true worktrees_removed=%d tracked_processes=%d lingering_temp_processes=0\n' \
238+
"${#WORKTREES[@]}" "${#BACKGROUND_PIDS[@]}"
188239
fi
189240
exit "$status"
190241
}
@@ -304,6 +355,22 @@ copy_fixture() {
304355
cp -a --reflink=auto -- "$source" "$target"
305356
}
306357

358+
copy_temp_file() {
359+
local source=$1
360+
local target=$2
361+
local target_parent
362+
target_parent=$(dirname -- "$target")
363+
assert_under_temp "$source"
364+
assert_under_temp "$target"
365+
assert_under_temp "$target_parent"
366+
[[ -f $source && ! -L $source ]] || die "temporary copy source must be a regular file: $source"
367+
mkdir -p -- "$target_parent"
368+
[[ ! -e $target ]] || die "temporary copy target already exists: $target"
369+
cp --preserve=mode,timestamps -- "$source" "$target"
370+
[[ $(sha256sum -- "$source" | awk '{print $1}') == $(sha256sum -- "$target" | awk '{print $1}') ]] || \
371+
die "temporary file copy hash mismatch: source=$source target=$target"
372+
}
373+
307374
printf 'PROVENANCE base=%s vector_v1=%s head=%s common_git_dir=%s temp_root=%s\n' \
308375
"$BASE_SHA" "$VECTOR_V1_SHA" "$HEAD_SHA" "$COMMON_GIT_DIR" "$TEMP_ROOT"
309376

@@ -345,13 +412,11 @@ VECTOR_STORAGE_EXE=$(copy_group_executable vector-storage "$VECTOR_WORKTREE" "$V
345412
HEAD_BUILD_LOG=$(build_test_group head "$HEAD_WORKTREE" "$HEAD_TARGET" \
346413
storage/test-fault-injection \
347414
storage:vector_storage_compat_external raft:vector_storage_snapshot_compat_external \
348-
storage:checkpoint_test raft:snapshot_roundtrip_test)
415+
raft:snapshot_roundtrip_test)
349416
HEAD_STORAGE_EXE=$(copy_group_executable head-storage "$HEAD_WORKTREE" "$HEAD_ARTIFACTS" \
350417
"$HEAD_BUILD_LOG" vector_storage_compat_external)
351418
HEAD_SNAPSHOT_EXE=$(copy_group_executable head-snapshot "$HEAD_WORKTREE" "$HEAD_ARTIFACTS" \
352419
"$HEAD_BUILD_LOG" vector_storage_snapshot_compat_external)
353-
HEAD_CHECKPOINT_EXE=$(copy_group_executable head-checkpoint "$HEAD_WORKTREE" "$HEAD_ARTIFACTS" \
354-
"$HEAD_BUILD_LOG" checkpoint_test)
355420
HEAD_ROUNDTRIP_EXE=$(copy_group_executable head-roundtrip "$HEAD_WORKTREE" "$HEAD_ARTIFACTS" \
356421
"$HEAD_BUILD_LOG" snapshot_roundtrip_test)
357422

@@ -404,11 +469,21 @@ for source_profile in base vector; do
404469
fi
405470
for fault in "${faults[@]}"; do
406471
case_root="$TEMP_ROOT/cases/retry-$source_profile-$fault"
472+
authority_root="$TEMP_ROOT/cases/authority-$source_profile-$fault"
407473
copy_fixture "$source_root" "$case_root"
408-
run_exact_test "$HEAD_STORAGE_EXE" migrate_fault_retry_external \
409-
KIWI_COMPAT_ROOT="$case_root" KIWI_COMPAT_PROFILE="$source_profile" KIWI_COMPAT_FAULT="$fault"
410-
safe_remove "$case_root"
474+
run_exact_test "$HEAD_STORAGE_EXE" inject_fault_and_assert_interrupted_external \
475+
KIWI_COMPAT_ROOT="$case_root" KIWI_COMPAT_AUTHORITY_ROOT="$authority_root" \
476+
KIWI_COMPAT_PROFILE="$source_profile" KIWI_COMPAT_FAULT="$fault"
477+
if [[ $source_profile == base ]]; then
478+
run_exact_test "$BASE_STORAGE_EXE" reopen_fixture KIWI_COMPAT_ROOT="$authority_root"
479+
else
480+
run_exact_test "$VECTOR_STORAGE_EXE" reopen_fixture KIWI_COMPAT_ROOT="$authority_root"
481+
fi
411482
((PHASE_COUNT += 1))
483+
run_exact_test "$HEAD_STORAGE_EXE" resume_after_asserted_fault_external \
484+
KIWI_COMPAT_ROOT="$case_root" KIWI_COMPAT_PROFILE="$source_profile"
485+
safe_remove "$case_root"
486+
safe_remove "$authority_root"
412487
done
413488
done
414489
[[ $PHASE_COUNT -eq 30 ]] || die "migration phase execution count mismatch: $PHASE_COUNT"
@@ -462,14 +537,44 @@ run_exact_test "$HEAD_SNAPSHOT_EXE" restore_exact_base_v1_archive_external \
462537
pass_gate base_v1_snapshot_restores_on_head
463538

464539
begin_gate v1_snapshot_with_unknown_or_vector_schema_is_rejected
465-
run_exact_test "$HEAD_CHECKPOINT_EXE" base_v1_snapshot_with_vector_data_cf_fails_before_pause
466-
run_exact_test "$HEAD_CHECKPOINT_EXE" base_v1_snapshot_with_vector_meta_fails_before_pause
467-
run_exact_test "$HEAD_CHECKPOINT_EXE" base_v1_snapshot_with_unknown_cf_fails_before_pause
540+
for mutation in unknown-cf vector-cf vector-meta; do
541+
MUTATED_BASE_ARCHIVE="$TEMP_ROOT/snapshots/base-v1-$mutation.snapshot"
542+
MUTATED_BASE_META="$TEMP_ROOT/snapshots/base-v1-$mutation.meta.json"
543+
MUTATION_ROOT="$TEMP_ROOT/snapshots/base-v1-$mutation-unpack"
544+
MUTATION_TARGET="$TEMP_ROOT/snapshots/base-v1-$mutation-target"
545+
MUTATION_WORK="$TEMP_ROOT/snapshots/base-v1-$mutation-work"
546+
copy_temp_file "$BASE_ARCHIVE" "$MUTATED_BASE_ARCHIVE"
547+
copy_temp_file "$BASE_SNAPSHOT_META" "$MUTATED_BASE_META"
548+
run_exact_test "$HEAD_SNAPSHOT_EXE" reject_mutated_exact_base_v1_archive_external \
549+
KIWI_COMPAT_ARCHIVE="$MUTATED_BASE_ARCHIVE" \
550+
KIWI_COMPAT_SNAPSHOT_META="$MUTATED_BASE_META" \
551+
KIWI_COMPAT_MUTATION="$mutation" KIWI_COMPAT_MUTATION_ROOT="$MUTATION_ROOT" \
552+
KIWI_COMPAT_TARGET="$MUTATION_TARGET" KIWI_COMPAT_SNAPSHOT_WORK="$MUTATION_WORK"
553+
done
468554
pass_gate v1_snapshot_with_unknown_or_vector_schema_is_rejected
469555

470556
begin_gate head_v2_snapshot_reopens_with_exact_manifest_pairing
471-
run_exact_test "$HEAD_CHECKPOINT_EXE" v2_snapshot_requires_exact_root_manifest_digest
472-
run_exact_test "$HEAD_CHECKPOINT_EXE" v2_snapshot_requires_every_instance_manifest_digest_and_incarnation
557+
HEAD_V2_SOURCE="$TEMP_ROOT/snapshots/head-v2-source"
558+
HEAD_V2_TARGET="$TEMP_ROOT/snapshots/head-v2-target"
559+
HEAD_V2_BUILD_WORK="$TEMP_ROOT/snapshots/head-v2-build-work"
560+
HEAD_V2_INSTALL_WORK="$TEMP_ROOT/snapshots/head-v2-install-work"
561+
HEAD_V2_NEGATIVE_ROOT="$TEMP_ROOT/snapshots/head-v2-negative"
562+
HEAD_V2_ARCHIVE="$TEMP_ROOT/snapshots/head-v2.snapshot"
563+
HEAD_V2_META="$TEMP_ROOT/snapshots/head-v2.meta.json"
564+
run_exact_test "$HEAD_SNAPSHOT_EXE" head_v2_two_instance_exact_pairing_external \
565+
KIWI_COMPAT_HEAD_SOURCE="$HEAD_V2_SOURCE" KIWI_COMPAT_TARGET="$HEAD_V2_TARGET" \
566+
KIWI_COMPAT_BUILD_WORK="$HEAD_V2_BUILD_WORK" \
567+
KIWI_COMPAT_SNAPSHOT_WORK="$HEAD_V2_INSTALL_WORK" \
568+
KIWI_COMPAT_NEGATIVE_ROOT="$HEAD_V2_NEGATIVE_ROOT" \
569+
KIWI_COMPAT_HEAD_ARCHIVE="$HEAD_V2_ARCHIVE" \
570+
KIWI_COMPAT_HEAD_SNAPSHOT_META="$HEAD_V2_META"
571+
[[ -s $HEAD_V2_ARCHIVE ]] || die "Head v2 snapshot archive is empty"
572+
[[ -s $HEAD_V2_META ]] || die "Head v2 SnapshotMeta sidecar is empty"
573+
printf 'SNAPSHOT head_v2_sha256=%s bytes=%s meta_sha256=%s meta_bytes=%s instances=2 negative_cases=4\n' \
574+
"$(sha256sum -- "$HEAD_V2_ARCHIVE" | awk '{print $1}')" \
575+
"$(stat -c %s -- "$HEAD_V2_ARCHIVE")" \
576+
"$(sha256sum -- "$HEAD_V2_META" | awk '{print $1}')" \
577+
"$(stat -c %s -- "$HEAD_V2_META")"
473578
run_exact_test "$HEAD_ROUNDTRIP_EXE" cursor_snapshot_roundtrip
474579
pass_gate head_v2_snapshot_reopens_with_exact_manifest_pairing
475580

0 commit comments

Comments
 (0)