diff --git a/Makefile b/Makefile index 10ce257..74192cb 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ # ============================================================================= -# Knowledge Genome - Makefile v. 1.11.1 +# Knowledge Genome - Makefile v. 1.12.0 # Orchestrates the setup and management of the knowledge base. # ============================================================================= diff --git a/deploy/nexus/genome-raw-commit.sh b/deploy/nexus/genome-raw-commit.sh index dae028e..3e1c084 100644 --- a/deploy/nexus/genome-raw-commit.sh +++ b/deploy/nexus/genome-raw-commit.sh @@ -28,7 +28,8 @@ set -a; . "${HOME}/.config/knowledge-genome.env"; set +a vault="${GENOME_VAULTS_ROOT}/${genome}" fid="${genome}-public" authors_map="${GENOME_VAULTS_ROOT}/.authors.json" -clone_url="http://${FORGEJO_USER}@${FORGEJO_HOST}/${FORGEJO_OWNER}/${genome}.git" +# GENOME_PUSH_URL is a test seam: defaults to the Forgejo loopback URL in production. +clone_url="${GENOME_PUSH_URL:-http://${FORGEJO_USER}@${FORGEJO_HOST}/${FORGEJO_OWNER}/${genome}.git}" export GIT_ASKPASS=/usr/local/bin/genome-askpass [[ -d "${vault}/.git" ]] || { printf '{"status":"error","reason":"vault absent","genome":"%s"}\n' "$genome"; exit 1; } @@ -42,8 +43,30 @@ grep -qxF 'raw/.stfolder' "${vault}/.git/info/exclude" 2>/dev/null || echo 'raw/ git add -A -- raw/ git reset -q -- raw/.stignore raw/.stfolder 2>/dev/null || true + +# --- Quiet window: only commit raw files that have STOPPED changing. ---------------- +# While a note is being written (Obsidian autosave -> Syncthing -> here) its mtime stays +# fresh; we leave it UNSTAGED so a half-written note never triggers an ingest. A file is +# committed only after it has been still for RAW_QUIET_MINUTES. Deletions (nothing on disk) +# are stable by definition and pass straight through. Deterministic — no model in the loop. +quiet_min="${RAW_QUIET_MINUTES:-2}" +held=0 +while IFS= read -r f; do + [[ -z "$f" ]] && continue + # Only an existing file can be "hot"; a staged deletion has nothing on disk to settle. + if [[ -e "$f" && -n "$(find "$f" -mmin -"$quiet_min" 2>/dev/null)" ]]; then + git reset -q -- "$f" 2>/dev/null || true + held=$((held+1)) + fi +done < <(git diff --cached --name-only -- raw/) + if git diff --cached --quiet; then - printf '{"status":"noop","genome":"%s"}\n' "$genome" + if [[ "$held" -gt 0 ]]; then + printf '{"status":"noop","reason":"raw still settling","genome":"%s","held":%d,"quiet_minutes":%d}\n' \ + "$genome" "$held" "$quiet_min" + else + printf '{"status":"noop","genome":"%s"}\n' "$genome" + fi exit 0 fi diff --git a/tests/open-pr-rolling.bats b/tests/open-pr-rolling.bats new file mode 100644 index 0000000..3a1d887 --- /dev/null +++ b/tests/open-pr-rolling.bats @@ -0,0 +1,48 @@ +#!/usr/bin/env bats +# open-pr-rolling.bats — a re-ingest of the same slug updates the OPEN PR's branch +# (force-with-lease) instead of failing. Uses the local bare remote from make_fixture_genome. +load helpers +setup_file() { :; } + +@test "open-pr: re-ingest of the same slug rolls the branch forward (force-with-lease)" { + command -v jq >/dev/null 2>&1 || skip "jq not installed" + G="$(make_fixture_genome)"; cd "$G" + export FORGEJO_URL="http://forgejo.local" FORGEJO_USER=u FORGEJO_TOKEN=t DRY_RUN=1 + body="$(mktemp)"; echo body > "$body" + + # first ingest of slug x (v1) + mkdir -p wiki/sources; printf 'v1\n' > wiki/sources/x.md + run bash "$SKILL_SCRIPTS/open-pr.sh" --slug x --title "feat: ingest x" --body-file "$body" --base main + [ "$status" -eq 0 ] + git rev-parse --verify feat/ai-ingest-x + first="$(git rev-parse feat/ai-ingest-x)" + + # simulate clean_start back to base, then an edited re-ingest (v2) + git switch -q main; git reset -q --hard origin/main; git clean -q -fd + printf 'v2-edited\n' > wiki/sources/x.md + run bash "$SKILL_SCRIPTS/open-pr.sh" --slug x --title "feat: ingest x" --body-file "$body" --base main + [ "$status" -eq 0 ] + second="$(git rev-parse feat/ai-ingest-x)" + + # the branch was REBUILT from base (diverged), not appended: second is not a descendant of first + run git merge-base --is-ancestor "$first" "$second" + [ "$status" -ne 0 ] + + # origin received the v2 content (force-with-lease pushed the rebuilt branch) + git fetch -q origin + run git show "origin/feat/ai-ingest-x:wiki/sources/x.md" + [ "$status" -eq 0 ] + [[ "$output" == *"v2-edited"* ]] +} + +@test "open-pr: prune branch override still works after the rolling change" { + command -v jq >/dev/null 2>&1 || skip "jq not installed" + G="$(make_fixture_genome)"; cd "$G" + export FORGEJO_URL="http://forgejo.local" FORGEJO_USER=u FORGEJO_TOKEN=t DRY_RUN=1 + body="$(mktemp)"; echo body > "$body" + mkdir -p wiki/sources; printf 'p\n' > wiki/sources/p.md + run bash "$SKILL_SCRIPTS/open-pr.sh" --branch "chore/prune-orphans-2026-06-30" \ + --title "chore: prune 1 orphaned source(s)" --body-file "$body" --base main + [ "$status" -eq 0 ] + git rev-parse --verify "chore/prune-orphans-2026-06-30" +} diff --git a/tests/raw-commit-quiet.bats b/tests/raw-commit-quiet.bats new file mode 100644 index 0000000..ee6a932 --- /dev/null +++ b/tests/raw-commit-quiet.bats @@ -0,0 +1,75 @@ +#!/usr/bin/env bats +# raw-commit-quiet.bats — quiet-window behaviour of genome-raw-commit.sh. +# No Syncthing (no API key -> default author); pushes to a local bare repo via GENOME_PUSH_URL. +setup() { + SCRIPT="${BATS_TEST_DIRNAME}/../deploy/nexus/genome-raw-commit.sh" + export HOME="${BATS_TEST_TMPDIR}/home"; mkdir -p "$HOME/.config" + root="${BATS_TEST_TMPDIR}/vaults"; mkdir -p "$root" + bare="${BATS_TEST_TMPDIR}/origin.git"; git init -q --bare "$bare" + cat > "$HOME/.config/knowledge-genome.env" </dev/null || mkdir -p "$vault" + ( cd "$vault" + git init -q 2>/dev/null || true + git config user.name n8n-bot; git config user.email n8n-bot@homelab; git config commit.gpgsign false + git checkout -q -b main 2>/dev/null || git switch -q main + mkdir -p raw/articles; echo seed > raw/articles/.gitkeep + git add -A; git commit -q -m init + git remote add origin "$bare" 2>/dev/null || git remote set-url origin "$bare" + git push -q -u origin main ) + export GENOME_PUSH_URL="$bare" # test seam -> push to the local bare repo +} +files() { ( cd "$vault" && git ls-files raw/ ) > "${BATS_TEST_TMPDIR}/f.txt"; } + +@test "raw-commit: holds a freshly-written raw, commits it once it settles" { + command -v jq >/dev/null 2>&1 || skip "jq not installed" + echo "still typing" > "$vault/raw/articles/hot.md" # fresh -> hot + echo "finished" > "$vault/raw/articles/stable.md" + touch -d "10 minutes ago" "$vault/raw/articles/stable.md" # settled + + run bash "$SCRIPT" "$g" + [ "$status" -eq 0 ] + echo "$output" | jq -e '.status=="ok"' + files + grep -q 'raw/articles/stable.md' "${BATS_TEST_TMPDIR}/f.txt" # committed + ! grep -q 'raw/articles/hot.md' "${BATS_TEST_TMPDIR}/f.txt" # held back + + touch -d "10 minutes ago" "$vault/raw/articles/hot.md" # now it settles + run bash "$SCRIPT" "$g" + [ "$status" -eq 0 ] + files + grep -q 'raw/articles/hot.md' "${BATS_TEST_TMPDIR}/f.txt" # now committed +} + +@test "raw-commit: noop with held count while everything is still settling" { + command -v jq >/dev/null 2>&1 || skip "jq not installed" + echo "typing" > "$vault/raw/articles/wip.md" # fresh -> hot + run bash "$SCRIPT" "$g" + [ "$status" -eq 0 ] + echo "$output" | jq -e '.status=="noop"' + echo "$output" | jq -e '.held==1' +} + +@test "raw-commit: a deletion is committed immediately (not subject to the quiet window)" { + command -v jq >/dev/null 2>&1 || skip "jq not installed" + # commit a settled file first + echo done > "$vault/raw/articles/old.md"; touch -d "10 minutes ago" "$vault/raw/articles/old.md" + run bash "$SCRIPT" "$g"; [ "$status" -eq 0 ] + files; grep -q 'raw/articles/old.md' "${BATS_TEST_TMPDIR}/f.txt" + # now delete it -> should commit the removal even though "just changed" + rm "$vault/raw/articles/old.md" + run bash "$SCRIPT" "$g" + [ "$status" -eq 0 ] + echo "$output" | jq -e '.status=="ok"' + files; ! grep -q 'raw/articles/old.md' "${BATS_TEST_TMPDIR}/f.txt" +}