diff --git a/plugin/hooks/scribe_after_write.sh b/plugin/hooks/scribe_after_write.sh index bb82f2e..8f6ac67 100644 --- a/plugin/hooks/scribe_after_write.sh +++ b/plugin/hooks/scribe_after_write.sh @@ -187,6 +187,11 @@ while IFS= read -r rel_path; do printf '%s' "$body" | jq -r '((.note_ids // []) - (.sync_note_ids // []))[]?' 2>/dev/null >> "$idfile" || true printf '%s' "$body" | jq -r '(.sync_note_ids // [])[]?' 2>/dev/null >> "$syncfile" || true printf '%s' "$body" | jq -r '(.derive_keys // [])[]?' 2>/dev/null >> "$derivefile" || true + # Several files in one call may name the same family: keep each + # token once, so the next request's exclude list stays exact. + for f in "$idfile" "$syncfile" "$derivefile"; do + [ -s "$f" ] && { sort -u -o "$f" "$f" 2>/dev/null || true; } + done fi fi fi diff --git a/tests/test_write_path_trigger.py b/tests/test_write_path_trigger.py index 72ac4f4..c71fb15 100644 --- a/tests/test_write_path_trigger.py +++ b/tests/test_write_path_trigger.py @@ -853,14 +853,18 @@ def test_hook_exits_silently_when_unconfigured(): def test_hook_skips_prose_and_data_files(): - """No round-trip for a markdown edit — the server would return nothing anyway.""" - src = HOOK.read_text() - skip = re.search(r"case \"\$file_path\" in\n(.*?)esac", src, re.S) - assert skip, "expected an extension skip list" + """No round-trip for a markdown edit — the server would return nothing + anyway. The list lives in the shared library (#2901) and the hook asks it.""" + lib = (PLUGIN / "hooks" / "scribe_defs.sh").read_text() + skip = re.search(r"scribe_skip_path\(\) \{\n case \"\$1\" in\n(.*?)esac", lib, re.S) + assert skip, "expected an extension skip list in scribe_defs.sh" for ext in ("*.md", "*.json", "*.lock", "*.png"): assert ext in skip.group(1) # Config formats are deliberately NOT skipped — a workflow file is reusable. assert "*.yml" not in skip.group(1) + src = HOOK.read_text() + assert 'scribe_skip_path "$file_path" && exit 0' in src + assert '/scribe_defs.sh"' in src # sourced, not copied def test_plugin_version_bumped_with_the_hook(): @@ -1150,7 +1154,9 @@ def test_hook_names_the_shapes_being_written(): that changes a body, not a signature — the definition enclosing the edit, found by walking the target file upward from the edited lines.""" src = HOOK.read_text() - assert "scribe_defs()" in src # one extractor, two consumers + lib = (PLUGIN / "hooks" / "scribe_defs.sh").read_text() + assert "scribe_defs()" in lib # one extractor, shared (#2901) + assert "scribe_defs()" not in src # ...not a second copy here assert ".tool_input.old_string" in src # the Edit's anchor assert "| tac | scribe_defs | head -1" in src # nearest definition above # The ledger feed sends NAMES, never bodies, and stays on the one GET.