@onlooker-community/ecosystem 0.23.1 → 0.25.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 +13 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.github/workflows/autofix.yml +65 -0
- package/.release-please-manifest.json +4 -3
- package/CHANGELOG.md +14 -0
- package/CLAUDE.md +1 -0
- package/package.json +3 -3
- package/plugins/assayer/.claude-plugin/plugin.json +14 -0
- package/plugins/assayer/CHANGELOG.md +10 -0
- package/plugins/assayer/README.md +114 -0
- package/plugins/assayer/config.json +14 -0
- package/plugins/assayer/docs/adr/001-verify-claims-against-transcript-evidence.md +57 -0
- package/plugins/assayer/docs/design.md +72 -0
- package/plugins/assayer/hooks/hooks.json +15 -0
- package/plugins/assayer/scripts/hooks/assayer-stop.sh +249 -0
- package/plugins/assayer/scripts/lib/assayer-config.sh +88 -0
- package/plugins/assayer/scripts/lib/assayer-events.sh +85 -0
- package/plugins/assayer/scripts/lib/assayer-extract.sh +87 -0
- package/plugins/assayer/scripts/lib/assayer-project-key.sh +69 -0
- package/plugins/assayer/scripts/lib/assayer-transcript.sh +99 -0
- package/plugins/assayer/scripts/lib/assayer-ulid.sh +46 -0
- package/plugins/assayer/scripts/lib/assayer-verify.sh +95 -0
- package/plugins/compass/README.md +173 -0
- package/plugins/counsel/README.md +98 -0
- package/plugins/governor/README.md +127 -0
- package/plugins/librarian/.claude-plugin/plugin.json +2 -2
- package/plugins/librarian/CHANGELOG.md +7 -0
- package/plugins/librarian/scripts/lib/librarian-cli.sh +339 -0
- package/plugins/librarian/skills/librarian/SKILL.md +63 -0
- package/plugins/scribe/.claude-plugin/plugin.json +1 -3
- package/plugins/scribe/README.md +118 -0
- package/plugins/warden/README.md +185 -0
- package/release-please-config.json +16 -0
- package/test/bats/assayer-config.bats +60 -0
- package/test/bats/assayer-events.bats +99 -0
- package/test/bats/assayer-extract.bats +76 -0
- package/test/bats/assayer-project-key.bats +58 -0
- package/test/bats/assayer-stop-hook.bats +81 -0
- package/test/bats/assayer-transcript.bats +72 -0
- package/test/bats/assayer-ulid.bats +31 -0
- package/test/bats/assayer-verify.bats +89 -0
- package/test/bats/librarian-cli.bats +305 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# Exercises the Assayer ULID generator.
|
|
4
|
+
|
|
5
|
+
setup() {
|
|
6
|
+
source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
|
|
7
|
+
setup_test_env
|
|
8
|
+
PLUGIN_ROOT="${REPO_ROOT}/plugins/assayer"
|
|
9
|
+
# shellcheck disable=SC1091
|
|
10
|
+
source "${PLUGIN_ROOT}/scripts/lib/assayer-ulid.sh"
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
@test "ulid is 26 chars of Crockford Base32" {
|
|
14
|
+
run assayer_ulid
|
|
15
|
+
[ "$status" -eq 0 ]
|
|
16
|
+
[ "${#output}" -eq 26 ]
|
|
17
|
+
[[ "$output" =~ ^[0-9A-HJKMNP-TV-Z]{26}$ ]]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@test "ulids are unique across calls" {
|
|
21
|
+
a=$(assayer_ulid)
|
|
22
|
+
b=$(assayer_ulid)
|
|
23
|
+
[ "$a" != "$b" ]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@test "ulids are lexicographically time-ordered" {
|
|
27
|
+
a=$(assayer_ulid)
|
|
28
|
+
sleep 0.01
|
|
29
|
+
b=$(assayer_ulid)
|
|
30
|
+
[[ "$a" < "$b" || "$a" == "$b" ]]
|
|
31
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# Exercises the deterministic claim verifier: assayer_classify_claim and
|
|
4
|
+
# assayer_audit_verdict. Pure logic — no LLM, no schema, no filesystem.
|
|
5
|
+
|
|
6
|
+
setup() {
|
|
7
|
+
source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
|
|
8
|
+
setup_test_env
|
|
9
|
+
PLUGIN_ROOT="${REPO_ROOT}/plugins/assayer"
|
|
10
|
+
# shellcheck disable=SC1091
|
|
11
|
+
source "${PLUGIN_ROOT}/scripts/lib/assayer-verify.sh"
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
CMDS_FAILING_TEST='[{"command":"npm test","is_error":true,"excerpt":"1 failed, 32 passed"},{"command":"git status","is_error":false,"excerpt":""}]'
|
|
15
|
+
|
|
16
|
+
@test "tests_pass claim is contradicted by a failing test command" {
|
|
17
|
+
run assayer_classify_claim '{"text":"tests pass","type":"tests_pass","command_keyword":"test","confidence":0.9}' "$CMDS_FAILING_TEST"
|
|
18
|
+
[ "$status" -eq 0 ]
|
|
19
|
+
[ "$(printf '%s' "$output" | jq -r '.verdict')" = "contradicted" ]
|
|
20
|
+
[ "$(printf '%s' "$output" | jq -r '.evidence_command')" = "npm test" ]
|
|
21
|
+
[ "$(printf '%s' "$output" | jq -r '.excerpt')" = "1 failed, 32 passed" ]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@test "build_succeeds claim is corroborated by a passing build" {
|
|
25
|
+
run assayer_classify_claim '{"text":"build is green","type":"build_succeeds","command_keyword":"build","confidence":0.9}' \
|
|
26
|
+
'[{"command":"npm run build","is_error":false,"excerpt":"done"}]'
|
|
27
|
+
[ "$status" -eq 0 ]
|
|
28
|
+
[ "$(printf '%s' "$output" | jq -r '.verdict')" = "corroborated" ]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@test "claim with no matching command is unverified (no_matching_command)" {
|
|
32
|
+
run assayer_classify_claim '{"text":"lint clean","type":"lint_clean","command_keyword":"lint","confidence":0.9}' \
|
|
33
|
+
'[{"command":"npm test","is_error":false,"excerpt":""}]'
|
|
34
|
+
[ "$status" -eq 0 ]
|
|
35
|
+
[ "$(printf '%s' "$output" | jq -r '.verdict')" = "unverified" ]
|
|
36
|
+
[ "$(printf '%s' "$output" | jq -r '.reason')" = "no_matching_command" ]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@test "generic claim with no keyword is unverified (ambiguous)" {
|
|
40
|
+
run assayer_classify_claim '{"text":"deploy healthy","type":"generic","command_keyword":"","confidence":0.9}' \
|
|
41
|
+
"$CMDS_FAILING_TEST"
|
|
42
|
+
[ "$status" -eq 0 ]
|
|
43
|
+
[ "$(printf '%s' "$output" | jq -r '.verdict')" = "unverified" ]
|
|
44
|
+
[ "$(printf '%s' "$output" | jq -r '.reason')" = "ambiguous" ]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@test "most recent matching command wins (fix-and-rerun)" {
|
|
48
|
+
# Failing test first, passing test after a fix — the later run is authoritative.
|
|
49
|
+
run assayer_classify_claim '{"text":"tests pass now","type":"tests_pass","command_keyword":"test","confidence":0.9}' \
|
|
50
|
+
'[{"command":"npm test","is_error":true,"excerpt":"fail"},{"command":"npm test","is_error":false,"excerpt":"pass"}]'
|
|
51
|
+
[ "$status" -eq 0 ]
|
|
52
|
+
[ "$(printf '%s' "$output" | jq -r '.verdict')" = "corroborated" ]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@test "types_check claim matches a tsc command" {
|
|
56
|
+
run assayer_classify_claim '{"text":"types check out","type":"types_check","command_keyword":"","confidence":0.9}' \
|
|
57
|
+
'[{"command":"npx tsc --noEmit","is_error":true,"excerpt":"TS2345"}]'
|
|
58
|
+
[ "$status" -eq 0 ]
|
|
59
|
+
[ "$(printf '%s' "$output" | jq -r '.verdict')" = "contradicted" ]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@test "command_keyword matches when type is generic" {
|
|
63
|
+
run assayer_classify_claim '{"text":"migration ran","type":"generic","command_keyword":"migrate","confidence":0.9}' \
|
|
64
|
+
'[{"command":"rails db:migrate","is_error":false,"excerpt":"migrated"}]'
|
|
65
|
+
[ "$status" -eq 0 ]
|
|
66
|
+
[ "$(printf '%s' "$output" | jq -r '.verdict')" = "corroborated" ]
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@test "empty claim defaults to unverified ambiguous" {
|
|
70
|
+
run assayer_classify_claim "" "$CMDS_FAILING_TEST"
|
|
71
|
+
[ "$status" -eq 0 ]
|
|
72
|
+
[ "$(printf '%s' "$output" | jq -r '.verdict')" = "unverified" ]
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@test "audit verdict is contradictions_found when any contradiction" {
|
|
76
|
+
[ "$(assayer_audit_verdict 1 3 0)" = "contradictions_found" ]
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@test "audit verdict is clean with corroborations and no contradictions" {
|
|
80
|
+
[ "$(assayer_audit_verdict 0 2 1)" = "clean" ]
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@test "audit verdict is clean when only unverified claims" {
|
|
84
|
+
[ "$(assayer_audit_verdict 0 0 2)" = "clean" ]
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@test "audit verdict is nothing_to_verify when all counts zero" {
|
|
88
|
+
[ "$(assayer_audit_verdict 0 0 0)" = "nothing_to_verify" ]
|
|
89
|
+
}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
#
|
|
3
|
+
# Exercises the librarian-cli surface that the /librarian review skill
|
|
4
|
+
# drives. Each test seeds one or more proposals directly into the
|
|
5
|
+
# librarian storage layer (skipping the SessionEnd scan pipeline) and
|
|
6
|
+
# verifies that list/show/accept/reject/defer/status behave as the
|
|
7
|
+
# skill expects:
|
|
8
|
+
#
|
|
9
|
+
# - list → returns a count + table, sized to pending proposals only
|
|
10
|
+
# - show → renders provenance + body, fails clean on unknown id
|
|
11
|
+
# - accept → writes the typed memory file with provenance frontmatter,
|
|
12
|
+
# appends to MEMORY.md, sets status=accepted, emits
|
|
13
|
+
# librarian.proposal.accepted
|
|
14
|
+
# - reject → writes a body-hash tombstone, sets status=rejected,
|
|
15
|
+
# emits librarian.proposal.rejected AND
|
|
16
|
+
# librarian.tombstone.created
|
|
17
|
+
# - defer → leaves status pending but stamps the proposal so a
|
|
18
|
+
# reviewer can tell it was visited
|
|
19
|
+
# - status → reports pending/accepted/rejected counts
|
|
20
|
+
#
|
|
21
|
+
# The CLI is sourced into the bats shell directly (it's a library, not
|
|
22
|
+
# a hook), so assertions can read both stdout and side-effects on disk.
|
|
23
|
+
|
|
24
|
+
setup() {
|
|
25
|
+
source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
|
|
26
|
+
setup_test_env
|
|
27
|
+
|
|
28
|
+
PLUGIN_ROOT="${REPO_ROOT}/plugins/librarian"
|
|
29
|
+
export CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT"
|
|
30
|
+
export ONLOOKER_ECOSYSTEM_ROOT="$REPO_ROOT"
|
|
31
|
+
|
|
32
|
+
PROJECT_REPO="${BATS_TEST_TMPDIR}/repo"
|
|
33
|
+
mkdir -p "$PROJECT_REPO"
|
|
34
|
+
git -C "$PROJECT_REPO" init -q
|
|
35
|
+
git -C "$PROJECT_REPO" config user.email t@example.com
|
|
36
|
+
git -C "$PROJECT_REPO" config user.name "Test"
|
|
37
|
+
git -C "$PROJECT_REPO" remote add origin git@github.com:org/librarian-cli-test.git
|
|
38
|
+
|
|
39
|
+
# Source the five libs the skill loads, in the same order the SKILL.md
|
|
40
|
+
# walkthrough sources them. librarian-cli depends on storage + emit +
|
|
41
|
+
# project-key (and indirectly config).
|
|
42
|
+
# shellcheck disable=SC1091
|
|
43
|
+
source "${PLUGIN_ROOT}/scripts/lib/librarian-config.sh"
|
|
44
|
+
# shellcheck disable=SC1091
|
|
45
|
+
source "${PLUGIN_ROOT}/scripts/lib/librarian-project-key.sh"
|
|
46
|
+
# shellcheck disable=SC1091
|
|
47
|
+
source "${PLUGIN_ROOT}/scripts/lib/librarian-storage.sh"
|
|
48
|
+
# shellcheck disable=SC1091
|
|
49
|
+
source "${PLUGIN_ROOT}/scripts/lib/librarian-emit.sh"
|
|
50
|
+
# shellcheck disable=SC1091
|
|
51
|
+
source "${PLUGIN_ROOT}/scripts/lib/librarian-cli.sh"
|
|
52
|
+
|
|
53
|
+
PROJECT_KEY=$(librarian_project_key "$PROJECT_REPO")
|
|
54
|
+
[ -n "$PROJECT_KEY" ]
|
|
55
|
+
|
|
56
|
+
LIBRARIAN_DIR="${ONLOOKER_DIR}/librarian/${PROJECT_KEY}"
|
|
57
|
+
ONLOOKER_EVENTS_LOG="${ONLOOKER_DIR}/logs/onlooker-events.jsonl"
|
|
58
|
+
export ONLOOKER_EVENTS_LOG
|
|
59
|
+
|
|
60
|
+
# Where librarian_cli_accept writes typed memory. The CLI derives the
|
|
61
|
+
# encoded path from cwd when CLAUDE_PROJECT_ENCODED is unset; mirror
|
|
62
|
+
# that derivation so tests can assert against the resulting file.
|
|
63
|
+
ABS_CWD=$(cd "$PROJECT_REPO" && pwd -P)
|
|
64
|
+
ENCODED=$(printf '%s' "$ABS_CWD" | sed -E 's#/#-#g')
|
|
65
|
+
MEM_DIR="${TEST_HOME}/.claude/projects/${ENCODED}/memory"
|
|
66
|
+
|
|
67
|
+
librarian_storage_init "$PROJECT_KEY"
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
# Seed a single proposal JSON file directly into the storage layer.
|
|
71
|
+
# Usage: _seed_proposal <id> <type> <title> <filename> <body> [confidence] [status]
|
|
72
|
+
_seed_proposal() {
|
|
73
|
+
local id="$1" type="$2" title="$3" filename="$4" body="$5"
|
|
74
|
+
local confidence="${6:-0.82}" status="${7:-pending}"
|
|
75
|
+
local now json
|
|
76
|
+
now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
77
|
+
json=$(jq -cn \
|
|
78
|
+
--arg id "$id" \
|
|
79
|
+
--arg type "$type" \
|
|
80
|
+
--arg title "$title" \
|
|
81
|
+
--arg filename "$filename" \
|
|
82
|
+
--arg body "$body" \
|
|
83
|
+
--argjson conf "$confidence" \
|
|
84
|
+
--arg status "$status" \
|
|
85
|
+
--arg now "$now" \
|
|
86
|
+
--arg project_key "$PROJECT_KEY" \
|
|
87
|
+
'{
|
|
88
|
+
id: $id,
|
|
89
|
+
project_key: $project_key,
|
|
90
|
+
status: $status,
|
|
91
|
+
conflict_state: "none",
|
|
92
|
+
created_at: $now,
|
|
93
|
+
updated_at: $now,
|
|
94
|
+
source_session_ids: ["sess-seeded"],
|
|
95
|
+
source_artifact_ids: ["01ARTIFACT00000000000000"],
|
|
96
|
+
proposed: {
|
|
97
|
+
type: $type,
|
|
98
|
+
title: $title,
|
|
99
|
+
filename: $filename,
|
|
100
|
+
body: $body,
|
|
101
|
+
classifier_confidence: $conf
|
|
102
|
+
}
|
|
103
|
+
}')
|
|
104
|
+
librarian_storage_write_proposal "$PROJECT_KEY" "$id" "$json" >/dev/null
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
@test "list reports no-pending when the queue is empty" {
|
|
108
|
+
run librarian_cli_list "$PROJECT_REPO"
|
|
109
|
+
[ "$status" -eq 0 ]
|
|
110
|
+
[[ "$output" == *"No pending proposals."* ]]
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@test "list summarizes pending proposals and ignores resolved ones" {
|
|
114
|
+
_seed_proposal "01LISTPENDINGA000000000000" \
|
|
115
|
+
"feedback" "Prefer functional patterns" "feedback_functional.md" \
|
|
116
|
+
"Body A."
|
|
117
|
+
_seed_proposal "01LISTPENDINGB000000000000" \
|
|
118
|
+
"project" "Auth rewrite is compliance" "project_auth_compliance.md" \
|
|
119
|
+
"Body B." "0.91"
|
|
120
|
+
_seed_proposal "01LISTACCEPTED0000000000000" \
|
|
121
|
+
"user" "Already accepted" "user_old.md" \
|
|
122
|
+
"Body C." "0.7" "accepted"
|
|
123
|
+
|
|
124
|
+
run librarian_cli_list "$PROJECT_REPO"
|
|
125
|
+
[ "$status" -eq 0 ]
|
|
126
|
+
# Header counts only pending entries (2 of 3).
|
|
127
|
+
[[ "$output" == *"2 pending proposal"* ]]
|
|
128
|
+
# Both pending titles surface.
|
|
129
|
+
[[ "$output" == *"Prefer functional patterns"* ]]
|
|
130
|
+
[[ "$output" == *"Auth rewrite is compliance"* ]]
|
|
131
|
+
# Pending rows include full IDs that can be used with show/accept/reject/defer.
|
|
132
|
+
[[ "$output" == *"01LISTPENDINGA000000000000"* ]]
|
|
133
|
+
[[ "$output" == *"01LISTPENDINGB000000000000"* ]]
|
|
134
|
+
# Accepted entry does NOT appear in the list output.
|
|
135
|
+
[[ "$output" != *"Already accepted"* ]]
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
@test "show renders provenance + body for an existing proposal" {
|
|
139
|
+
_seed_proposal "01SHOWPROPOSAL0000000000000" \
|
|
140
|
+
"feedback" "Some title" "feedback_x.md" \
|
|
141
|
+
"Body of the memory."
|
|
142
|
+
|
|
143
|
+
run librarian_cli_show "01SHOWPROPOSAL0000000000000" "$PROJECT_REPO"
|
|
144
|
+
[ "$status" -eq 0 ]
|
|
145
|
+
[[ "$output" == *"01SHOWPROPOSAL0000000000000"* ]]
|
|
146
|
+
[[ "$output" == *"type: feedback"* ]]
|
|
147
|
+
[[ "$output" == *"filename: feedback_x.md"* ]]
|
|
148
|
+
[[ "$output" == *"classifier_confidence: 0.82"* ]]
|
|
149
|
+
[[ "$output" == *"Body of the memory."* ]]
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
@test "show fails clean on an unknown proposal id" {
|
|
153
|
+
run librarian_cli_show "01NOSUCHPROPOSAL00000000000" "$PROJECT_REPO"
|
|
154
|
+
[ "$status" -eq 1 ]
|
|
155
|
+
[[ "$output" == *"not found"* ]]
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
@test "accept writes a memory file with provenance frontmatter" {
|
|
159
|
+
_seed_proposal "01ACCEPTPROPOSAL000000000000" \
|
|
160
|
+
"feedback" "Prefer functional patterns" "feedback_functional.md" \
|
|
161
|
+
"User prefers functional patterns.
|
|
162
|
+
|
|
163
|
+
**Why:** Stated explicitly.
|
|
164
|
+
**How to apply:** Default to plain functions."
|
|
165
|
+
|
|
166
|
+
run librarian_cli_accept "01ACCEPTPROPOSAL000000000000" "$PROJECT_REPO"
|
|
167
|
+
[ "$status" -eq 0 ]
|
|
168
|
+
[[ "$output" == *"Accepted."* ]]
|
|
169
|
+
|
|
170
|
+
local out_file="${MEM_DIR}/feedback_functional.md"
|
|
171
|
+
[ -f "$out_file" ]
|
|
172
|
+
|
|
173
|
+
# Frontmatter records who promoted it, when, and from where.
|
|
174
|
+
grep -q "^source: librarian$" "$out_file"
|
|
175
|
+
grep -q "^type: feedback$" "$out_file"
|
|
176
|
+
grep -q "^name: Prefer functional patterns$" "$out_file"
|
|
177
|
+
grep -q "^classifier_confidence: 0.82$" "$out_file"
|
|
178
|
+
grep -q "^promoted_at: " "$out_file"
|
|
179
|
+
# Body survives in full.
|
|
180
|
+
grep -q "Default to plain functions" "$out_file"
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
@test "accept appends to MEMORY.md and creates it if missing" {
|
|
184
|
+
_seed_proposal "01ACCEPTINDEX000000000000000" \
|
|
185
|
+
"project" "Auth rewrite is compliance" "project_auth_compliance.md" \
|
|
186
|
+
"Compliance-driven."
|
|
187
|
+
|
|
188
|
+
[ ! -f "${MEM_DIR}/MEMORY.md" ]
|
|
189
|
+
|
|
190
|
+
run librarian_cli_accept "01ACCEPTINDEX000000000000000" "$PROJECT_REPO"
|
|
191
|
+
[ "$status" -eq 0 ]
|
|
192
|
+
|
|
193
|
+
[ -f "${MEM_DIR}/MEMORY.md" ]
|
|
194
|
+
grep -F -q "(project_auth_compliance.md)" "${MEM_DIR}/MEMORY.md"
|
|
195
|
+
grep -F -q "Auth rewrite is compliance" "${MEM_DIR}/MEMORY.md"
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
@test "accept marks the proposal accepted and emits librarian.proposal.accepted" {
|
|
199
|
+
_seed_proposal "01ACCEPTEVENT000000000000000" \
|
|
200
|
+
"user" "User role" "user_role.md" "Body."
|
|
201
|
+
|
|
202
|
+
run librarian_cli_accept "01ACCEPTEVENT000000000000000" "$PROJECT_REPO"
|
|
203
|
+
[ "$status" -eq 0 ]
|
|
204
|
+
|
|
205
|
+
# Proposal file flipped to accepted.
|
|
206
|
+
local proposal_path="${LIBRARIAN_DIR}/proposals/01ACCEPTEVENT000000000000000.json"
|
|
207
|
+
jq -e '.status == "accepted"' "$proposal_path" >/dev/null
|
|
208
|
+
jq -e '.final_filename == "user_role.md"' "$proposal_path" >/dev/null
|
|
209
|
+
|
|
210
|
+
# Event landed in the canonical events log.
|
|
211
|
+
[ -f "$ONLOOKER_EVENTS_LOG" ]
|
|
212
|
+
grep -q '"event_type":"librarian.proposal.accepted"' "$ONLOOKER_EVENTS_LOG"
|
|
213
|
+
grep '"event_type":"librarian.proposal.accepted"' "$ONLOOKER_EVENTS_LOG" \
|
|
214
|
+
| jq -e '.payload.proposal_id == "01ACCEPTEVENT000000000000000" and .payload.final_filename == "user_role.md"' >/dev/null
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
@test "accept refuses path-traversal filenames and writes nothing" {
|
|
218
|
+
_seed_proposal "01ACCEPTUNSAFE00000000000000" \
|
|
219
|
+
"user" "Bad filename" "../escape.md" "Body."
|
|
220
|
+
|
|
221
|
+
run librarian_cli_accept "01ACCEPTUNSAFE00000000000000" "$PROJECT_REPO"
|
|
222
|
+
[ "$status" -eq 1 ]
|
|
223
|
+
[[ "$output" == *"Failed to write memory file."* ]]
|
|
224
|
+
|
|
225
|
+
# No memory file written anywhere under MEM_DIR or its parent.
|
|
226
|
+
[ ! -f "${MEM_DIR}/../escape.md" ]
|
|
227
|
+
[ ! -f "${MEM_DIR}/escape.md" ]
|
|
228
|
+
|
|
229
|
+
# Proposal stays pending so a reviewer can resolve it manually.
|
|
230
|
+
local proposal_path="${LIBRARIAN_DIR}/proposals/01ACCEPTUNSAFE00000000000000.json"
|
|
231
|
+
jq -e '.status == "pending"' "$proposal_path" >/dev/null
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
@test "reject writes a tombstone and marks the proposal rejected" {
|
|
235
|
+
local body="This is the body whose hash anchors the tombstone."
|
|
236
|
+
_seed_proposal "01REJECTPROPOSAL00000000000" \
|
|
237
|
+
"feedback" "Some idea" "feedback_some_idea.md" "$body"
|
|
238
|
+
|
|
239
|
+
run librarian_cli_reject "01REJECTPROPOSAL00000000000" "stale guidance" "$PROJECT_REPO"
|
|
240
|
+
[ "$status" -eq 0 ]
|
|
241
|
+
[[ "$output" == *"Rejected"* ]]
|
|
242
|
+
[[ "$output" == *"stale guidance"* ]]
|
|
243
|
+
|
|
244
|
+
# Proposal status now rejected with the reason captured.
|
|
245
|
+
local proposal_path="${LIBRARIAN_DIR}/proposals/01REJECTPROPOSAL00000000000.json"
|
|
246
|
+
jq -e '.status == "rejected"' "$proposal_path" >/dev/null
|
|
247
|
+
jq -e '.reason == "stale guidance"' "$proposal_path" >/dev/null
|
|
248
|
+
|
|
249
|
+
# Tombstone file present, keyed on the body hash.
|
|
250
|
+
local expected_hash
|
|
251
|
+
expected_hash=$(librarian_body_hash "$body")
|
|
252
|
+
[ -n "$expected_hash" ]
|
|
253
|
+
[ -f "${LIBRARIAN_DIR}/tombstones/${expected_hash}.json" ]
|
|
254
|
+
librarian_storage_has_tombstone "$PROJECT_KEY" "$expected_hash"
|
|
255
|
+
|
|
256
|
+
# Both events fired.
|
|
257
|
+
grep -q '"event_type":"librarian.proposal.rejected"' "$ONLOOKER_EVENTS_LOG"
|
|
258
|
+
grep -q '"event_type":"librarian.tombstone.created"' "$ONLOOKER_EVENTS_LOG"
|
|
259
|
+
grep '"event_type":"librarian.proposal.rejected"' "$ONLOOKER_EVENTS_LOG" \
|
|
260
|
+
| jq -e '.payload.reason == "stale guidance"' >/dev/null
|
|
261
|
+
grep '"event_type":"librarian.tombstone.created"' "$ONLOOKER_EVENTS_LOG" \
|
|
262
|
+
| jq -e --arg h "$expected_hash" '.payload.body_hash == $h' >/dev/null
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
@test "defer stamps the proposal but leaves it pending" {
|
|
266
|
+
_seed_proposal "01DEFERPROPOSAL000000000000" \
|
|
267
|
+
"user" "Maybe later" "user_maybe.md" "Body."
|
|
268
|
+
|
|
269
|
+
run librarian_cli_defer "01DEFERPROPOSAL000000000000" "$PROJECT_REPO"
|
|
270
|
+
[ "$status" -eq 0 ]
|
|
271
|
+
[[ "$output" == *"Deferred"* ]]
|
|
272
|
+
|
|
273
|
+
local proposal_path="${LIBRARIAN_DIR}/proposals/01DEFERPROPOSAL000000000000.json"
|
|
274
|
+
jq -e '.status == "pending"' "$proposal_path" >/dev/null
|
|
275
|
+
jq -e '.deferred == true' "$proposal_path" >/dev/null
|
|
276
|
+
|
|
277
|
+
# Defer should NOT touch the memory store.
|
|
278
|
+
[ ! -d "$MEM_DIR" ] || [ -z "$(ls -A "$MEM_DIR" 2>/dev/null)" ]
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
@test "status reports counts across pending, accepted, and rejected" {
|
|
282
|
+
_seed_proposal "01STATUSPENDING0000000000000" "user" "P" "user_p.md" "p" "0.7" "pending"
|
|
283
|
+
_seed_proposal "01STATUSACCEPTED000000000000" "user" "A" "user_a.md" "a" "0.7" "accepted"
|
|
284
|
+
_seed_proposal "01STATUSREJECTED000000000000" "user" "R" "user_r.md" "r" "0.7" "rejected"
|
|
285
|
+
|
|
286
|
+
run librarian_cli_status "$PROJECT_REPO"
|
|
287
|
+
[ "$status" -eq 0 ]
|
|
288
|
+
[[ "$output" == *"pending: 1"* ]]
|
|
289
|
+
[[ "$output" == *"accepted: 1"* ]]
|
|
290
|
+
[[ "$output" == *"rejected: 1"* ]]
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
@test "librarian_cli dispatch routes to the right subcommand" {
|
|
294
|
+
_seed_proposal "01DISPATCH00000000000000000" \
|
|
295
|
+
"user" "Dispatch test" "user_dispatch.md" "Body."
|
|
296
|
+
|
|
297
|
+
# Unknown action returns exit 2.
|
|
298
|
+
run librarian_cli "explode"
|
|
299
|
+
[ "$status" -eq 2 ]
|
|
300
|
+
|
|
301
|
+
# Known action delegates correctly.
|
|
302
|
+
run librarian_cli "show" "01DISPATCH00000000000000000" "$PROJECT_REPO"
|
|
303
|
+
[ "$status" -eq 0 ]
|
|
304
|
+
[[ "$output" == *"Dispatch test"* ]]
|
|
305
|
+
}
|