Retention was keyed on comment type, so public and post-key-signed comments carried a randomized 30-365d expiry inside their signed digest. Under the ruling neither should ever expire; only the private, non-post-key-signed (open-slot/greeting) channel gets an automatic TTL, since that is the throwaway-identity retirement mechanism. New crates/core/src/comment_ttl.rs is the single authority: CommentClass (Public / PostKeySigned / OpenSlot / Unverifiable) x CommentTtlRule (Never | Window | UnknownParent), with draw_expiry (writer) and ttl_ok (holder). expires_at_ms == 0 is the never-expires sentinel, honored by the sweep, the ingest gate, and the store_comment upsert. - Post.comment_ttl: Option<CommentTtlPolicy> — a GENERIC per-post policy, so an author-set TTL is a future config surface, not a redesign. Registry posts declare a flat 30d policy that binds EVERY comment on them (registrations, duplicate reports, anything else), replacing the registration-only rule. - OpenSlotDecl.max_comments: author-declarable cap on private PK-unsigned comments, enforced holder-side (clamped to the holder default), replacing the hardcoded per-bio greeting cap. Refusal remains "declare no slot". Node::set_greetings_max + `greetings-max` CLI command to write it. - Holder enforcement rejects TTLs contradicting the parent's policy in both directions; a comment naming a different post than its envelope is rejected. - UnknownParent rule: bounded TTLs accepted from unheld parents (self-heal), never-expires refused — permanence is not granted on unseen evidence. Also fixed while here: five Post-reconstructing queries silently dropped comment_ttl AND the pre-existing fof_gating (shipped in v0.8.0-alpha), so any gated or policy-carrying post failed BLAKE3 verification on sync/export and was discarded with no diagnostic. All hydration now goes through one POST_COLUMNS/post_from_row path; export/import round-trips the policy. Registry frozen bytes regenerated for the policy field; REGISTRY_POST_ID is now 10a1be3383efb2977607fe45c4a7b3f1b5e626e81d0ac1af9c0f3d7eb9864d32. design.html section 21 rewritten to the corrected taxonomy. 250 core tests (was 228); a3 integration 12/12 (new step 6 asserts registry comments hold exactly 30d while greetings randomize); c_topology 33/33. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LGiPD2cF75mnvneSCjdDC5
180 lines
7.4 KiB
Bash
Executable file
180 lines
7.4 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# v0.8 A3 integration test — registry + greetings across 3 local nodes.
|
|
#
|
|
# Spec: A3-SPEC §6 multi-node scenario. Run from the repo root after
|
|
# `cargo build -p itsgoin-cli`. Uses FIFOs to drive the interactive REPL.
|
|
#
|
|
# 1. All nodes self-materialize the same registry post ID; node1 also
|
|
# runs the --publish-registry genesis one-shot (debug gate override).
|
|
# 2. node2 registers "Alice rust p2p" → node3 `search rust` finds her.
|
|
# node2 `unregister` → node3 re-search finds nothing.
|
|
# 3. node2's bio has a greeting slot; node3 greets it; node2 sees the
|
|
# unsealed text + real sender persona; node2 replies; node3 sees the
|
|
# reply. node1 (holder) stores only ciphertext.
|
|
# 5. Newest-wins: node2 re-registers; node3 sees exactly one entry.
|
|
# 6. Retention taxonomy (per-post policy x signing class): every comment
|
|
# on the registry post carries the post's FLAT 30d policy — including
|
|
# on holders that never registered — while greetings on a bio's open
|
|
# slot carry the randomized 30-365d stranger TTL. Asserted against
|
|
# each node's DB, so it also covers the debug ITSGOIN_TEST_TTL_SECS
|
|
# hook staying scoped to randomized windows (a flat policy must keep
|
|
# its exact 30 days, or the registry auto-renew loop never settles).
|
|
#
|
|
# Exit code 0 = all checks passed. Logs: /tmp/itsgoin-cli{1,2,3}.log
|
|
set -u
|
|
cd "$(dirname "$0")/.."
|
|
BIN=target/debug/itsgoin
|
|
[ -x "$BIN" ] || { echo "build first: cargo build -p itsgoin-cli"; exit 2; }
|
|
|
|
PASS=0; FAIL=0
|
|
check() { # check <desc> <cmd...>
|
|
local desc="$1"; shift
|
|
if "$@" >/dev/null 2>&1; then PASS=$((PASS+1)); echo "PASS: $desc";
|
|
else FAIL=$((FAIL+1)); echo "FAIL: $desc"; fi
|
|
}
|
|
strip_ansi() { sed 's/\x1b\[[0-9;]*m//g' "$1" | grep -v "WARN.*netlink\|WARN.*buffer_tool"; }
|
|
# Same noise filter for the tail-window checks below. The netlink/mDNS watcher
|
|
# logs in unpredictable bursts, and a burst landing between a REPL command and
|
|
# its assertion pushes the answer out of a raw `tail -N` window — a false
|
|
# failure with nothing wrong in the node.
|
|
quiet_tail() { grep -av "netlink\|buffer_tool\|iroh_quinn\|swarm_discovery" "$1" | tail -"$2"; }
|
|
export -f quiet_tail
|
|
sq() { sqlite3 "/tmp/itsgoin-test$1/itsgoin.db" "$2"; }
|
|
|
|
cleanup() {
|
|
{ exec 13>&- 14>&- 15>&-; } 2>/dev/null || true
|
|
kill $(pgrep -f 'itsgoin.*itsgoin-test') 2>/dev/null
|
|
rm -f /tmp/itsgoin-cmd{1,2,3}
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
echo "== setup =="
|
|
kill $(pgrep -f 'itsgoin.*itsgoin-test') 2>/dev/null; sleep 1
|
|
for i in 1 2 3; do
|
|
rm -rf /tmp/itsgoin-test$i /tmp/itsgoin-cmd$i /tmp/itsgoin-cli$i.log
|
|
mkdir -p /tmp/itsgoin-test$i
|
|
mkfifo /tmp/itsgoin-cmd$i
|
|
done
|
|
|
|
export ITSGOIN_TEST_ALLOW_REGISTRY_GENESIS=1
|
|
|
|
# Genesis one-shot on node1's data dir (debug-gate override).
|
|
"$BIN" /tmp/itsgoin-test1 --bind 127.0.0.1:18411 --publish-registry > /tmp/itsgoin-genesis.log 2>&1
|
|
check "genesis prints registry post id matching shipped constant" \
|
|
bash -c 'g=$(grep registry_post_id /tmp/itsgoin-genesis.log | awk "{print \$2}");
|
|
c=$(grep shipped_constant /tmp/itsgoin-genesis.log | awk "{print \$2}");
|
|
[ -n "$g" ] && [ "$g" = "$c" ]'
|
|
|
|
# Start the three nodes with FIFO-driven stdin.
|
|
for i in 1 2 3; do
|
|
( exec "$BIN" /tmp/itsgoin-test$i --bind 127.0.0.1:1841$i \
|
|
< /tmp/itsgoin-cmd$i > /tmp/itsgoin-cli$i.log 2>&1 ) &
|
|
done
|
|
# Keep FIFO write ends open for the whole run.
|
|
exec 13>/tmp/itsgoin-cmd1 14>/tmp/itsgoin-cmd2 15>/tmp/itsgoin-cmd3
|
|
sleep 6
|
|
|
|
n1_id=$(strip_ansi /tmp/itsgoin-cli1.log | grep -m1 "Node ID:" | awk '{print $3}')
|
|
n2_id=$(strip_ansi /tmp/itsgoin-cli2.log | grep -m1 "Node ID:" | awk '{print $3}')
|
|
echo "node1=$n1_id node2=$n2_id"
|
|
check "all nodes self-materialized the registry post" \
|
|
bash -c 'for i in 1 2 3; do sqlite3 /tmp/itsgoin-test$i/itsgoin.db \
|
|
"SELECT count(*) FROM posts WHERE hex(id) = upper(\"'"$(grep shipped_constant /tmp/itsgoin-genesis.log | awk '{print $2}')"'\")" \
|
|
| grep -q 1 || exit 1; done'
|
|
|
|
echo "== mesh: 2,3 -> 1; 3 -> 2 =="
|
|
echo "connect $n1_id@127.0.0.1:18411" >&14
|
|
echo "connect $n1_id@127.0.0.1:18411" >&15
|
|
sleep 4
|
|
echo "connect $n2_id@127.0.0.1:18412" >&15
|
|
sleep 4
|
|
|
|
echo "== step 2: register / search / unregister =="
|
|
echo "name Alice" >&14
|
|
sleep 3
|
|
echo "register Alice rust p2p" >&14
|
|
sleep 3
|
|
echo "search rust" >&15
|
|
sleep 8
|
|
check "node3 search finds Alice" grep -q "Alice" /tmp/itsgoin-cli3.log
|
|
|
|
echo "unregister" >&14
|
|
sleep 3
|
|
echo "search rust" >&15
|
|
sleep 8
|
|
check "node3 re-search finds nothing after signed delete" \
|
|
bash -c 'quiet_tail /tmp/itsgoin-cli3.log 20 | grep -q "no registry matches"'
|
|
|
|
echo "== step 3: greeting roundtrip =="
|
|
# node2's bio post id (latest Profile post) from its DB.
|
|
bio2=$(sq 2 'SELECT lower(hex(id)) FROM posts WHERE visibility_intent = '"'"'"Profile"'"'"' ORDER BY timestamp_ms DESC LIMIT 1')
|
|
check "node2 has a bio post with a greeting slot" \
|
|
bash -c '[ -n "'"$bio2"'" ] && sqlite3 /tmp/itsgoin-test2/itsgoin.db \
|
|
"SELECT fof_gating_json FROM posts WHERE lower(hex(id))=\"'"$bio2"'\"" | grep -q Greeting'
|
|
|
|
echo "name Bob" >&15
|
|
sleep 3
|
|
echo "greet $bio2 hello alice from bob" >&15
|
|
sleep 6
|
|
echo "greetings" >&14
|
|
sleep 4
|
|
check "node2 unseals the greeting text" grep -q "hello alice from bob" /tmp/itsgoin-cli2.log
|
|
|
|
echo "reply 0 hello back bob" >&14
|
|
sleep 6
|
|
echo "greetings" >&15
|
|
sleep 4
|
|
check "node3 unseals the reply via its stored fresh reply key" \
|
|
grep -q "hello back bob" /tmp/itsgoin-cli3.log
|
|
|
|
# Holder-side opacity: node1 never sees plaintext greeting bodies.
|
|
check "node1 stores only ciphertext (no greeting plaintext in db)" \
|
|
bash -c '! sqlite3 /tmp/itsgoin-test1/itsgoin.db \
|
|
"SELECT content FROM comments" | grep -q "hello alice"'
|
|
|
|
echo "== step 5: newest-wins =="
|
|
echo "register Alice rust p2p" >&14
|
|
sleep 2
|
|
echo "register AliceV2 rust mesh" >&14
|
|
sleep 4
|
|
echo "search rust" >&15
|
|
sleep 8
|
|
check "node3 sees exactly one (newest) entry for node2's persona" \
|
|
bash -c 'quiet_tail /tmp/itsgoin-cli3.log 6 | grep -c "rust" | grep -q "^1$" &&
|
|
quiet_tail /tmp/itsgoin-cli3.log 6 | grep -q AliceV2'
|
|
|
|
echo "== step 6: retention taxonomy =="
|
|
# (parent post's comment-TTL policy) x (comment's signing class).
|
|
DAY_MS=86400000
|
|
FLAT30=$((30 * DAY_MS))
|
|
YEAR365=$((365 * DAY_MS))
|
|
reg_hex=$(grep shipped_constant /tmp/itsgoin-genesis.log | awk '{print $2}' | tr 'a-z' 'A-Z')
|
|
bio2_hex=$(echo "$bio2" | tr 'a-z' 'A-Z')
|
|
|
|
reg_total=0; reg_bad=0
|
|
for i in 1 2 3; do
|
|
reg_total=$((reg_total + $(sq $i "SELECT count(*) FROM comments WHERE hex(post_id)='$reg_hex'")))
|
|
reg_bad=$((reg_bad + $(sq $i "SELECT count(*) FROM comments WHERE hex(post_id)='$reg_hex'
|
|
AND (expires_at IS NULL OR expires_at - timestamp_ms != $FLAT30)")))
|
|
done
|
|
check "every registry-post comment carries the post's flat 30d policy" \
|
|
bash -c "[ $reg_total -gt 0 ] && [ $reg_bad -eq 0 ]"
|
|
|
|
greet_total=$(sq 2 "SELECT count(*) FROM comments WHERE hex(post_id)='$bio2_hex'")
|
|
greet_ok=$(sq 2 "SELECT count(*) FROM comments WHERE hex(post_id)='$bio2_hex'
|
|
AND expires_at IS NOT NULL
|
|
AND expires_at - timestamp_ms BETWEEN $FLAT30 AND $YEAR365")
|
|
check "greetings on the bio open slot carry the randomized 30-365d TTL" \
|
|
bash -c "[ $greet_total -gt 0 ] && [ $greet_ok -eq $greet_total ]"
|
|
|
|
over_cap=0
|
|
for i in 1 2 3; do
|
|
over_cap=$((over_cap + $(sq $i "SELECT count(*) FROM comments
|
|
WHERE expires_at IS NOT NULL AND expires_at - timestamp_ms > $((366 * DAY_MS))")))
|
|
done
|
|
check "no stored comment claims a TTL past the 366d holder ceiling" \
|
|
bash -c "[ $over_cap -eq 0 ]"
|
|
|
|
echo
|
|
echo "== results: $PASS passed, $FAIL failed =="
|
|
[ "$FAIL" -eq 0 ]
|