@onlooker-community/ecosystem 0.34.0 → 0.43.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/skills/writing-tests/SKILL.md +27 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.github/workflows/release.yml +14 -14
- package/.release-please-manifest.json +7 -7
- package/AGENTS.md +116 -0
- package/CHANGELOG.md +175 -0
- package/CLAUDE.md +5 -0
- package/docs/lesson-promotion-pipeline.md +6 -6
- package/docs/superpowers/plans/2026-08-10-lesson-confirmation.md +1273 -0
- package/docs/superpowers/plans/2026-08-11-lesson-judging.md +1232 -0
- package/docs/superpowers/plans/2026-08-11-lesson-unconfirm.md +458 -0
- package/docs/superpowers/plans/2026-08-12-author-key.md +567 -0
- package/docs/superpowers/plans/2026-08-13-approved-pool.md +747 -0
- package/docs/superpowers/plans/2026-08-13-librarian-cluster.md +392 -0
- package/docs/superpowers/plans/2026-08-14-criterion-scores-consumers.md +1374 -0
- package/docs/superpowers/plans/2026-08-14-criterion-scores-schema.md +277 -0
- package/docs/superpowers/plans/2026-08-16-cartographer-undocumented-entity.md +741 -0
- package/docs/superpowers/specs/2026-08-10-lesson-confirmation-design.md +213 -0
- package/docs/superpowers/specs/2026-08-11-lesson-judging-design.md +267 -0
- package/docs/superpowers/specs/2026-08-11-lesson-unconfirm-design.md +152 -0
- package/docs/superpowers/specs/2026-08-12-author-key-design.md +317 -0
- package/docs/superpowers/specs/2026-08-13-approved-pool-design.md +232 -0
- package/docs/superpowers/specs/2026-08-14-criterion-scores-design.md +254 -0
- package/docs/superpowers/specs/2026-08-16-cartographer-undocumented-entity-design.md +249 -0
- package/package.json +2 -2
- package/plugins/assayer/.claude-plugin/plugin.json +1 -1
- package/plugins/assayer/CHANGELOG.md +7 -0
- package/plugins/assayer/scripts/lib/assayer-config.sh +6 -0
- package/plugins/cartographer/.claude-plugin/plugin.json +1 -1
- package/plugins/cartographer/CHANGELOG.md +54 -0
- package/plugins/cartographer/README.md +56 -1
- package/plugins/cartographer/config.json +7 -1
- package/plugins/cartographer/scripts/hooks/cartographer-post-write.sh +4 -4
- package/plugins/cartographer/scripts/hooks/cartographer-session-start.sh +7 -4
- package/plugins/cartographer/scripts/lib/cartographer-collect.sh +8 -1
- package/plugins/cartographer/scripts/lib/cartographer-config.sh +26 -0
- package/plugins/cartographer/scripts/lib/cartographer-events.sh +90 -0
- package/plugins/cartographer/scripts/lib/cartographer-filter.sh +105 -0
- package/plugins/cartographer/scripts/lib/cartographer-omission.sh +130 -0
- package/plugins/cartographer/scripts/lib/cartographer-resolve.sh +132 -0
- package/plugins/cartographer/scripts/run-audit.sh +136 -53
- package/plugins/cartographer/skills/cartographer/SKILL.md +33 -9
- package/plugins/curator/.claude-plugin/plugin.json +1 -1
- package/plugins/curator/CHANGELOG.md +7 -0
- package/plugins/curator/scripts/lib/curator-emit.sh +2 -1
- package/plugins/historian/.claude-plugin/plugin.json +1 -1
- package/plugins/historian/CHANGELOG.md +7 -0
- package/plugins/historian/scripts/lib/historian-emit.sh +2 -1
- package/plugins/librarian/.claude-plugin/plugin.json +1 -1
- package/plugins/librarian/CHANGELOG.md +71 -0
- package/plugins/librarian/config.json +30 -1
- package/plugins/librarian/docs/adr/002-agent-definitions-are-shared-assets.md +82 -0
- package/plugins/librarian/scripts/hooks/librarian-session-end.sh +28 -0
- package/plugins/librarian/scripts/hooks/librarian-session-start.sh +42 -12
- package/plugins/librarian/scripts/lib/librarian-author-key.sh +279 -0
- package/plugins/librarian/scripts/lib/librarian-cli.sh +346 -2
- package/plugins/librarian/scripts/lib/librarian-emit.sh +2 -1
- package/plugins/librarian/scripts/lib/librarian-lesson-judge.sh +349 -0
- package/plugins/librarian/scripts/lib/librarian-lesson-promote.sh +225 -0
- package/plugins/librarian/scripts/lib/librarian-lesson-review.sh +312 -0
- package/plugins/librarian/scripts/lib/librarian-lesson-rubric.sh +56 -0
- package/plugins/librarian/scripts/lib/librarian-lesson-storage.sh +82 -13
- package/plugins/librarian/scripts/lib/librarian-lesson-transform.sh +6 -1
- package/plugins/librarian/scripts/lib/librarian-lesson-validate.sh +131 -62
- package/plugins/librarian/skills/librarian/SKILL.md +98 -1
- package/plugins/tribunal/.claude-plugin/plugin.json +1 -1
- package/plugins/tribunal/CHANGELOG.md +63 -0
- package/plugins/tribunal/agents/tribunal-judge-adversarial.md +34 -0
- package/plugins/tribunal/agents/tribunal-judge-security.md +33 -0
- package/plugins/tribunal/agents/tribunal-judge-standard.md +12 -0
- package/plugins/tribunal/config.json +1 -0
- package/plugins/tribunal/scripts/lib/tribunal-aggregate.sh +154 -9
- package/plugins/tribunal/scripts/lib/tribunal-gate.sh +195 -16
- package/plugins/tribunal/skills/tribunal/SKILL.md +5 -3
- package/scripts/lib/prompt-rules.sh +6 -1
- package/test/bats/archivist-inject.bats +1 -1
- package/test/bats/assayer-extract.bats +2 -2
- package/test/bats/bursar-session-start.bats +3 -3
- package/test/bats/cartographer-config.bats +43 -0
- package/test/bats/cartographer-events.bats +267 -0
- package/test/bats/cartographer-filter.bats +169 -0
- package/test/bats/cartographer-lock.bats +3 -3
- package/test/bats/cartographer-omission.bats +202 -0
- package/test/bats/cartographer-resolve.bats +287 -0
- package/test/bats/cartographer-run-audit.bats +325 -0
- package/test/bats/compass-sanitizer.bats +11 -11
- package/test/bats/compass-transcript.bats +2 -2
- package/test/bats/config.bats +15 -15
- package/test/bats/curator-session-start.bats +10 -3
- package/test/bats/emit-payload-default.bats +52 -0
- package/test/bats/governor-ledger.bats +1 -1
- package/test/bats/historian-prompt-submit.bats +1 -1
- package/test/bats/inspector-post-write-hook.bats +4 -4
- package/test/bats/librarian-author-key.bats +477 -0
- package/test/bats/librarian-cli.bats +16 -16
- package/test/bats/librarian-lesson-judge.bats +884 -0
- package/test/bats/librarian-lesson-promote.bats +552 -0
- package/test/bats/librarian-lesson-review.bats +900 -0
- package/test/bats/librarian-lesson-transform.bats +17 -3
- package/test/bats/librarian-session-end.bats +161 -0
- package/test/bats/librarian-session-start.bats +36 -2
- package/test/bats/lineage-config.bats +1 -1
- package/test/bats/lineage-redact.bats +5 -5
- package/test/bats/session-tracker.bats +4 -4
- package/test/bats/tribunal-aggregate.bats +354 -1
- package/test/bats/tribunal-gate.bats +631 -0
- package/test/bats/tribunal-judge-agents.bats +119 -0
- package/test/bats/tribunal-jury.bats +1 -1
- package/test/bats/turn-tracker.bats +1 -1
- package/test/bats/warden-sanitizer.bats +3 -3
- package/test/bats/worktree-tracker.bats +2 -2
- package/test/node/lesson-validate-agreement.test.mjs +35 -6
|
@@ -0,0 +1,900 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
#
|
|
3
|
+
# Lesson confirmation: the human intent filter between the transform and the
|
|
4
|
+
# jury. Assertions use [ ] or `|| return 1` so every one gates under bash 3.2.
|
|
5
|
+
|
|
6
|
+
setup() {
|
|
7
|
+
source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
|
|
8
|
+
setup_test_env
|
|
9
|
+
PLUGIN_ROOT="${REPO_ROOT}/plugins/librarian"
|
|
10
|
+
export CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT"
|
|
11
|
+
# shellcheck disable=SC1091
|
|
12
|
+
source "${PLUGIN_ROOT}/scripts/lib/librarian-lesson-validate.sh"
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
_evidence() {
|
|
16
|
+
printf '%s' '{"artifact_ids":["01KZ45MKAM734ZS7JK24D2DK0R"],"session_ids":["s1"],"project_key":"6a7678979e31","observed_at":"2026-08-03T15:59:48Z","resolution":"Pin vitest to 3.x."}'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
# Usage: _candidate <scope_json>
|
|
20
|
+
_candidate() {
|
|
21
|
+
jq -cn --argjson ev "$(_evidence)" --argjson scope "$1" \
|
|
22
|
+
'{claim: "c", rationale: "r", evidence: $ev,
|
|
23
|
+
applies_to: {stack: ["vite"], scope: $scope, file_patterns: [], task_kinds: []}}'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
_versioned() { printf '%s' '{"kind":"versioned","versions":{"vite":"<6"}}'; }
|
|
27
|
+
_indep() { printf '%s' '{"kind":"version_independent","justification":"git aborts checkout on a dirty tree regardless of version."}'; }
|
|
28
|
+
|
|
29
|
+
@test "confirmed validator accepts a versioned candidate" {
|
|
30
|
+
run librarian_lesson_validate_confirmed "$(_candidate "$(_versioned)")"
|
|
31
|
+
[ "$status" -eq 0 ]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@test "confirmed validator accepts version_independent with a justification" {
|
|
35
|
+
run librarian_lesson_validate_confirmed "$(_candidate "$(_indep)")"
|
|
36
|
+
[ "$status" -eq 0 ]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@test "confirmed validator rejects version_independent with an empty justification" {
|
|
40
|
+
run librarian_lesson_validate_confirmed \
|
|
41
|
+
"$(_candidate '{"kind":"version_independent","justification":""}')"
|
|
42
|
+
[ "$status" -eq 1 ]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@test "confirmed validator rejects version_independent with no justification key" {
|
|
46
|
+
run librarian_lesson_validate_confirmed \
|
|
47
|
+
"$(_candidate '{"kind":"version_independent"}')"
|
|
48
|
+
[ "$status" -eq 1 ]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@test "confirmed validator rejects an unknown scope kind" {
|
|
52
|
+
run librarian_lesson_validate_confirmed \
|
|
53
|
+
"$(_candidate '{"kind":"whenever","justification":"x"}')"
|
|
54
|
+
[ "$status" -eq 1 ]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@test "confirmed validator still enforces the range pattern on versioned scope" {
|
|
58
|
+
run librarian_lesson_validate_confirmed \
|
|
59
|
+
"$(_candidate '{"kind":"versioned","versions":{"vite":"^5.4.21"}}')"
|
|
60
|
+
[ "$status" -eq 1 ]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@test "confirmed validator still enforces provenance" {
|
|
64
|
+
candidate=$(_candidate "$(_versioned)" | jq -c '.evidence.session_ids = [""]')
|
|
65
|
+
run librarian_lesson_validate_confirmed "$candidate"
|
|
66
|
+
[ "$status" -eq 1 ]
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@test "confirmed validator still enforces the versions-subset-of-stack rule" {
|
|
70
|
+
candidate=$(_candidate '{"kind":"versioned","versions":{"vite":"<6","vitest":">=4"}}')
|
|
71
|
+
run librarian_lesson_validate_confirmed "$candidate"
|
|
72
|
+
[ "$status" -eq 1 ]
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@test "the transform validator still refuses version_independent" {
|
|
76
|
+
run librarian_lesson_validate_candidate "$(_candidate "$(_indep)")"
|
|
77
|
+
[ "$status" -eq 1 ]
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
_review_setup() {
|
|
81
|
+
# shellcheck disable=SC1091
|
|
82
|
+
source "${PLUGIN_ROOT}/scripts/lib/librarian-project-key.sh"
|
|
83
|
+
# shellcheck disable=SC1091
|
|
84
|
+
source "${PLUGIN_ROOT}/scripts/lib/librarian-ulid.sh"
|
|
85
|
+
# shellcheck disable=SC1091
|
|
86
|
+
source "${PLUGIN_ROOT}/scripts/lib/librarian-storage.sh"
|
|
87
|
+
# shellcheck disable=SC1091
|
|
88
|
+
source "${PLUGIN_ROOT}/scripts/lib/librarian-lesson-storage.sh"
|
|
89
|
+
# shellcheck disable=SC1091
|
|
90
|
+
source "${PLUGIN_ROOT}/scripts/lib/librarian-lesson-review.sh"
|
|
91
|
+
# shellcheck disable=SC1091
|
|
92
|
+
source "${PLUGIN_ROOT}/scripts/lib/librarian-config.sh"
|
|
93
|
+
# shellcheck disable=SC1091
|
|
94
|
+
source "${PLUGIN_ROOT}/scripts/lib/librarian-lesson-rubric.sh"
|
|
95
|
+
# shellcheck disable=SC1091
|
|
96
|
+
source "${PLUGIN_ROOT}/scripts/lib/librarian-lesson-judge.sh"
|
|
97
|
+
|
|
98
|
+
PROJECT_REPO="${BATS_TEST_TMPDIR}/repo"
|
|
99
|
+
mkdir -p "$PROJECT_REPO"
|
|
100
|
+
git -C "$PROJECT_REPO" init -q
|
|
101
|
+
git -C "$PROJECT_REPO" config user.email t@example.com
|
|
102
|
+
git -C "$PROJECT_REPO" config user.name "Test"
|
|
103
|
+
git -C "$PROJECT_REPO" remote add origin git@github.com:org/lesson-review.git
|
|
104
|
+
PROJECT_KEY=$(librarian_project_key "$PROJECT_REPO")
|
|
105
|
+
[ -n "$PROJECT_KEY" ]
|
|
106
|
+
LESSONS_DIR="${ONLOOKER_DIR}/librarian/${PROJECT_KEY}/lessons"
|
|
107
|
+
librarian_lesson_storage_init "$PROJECT_KEY"
|
|
108
|
+
librarian_config_load "$PROJECT_REPO"
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
# Seeds one pending proposal and prints its id.
|
|
112
|
+
_seed_pending() {
|
|
113
|
+
librarian_lesson_write_proposal "$PROJECT_KEY" \
|
|
114
|
+
"$(_candidate "$(_versioned)")" "01KZ45MKAM734ZS7JK24D2DK0R"
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
@test "list_pending returns an empty array when the queue is empty" {
|
|
118
|
+
_review_setup
|
|
119
|
+
run librarian_lesson_list_pending "$PROJECT_KEY"
|
|
120
|
+
[ "$status" -eq 0 ]
|
|
121
|
+
[ "$output" = "[]" ]
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@test "list_pending returns a seeded pending proposal" {
|
|
125
|
+
_review_setup
|
|
126
|
+
id=$(_seed_pending)
|
|
127
|
+
run librarian_lesson_list_pending "$PROJECT_KEY"
|
|
128
|
+
[ "$status" -eq 0 ]
|
|
129
|
+
printf '%s' "$output" | jq -e --arg id "$id" 'length == 1 and .[0].id == $id' >/dev/null
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@test "list_by_status selects on the status it is handed, not just pending" {
|
|
133
|
+
_review_setup
|
|
134
|
+
a=$(_seed_pending)
|
|
135
|
+
b=$(_seed_pending)
|
|
136
|
+
librarian_lesson_confirm "$PROJECT_KEY" "$a" "org"
|
|
137
|
+
run librarian_lesson_list_by_status "$PROJECT_KEY" "confirmed"
|
|
138
|
+
[ "$status" -eq 0 ]
|
|
139
|
+
printf '%s' "$output" \
|
|
140
|
+
| jq -e --arg a "$a" --arg b "$b" \
|
|
141
|
+
'length == 1 and .[0].id == $a and .[0].id != $b' >/dev/null
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
@test "confirm records status and visibility on the proposal" {
|
|
145
|
+
_review_setup
|
|
146
|
+
id=$(_seed_pending)
|
|
147
|
+
run librarian_lesson_confirm "$PROJECT_KEY" "$id" "org"
|
|
148
|
+
[ "$status" -eq 0 ]
|
|
149
|
+
jq -e '.status == "confirmed" and .visibility == "org"' \
|
|
150
|
+
"${LESSONS_DIR}/proposals/${id}.json" >/dev/null
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
@test "confirm refuses an empty visibility and leaves the proposal pending" {
|
|
154
|
+
_review_setup
|
|
155
|
+
id=$(_seed_pending)
|
|
156
|
+
run librarian_lesson_confirm "$PROJECT_KEY" "$id" ""
|
|
157
|
+
[ "$status" -ne 0 ]
|
|
158
|
+
jq -e '.status == "pending"' "${LESSONS_DIR}/proposals/${id}.json" >/dev/null
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
@test "confirm refuses an unknown visibility" {
|
|
162
|
+
_review_setup
|
|
163
|
+
id=$(_seed_pending)
|
|
164
|
+
run librarian_lesson_confirm "$PROJECT_KEY" "$id" "everyone"
|
|
165
|
+
[ "$status" -ne 0 ]
|
|
166
|
+
jq -e '.status == "pending"' "${LESSONS_DIR}/proposals/${id}.json" >/dev/null
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
@test "confirm with a justification rewrites scope to version_independent" {
|
|
170
|
+
_review_setup
|
|
171
|
+
id=$(_seed_pending)
|
|
172
|
+
run librarian_lesson_confirm "$PROJECT_KEY" "$id" "org" "git behavior is stable across versions."
|
|
173
|
+
[ "$status" -eq 0 ]
|
|
174
|
+
jq -e '.candidate.applies_to.scope.kind == "version_independent"
|
|
175
|
+
and (.candidate.applies_to.scope.justification | length) > 0' \
|
|
176
|
+
"${LESSONS_DIR}/proposals/${id}.json" >/dev/null
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
@test "confirm refuses version_independent at private visibility" {
|
|
180
|
+
_review_setup
|
|
181
|
+
id=$(_seed_pending)
|
|
182
|
+
run librarian_lesson_confirm "$PROJECT_KEY" "$id" "private" "stable across versions."
|
|
183
|
+
[ "$status" -ne 0 ]
|
|
184
|
+
jq -e '.status == "pending" and .candidate.applies_to.scope.kind == "versioned"' \
|
|
185
|
+
"${LESSONS_DIR}/proposals/${id}.json" >/dev/null
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
@test "confirm refuses a candidate that already carries version_independent scope at private visibility" {
|
|
189
|
+
_review_setup
|
|
190
|
+
# Seeded directly with version_independent scope and no justification
|
|
191
|
+
# argument on this call — librarian_lesson_write_proposal performs no
|
|
192
|
+
# validation, so this is reachable without going through the input-side
|
|
193
|
+
# guard at all. The guard must key on the resulting state, not on
|
|
194
|
+
# whether THIS call passed a justification.
|
|
195
|
+
id=$(librarian_lesson_write_proposal "$PROJECT_KEY" \
|
|
196
|
+
"$(_candidate "$(_indep)")" "01KZ45MKAM734ZS7JK24D2DK0R")
|
|
197
|
+
run librarian_lesson_confirm "$PROJECT_KEY" "$id" "private"
|
|
198
|
+
[ "$status" -ne 0 ]
|
|
199
|
+
jq -e '.status == "pending" and .candidate.applies_to.scope.kind == "version_independent"' \
|
|
200
|
+
"${LESSONS_DIR}/proposals/${id}.json" >/dev/null
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
@test "confirm accepts a candidate that already carries version_independent scope at org visibility" {
|
|
204
|
+
_review_setup
|
|
205
|
+
id=$(librarian_lesson_write_proposal "$PROJECT_KEY" \
|
|
206
|
+
"$(_candidate "$(_indep)")" "01KZ45MKAM734ZS7JK24D2DK0R")
|
|
207
|
+
run librarian_lesson_confirm "$PROJECT_KEY" "$id" "org"
|
|
208
|
+
[ "$status" -eq 0 ]
|
|
209
|
+
jq -e '.status == "confirmed" and .candidate.applies_to.scope.kind == "version_independent"' \
|
|
210
|
+
"${LESSONS_DIR}/proposals/${id}.json" >/dev/null
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
@test "confirm refuses a candidate that fails the confirmed validator" {
|
|
214
|
+
_review_setup
|
|
215
|
+
id=$(_seed_pending)
|
|
216
|
+
# Corrupt the stored candidate so validation must reject it.
|
|
217
|
+
tmp="${BATS_TEST_TMPDIR}/bad.json"
|
|
218
|
+
jq '.candidate.evidence.resolution = ""' "${LESSONS_DIR}/proposals/${id}.json" > "$tmp"
|
|
219
|
+
mv "$tmp" "${LESSONS_DIR}/proposals/${id}.json"
|
|
220
|
+
run librarian_lesson_confirm "$PROJECT_KEY" "$id" "org"
|
|
221
|
+
[ "$status" -ne 0 ]
|
|
222
|
+
jq -e '.status == "pending"' "${LESSONS_DIR}/proposals/${id}.json" >/dev/null
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
@test "pass marks the proposal and appends one ledger line" {
|
|
226
|
+
_review_setup
|
|
227
|
+
id=$(_seed_pending)
|
|
228
|
+
run librarian_lesson_pass "$PROJECT_KEY" "$id" "not worth sharing"
|
|
229
|
+
[ "$status" -eq 0 ]
|
|
230
|
+
jq -e '.status == "passed"' "${LESSONS_DIR}/proposals/${id}.json" >/dev/null
|
|
231
|
+
[ "$(wc -l < "${LESSONS_DIR}/passed.jsonl")" -eq 1 ]
|
|
232
|
+
tail -n 1 "${LESSONS_DIR}/passed.jsonl" \
|
|
233
|
+
| jq -e --arg id "$id" '.lesson_id == $id and .reason == "not worth sharing"' >/dev/null
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
@test "a passed proposal keeps its file so the artifact stays seen" {
|
|
237
|
+
_review_setup
|
|
238
|
+
id=$(_seed_pending)
|
|
239
|
+
librarian_lesson_pass "$PROJECT_KEY" "$id"
|
|
240
|
+
[ -f "${LESSONS_DIR}/proposals/${id}.json" ]
|
|
241
|
+
run librarian_lesson_seen "$PROJECT_KEY" "01KZ45MKAM734ZS7JK24D2DK0R"
|
|
242
|
+
[ "$status" -eq 0 ]
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
@test "this stage never writes to declined.jsonl" {
|
|
246
|
+
_review_setup
|
|
247
|
+
id=$(_seed_pending)
|
|
248
|
+
librarian_lesson_confirm "$PROJECT_KEY" "$id" "org"
|
|
249
|
+
id2=$(_seed_pending)
|
|
250
|
+
librarian_lesson_pass "$PROJECT_KEY" "$id2"
|
|
251
|
+
[ ! -f "${LESSONS_DIR}/declined.jsonl" ]
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
@test "confirming never invokes a model" {
|
|
255
|
+
_review_setup
|
|
256
|
+
# A claude on PATH that fails loudly if called at all.
|
|
257
|
+
stub="${BATS_TEST_TMPDIR}/bin"
|
|
258
|
+
mkdir -p "$stub"
|
|
259
|
+
printf '#!/usr/bin/env bash\necho "MODEL WAS INVOKED" >&2\nexit 42\n' > "${stub}/claude"
|
|
260
|
+
chmod +x "${stub}/claude"
|
|
261
|
+
PATH="${stub}:${PATH}"
|
|
262
|
+
|
|
263
|
+
id=$(_seed_pending)
|
|
264
|
+
run librarian_lesson_confirm "$PROJECT_KEY" "$id" "public"
|
|
265
|
+
[ "$status" -eq 0 ]
|
|
266
|
+
[[ "$output" != *"MODEL WAS INVOKED"* ]] || return 1
|
|
267
|
+
jq -e '.status == "confirmed"' "${LESSONS_DIR}/proposals/${id}.json" >/dev/null
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
@test "list_pending excludes confirmed and passed proposals" {
|
|
271
|
+
_review_setup
|
|
272
|
+
a=$(_seed_pending); b=$(_seed_pending); c=$(_seed_pending)
|
|
273
|
+
librarian_lesson_confirm "$PROJECT_KEY" "$a" "org"
|
|
274
|
+
librarian_lesson_pass "$PROJECT_KEY" "$b"
|
|
275
|
+
run librarian_lesson_list_pending "$PROJECT_KEY"
|
|
276
|
+
printf '%s' "$output" | jq -e --arg c "$c" 'length == 1 and .[0].id == $c' >/dev/null
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
@test "confirming an already-confirmed lesson with the same visibility succeeds and does not write twice" {
|
|
280
|
+
_review_setup
|
|
281
|
+
id=$(_seed_pending)
|
|
282
|
+
librarian_lesson_confirm "$PROJECT_KEY" "$id" "org"
|
|
283
|
+
before=$(cat "${LESSONS_DIR}/proposals/${id}.json")
|
|
284
|
+
run librarian_lesson_confirm "$PROJECT_KEY" "$id" "org"
|
|
285
|
+
[ "$status" -eq 0 ]
|
|
286
|
+
after=$(cat "${LESSONS_DIR}/proposals/${id}.json")
|
|
287
|
+
[ "$before" = "$after" ]
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
@test "confirming an already-confirmed lesson with a different visibility is refused" {
|
|
291
|
+
_review_setup
|
|
292
|
+
id=$(_seed_pending)
|
|
293
|
+
librarian_lesson_confirm "$PROJECT_KEY" "$id" "org"
|
|
294
|
+
run librarian_lesson_confirm "$PROJECT_KEY" "$id" "public"
|
|
295
|
+
[ "$status" -ne 0 ]
|
|
296
|
+
jq -e '.status == "confirmed" and .visibility == "org"' \
|
|
297
|
+
"${LESSONS_DIR}/proposals/${id}.json" >/dev/null
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
@test "confirming a passed lesson is refused" {
|
|
301
|
+
_review_setup
|
|
302
|
+
id=$(_seed_pending)
|
|
303
|
+
librarian_lesson_pass "$PROJECT_KEY" "$id"
|
|
304
|
+
run librarian_lesson_confirm "$PROJECT_KEY" "$id" "org"
|
|
305
|
+
[ "$status" -ne 0 ]
|
|
306
|
+
jq -e '.status == "passed"' "${LESSONS_DIR}/proposals/${id}.json" >/dev/null
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
@test "passing an already-passed lesson does not append a second ledger line" {
|
|
310
|
+
_review_setup
|
|
311
|
+
id=$(_seed_pending)
|
|
312
|
+
librarian_lesson_pass "$PROJECT_KEY" "$id" "not worth sharing"
|
|
313
|
+
run librarian_lesson_pass "$PROJECT_KEY" "$id" "still not worth sharing"
|
|
314
|
+
[ "$status" -eq 0 ]
|
|
315
|
+
[ "$(wc -l < "${LESSONS_DIR}/passed.jsonl")" -eq 1 ]
|
|
316
|
+
tail -n 1 "${LESSONS_DIR}/passed.jsonl" \
|
|
317
|
+
| jq -e --arg id "$id" '.lesson_id == $id and .reason == "not worth sharing"' >/dev/null
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
@test "passing a confirmed lesson is refused" {
|
|
321
|
+
_review_setup
|
|
322
|
+
id=$(_seed_pending)
|
|
323
|
+
librarian_lesson_confirm "$PROJECT_KEY" "$id" "org"
|
|
324
|
+
run librarian_lesson_pass "$PROJECT_KEY" "$id"
|
|
325
|
+
[ "$status" -ne 0 ]
|
|
326
|
+
jq -e '.status == "confirmed"' "${LESSONS_DIR}/proposals/${id}.json" >/dev/null
|
|
327
|
+
[ ! -f "${LESSONS_DIR}/passed.jsonl" ]
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
@test "list_pending still returns the good entries when the directory also contains a truncated file, a bare-number file, and an empty file" {
|
|
331
|
+
_review_setup
|
|
332
|
+
id=$(_seed_pending)
|
|
333
|
+
printf '{"id":"bad","status":"pending"' > "${LESSONS_DIR}/proposals/truncated.json"
|
|
334
|
+
printf '123' > "${LESSONS_DIR}/proposals/barenum.json"
|
|
335
|
+
: > "${LESSONS_DIR}/proposals/empty.json"
|
|
336
|
+
run librarian_lesson_list_pending "$PROJECT_KEY"
|
|
337
|
+
[ "$status" -eq 0 ]
|
|
338
|
+
printf '%s' "$output" | jq -e --arg id "$id" 'length == 1 and .[0].id == $id' >/dev/null
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
@test "confirming with a justification stores scope with exactly kind and justification, no versions key surviving" {
|
|
342
|
+
_review_setup
|
|
343
|
+
id=$(_seed_pending)
|
|
344
|
+
run librarian_lesson_confirm "$PROJECT_KEY" "$id" "org" "git behavior is stable across versions."
|
|
345
|
+
[ "$status" -eq 0 ]
|
|
346
|
+
jq -e '.candidate.applies_to.scope | keys == ["justification", "kind"]' \
|
|
347
|
+
"${LESSONS_DIR}/proposals/${id}.json" >/dev/null
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
@test "the stored candidate still passes the confirmed validator after a version_independent confirm" {
|
|
351
|
+
_review_setup
|
|
352
|
+
id=$(_seed_pending)
|
|
353
|
+
librarian_lesson_confirm "$PROJECT_KEY" "$id" "org" "git behavior is stable across versions."
|
|
354
|
+
stored=$(jq -c '.candidate' "${LESSONS_DIR}/proposals/${id}.json")
|
|
355
|
+
run librarian_lesson_validate_confirmed "$stored"
|
|
356
|
+
[ "$status" -eq 0 ]
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
@test "an identical repeat confirm with a justification is idempotent and succeeds" {
|
|
360
|
+
_review_setup
|
|
361
|
+
id=$(_seed_pending)
|
|
362
|
+
librarian_lesson_confirm "$PROJECT_KEY" "$id" "org" "git behavior is stable across versions."
|
|
363
|
+
before=$(cat "${LESSONS_DIR}/proposals/${id}.json")
|
|
364
|
+
run librarian_lesson_confirm "$PROJECT_KEY" "$id" "org" "git behavior is stable across versions."
|
|
365
|
+
[ "$status" -eq 0 ]
|
|
366
|
+
after=$(cat "${LESSONS_DIR}/proposals/${id}.json")
|
|
367
|
+
[ "$before" = "$after" ]
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
@test "a repeat confirm with a different justification is refused" {
|
|
371
|
+
_review_setup
|
|
372
|
+
id=$(_seed_pending)
|
|
373
|
+
librarian_lesson_confirm "$PROJECT_KEY" "$id" "org" "git behavior is stable across versions."
|
|
374
|
+
run librarian_lesson_confirm "$PROJECT_KEY" "$id" "org" "a completely different justification."
|
|
375
|
+
[ "$status" -ne 0 ]
|
|
376
|
+
jq -e '.candidate.applies_to.scope.justification == "git behavior is stable across versions."' \
|
|
377
|
+
"${LESSONS_DIR}/proposals/${id}.json" >/dev/null
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
_cli_setup() {
|
|
381
|
+
_review_setup
|
|
382
|
+
# shellcheck disable=SC1091
|
|
383
|
+
source "${PLUGIN_ROOT}/scripts/lib/librarian-config.sh"
|
|
384
|
+
# shellcheck disable=SC1091
|
|
385
|
+
source "${PLUGIN_ROOT}/scripts/lib/librarian-emit.sh"
|
|
386
|
+
# shellcheck disable=SC1091
|
|
387
|
+
source "${PLUGIN_ROOT}/scripts/lib/librarian-cli.sh"
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
@test "lessons status reports zero on an empty queue" {
|
|
391
|
+
_cli_setup
|
|
392
|
+
run librarian_cli lessons status "$PROJECT_REPO"
|
|
393
|
+
[ "$status" -eq 0 ]
|
|
394
|
+
[[ "$output" == *"0"* ]] || return 1
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
@test "lessons list shows a pending lesson" {
|
|
398
|
+
_cli_setup
|
|
399
|
+
id=$(_seed_pending)
|
|
400
|
+
run librarian_cli lessons list "$PROJECT_REPO"
|
|
401
|
+
[ "$status" -eq 0 ]
|
|
402
|
+
[[ "$output" == *"$id"* ]] || return 1
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
@test "lessons list --confirmed shows a confirmed lesson the pending list hides" {
|
|
406
|
+
_cli_setup
|
|
407
|
+
id=$(_seed_pending)
|
|
408
|
+
librarian_cli lessons confirm "$id" public "$PROJECT_REPO" >/dev/null
|
|
409
|
+
|
|
410
|
+
run librarian_cli lessons list "$PROJECT_REPO"
|
|
411
|
+
[[ "$output" == *"No pending lessons."* ]] || return 1
|
|
412
|
+
|
|
413
|
+
run librarian_cli lessons list --confirmed "$PROJECT_REPO"
|
|
414
|
+
[ "$status" -eq 0 ]
|
|
415
|
+
[[ "$output" == *"$id"* ]] || return 1
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
@test "lessons list --confirmed reports an empty set of its own" {
|
|
419
|
+
_cli_setup
|
|
420
|
+
_seed_pending
|
|
421
|
+
# A pending lesson exists, so an implementation that ignored the flag
|
|
422
|
+
# would print that row instead of the empty state.
|
|
423
|
+
run librarian_cli lessons list --confirmed "$PROJECT_REPO"
|
|
424
|
+
[ "$status" -eq 0 ]
|
|
425
|
+
[[ "$output" == *"No confirmed lessons."* ]] || return 1
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
@test "lessons list rejects an unknown flag instead of reading it as cwd" {
|
|
429
|
+
_cli_setup
|
|
430
|
+
_seed_pending
|
|
431
|
+
cd "$PROJECT_REPO" || return 1
|
|
432
|
+
run librarian_cli lessons list --passed
|
|
433
|
+
[ "$status" -ne 0 ]
|
|
434
|
+
# cd'd into the repo first so the cwd fallback resolves a real key: without
|
|
435
|
+
# the guard this would print the pending queue and exit 0, not a path error.
|
|
436
|
+
[[ "$output" == *"unknown option"* && "$output" == *"--passed"* ]] || return 1
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
@test "lessons show renders the visibility a confirmed lesson sits at" {
|
|
440
|
+
_cli_setup
|
|
441
|
+
id=$(_seed_pending)
|
|
442
|
+
librarian_cli lessons confirm "$id" public "$PROJECT_REPO" >/dev/null
|
|
443
|
+
run librarian_cli lessons show "$id" "$PROJECT_REPO"
|
|
444
|
+
[ "$status" -eq 0 ]
|
|
445
|
+
[[ "$output" == *"visibility: public"* ]] || return 1
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
@test "lessons show renders a dash for a pending lesson's visibility" {
|
|
449
|
+
_cli_setup
|
|
450
|
+
id=$(_seed_pending)
|
|
451
|
+
run librarian_cli lessons show "$id" "$PROJECT_REPO"
|
|
452
|
+
[ "$status" -eq 0 ]
|
|
453
|
+
[[ "$output" == *"visibility: —"* ]] || return 1
|
|
454
|
+
[[ "$output" != *"null"* ]] || return 1
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
@test "lessons show prints the claim" {
|
|
458
|
+
_cli_setup
|
|
459
|
+
claim="pin vitest below six to avoid the esm regression"
|
|
460
|
+
candidate=$(_candidate "$(_versioned)" | jq -c --arg c "$claim" '.claim = $c')
|
|
461
|
+
id=$(librarian_lesson_write_proposal "$PROJECT_KEY" "$candidate" "01KZ45MKAM734ZS7JK24D2DK0R")
|
|
462
|
+
run librarian_cli lessons show "$id" "$PROJECT_REPO"
|
|
463
|
+
[ "$status" -eq 0 ]
|
|
464
|
+
[[ "$output" == *"claim: ${claim}"* ]] || return 1
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
@test "lessons confirm requires a visibility argument" {
|
|
468
|
+
_cli_setup
|
|
469
|
+
id=$(_seed_pending)
|
|
470
|
+
run librarian_cli lessons confirm "$id"
|
|
471
|
+
[ "$status" -ne 0 ]
|
|
472
|
+
jq -e '.status == "pending"' "${LESSONS_DIR}/proposals/${id}.json" >/dev/null
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
@test "lessons confirm sets status and visibility" {
|
|
476
|
+
_cli_setup
|
|
477
|
+
id=$(_seed_pending)
|
|
478
|
+
run librarian_cli lessons confirm "$id" public "$PROJECT_REPO"
|
|
479
|
+
[ "$status" -eq 0 ]
|
|
480
|
+
jq -e '.status == "confirmed" and .visibility == "public"' \
|
|
481
|
+
"${LESSONS_DIR}/proposals/${id}.json" >/dev/null
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
@test "lessons confirm treats a bare trailing positional as cwd, not justification" {
|
|
485
|
+
_cli_setup
|
|
486
|
+
id=$(_seed_pending)
|
|
487
|
+
run librarian_cli lessons confirm "$id" org "$PROJECT_REPO"
|
|
488
|
+
[ "$status" -eq 0 ]
|
|
489
|
+
jq -e '.status == "confirmed" and .candidate.applies_to.scope.kind == "versioned"' \
|
|
490
|
+
"${LESSONS_DIR}/proposals/${id}.json" >/dev/null
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
@test "lessons confirm threads --justification through to a version_independent rewrite" {
|
|
494
|
+
_cli_setup
|
|
495
|
+
id=$(_seed_pending)
|
|
496
|
+
run librarian_cli lessons confirm "$id" org --justification "git behavior is stable across versions." "$PROJECT_REPO"
|
|
497
|
+
[ "$status" -eq 0 ]
|
|
498
|
+
jq -e '.candidate.applies_to.scope.kind == "version_independent"' \
|
|
499
|
+
"${LESSONS_DIR}/proposals/${id}.json" >/dev/null
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
@test "lessons confirm refuses a whitespace-only --justification instead of flipping scope" {
|
|
503
|
+
_cli_setup
|
|
504
|
+
id=$(_seed_pending)
|
|
505
|
+
run librarian_cli lessons confirm "$id" org --justification " " "$PROJECT_REPO"
|
|
506
|
+
[ "$status" -ne 0 ]
|
|
507
|
+
jq -e '.status == "pending" and .candidate.applies_to.scope.kind == "versioned"' \
|
|
508
|
+
"${LESSONS_DIR}/proposals/${id}.json" >/dev/null
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
@test "lessons confirm refuses an unknown flag instead of swallowing it" {
|
|
512
|
+
_cli_setup
|
|
513
|
+
id=$(_seed_pending)
|
|
514
|
+
run librarian_cli lessons confirm "$id" org --foo "$PROJECT_REPO"
|
|
515
|
+
[ "$status" -ne 0 ]
|
|
516
|
+
jq -e '.status == "pending"' "${LESSONS_DIR}/proposals/${id}.json" >/dev/null
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
@test "lessons confirm refuses a typo'd --justifcation instead of confirming with an empty justification" {
|
|
520
|
+
_cli_setup
|
|
521
|
+
id=$(_seed_pending)
|
|
522
|
+
run librarian_cli lessons confirm "$id" org --justifcation "reason" "$PROJECT_REPO"
|
|
523
|
+
[ "$status" -ne 0 ]
|
|
524
|
+
jq -e '.status == "pending" and .candidate.applies_to.scope.kind == "versioned"' \
|
|
525
|
+
"${LESSONS_DIR}/proposals/${id}.json" >/dev/null
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
@test "lessons pass marks it passed" {
|
|
529
|
+
_cli_setup
|
|
530
|
+
id=$(_seed_pending)
|
|
531
|
+
run librarian_cli lessons pass "$id" "" "$PROJECT_REPO"
|
|
532
|
+
[ "$status" -eq 0 ]
|
|
533
|
+
jq -e '.status == "passed"' "${LESSONS_DIR}/proposals/${id}.json" >/dev/null
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
@test "lessons pass refuses a flag-shaped reason instead of storing it literally" {
|
|
537
|
+
_cli_setup
|
|
538
|
+
id=$(_seed_pending)
|
|
539
|
+
# The likeliest typo for confirm's --justification. Positionally this
|
|
540
|
+
# would otherwise land as the literal reason, with cwd falling back to
|
|
541
|
+
# $(pwd) — and passed.jsonl is append-only, so a bad reason here has no
|
|
542
|
+
# CLI path back (see librarian_lesson_pass's `passed) return 0` guard).
|
|
543
|
+
# cd into PROJECT_REPO first so a missing guard would actually resolve a
|
|
544
|
+
# real project key via the $(pwd) fallback, the same way the reported
|
|
545
|
+
# bug does — running this from an arbitrary bats tmpdir would mask a
|
|
546
|
+
# missing guard behind an unrelated "project key not found".
|
|
547
|
+
cd "$PROJECT_REPO" || return 1
|
|
548
|
+
run librarian_cli lessons pass "$id" --reason
|
|
549
|
+
[ "$status" -ne 0 ]
|
|
550
|
+
jq -e '.status == "pending"' "${LESSONS_DIR}/proposals/${id}.json" >/dev/null
|
|
551
|
+
[ ! -f "${LESSONS_DIR}/passed.jsonl" ]
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
@test "lessons pass still works with a real reason and an explicit cwd" {
|
|
555
|
+
_cli_setup
|
|
556
|
+
id=$(_seed_pending)
|
|
557
|
+
run librarian_cli lessons pass "$id" "a real reason" "$PROJECT_REPO"
|
|
558
|
+
[ "$status" -eq 0 ]
|
|
559
|
+
jq -e '.status == "passed"' "${LESSONS_DIR}/proposals/${id}.json" >/dev/null
|
|
560
|
+
tail -n 1 "${LESSONS_DIR}/passed.jsonl" \
|
|
561
|
+
| jq -e --arg id "$id" '.lesson_id == $id and .reason == "a real reason"' >/dev/null
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
@test "lessons defer leaves it pending" {
|
|
565
|
+
_cli_setup
|
|
566
|
+
id=$(_seed_pending)
|
|
567
|
+
run librarian_cli lessons defer "$id" "$PROJECT_REPO"
|
|
568
|
+
[ "$status" -eq 0 ]
|
|
569
|
+
jq -e '.status == "pending"' "${LESSONS_DIR}/proposals/${id}.json" >/dev/null
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
@test "an unknown lessons verb is rejected" {
|
|
573
|
+
_cli_setup
|
|
574
|
+
run librarian_cli lessons frobnicate
|
|
575
|
+
[ "$status" -ne 0 ]
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
@test "memory verbs are unaffected by the lessons namespace" {
|
|
579
|
+
_cli_setup
|
|
580
|
+
run librarian_cli status "$PROJECT_REPO"
|
|
581
|
+
[ "$status" -eq 0 ]
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
@test "confirm without a justification writes no snapshot" {
|
|
585
|
+
_review_setup
|
|
586
|
+
id=$(_seed_pending)
|
|
587
|
+
librarian_lesson_confirm "$PROJECT_KEY" "$id" "org"
|
|
588
|
+
run jq -e 'has("candidate_before_confirm")' "${LESSONS_DIR}/proposals/${id}.json"
|
|
589
|
+
[ "$status" -ne 0 ]
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
@test "confirm without a justification clears a snapshot the proposal arrived with" {
|
|
593
|
+
_review_setup
|
|
594
|
+
id=$(_seed_pending)
|
|
595
|
+
# Nothing in the shipped verbs writes this field onto a pending proposal
|
|
596
|
+
# today, so seed it directly. The invariant unconfirm leans on is "absence
|
|
597
|
+
# means the candidate was never mutated" — a stale snapshot surviving a
|
|
598
|
+
# plain confirm would make the next unconfirm silently swap .candidate for
|
|
599
|
+
# a rewrite that never happened, stranding the user in the dead end this
|
|
600
|
+
# verb exists to escape.
|
|
601
|
+
tmp="${BATS_TEST_TMPDIR}/stale.json"
|
|
602
|
+
jq --argjson cb "$(_candidate "$(_indep)")" '.candidate_before_confirm = $cb' \
|
|
603
|
+
"${LESSONS_DIR}/proposals/${id}.json" > "$tmp"
|
|
604
|
+
mv "$tmp" "${LESSONS_DIR}/proposals/${id}.json"
|
|
605
|
+
|
|
606
|
+
librarian_lesson_confirm "$PROJECT_KEY" "$id" "org"
|
|
607
|
+
jq -e 'has("candidate_before_confirm") | not' \
|
|
608
|
+
"${LESSONS_DIR}/proposals/${id}.json" >/dev/null || return 1
|
|
609
|
+
|
|
610
|
+
# And the harm it would have caused: unconfirm must find nothing to restore.
|
|
611
|
+
librarian_lesson_unconfirm "$PROJECT_KEY" "$id"
|
|
612
|
+
run jq -e '.candidate.applies_to.scope.kind == "versioned"' \
|
|
613
|
+
"${LESSONS_DIR}/proposals/${id}.json"
|
|
614
|
+
[ "$status" -eq 0 ]
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
@test "confirm with a justification snapshots the pre-rewrite candidate" {
|
|
618
|
+
_review_setup
|
|
619
|
+
id=$(_seed_pending)
|
|
620
|
+
librarian_lesson_confirm "$PROJECT_KEY" "$id" "org" "git aborts on a dirty tree regardless of version"
|
|
621
|
+
run jq -e '.candidate_before_confirm.applies_to.scope.kind == "versioned"' \
|
|
622
|
+
"${LESSONS_DIR}/proposals/${id}.json"
|
|
623
|
+
[ "$status" -eq 0 ]
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
@test "unconfirm returns a confirmed lesson to pending and clears the decision" {
|
|
627
|
+
_review_setup
|
|
628
|
+
id=$(_seed_pending)
|
|
629
|
+
librarian_lesson_confirm "$PROJECT_KEY" "$id" "public"
|
|
630
|
+
run librarian_lesson_unconfirm "$PROJECT_KEY" "$id"
|
|
631
|
+
[ "$status" -eq 0 ]
|
|
632
|
+
run jq -e '.status == "pending" and (has("visibility") | not) and (has("confirmed_at") | not)' \
|
|
633
|
+
"${LESSONS_DIR}/proposals/${id}.json"
|
|
634
|
+
[ "$status" -eq 0 ]
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
@test "the round trip leaves the proposal byte-identical to its pre-confirm state" {
|
|
638
|
+
_review_setup
|
|
639
|
+
id=$(_seed_pending)
|
|
640
|
+
before="${BATS_TEST_TMPDIR}/before.json"
|
|
641
|
+
cp "${LESSONS_DIR}/proposals/${id}.json" "$before"
|
|
642
|
+
|
|
643
|
+
librarian_lesson_confirm "$PROJECT_KEY" "$id" "public" "git behavior is stable across versions"
|
|
644
|
+
librarian_lesson_unconfirm "$PROJECT_KEY" "$id"
|
|
645
|
+
|
|
646
|
+
run diff <(jq -S . "$before") <(jq -S . "${LESSONS_DIR}/proposals/${id}.json")
|
|
647
|
+
[ "$status" -eq 0 ]
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
@test "unconfirm restores versioned scope after a justification confirm" {
|
|
651
|
+
_review_setup
|
|
652
|
+
id=$(_seed_pending)
|
|
653
|
+
librarian_lesson_confirm "$PROJECT_KEY" "$id" "org" "stable across versions"
|
|
654
|
+
librarian_lesson_unconfirm "$PROJECT_KEY" "$id"
|
|
655
|
+
run jq -e '.candidate.applies_to.scope.kind == "versioned"
|
|
656
|
+
and (has("candidate_before_confirm") | not)' \
|
|
657
|
+
"${LESSONS_DIR}/proposals/${id}.json"
|
|
658
|
+
[ "$status" -eq 0 ]
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
@test "after unconfirm a fresh confirm at a different visibility succeeds" {
|
|
662
|
+
_review_setup
|
|
663
|
+
id=$(_seed_pending)
|
|
664
|
+
librarian_lesson_confirm "$PROJECT_KEY" "$id" "public"
|
|
665
|
+
librarian_lesson_unconfirm "$PROJECT_KEY" "$id"
|
|
666
|
+
run librarian_lesson_confirm "$PROJECT_KEY" "$id" "private"
|
|
667
|
+
[ "$status" -eq 0 ]
|
|
668
|
+
run jq -e '.status == "confirmed" and .visibility == "private"' \
|
|
669
|
+
"${LESSONS_DIR}/proposals/${id}.json"
|
|
670
|
+
[ "$status" -eq 0 ]
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
@test "after unconfirm a justification confirm can be redone at private visibility" {
|
|
674
|
+
_review_setup
|
|
675
|
+
id=$(_seed_pending)
|
|
676
|
+
# The composite the design actually argues for. A justification rewrites
|
|
677
|
+
# scope to version_independent, and private refuses that scope — so without
|
|
678
|
+
# the snapshot restore this last confirm is refused for a rewrite the user
|
|
679
|
+
# already took back, trading one dead end for another. The plain-confirm
|
|
680
|
+
# case above still passes with the snapshot mechanism deleted entirely;
|
|
681
|
+
# this one does not.
|
|
682
|
+
librarian_lesson_confirm "$PROJECT_KEY" "$id" "public" \
|
|
683
|
+
"git aborts on a dirty tree regardless of version"
|
|
684
|
+
librarian_lesson_unconfirm "$PROJECT_KEY" "$id"
|
|
685
|
+
run librarian_lesson_confirm "$PROJECT_KEY" "$id" "private"
|
|
686
|
+
[ "$status" -eq 0 ]
|
|
687
|
+
run jq -e '.status == "confirmed" and .visibility == "private"
|
|
688
|
+
and .candidate.applies_to.scope.kind == "versioned"' \
|
|
689
|
+
"${LESSONS_DIR}/proposals/${id}.json"
|
|
690
|
+
[ "$status" -eq 0 ]
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
@test "unconfirm from pending is a no-op success" {
|
|
694
|
+
_review_setup
|
|
695
|
+
id=$(_seed_pending)
|
|
696
|
+
run librarian_lesson_unconfirm "$PROJECT_KEY" "$id"
|
|
697
|
+
[ "$status" -eq 0 ]
|
|
698
|
+
run jq -e '.status == "pending"' "${LESSONS_DIR}/proposals/${id}.json"
|
|
699
|
+
[ "$status" -eq 0 ]
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
@test "unconfirm refuses a passed lesson and leaves the ledger untouched" {
|
|
703
|
+
_review_setup
|
|
704
|
+
id=$(_seed_pending)
|
|
705
|
+
librarian_lesson_pass "$PROJECT_KEY" "$id" "not worth sharing"
|
|
706
|
+
before_lines=$(wc -l < "${LESSONS_DIR}/passed.jsonl")
|
|
707
|
+
|
|
708
|
+
run librarian_lesson_unconfirm "$PROJECT_KEY" "$id"
|
|
709
|
+
[ "$status" -ne 0 ]
|
|
710
|
+
run jq -e '.status == "passed"' "${LESSONS_DIR}/proposals/${id}.json"
|
|
711
|
+
[ "$status" -eq 0 ]
|
|
712
|
+
[ "$(wc -l < "${LESSONS_DIR}/passed.jsonl")" -eq "$before_lines" ]
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
@test "unconfirm refuses an unrecognized status and names it" {
|
|
716
|
+
_review_setup
|
|
717
|
+
id=$(_seed_pending)
|
|
718
|
+
tmp="${BATS_TEST_TMPDIR}/mut.json"
|
|
719
|
+
jq '.status = "judging"' "${LESSONS_DIR}/proposals/${id}.json" > "$tmp"
|
|
720
|
+
mv "$tmp" "${LESSONS_DIR}/proposals/${id}.json"
|
|
721
|
+
|
|
722
|
+
run librarian_lesson_unconfirm "$PROJECT_KEY" "$id"
|
|
723
|
+
[ "$status" -ne 0 ]
|
|
724
|
+
[[ "$output" == *"judging"* ]] || return 1
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
@test "unconfirm refuses a lesson that does not exist" {
|
|
728
|
+
_review_setup
|
|
729
|
+
run librarian_lesson_unconfirm "$PROJECT_KEY" "01KZNOSUCHLESSON0000000000"
|
|
730
|
+
[ "$status" -ne 0 ]
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
@test "unconfirm never invokes a model" {
|
|
734
|
+
_review_setup
|
|
735
|
+
stub="${BATS_TEST_TMPDIR}/bin"
|
|
736
|
+
mkdir -p "$stub"
|
|
737
|
+
printf '#!/usr/bin/env bash\necho "MODEL WAS INVOKED" >&2\nexit 42\n' > "${stub}/claude"
|
|
738
|
+
chmod +x "${stub}/claude"
|
|
739
|
+
PATH="${stub}:${PATH}"
|
|
740
|
+
|
|
741
|
+
id=$(_seed_pending)
|
|
742
|
+
librarian_lesson_confirm "$PROJECT_KEY" "$id" "org"
|
|
743
|
+
run librarian_lesson_unconfirm "$PROJECT_KEY" "$id"
|
|
744
|
+
[ "$status" -eq 0 ]
|
|
745
|
+
[[ "$output" != *"MODEL WAS INVOKED"* ]] || return 1
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
@test "lessons unconfirm returns a confirmed lesson to pending" {
|
|
749
|
+
_cli_setup
|
|
750
|
+
id=$(_seed_pending)
|
|
751
|
+
librarian_cli lessons confirm "$id" public "$PROJECT_REPO" >/dev/null
|
|
752
|
+
run librarian_cli lessons unconfirm "$id" "$PROJECT_REPO"
|
|
753
|
+
[ "$status" -eq 0 ]
|
|
754
|
+
run jq -e '.status == "pending"' "${LESSONS_DIR}/proposals/${id}.json"
|
|
755
|
+
[ "$status" -eq 0 ]
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
@test "lessons unconfirm on a pending lesson does not claim it undid a confirmation" {
|
|
759
|
+
_cli_setup
|
|
760
|
+
id=$(_seed_pending)
|
|
761
|
+
run librarian_cli lessons unconfirm "$id" "$PROJECT_REPO"
|
|
762
|
+
[ "$status" -eq 0 ]
|
|
763
|
+
# The no-op is a success, but reporting "Unconfirmed <id>" for a lesson
|
|
764
|
+
# nobody confirmed asserts an action that never happened — and a user
|
|
765
|
+
# chasing a mistaken confirm would read it as proof they fixed it.
|
|
766
|
+
[[ "$output" == *"already pending"* ]] || return 1
|
|
767
|
+
[[ "$output" != *"Unconfirmed"* ]] || return 1
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
@test "lessons unconfirm still reports the action when it took one" {
|
|
771
|
+
_cli_setup
|
|
772
|
+
id=$(_seed_pending)
|
|
773
|
+
librarian_cli lessons confirm "$id" public "$PROJECT_REPO" >/dev/null
|
|
774
|
+
run librarian_cli lessons unconfirm "$id" "$PROJECT_REPO"
|
|
775
|
+
[ "$status" -eq 0 ]
|
|
776
|
+
[[ "$output" == *"Unconfirmed ${id}"* ]] || return 1
|
|
777
|
+
[[ "$output" != *"already pending"* ]] || return 1
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
@test "lessons unconfirm requires a lesson id" {
|
|
781
|
+
_cli_setup
|
|
782
|
+
run librarian_cli lessons unconfirm
|
|
783
|
+
[ "$status" -ne 0 ]
|
|
784
|
+
# With no lesson id, cwd is empty too, so _librarian_cli_project_key
|
|
785
|
+
# falls back to $(pwd) and resolves a valid key in this sandbox, and
|
|
786
|
+
# librarian_lesson_unconfirm's own empty-id guard also returns 1. Exit
|
|
787
|
+
# status alone can't tell the CLI's usage guard apart from that
|
|
788
|
+
# fallback path. Pin the message: without the CLI guard this test would
|
|
789
|
+
# pass while printing nothing useful.
|
|
790
|
+
[[ "$output" == *"usage:"* && "$output" == *"unconfirm"* ]] || return 1
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
@test "lessons unconfirm rejects an unknown flag" {
|
|
794
|
+
_cli_setup
|
|
795
|
+
id=$(_seed_pending)
|
|
796
|
+
librarian_cli lessons confirm "$id" public "$PROJECT_REPO" >/dev/null
|
|
797
|
+
run librarian_cli lessons unconfirm "$id" --force
|
|
798
|
+
[ "$status" -ne 0 ]
|
|
799
|
+
# An invalid cwd already fails project-key resolution on its own, so the
|
|
800
|
+
# exit code alone can't tell the guard's diagnostic apart from that
|
|
801
|
+
# fallback path. Pin the message too: without the guard this would read
|
|
802
|
+
# "No project key resolvable from this directory." instead.
|
|
803
|
+
[[ "$output" == *"unknown option"* && "$output" == *"--force"* ]] || return 1
|
|
804
|
+
run jq -e '.status == "confirmed"' "${LESSONS_DIR}/proposals/${id}.json"
|
|
805
|
+
[ "$status" -eq 0 ]
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
@test "unconfirm refuses an approved lesson, naming the status" {
|
|
809
|
+
_review_setup
|
|
810
|
+
local id
|
|
811
|
+
id=$(_seed_pending)
|
|
812
|
+
librarian_lesson_confirm "$PROJECT_KEY" "$id" "org"
|
|
813
|
+
# criterion_scores must cover the rubric's floored criteria or the judge
|
|
814
|
+
# refuses the panel as UNJUDGED and this lesson never reaches `approved`.
|
|
815
|
+
librarian_lesson_judge "$PROJECT_KEY" "$id" \
|
|
816
|
+
'[{"judge_type":"standard","score":0.9,"passed":true,"criterion_scores":{"grounding":0.9,"scope_accuracy":0.9,"generality":0.9}},{"judge_type":"adversarial","score":0.8,"passed":true,"criterion_scores":{"grounding":0.8,"scope_accuracy":0.8,"generality":0.8}}]'
|
|
817
|
+
|
|
818
|
+
run librarian_lesson_unconfirm "$PROJECT_KEY" "$id"
|
|
819
|
+
[ "$status" -ne 0 ]
|
|
820
|
+
[[ "$output" == *"unrecognized status"* && "$output" == *"approved"* ]] || return 1
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
@test "unconfirm refuses a rejected lesson, naming the status" {
|
|
824
|
+
_review_setup
|
|
825
|
+
local id
|
|
826
|
+
id=$(_seed_pending)
|
|
827
|
+
librarian_lesson_confirm "$PROJECT_KEY" "$id" "public"
|
|
828
|
+
# As above: the public rubric floors disclosure too, so it must be scored
|
|
829
|
+
# for the panel to produce a verdict at all. The jury split is what rejects.
|
|
830
|
+
librarian_lesson_judge "$PROJECT_KEY" "$id" \
|
|
831
|
+
'[{"judge_type":"standard","score":0.95,"passed":true,"criterion_scores":{"grounding":0.95,"scope_accuracy":0.95,"generality":0.95,"disclosure":0.95}},{"judge_type":"adversarial","score":0.75,"passed":false,"criterion_scores":{"grounding":0.75,"scope_accuracy":0.75,"generality":0.75,"disclosure":0.95}}]'
|
|
832
|
+
|
|
833
|
+
run librarian_lesson_unconfirm "$PROJECT_KEY" "$id"
|
|
834
|
+
[ "$status" -ne 0 ]
|
|
835
|
+
[[ "$output" == *"unrecognized status"* && "$output" == *"rejected"* ]] || return 1
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
@test "confirm writes the proposal atomically, never truncating in place" {
|
|
839
|
+
# `printf > path` truncates first, so an interrupted write leaves a
|
|
840
|
+
# zero-byte proposal that every verb refuses and list_pending hides —
|
|
841
|
+
# unrecoverable even by unconfirm. A read-only-dir test would NOT catch
|
|
842
|
+
# this: it blocks the open entirely, so the truncating code also leaves
|
|
843
|
+
# the original intact. What distinguishes atomic from not is that the
|
|
844
|
+
# write lands somewhere else first, so spy on the rename.
|
|
845
|
+
_review_setup
|
|
846
|
+
local id
|
|
847
|
+
id=$(_seed_pending)
|
|
848
|
+
|
|
849
|
+
local marker="${BATS_TEST_TMPDIR}/mv-called"
|
|
850
|
+
rm -f "$marker"
|
|
851
|
+
mv() { printf '%s -> %s\n' "$1" "$2" >> "$marker"; command mv "$@"; }
|
|
852
|
+
|
|
853
|
+
librarian_lesson_confirm "$PROJECT_KEY" "$id" "org"
|
|
854
|
+
|
|
855
|
+
[ -f "$marker" ]
|
|
856
|
+
grep -q "proposals/${id}.json" "$marker" || return 1
|
|
857
|
+
[ "$(jq -r '.status' "$(librarian_lessons_dir "$PROJECT_KEY")/proposals/${id}.json")" = "confirmed" ]
|
|
858
|
+
unset -f mv
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
@test "pass writes the proposal atomically, never truncating in place" {
|
|
862
|
+
# Same discriminator as confirm's atomic-write test: a read-only-dir test
|
|
863
|
+
# would not catch a reverted fix here either, since the truncating write
|
|
864
|
+
# also never opens the file. Spy on the rename instead.
|
|
865
|
+
_review_setup
|
|
866
|
+
local id
|
|
867
|
+
id=$(_seed_pending)
|
|
868
|
+
|
|
869
|
+
local marker="${BATS_TEST_TMPDIR}/mv-called"
|
|
870
|
+
rm -f "$marker"
|
|
871
|
+
mv() { printf '%s -> %s\n' "$1" "$2" >> "$marker"; command mv "$@"; }
|
|
872
|
+
|
|
873
|
+
librarian_lesson_pass "$PROJECT_KEY" "$id" "not worth sharing"
|
|
874
|
+
|
|
875
|
+
[ -f "$marker" ]
|
|
876
|
+
grep -q "proposals/${id}.json" "$marker" || return 1
|
|
877
|
+
[ "$(jq -r '.status' "$(librarian_lessons_dir "$PROJECT_KEY")/proposals/${id}.json")" = "passed" ]
|
|
878
|
+
unset -f mv
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
@test "unconfirm writes the proposal atomically, never truncating in place" {
|
|
882
|
+
# Same discriminator as confirm's atomic-write test. unconfirm is the
|
|
883
|
+
# recovery verb for a stuck proposal, so a truncating write here is the
|
|
884
|
+
# one that would leave it unrecoverable even from its own remedy.
|
|
885
|
+
_review_setup
|
|
886
|
+
local id
|
|
887
|
+
id=$(_seed_pending)
|
|
888
|
+
librarian_lesson_confirm "$PROJECT_KEY" "$id" "org"
|
|
889
|
+
|
|
890
|
+
local marker="${BATS_TEST_TMPDIR}/mv-called"
|
|
891
|
+
rm -f "$marker"
|
|
892
|
+
mv() { printf '%s -> %s\n' "$1" "$2" >> "$marker"; command mv "$@"; }
|
|
893
|
+
|
|
894
|
+
librarian_lesson_unconfirm "$PROJECT_KEY" "$id"
|
|
895
|
+
|
|
896
|
+
[ -f "$marker" ]
|
|
897
|
+
grep -q "proposals/${id}.json" "$marker" || return 1
|
|
898
|
+
[ "$(jq -r '.status' "$(librarian_lessons_dir "$PROJECT_KEY")/proposals/${id}.json")" = "pending" ]
|
|
899
|
+
unset -f mv
|
|
900
|
+
}
|