@onlooker-community/ecosystem 0.26.1 → 0.28.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/.claude-plugin/marketplace.json +26 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.release-please-manifest.json +4 -2
- package/CHANGELOG.md +14 -0
- package/CLAUDE.md +4 -0
- package/docs/architecture.md +8 -0
- package/package.json +3 -3
- package/plugins/bursar/.claude-plugin/plugin.json +14 -0
- package/plugins/bursar/CHANGELOG.md +10 -0
- package/plugins/bursar/README.md +100 -0
- package/plugins/bursar/config.json +11 -0
- package/plugins/bursar/hooks/hooks.json +26 -0
- package/plugins/bursar/scripts/hooks/bursar-session-end.sh +142 -0
- package/plugins/bursar/scripts/hooks/bursar-session-start.sh +130 -0
- package/plugins/bursar/scripts/lib/bursar-config.sh +108 -0
- package/plugins/bursar/scripts/lib/bursar-events.sh +82 -0
- package/plugins/bursar/scripts/lib/bursar-ledger.sh +152 -0
- package/plugins/bursar/scripts/lib/bursar-project-key.sh +85 -0
- package/plugins/bursar/scripts/lib/bursar-ulid.sh +53 -0
- package/plugins/bursar/scripts/lib/portable-lock.sh +59 -0
- package/plugins/lineage/.claude-plugin/plugin.json +14 -0
- package/plugins/lineage/CHANGELOG.md +9 -0
- package/plugins/lineage/README.md +133 -0
- package/plugins/lineage/config.json +11 -0
- package/plugins/lineage/hooks/hooks.json +33 -0
- package/plugins/lineage/scripts/hooks/lineage-post-tool-use.sh +132 -0
- package/plugins/lineage/scripts/lib/lineage-config.sh +100 -0
- package/plugins/lineage/scripts/lib/lineage-events.sh +81 -0
- package/plugins/lineage/scripts/lib/lineage-project-key.sh +85 -0
- package/plugins/lineage/scripts/lib/lineage-query.sh +88 -0
- package/plugins/lineage/scripts/lib/lineage-record.sh +132 -0
- package/plugins/lineage/scripts/lib/lineage-redact.sh +51 -0
- package/plugins/lineage/scripts/lib/lineage-ulid.sh +53 -0
- package/plugins/lineage/scripts/lib/portable-lock.sh +59 -0
- package/plugins/lineage/skills/lineage/SKILL.md +165 -0
- package/release-please-config.json +32 -0
- package/test/bats/bursar-config.bats +79 -0
- package/test/bats/bursar-events.bats +73 -0
- package/test/bats/bursar-ledger.bats +116 -0
- package/test/bats/bursar-project-key.bats +51 -0
- package/test/bats/bursar-session-end.bats +131 -0
- package/test/bats/bursar-session-start.bats +126 -0
- package/test/bats/bursar-ulid.bats +28 -0
- package/test/bats/lineage-config.bats +73 -0
- package/test/bats/lineage-events.bats +81 -0
- package/test/bats/lineage-post-tool-use.bats +115 -0
- package/test/bats/lineage-project-key.bats +51 -0
- package/test/bats/lineage-query.bats +85 -0
- package/test/bats/lineage-record.bats +79 -0
- package/test/bats/lineage-redact.bats +63 -0
- package/test/bats/lineage-ulid.bats +28 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# Exercises the PostToolUse hook end-to-end against an isolated $ONLOOKER_DIR.
|
|
4
|
+
|
|
5
|
+
setup() {
|
|
6
|
+
source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
|
|
7
|
+
setup_test_env
|
|
8
|
+
|
|
9
|
+
PLUGIN_ROOT="${REPO_ROOT}/plugins/lineage"
|
|
10
|
+
HOOK="${PLUGIN_ROOT}/scripts/hooks/lineage-post-tool-use.sh"
|
|
11
|
+
export _ONLOOKER_EVENT_JS="${REPO_ROOT}/scripts/lib/onlooker-event.mjs"
|
|
12
|
+
export ONLOOKER_EVENTS_LOG="${ONLOOKER_DIR}/logs/onlooker-events.jsonl"
|
|
13
|
+
mkdir -p "$(dirname "$ONLOOKER_EVENTS_LOG")" "${ONLOOKER_DIR}/session-trackers"
|
|
14
|
+
|
|
15
|
+
# shellcheck disable=SC1091
|
|
16
|
+
source "${PLUGIN_ROOT}/scripts/lib/lineage-project-key.sh"
|
|
17
|
+
|
|
18
|
+
REPO="${BATS_TEST_TMPDIR}/proj"
|
|
19
|
+
mkdir -p "$REPO"
|
|
20
|
+
git init -q "$REPO" 2>/dev/null
|
|
21
|
+
git -C "$REPO" remote add origin https://example.com/onlooker/lineage-test.git 2>/dev/null
|
|
22
|
+
KEY=$(lineage_project_key "$REPO")
|
|
23
|
+
SID="bats-lin-001"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
_enable() {
|
|
27
|
+
mkdir -p "${HOME}/.claude"
|
|
28
|
+
printf '%s\n' '{"lineage":{"enabled":true}}' > "${HOME}/.claude/settings.json"
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
_ledger() { printf '%s/lineage/%s/changes.jsonl' "$ONLOOKER_DIR" "$KEY"; }
|
|
32
|
+
|
|
33
|
+
# _run <tool> <file_path> <tool_input_json>
|
|
34
|
+
_run() {
|
|
35
|
+
local tool="$1" file="$2" ti="$3"
|
|
36
|
+
jq -nc --arg sid "$SID" --arg cwd "$REPO" --arg tool "$tool" --argjson ti "$ti" \
|
|
37
|
+
'{session_id:$sid, cwd:$cwd, tool_name:$tool, tool_use_id:"toolu_x", transcript_path:"", tool_input:$ti}' \
|
|
38
|
+
> "${BATS_TEST_TMPDIR}/in.json"
|
|
39
|
+
run bash "$HOOK" < "${BATS_TEST_TMPDIR}/in.json"
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@test "records an Edit change into the project ledger" {
|
|
43
|
+
_enable
|
|
44
|
+
_run Edit "${REPO}/foo.py" "$(jq -nc --arg f "${REPO}/foo.py" '{file_path:$f, old_string:"x = 1", new_string:"x = 2"}')"
|
|
45
|
+
[ "$status" -eq 0 ]
|
|
46
|
+
[ -f "$(_ledger)" ]
|
|
47
|
+
[ "$(jq -rs '.[0].file_path' "$(_ledger)")" = "${REPO}/foo.py" ]
|
|
48
|
+
[ "$(jq -rs '.[0].tool' "$(_ledger)")" = "Edit" ]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@test "records a single-file MultiEdit (top-level file_path)" {
|
|
52
|
+
_enable
|
|
53
|
+
_run MultiEdit "${REPO}/foo.py" "$(jq -nc --arg f "${REPO}/foo.py" \
|
|
54
|
+
'{file_path:$f, edits:[{old_string:"a", new_string:"a1"},{old_string:"b", new_string:"b1"}]}')"
|
|
55
|
+
[ "$status" -eq 0 ]
|
|
56
|
+
[ -f "$(_ledger)" ]
|
|
57
|
+
[ "$(jq -rs '.[0].file_path' "$(_ledger)")" = "${REPO}/foo.py" ]
|
|
58
|
+
[ "$(jq -rs '.[0].tool' "$(_ledger)")" = "MultiEdit" ]
|
|
59
|
+
[ "$(jq -rs '.[0].operation' "$(_ledger)")" = "multi_edit" ]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@test "skips a MultiEdit whose edits span multiple distinct files" {
|
|
63
|
+
_enable
|
|
64
|
+
# Hypothetical per-edit-file_path shape spanning two files — skip to avoid misattribution.
|
|
65
|
+
local ti
|
|
66
|
+
ti=$(jq -nc --arg a "${REPO}/a.py" --arg b "${REPO}/b.py" \
|
|
67
|
+
'{edits:[{file_path:$a, old_string:"x", new_string:"x1"},{file_path:$b, old_string:"y", new_string:"y1"}]}')
|
|
68
|
+
_run MultiEdit "${REPO}/a.py" "$ti"
|
|
69
|
+
[ "$status" -eq 0 ]
|
|
70
|
+
[ ! -f "$(_ledger)" ]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@test "records the turn number from the session tracker" {
|
|
74
|
+
_enable
|
|
75
|
+
printf '%s' '{"turn_number":7}' > "${ONLOOKER_DIR}/session-trackers/${SID}"
|
|
76
|
+
_run Write "${REPO}/bar.py" "$(jq -nc --arg f "${REPO}/bar.py" '{file_path:$f, content:"print(1)"}')"
|
|
77
|
+
[ "$(jq -rs '.[0].turn' "$(_ledger)")" = "7" ]
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@test "writes nothing when lineage is disabled" {
|
|
81
|
+
_run Edit "${REPO}/foo.py" "$(jq -nc --arg f "${REPO}/foo.py" '{file_path:$f, old_string:"a", new_string:"b"}')"
|
|
82
|
+
[ "$status" -eq 0 ]
|
|
83
|
+
[ ! -d "${ONLOOKER_DIR}/lineage" ]
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@test "skips paths matching ignore_globs" {
|
|
87
|
+
_enable
|
|
88
|
+
mkdir -p "${REPO}/node_modules"
|
|
89
|
+
_run Write "${REPO}/node_modules/x.js" "$(jq -nc --arg f "${REPO}/node_modules/x.js" '{file_path:$f, content:"y"}')"
|
|
90
|
+
[ "$status" -eq 0 ]
|
|
91
|
+
[ ! -f "$(_ledger)" ]
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@test "skips files outside the repo" {
|
|
95
|
+
_enable
|
|
96
|
+
mkdir -p "${BATS_TEST_TMPDIR}/outside"
|
|
97
|
+
local f="${BATS_TEST_TMPDIR}/outside/x.js"
|
|
98
|
+
_run Write "$f" "$(jq -nc --arg f "$f" '{file_path:$f, content:"y"}')"
|
|
99
|
+
[ "$status" -eq 0 ]
|
|
100
|
+
[ ! -f "$(_ledger)" ]
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@test "emits lineage.change.recorded" {
|
|
104
|
+
_enable
|
|
105
|
+
_run Edit "${REPO}/foo.py" "$(jq -nc --arg f "${REPO}/foo.py" '{file_path:$f, old_string:"a", new_string:"b"}')"
|
|
106
|
+
run grep -c '"event_type":"lineage.change.recorded"' "$ONLOOKER_EVENTS_LOG"
|
|
107
|
+
[ "$output" -ge 1 ]
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
@test "a distinct subagent session_id is recorded as-is" {
|
|
111
|
+
_enable
|
|
112
|
+
SID="bats-subagent-999"
|
|
113
|
+
_run Edit "${REPO}/foo.py" "$(jq -nc --arg f "${REPO}/foo.py" '{file_path:$f, old_string:"a", new_string:"b"}')"
|
|
114
|
+
[ "$(jq -rs '.[0].session_id' "$(_ledger)")" = "bats-subagent-999" ]
|
|
115
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
setup() {
|
|
4
|
+
source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
|
|
5
|
+
setup_test_env
|
|
6
|
+
|
|
7
|
+
PLUGIN_ROOT="${REPO_ROOT}/plugins/lineage"
|
|
8
|
+
# shellcheck disable=SC1091
|
|
9
|
+
source "${PLUGIN_ROOT}/scripts/lib/lineage-project-key.sh"
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
_mk_repo_with_remote() {
|
|
13
|
+
local dir="$1" url="$2"
|
|
14
|
+
mkdir -p "$dir"
|
|
15
|
+
git init -q "$dir" 2>/dev/null
|
|
16
|
+
git -C "$dir" remote add origin "$url" 2>/dev/null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@test "key is a 12-char hex string for a repo with a remote" {
|
|
20
|
+
local repo="${BATS_TEST_TMPDIR}/repo-a"
|
|
21
|
+
_mk_repo_with_remote "$repo" "https://example.com/onlooker/a.git"
|
|
22
|
+
run lineage_project_key "$repo"
|
|
23
|
+
[ "$status" -eq 0 ]
|
|
24
|
+
[ "${#output}" -eq 12 ]
|
|
25
|
+
[[ "$output" =~ ^[0-9a-f]{12}$ ]]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@test "same cwd yields a stable key" {
|
|
29
|
+
local repo="${BATS_TEST_TMPDIR}/repo-b"
|
|
30
|
+
_mk_repo_with_remote "$repo" "https://example.com/onlooker/b.git"
|
|
31
|
+
local a b
|
|
32
|
+
a=$(lineage_project_key "$repo")
|
|
33
|
+
b=$(lineage_project_key "$repo")
|
|
34
|
+
[ -n "$a" ]
|
|
35
|
+
[ "$a" = "$b" ]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@test "different remotes yield different keys" {
|
|
39
|
+
local r1="${BATS_TEST_TMPDIR}/repo-c" r2="${BATS_TEST_TMPDIR}/repo-d"
|
|
40
|
+
_mk_repo_with_remote "$r1" "https://example.com/onlooker/c.git"
|
|
41
|
+
_mk_repo_with_remote "$r2" "https://example.com/onlooker/d.git"
|
|
42
|
+
[ "$(lineage_project_key "$r1")" != "$(lineage_project_key "$r2")" ]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@test "empty key for a non-git directory" {
|
|
46
|
+
local plain="${BATS_TEST_TMPDIR}/not-a-repo"
|
|
47
|
+
mkdir -p "$plain"
|
|
48
|
+
run lineage_project_key "$plain"
|
|
49
|
+
[ "$status" -eq 0 ]
|
|
50
|
+
[ -z "$output" ]
|
|
51
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
setup() {
|
|
4
|
+
source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
|
|
5
|
+
setup_test_env
|
|
6
|
+
PLUGIN_ROOT="${REPO_ROOT}/plugins/lineage"
|
|
7
|
+
# shellcheck disable=SC1091
|
|
8
|
+
source "${PLUGIN_ROOT}/scripts/lib/portable-lock.sh"
|
|
9
|
+
# shellcheck disable=SC1091
|
|
10
|
+
source "${PLUGIN_ROOT}/scripts/lib/lineage-redact.sh"
|
|
11
|
+
# shellcheck disable=SC1091
|
|
12
|
+
source "${PLUGIN_ROOT}/scripts/lib/lineage-record.sh"
|
|
13
|
+
# shellcheck disable=SC1091
|
|
14
|
+
source "${PLUGIN_ROOT}/scripts/lib/lineage-query.sh"
|
|
15
|
+
|
|
16
|
+
KEY="projqueryabc"
|
|
17
|
+
FILE="src/a.py"
|
|
18
|
+
# Two changes to the same file, appended oldest-first.
|
|
19
|
+
local dir
|
|
20
|
+
dir=$(lineage_record_dir "$KEY")
|
|
21
|
+
mkdir -p "$dir"
|
|
22
|
+
{
|
|
23
|
+
jq -nc '{change_id:"C1", ts:"2026-06-12T00:00:01Z", ts_epoch:1, session_id:"s1", turn:2, tool:"Edit", operation:"edit", file_path:"src/a.py", added_snippets:["def foo():"], transcript_path:""}'
|
|
24
|
+
jq -nc '{change_id:"C2", ts:"2026-06-12T00:00:02Z", ts_epoch:2, session_id:"s1", turn:5, tool:"Edit", operation:"edit", file_path:"src/a.py", added_snippets:[" return 42"], transcript_path:""}'
|
|
25
|
+
} > "${dir}/changes.jsonl"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@test "changes_for_file returns records newest-first" {
|
|
29
|
+
run lineage_changes_for_file "$KEY" "$FILE"
|
|
30
|
+
[ "$status" -eq 0 ]
|
|
31
|
+
local first
|
|
32
|
+
first=$(printf '%s\n' "$output" | head -1)
|
|
33
|
+
[ "$(jq -r '.change_id' <<<"$first")" = "C2" ]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@test "match_line content-anchors a line to the change that introduced it" {
|
|
37
|
+
local rec
|
|
38
|
+
rec=$(lineage_match_line "$KEY" "$FILE" "return 42")
|
|
39
|
+
[ "$(jq -r '.change_id' <<<"$rec")" = "C2" ]
|
|
40
|
+
|
|
41
|
+
rec=$(lineage_match_line "$KEY" "$FILE" "def foo")
|
|
42
|
+
[ "$(jq -r '.change_id' <<<"$rec")" = "C1" ]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@test "match_line returns nothing for content no change introduced" {
|
|
46
|
+
run lineage_match_line "$KEY" "$FILE" "nonexistent content"
|
|
47
|
+
[ "$status" -eq 0 ]
|
|
48
|
+
[ -z "$output" ]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@test "match_line ignores an empty needle" {
|
|
52
|
+
run lineage_match_line "$KEY" "$FILE" " "
|
|
53
|
+
[ -z "$output" ]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@test "resolve_prompt reads historian's chunk for the turn range" {
|
|
57
|
+
local hist="${ONLOOKER_DIR}/historian/${KEY}/sessions"
|
|
58
|
+
mkdir -p "$hist"
|
|
59
|
+
printf '%s\n' '{"session_id":"s1","start_turn_index":1,"end_turn_index":6,"body_redacted":"user: write the foo function\n\nassistant: done"}' \
|
|
60
|
+
> "${hist}/s1.jsonl"
|
|
61
|
+
run lineage_resolve_prompt "$KEY" "s1" "5" "" "historian_then_transcript"
|
|
62
|
+
[ "$status" -eq 0 ]
|
|
63
|
+
[ "$(jq -r '.resolved_via' <<<"$output")" = "historian" ]
|
|
64
|
+
[[ "$(jq -r '.prompt' <<<"$output")" == *"write the foo function"* ]]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@test "resolve_prompt falls back to the transcript when historian has nothing" {
|
|
68
|
+
local tp="${BATS_TEST_TMPDIR}/transcript.jsonl"
|
|
69
|
+
{
|
|
70
|
+
printf '%s\n' '{"role":"user","content":"first prompt"}'
|
|
71
|
+
printf '%s\n' '{"role":"assistant","content":"ok"}'
|
|
72
|
+
printf '%s\n' '{"role":"user","content":"second prompt about bar"}'
|
|
73
|
+
} > "$tp"
|
|
74
|
+
run lineage_resolve_prompt "$KEY" "s2" "2" "$tp" "historian_then_transcript"
|
|
75
|
+
[ "$status" -eq 0 ]
|
|
76
|
+
[ "$(jq -r '.resolved_via' <<<"$output")" = "transcript" ]
|
|
77
|
+
[[ "$(jq -r '.prompt' <<<"$output")" == *"second prompt about bar"* ]]
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@test "resolve_prompt reports none when neither source is available" {
|
|
81
|
+
run lineage_resolve_prompt "$KEY" "s3" "1" "" "historian_then_transcript"
|
|
82
|
+
[ "$status" -eq 0 ]
|
|
83
|
+
[ "$(jq -r '.resolved_via' <<<"$output")" = "none" ]
|
|
84
|
+
[ "$(jq -r '.prompt' <<<"$output")" = "" ]
|
|
85
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
setup() {
|
|
4
|
+
source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
|
|
5
|
+
setup_test_env
|
|
6
|
+
PLUGIN_ROOT="${REPO_ROOT}/plugins/lineage"
|
|
7
|
+
# shellcheck disable=SC1091
|
|
8
|
+
source "${PLUGIN_ROOT}/scripts/lib/portable-lock.sh"
|
|
9
|
+
# shellcheck disable=SC1091
|
|
10
|
+
source "${PLUGIN_ROOT}/scripts/lib/lineage-redact.sh"
|
|
11
|
+
# shellcheck disable=SC1091
|
|
12
|
+
source "${PLUGIN_ROOT}/scripts/lib/lineage-record.sh"
|
|
13
|
+
KEY="proj0123abcd"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@test "Edit record captures operation, line counts, snippet, and digest" {
|
|
17
|
+
local ti rec
|
|
18
|
+
ti=$(jq -nc '{file_path:"src/a.py", old_string:"a\nb", new_string:"a\nb\nc"}')
|
|
19
|
+
rec=$(lineage_build_record "CID1" "2026-06-12T00:00:00Z" 1781280000 "sess1" "5" "Edit" "src/a.py" "$ti" 4000 true "")
|
|
20
|
+
[ "$(jq -r '.tool' <<<"$rec")" = "Edit" ]
|
|
21
|
+
[ "$(jq -r '.operation' <<<"$rec")" = "edit" ]
|
|
22
|
+
[ "$(jq -r '.lines_added' <<<"$rec")" = "3" ]
|
|
23
|
+
[ "$(jq -r '.lines_removed' <<<"$rec")" = "2" ]
|
|
24
|
+
[ "$(jq -r '.turn' <<<"$rec")" = "5" ]
|
|
25
|
+
[ "$(jq -r '.added_snippets[0]' <<<"$rec")" = "a
|
|
26
|
+
b
|
|
27
|
+
c" ]
|
|
28
|
+
[ -n "$(jq -r '.content_sha256' <<<"$rec")" ]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@test "Write record is operation=create with no removed lines" {
|
|
32
|
+
local ti rec
|
|
33
|
+
ti=$(jq -nc '{file_path:"README.md", content:"line1\nline2"}')
|
|
34
|
+
rec=$(lineage_build_record "CID2" "2026-06-12T00:00:00Z" 1781280000 "sess1" "" "Write" "README.md" "$ti" 4000 true "")
|
|
35
|
+
[ "$(jq -r '.tool' <<<"$rec")" = "Write" ]
|
|
36
|
+
[ "$(jq -r '.operation' <<<"$rec")" = "create" ]
|
|
37
|
+
[ "$(jq -r '.lines_added' <<<"$rec")" = "2" ]
|
|
38
|
+
[ "$(jq -r '.lines_removed' <<<"$rec")" = "0" ]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@test "turn is omitted when empty" {
|
|
42
|
+
local ti rec
|
|
43
|
+
ti=$(jq -nc '{file_path:"README.md", content:"x"}')
|
|
44
|
+
rec=$(lineage_build_record "CID3" "2026-06-12T00:00:00Z" 1781280000 "sess1" "" "Write" "README.md" "$ti" 4000 true "")
|
|
45
|
+
[ "$(jq -r 'has("turn")' <<<"$rec")" = "false" ]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@test "MultiEdit record reports edit_count and joins added content" {
|
|
49
|
+
local ti rec
|
|
50
|
+
ti=$(jq -nc '{file_path:"src/b.py", edits:[{old_string:"x", new_string:"x1"},{old_string:"y", new_string:"y1\ny2"}]}')
|
|
51
|
+
rec=$(lineage_build_record "CID4" "2026-06-12T00:00:00Z" 1781280000 "sess1" "2" "MultiEdit" "src/b.py" "$ti" 4000 true "")
|
|
52
|
+
[ "$(jq -r '.tool' <<<"$rec")" = "MultiEdit" ]
|
|
53
|
+
[ "$(jq -r '.operation' <<<"$rec")" = "multi_edit" ]
|
|
54
|
+
[ "$(jq -r '.edit_count' <<<"$rec")" = "2" ]
|
|
55
|
+
[ "$(jq -r '.lines_added' <<<"$rec")" = "3" ]
|
|
56
|
+
[[ "$(jq -r '.added_snippets[0]' <<<"$rec")" == *"y2"* ]]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@test "append writes one line per record (append-only)" {
|
|
60
|
+
local ti r1 r2
|
|
61
|
+
ti=$(jq -nc '{file_path:"src/a.py", content:"a"}')
|
|
62
|
+
r1=$(lineage_build_record "CID-A" "2026-06-12T00:00:00Z" 1781280000 "s" "1" "Write" "src/a.py" "$ti" 4000 true "")
|
|
63
|
+
r2=$(lineage_build_record "CID-B" "2026-06-12T00:00:01Z" 1781280001 "s" "2" "Write" "src/a.py" "$ti" 4000 true "")
|
|
64
|
+
lineage_append "$KEY" "$r1"
|
|
65
|
+
lineage_append "$KEY" "$r2"
|
|
66
|
+
local path
|
|
67
|
+
path=$(lineage_record_path "$KEY")
|
|
68
|
+
[ -f "$path" ]
|
|
69
|
+
[ "$(wc -l < "$path")" -eq 2 ]
|
|
70
|
+
[ "$(jq -rs '.[0].change_id' "$path")" = "CID-A" ]
|
|
71
|
+
[ "$(jq -rs '.[1].change_id' "$path")" = "CID-B" ]
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@test "redaction disabled keeps the raw snippet" {
|
|
75
|
+
local ti rec
|
|
76
|
+
ti=$(jq -nc '{file_path:"x", content:"plain text body"}')
|
|
77
|
+
rec=$(lineage_build_record "CID5" "2026-06-12T00:00:00Z" 1781280000 "s" "1" "Write" "x" "$ti" 4000 false "")
|
|
78
|
+
[ "$(jq -r '.added_snippets[0]' <<<"$rec")" = "plain text body" ]
|
|
79
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# Secret-shaped strings are assembled at runtime (prefix + filler) so this test
|
|
4
|
+
# file itself contains no literal secret token a scanner would flag.
|
|
5
|
+
|
|
6
|
+
setup() {
|
|
7
|
+
source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
|
|
8
|
+
setup_test_env
|
|
9
|
+
PLUGIN_ROOT="${REPO_ROOT}/plugins/lineage"
|
|
10
|
+
# shellcheck disable=SC1091
|
|
11
|
+
source "${PLUGIN_ROOT}/scripts/lib/lineage-redact.sh"
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
@test "ordinary code passes through unchanged" {
|
|
15
|
+
local out
|
|
16
|
+
out=$(printf '%s' "x = 2 # bumped" | lineage_redact 4000 true)
|
|
17
|
+
[ "$out" = "x = 2 # bumped" ]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@test "an Anthropic-style API key is redacted" {
|
|
21
|
+
local tok out
|
|
22
|
+
tok="sk-ant-$(printf 'a%.0s' $(seq 1 28))"
|
|
23
|
+
out=$(printf '%s' "key=\"${tok}\"" | lineage_redact 4000 true)
|
|
24
|
+
[[ "$out" == *"[REDACTED:secret]"* ]]
|
|
25
|
+
[[ "$out" != *"$tok"* ]]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@test "a GitHub-style token is redacted" {
|
|
29
|
+
local tok out
|
|
30
|
+
tok="ghp_$(printf '0%.0s' $(seq 1 36))"
|
|
31
|
+
out=$(printf '%s' "$tok" | lineage_redact 4000 true)
|
|
32
|
+
[[ "$out" == *"[REDACTED:secret]"* ]]
|
|
33
|
+
[[ "$out" != *"$tok"* ]]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@test "a KEY=value secret keeps the key but redacts the value" {
|
|
37
|
+
local val out
|
|
38
|
+
val=$(printf 'x%.0s' $(seq 1 24))
|
|
39
|
+
out=$(printf '%s' "API_TOKEN=${val}" | lineage_redact 4000 true)
|
|
40
|
+
[[ "$out" == *"API_TOKEN="* ]]
|
|
41
|
+
[[ "$out" == *"[REDACTED:secret]"* ]]
|
|
42
|
+
[[ "$out" != *"$val"* ]]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@test "redaction is skipped when disabled" {
|
|
46
|
+
local tok out
|
|
47
|
+
tok="ghp_$(printf '0%.0s' $(seq 1 36))"
|
|
48
|
+
out=$(printf '%s' "$tok" | lineage_redact 4000 false)
|
|
49
|
+
[ "$out" = "$tok" ]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@test "content longer than the cap is truncated with a marker" {
|
|
53
|
+
local out
|
|
54
|
+
out=$(printf '%s' "abcdefghij" | lineage_redact 4 true)
|
|
55
|
+
[[ "$out" == "abcd"* ]]
|
|
56
|
+
[[ "$out" == *"truncated 6 chars"* ]]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@test "content within the cap is not truncated" {
|
|
60
|
+
local out
|
|
61
|
+
out=$(printf '%s' "abcd" | lineage_redact 4 true)
|
|
62
|
+
[ "$out" = "abcd" ]
|
|
63
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
setup() {
|
|
4
|
+
source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
|
|
5
|
+
setup_test_env
|
|
6
|
+
|
|
7
|
+
PLUGIN_ROOT="${REPO_ROOT}/plugins/lineage"
|
|
8
|
+
# shellcheck disable=SC1091
|
|
9
|
+
source "${PLUGIN_ROOT}/scripts/lib/lineage-ulid.sh"
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
@test "lineage_ulid is 26 characters" {
|
|
13
|
+
run lineage_ulid
|
|
14
|
+
[ "$status" -eq 0 ]
|
|
15
|
+
[ "${#output}" -eq 26 ]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@test "lineage_ulid uses only Crockford base32 (no I, L, O, U)" {
|
|
19
|
+
run lineage_ulid
|
|
20
|
+
[[ "$output" =~ ^[0-9A-HJKMNP-TV-Z]+$ ]]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@test "two ulids differ" {
|
|
24
|
+
local a b
|
|
25
|
+
a=$(lineage_ulid)
|
|
26
|
+
b=$(lineage_ulid)
|
|
27
|
+
[ "$a" != "$b" ]
|
|
28
|
+
}
|