@onlooker-community/ecosystem 0.10.0 → 0.15.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 +39 -1
- package/.claude-plugin/plugin.json +2 -2
- package/.github/copilot-instructions.md +46 -0
- package/.github/workflows/coverage.yml +78 -0
- package/.github/workflows/release.yml +24 -8
- package/.github/workflows/test.yml +3 -0
- package/.markdownlintignore +3 -0
- package/.release-please-manifest.json +5 -1
- package/CHANGELOG.md +44 -0
- package/README.md +58 -13
- package/config.json +6 -1
- package/docs/adr/001-claude-code-hooks-as-integration-surface.md +43 -0
- package/docs/adr/002-centralized-jsonl-event-log.md +39 -0
- package/docs/adr/003-ulid-over-uuid.md +40 -0
- package/docs/adr/004-plugin-config-with-settings-overlay.md +34 -0
- package/docs/architecture.md +123 -0
- package/hooks/hooks.json +4 -0
- package/package.json +13 -7
- package/plugins/archivist/.claude-plugin/plugin.json +14 -0
- package/plugins/archivist/CHANGELOG.md +8 -0
- package/plugins/archivist/README.md +105 -0
- package/plugins/archivist/config.json +18 -0
- package/plugins/archivist/hooks/hooks.json +35 -0
- package/plugins/archivist/scripts/hooks/archivist-extract.sh +238 -0
- package/plugins/archivist/scripts/hooks/archivist-inject.sh +159 -0
- package/plugins/archivist/scripts/lib/archivist-config.sh +66 -0
- package/plugins/archivist/scripts/lib/archivist-project-key.sh +91 -0
- package/plugins/archivist/scripts/lib/archivist-storage.sh +215 -0
- package/plugins/archivist/scripts/lib/archivist-ulid.sh +52 -0
- package/plugins/cartographer/.claude-plugin/plugin.json +14 -0
- package/plugins/cartographer/CHANGELOG.md +27 -0
- package/plugins/cartographer/README.md +113 -0
- package/plugins/cartographer/config.json +21 -0
- package/plugins/cartographer/docs/adr/001-background-audit-launch.md +28 -0
- package/plugins/cartographer/docs/adr/002-flock-pid-file-fallback.md +30 -0
- package/plugins/cartographer/docs/adr/003-at-least-once-event-delivery.md +32 -0
- package/plugins/cartographer/docs/adr/004-exclude-paths-replace-semantics.md +27 -0
- package/plugins/cartographer/hooks/hooks.json +44 -0
- package/plugins/cartographer/scripts/hooks/cartographer-post-write.sh +87 -0
- package/plugins/cartographer/scripts/hooks/cartographer-session-start.sh +89 -0
- package/plugins/cartographer/scripts/lib/cartographer-analyze.sh +286 -0
- package/plugins/cartographer/scripts/lib/cartographer-collect.sh +59 -0
- package/plugins/cartographer/scripts/lib/cartographer-config.sh +105 -0
- package/plugins/cartographer/scripts/lib/cartographer-events.sh +82 -0
- package/plugins/cartographer/scripts/lib/cartographer-lock.sh +38 -0
- package/plugins/cartographer/scripts/lib/cartographer-project-key.sh +55 -0
- package/plugins/cartographer/scripts/lib/cartographer-ulid.sh +47 -0
- package/plugins/cartographer/scripts/run-audit.sh +309 -0
- package/plugins/cartographer/skills/cartographer/SKILL.md +154 -0
- package/plugins/echo/.claude-plugin/plugin.json +14 -0
- package/plugins/echo/CHANGELOG.md +24 -0
- package/plugins/echo/README.md +110 -0
- package/plugins/echo/config.json +15 -0
- package/plugins/echo/docs/adr/001-echo-as-separate-plugin.md +33 -0
- package/plugins/echo/docs/adr/002-direct-evaluation-vs-tribunal-pipeline.md +35 -0
- package/plugins/echo/docs/adr/003-stop-hook-trigger.md +40 -0
- package/plugins/echo/hooks/hooks.json +15 -0
- package/plugins/echo/scripts/hooks/echo-stop-gate.sh +366 -0
- package/plugins/echo/scripts/lib/echo-config.sh +108 -0
- package/plugins/echo/scripts/lib/echo-events.sh +74 -0
- package/plugins/echo/scripts/lib/echo-project-key.sh +81 -0
- package/plugins/echo/scripts/lib/echo-ulid.sh +46 -0
- package/plugins/tribunal/.claude-plugin/plugin.json +20 -0
- package/plugins/tribunal/CHANGELOG.md +10 -0
- package/plugins/tribunal/README.md +134 -0
- package/plugins/tribunal/agents/tribunal-actor.md +35 -0
- package/plugins/tribunal/agents/tribunal-judge-adversarial.md +51 -0
- package/plugins/tribunal/agents/tribunal-judge-security.md +47 -0
- package/plugins/tribunal/agents/tribunal-judge-standard.md +47 -0
- package/plugins/tribunal/agents/tribunal-meta-judge.md +61 -0
- package/plugins/tribunal/config.json +50 -0
- package/plugins/tribunal/docs/adr/001-actor-jury-meta-gate-loop.md +40 -0
- package/plugins/tribunal/docs/adr/002-majority-gate-policy.md +48 -0
- package/plugins/tribunal/hooks/hooks.json +15 -0
- package/plugins/tribunal/scripts/hooks/tribunal-stop-gate.sh +267 -0
- package/plugins/tribunal/scripts/lib/tribunal-aggregate.sh +65 -0
- package/plugins/tribunal/scripts/lib/tribunal-config.sh +101 -0
- package/plugins/tribunal/scripts/lib/tribunal-events.sh +97 -0
- package/plugins/tribunal/scripts/lib/tribunal-gate.sh +111 -0
- package/plugins/tribunal/scripts/lib/tribunal-jury.sh +102 -0
- package/plugins/tribunal/scripts/lib/tribunal-project-key.sh +84 -0
- package/plugins/tribunal/scripts/lib/tribunal-rubric.sh +153 -0
- package/plugins/tribunal/scripts/lib/tribunal-ulid.sh +50 -0
- package/plugins/tribunal/scripts/lib/tribunal-verdict.sh +127 -0
- package/plugins/tribunal/skills/tribunal/SKILL.md +129 -0
- package/release-please-config.json +59 -5
- package/scripts/coverage/bash-coverage.mjs +169 -0
- package/scripts/coverage/format-comment.mjs +120 -0
- package/scripts/coverage/run-coverage.mjs +151 -0
- package/scripts/hooks/agent-spawn-tracker.sh +4 -4
- package/scripts/hooks/prompt-rule-injector.sh +122 -0
- package/scripts/lib/portable-lock.sh +48 -0
- package/scripts/lib/prompt-rules.sh +207 -0
- package/scripts/lib/tool-history.sh +7 -8
- package/scripts/lib/validate-path.sh +4 -0
- package/scripts/lint/check-manifests.mjs +314 -0
- package/scripts/lint/check-references.mjs +311 -0
- package/skills/list-prompt-rules/SKILL.md +15 -0
- package/test/bats/archivist-config-files.bats +60 -0
- package/test/bats/archivist-config.bats +54 -0
- package/test/bats/archivist-inject.bats +73 -0
- package/test/bats/archivist-project-key.bats +75 -0
- package/test/bats/archivist-storage.bats +119 -0
- package/test/bats/archivist-ulid.bats +36 -0
- package/test/bats/cartographer-config.bats +107 -0
- package/test/bats/cartographer-lock.bats +77 -0
- package/test/bats/cartographer-ulid.bats +56 -0
- package/test/bats/config.bats +10 -10
- package/test/bats/echo-config.bats +90 -0
- package/test/bats/echo-events.bats +121 -0
- package/test/bats/echo-project-key.bats +115 -0
- package/test/bats/echo-stop-hook.bats +101 -0
- package/test/bats/echo-ulid.bats +38 -0
- package/test/bats/portable-lock.bats +62 -0
- package/test/bats/prompt-rules.bats +269 -0
- package/test/bats/tribunal-aggregate.bats +77 -0
- package/test/bats/tribunal-config.bats +86 -0
- package/test/bats/tribunal-events.bats +209 -0
- package/test/bats/tribunal-gate.bats +95 -0
- package/test/bats/tribunal-jury.bats +80 -0
- package/test/bats/tribunal-rubric.bats +119 -0
- package/test/bats/tribunal-stop-hook.bats +73 -0
- package/test/bats/tribunal-verdict.bats +71 -0
- package/test/fixtures/hook-inputs/user-prompt-submit-rule-match.json +8 -0
- package/test/fixtures/hook-inputs/user-prompt-submit-rule-nomatch.json +8 -0
- package/test/helpers/setup.bash +9 -0
- package/test/node/check-manifests.test.mjs +173 -0
- package/test/node/check-references.test.mjs +279 -0
- package/test/node/coverage.test.mjs +143 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
setup() {
|
|
4
|
+
# shellcheck source=../helpers/setup.bash
|
|
5
|
+
source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
|
|
6
|
+
load_validate_path
|
|
7
|
+
# shellcheck source=../../scripts/lib/prompt-rules.sh
|
|
8
|
+
source "${REPO_ROOT}/scripts/lib/prompt-rules.sh"
|
|
9
|
+
export CLAUDE_PLUGIN_ROOT="${REPO_ROOT}"
|
|
10
|
+
|
|
11
|
+
ensure_dir_exists "$ONLOOKER_PROMPT_RULES_SESSIONS_DIR"
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
write_global_rules() {
|
|
15
|
+
local body="$1"
|
|
16
|
+
printf '%s\n' "$body" > "$(prompt_rules_global_path)"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
write_project_rules() {
|
|
20
|
+
local project_dir="$1"
|
|
21
|
+
local body="$2"
|
|
22
|
+
mkdir -p "${project_dir}/.claude"
|
|
23
|
+
printf '%s\n' "$body" > "${project_dir}/.claude/prompt-rules.json"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@test "load_merged returns empty array when no rule files exist" {
|
|
27
|
+
local rules
|
|
28
|
+
rules=$(prompt_rules_load_merged "$TEST_HOME")
|
|
29
|
+
[ "$(echo "$rules" | jq 'length')" = "0" ]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@test "load_merged surfaces global rules when only global exists" {
|
|
33
|
+
write_global_rules '{
|
|
34
|
+
"rules": [
|
|
35
|
+
{"id": "r1", "pattern": "foo", "guidance": "global rule", "enabled": true}
|
|
36
|
+
]
|
|
37
|
+
}'
|
|
38
|
+
local rules
|
|
39
|
+
rules=$(prompt_rules_load_merged "$TEST_HOME")
|
|
40
|
+
[ "$(echo "$rules" | jq 'length')" = "1" ]
|
|
41
|
+
[ "$(echo "$rules" | jq -r '.[0].guidance')" = "global rule" ]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@test "load_merged: project overrides global by id" {
|
|
45
|
+
write_global_rules '{
|
|
46
|
+
"rules": [
|
|
47
|
+
{"id": "r1", "pattern": "foo", "guidance": "global version"},
|
|
48
|
+
{"id": "r2", "pattern": "bar", "guidance": "global only"}
|
|
49
|
+
]
|
|
50
|
+
}'
|
|
51
|
+
write_project_rules "$TEST_HOME" '{
|
|
52
|
+
"rules": [
|
|
53
|
+
{"id": "r1", "pattern": "foo", "guidance": "project version"}
|
|
54
|
+
]
|
|
55
|
+
}'
|
|
56
|
+
local rules
|
|
57
|
+
rules=$(prompt_rules_load_merged "$TEST_HOME")
|
|
58
|
+
[ "$(echo "$rules" | jq 'length')" = "2" ]
|
|
59
|
+
[ "$(echo "$rules" | jq -r '.[] | select(.id=="r1") | .guidance')" = "project version" ]
|
|
60
|
+
[ "$(echo "$rules" | jq -r '.[] | select(.id=="r2") | .guidance')" = "global only" ]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@test "load_merged filters out enabled: false rules" {
|
|
64
|
+
write_global_rules '{
|
|
65
|
+
"rules": [
|
|
66
|
+
{"id": "r1", "pattern": "foo", "guidance": "on"},
|
|
67
|
+
{"id": "r2", "pattern": "bar", "guidance": "off", "enabled": false}
|
|
68
|
+
]
|
|
69
|
+
}'
|
|
70
|
+
local rules
|
|
71
|
+
rules=$(prompt_rules_load_merged "$TEST_HOME")
|
|
72
|
+
[ "$(echo "$rules" | jq 'length')" = "1" ]
|
|
73
|
+
[ "$(echo "$rules" | jq -r '.[0].id')" = "r1" ]
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@test "pattern_matches: hit, miss, empty pattern" {
|
|
77
|
+
run prompt_rules_pattern_matches "hello world" "world"
|
|
78
|
+
[ "$status" -eq 0 ]
|
|
79
|
+
run prompt_rules_pattern_matches "hello world" "xyz"
|
|
80
|
+
[ "$status" -eq 1 ]
|
|
81
|
+
run prompt_rules_pattern_matches "hello world" ""
|
|
82
|
+
[ "$status" -eq 1 ]
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@test "pattern_matches: invalid ERE returns non-match without leaking stderr" {
|
|
86
|
+
# Unbalanced bracket — bash would normally print "syntax error in regular
|
|
87
|
+
# expression" to stderr and return 2. The helper must swallow both.
|
|
88
|
+
run prompt_rules_pattern_matches "anything" "[unterminated"
|
|
89
|
+
[ "$status" -eq 1 ]
|
|
90
|
+
[ -z "$stderr" ] || [ -z "${stderr// /}" ]
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@test "load_merged: tolerates non-array .rules and entries missing id" {
|
|
94
|
+
# Object instead of array, and rule entries with missing/non-string ids.
|
|
95
|
+
write_global_rules '{"rules": "not-an-array"}'
|
|
96
|
+
write_project_rules "$TEST_HOME" '{
|
|
97
|
+
"rules": [
|
|
98
|
+
{"pattern": "no-id"},
|
|
99
|
+
{"id": null, "pattern": "null-id"},
|
|
100
|
+
{"id": 42, "pattern": "non-string-id"},
|
|
101
|
+
{"id": "good", "pattern": "ok", "guidance": "ok"}
|
|
102
|
+
]
|
|
103
|
+
}'
|
|
104
|
+
local rules
|
|
105
|
+
rules=$(prompt_rules_load_merged "$TEST_HOME")
|
|
106
|
+
[ "$(echo "$rules" | jq 'length')" = "1" ]
|
|
107
|
+
[ "$(echo "$rules" | jq -r '.[0].id')" = "good" ]
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
@test "mark_fired + load_fired round-trip is idempotent" {
|
|
111
|
+
prompt_rules_mark_fired "sess-A" "rule-1"
|
|
112
|
+
prompt_rules_mark_fired "sess-A" "rule-1"
|
|
113
|
+
prompt_rules_mark_fired "sess-A" "rule-2"
|
|
114
|
+
local fired
|
|
115
|
+
fired=$(prompt_rules_load_fired "sess-A")
|
|
116
|
+
[ "$(echo "$fired" | jq 'length')" = "2" ]
|
|
117
|
+
[ "$(echo "$fired" | jq -r '. | sort | join(",")')" = "rule-1,rule-2" ]
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@test "hook injects additionalContext when prompt matches a rule" {
|
|
121
|
+
write_global_rules '{
|
|
122
|
+
"rules": [
|
|
123
|
+
{"id": "rule-no-verify", "pattern": "--no-verify", "guidance": "Skipping hooks usually masks the real issue."}
|
|
124
|
+
]
|
|
125
|
+
}'
|
|
126
|
+
local fixture="${REPO_ROOT}/test/fixtures/hook-inputs/user-prompt-submit-rule-match.json"
|
|
127
|
+
|
|
128
|
+
run bash -c "cat '${fixture}' | '${REPO_ROOT}/scripts/hooks/prompt-rule-injector.sh' 2>/dev/null"
|
|
129
|
+
[ "$status" -eq 0 ]
|
|
130
|
+
echo "$output" | jq -e \
|
|
131
|
+
'.hookSpecificOutput.hookEventName == "UserPromptSubmit"
|
|
132
|
+
and (.hookSpecificOutput.additionalContext | contains("Skipping hooks"))' >/dev/null
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
@test "hook outputs nothing when no rule matches" {
|
|
136
|
+
write_global_rules '{
|
|
137
|
+
"rules": [
|
|
138
|
+
{"id": "rule-no-verify", "pattern": "--no-verify", "guidance": "Skipping hooks usually masks the real issue."}
|
|
139
|
+
]
|
|
140
|
+
}'
|
|
141
|
+
local fixture="${REPO_ROOT}/test/fixtures/hook-inputs/user-prompt-submit-rule-nomatch.json"
|
|
142
|
+
|
|
143
|
+
run bash -c "cat '${fixture}' | '${REPO_ROOT}/scripts/hooks/prompt-rule-injector.sh' 2>/dev/null"
|
|
144
|
+
[ "$status" -eq 0 ]
|
|
145
|
+
[ -z "$output" ]
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
@test "hook fires a rule once per session" {
|
|
149
|
+
write_global_rules '{
|
|
150
|
+
"rules": [
|
|
151
|
+
{"id": "rule-no-verify", "pattern": "--no-verify", "guidance": "Skipping hooks usually masks the real issue."}
|
|
152
|
+
]
|
|
153
|
+
}'
|
|
154
|
+
local fixture="${REPO_ROOT}/test/fixtures/hook-inputs/user-prompt-submit-rule-match.json"
|
|
155
|
+
|
|
156
|
+
# First invocation: injects
|
|
157
|
+
local first_output
|
|
158
|
+
first_output=$(cat "$fixture" | "${REPO_ROOT}/scripts/hooks/prompt-rule-injector.sh" 2>/dev/null)
|
|
159
|
+
echo "$first_output" | jq -e '.hookSpecificOutput.additionalContext | contains("Skipping hooks")' >/dev/null
|
|
160
|
+
|
|
161
|
+
# Second invocation with same session: no injection
|
|
162
|
+
local second_output
|
|
163
|
+
second_output=$(cat "$fixture" | "${REPO_ROOT}/scripts/hooks/prompt-rule-injector.sh" 2>/dev/null)
|
|
164
|
+
[ -z "$second_output" ]
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
@test "hook emits prompt_rule.matched and prompt_rule.applied to events log" {
|
|
168
|
+
write_global_rules '{
|
|
169
|
+
"rules": [
|
|
170
|
+
{"id": "rule-no-verify", "pattern": "--no-verify", "guidance": "Skipping hooks usually masks the real issue."}
|
|
171
|
+
]
|
|
172
|
+
}'
|
|
173
|
+
: >"$ONLOOKER_EVENTS_LOG"
|
|
174
|
+
local fixture="${REPO_ROOT}/test/fixtures/hook-inputs/user-prompt-submit-rule-match.json"
|
|
175
|
+
|
|
176
|
+
cat "$fixture" | "${REPO_ROOT}/scripts/hooks/prompt-rule-injector.sh" >/dev/null 2>&1
|
|
177
|
+
|
|
178
|
+
grep -q '"event_type":"prompt_rule.matched"' "$ONLOOKER_EVENTS_LOG"
|
|
179
|
+
grep -q '"event_type":"prompt_rule.applied"' "$ONLOOKER_EVENTS_LOG"
|
|
180
|
+
grep -q '"rule_id":"rule-no-verify"' "$ONLOOKER_EVENTS_LOG"
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
@test "hook fires repeatedly when fire_once_per_session is false" {
|
|
184
|
+
write_global_rules '{
|
|
185
|
+
"rules": [
|
|
186
|
+
{"id": "rule-no-verify", "pattern": "--no-verify", "guidance": "Skipping hooks usually masks the real issue.", "fire_once_per_session": false}
|
|
187
|
+
]
|
|
188
|
+
}'
|
|
189
|
+
local fixture="${REPO_ROOT}/test/fixtures/hook-inputs/user-prompt-submit-rule-match.json"
|
|
190
|
+
|
|
191
|
+
# Both invocations must inject — explicit false should not get coerced to true.
|
|
192
|
+
local first second
|
|
193
|
+
first=$(cat "$fixture" | "${REPO_ROOT}/scripts/hooks/prompt-rule-injector.sh" 2>/dev/null)
|
|
194
|
+
second=$(cat "$fixture" | "${REPO_ROOT}/scripts/hooks/prompt-rule-injector.sh" 2>/dev/null)
|
|
195
|
+
|
|
196
|
+
echo "$first" | jq -e '.hookSpecificOutput.additionalContext | contains("Skipping hooks")' >/dev/null
|
|
197
|
+
echo "$second" | jq -e '.hookSpecificOutput.additionalContext | contains("Skipping hooks")' >/dev/null
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
@test "hook still emits prompt_rule.matched on subsequent match but not prompt_rule.applied" {
|
|
201
|
+
write_global_rules '{
|
|
202
|
+
"rules": [
|
|
203
|
+
{"id": "rule-no-verify", "pattern": "--no-verify", "guidance": "msg"}
|
|
204
|
+
]
|
|
205
|
+
}'
|
|
206
|
+
local fixture="${REPO_ROOT}/test/fixtures/hook-inputs/user-prompt-submit-rule-match.json"
|
|
207
|
+
|
|
208
|
+
cat "$fixture" | "${REPO_ROOT}/scripts/hooks/prompt-rule-injector.sh" >/dev/null 2>&1
|
|
209
|
+
: >"$ONLOOKER_EVENTS_LOG"
|
|
210
|
+
cat "$fixture" | "${REPO_ROOT}/scripts/hooks/prompt-rule-injector.sh" >/dev/null 2>&1
|
|
211
|
+
|
|
212
|
+
grep -q '"event_type":"prompt_rule.matched"' "$ONLOOKER_EVENTS_LOG"
|
|
213
|
+
! grep -q '"event_type":"prompt_rule.applied"' "$ONLOOKER_EVENTS_LOG"
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
@test "hook respects per_turn_max_chars: drops overflowing rules" {
|
|
217
|
+
write_global_rules '{
|
|
218
|
+
"rules": [
|
|
219
|
+
{"id": "r1", "pattern": "no-verify", "guidance": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"},
|
|
220
|
+
{"id": "r2", "pattern": "no-verify", "guidance": "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"}
|
|
221
|
+
]
|
|
222
|
+
}'
|
|
223
|
+
# Use a temp plugin root with a tiny char budget
|
|
224
|
+
local tmp_root="${BATS_TEST_TMPDIR}/tiny-budget-root"
|
|
225
|
+
mkdir -p "$tmp_root"
|
|
226
|
+
printf '%s\n' '{"plugin_name":"onlooker","prompt_rules":{"enabled":true,"per_turn_max_chars":120}}' > "$tmp_root/config.json"
|
|
227
|
+
|
|
228
|
+
local fixture="${REPO_ROOT}/test/fixtures/hook-inputs/user-prompt-submit-rule-match.json"
|
|
229
|
+
local output
|
|
230
|
+
output=$(CLAUDE_PLUGIN_ROOT="$tmp_root" cat "$fixture" | CLAUDE_PLUGIN_ROOT="$tmp_root" "${REPO_ROOT}/scripts/hooks/prompt-rule-injector.sh" 2>/dev/null)
|
|
231
|
+
|
|
232
|
+
# First rule fits (~98 chars); second pushes past 120, gets dropped.
|
|
233
|
+
echo "$output" | jq -e '.hookSpecificOutput.additionalContext | contains("AAAA") and (contains("BBBB") | not)' >/dev/null
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
@test "hook short-circuits when prompt_rules.enabled = false" {
|
|
237
|
+
write_global_rules '{
|
|
238
|
+
"rules": [
|
|
239
|
+
{"id": "r1", "pattern": "no-verify", "guidance": "should not fire"}
|
|
240
|
+
]
|
|
241
|
+
}'
|
|
242
|
+
local tmp_root="${BATS_TEST_TMPDIR}/disabled-root"
|
|
243
|
+
mkdir -p "$tmp_root"
|
|
244
|
+
printf '%s\n' '{"plugin_name":"onlooker","prompt_rules":{"enabled":false}}' > "$tmp_root/config.json"
|
|
245
|
+
|
|
246
|
+
local fixture="${REPO_ROOT}/test/fixtures/hook-inputs/user-prompt-submit-rule-match.json"
|
|
247
|
+
local output
|
|
248
|
+
output=$(CLAUDE_PLUGIN_ROOT="$tmp_root" "${REPO_ROOT}/scripts/hooks/prompt-rule-injector.sh" < "$fixture" 2>/dev/null)
|
|
249
|
+
[ -z "$output" ]
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
@test "list_table reports active rules and per-session fire status" {
|
|
253
|
+
write_global_rules '{
|
|
254
|
+
"rules": [
|
|
255
|
+
{"id": "r1", "pattern": "foo", "guidance": "global one"},
|
|
256
|
+
{"id": "r2", "pattern": "bar", "guidance": "global two"}
|
|
257
|
+
]
|
|
258
|
+
}'
|
|
259
|
+
prompt_rules_mark_fired "session-list" "r1"
|
|
260
|
+
|
|
261
|
+
local out
|
|
262
|
+
out=$(prompt_rules_list_table "session-list" "$TEST_HOME")
|
|
263
|
+
echo "$out" | grep -q "active rules: 2"
|
|
264
|
+
echo "$out" | grep -q "id: r1"
|
|
265
|
+
echo "$out" | grep -q "id: r2"
|
|
266
|
+
# r1 is fired, r2 is not
|
|
267
|
+
echo "$out" | grep -A2 "id: r1" | grep -q "fired: yes"
|
|
268
|
+
echo "$out" | grep -A2 "id: r2" | grep -q "fired: no"
|
|
269
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
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/tribunal"
|
|
8
|
+
export CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT"
|
|
9
|
+
# shellcheck disable=SC1091
|
|
10
|
+
source "${PLUGIN_ROOT}/scripts/lib/tribunal-aggregate.sh"
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
VERDICTS='[{"judge_id":"a","score":0.8},{"judge_id":"b","score":0.6},{"judge_id":"c","score":0.4}]'
|
|
14
|
+
|
|
15
|
+
@test "mean of [0.8, 0.6, 0.4] is 0.6" {
|
|
16
|
+
local v
|
|
17
|
+
v=$(tribunal_aggregate "mean" "$VERDICTS")
|
|
18
|
+
awk -v v="$v" 'BEGIN { exit !(v > 0.59 && v < 0.61) }'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@test "median of three is the middle" {
|
|
22
|
+
local v
|
|
23
|
+
v=$(tribunal_aggregate "median" "$VERDICTS")
|
|
24
|
+
awk -v v="$v" 'BEGIN { exit !(v > 0.59 && v < 0.61) }'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@test "median of four averages the two middle scores" {
|
|
28
|
+
local four='[{"judge_id":"a","score":0.2},{"judge_id":"b","score":0.4},{"judge_id":"c","score":0.6},{"judge_id":"d","score":0.8}]'
|
|
29
|
+
local v
|
|
30
|
+
v=$(tribunal_aggregate "median" "$four")
|
|
31
|
+
awk -v v="$v" 'BEGIN { exit !(v > 0.49 && v < 0.51) }'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@test "min picks the lowest score" {
|
|
35
|
+
local v
|
|
36
|
+
v=$(tribunal_aggregate "min" "$VERDICTS")
|
|
37
|
+
awk -v v="$v" 'BEGIN { exit !(v > 0.39 && v < 0.41) }'
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@test "weighted_mean degrades to mean in v0.1" {
|
|
41
|
+
local v
|
|
42
|
+
v=$(tribunal_aggregate "weighted_mean" "$VERDICTS")
|
|
43
|
+
awk -v v="$v" 'BEGIN { exit !(v > 0.59 && v < 0.61) }'
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@test "unknown method falls back to mean with warning on stderr" {
|
|
47
|
+
run bash -c '
|
|
48
|
+
source "${REPO_ROOT}/plugins/tribunal/scripts/lib/tribunal-aggregate.sh"
|
|
49
|
+
tribunal_aggregate "lottery" "[{\"score\":0.5},{\"score\":0.7}]" 2>&1
|
|
50
|
+
'
|
|
51
|
+
[ "$status" -eq 0 ]
|
|
52
|
+
[[ "$output" == *"unknown method lottery"* ]]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@test "empty verdicts aggregates to 0" {
|
|
56
|
+
local v
|
|
57
|
+
v=$(tribunal_aggregate "mean" "[]")
|
|
58
|
+
[ "$v" = "0" ]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@test "disagreement of identical scores is 0" {
|
|
62
|
+
local d
|
|
63
|
+
d=$(tribunal_disagreement '[{"score":0.7},{"score":0.7}]')
|
|
64
|
+
awk -v d="$d" 'BEGIN { exit !(d < 0.01) }'
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@test "disagreement of [0.2, 0.8] is 0.6" {
|
|
68
|
+
local d
|
|
69
|
+
d=$(tribunal_disagreement '[{"score":0.2},{"score":0.8}]')
|
|
70
|
+
awk -v d="$d" 'BEGIN { exit !(d > 0.59 && d < 0.61) }'
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@test "disagreement of single verdict is 0" {
|
|
74
|
+
local d
|
|
75
|
+
d=$(tribunal_disagreement '[{"score":0.7}]')
|
|
76
|
+
[ "$d" = "0" ]
|
|
77
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
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/tribunal"
|
|
8
|
+
export CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT"
|
|
9
|
+
# shellcheck disable=SC1091
|
|
10
|
+
source "${PLUGIN_ROOT}/scripts/lib/tribunal-config.sh"
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
@test "config defaults from plugin config.json: enabled" {
|
|
14
|
+
tribunal_config_load ""
|
|
15
|
+
run tribunal_config_enabled
|
|
16
|
+
[ "$status" -eq 0 ]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@test "stop_hook defaults to disabled" {
|
|
20
|
+
tribunal_config_load ""
|
|
21
|
+
run tribunal_config_stop_hook_enabled
|
|
22
|
+
[ "$status" -ne 0 ]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@test "user-level settings.json overlay can disable tribunal" {
|
|
26
|
+
mkdir -p "${HOME}/.claude"
|
|
27
|
+
printf '%s\n' '{"tribunal":{"enabled":false}}' > "${HOME}/.claude/settings.json"
|
|
28
|
+
tribunal_config_load ""
|
|
29
|
+
run tribunal_config_enabled
|
|
30
|
+
[ "$status" -ne 0 ]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@test "repo-level settings.json overrides user-level" {
|
|
34
|
+
mkdir -p "${HOME}/.claude"
|
|
35
|
+
printf '%s\n' '{"tribunal":{"enabled":false}}' > "${HOME}/.claude/settings.json"
|
|
36
|
+
local repo="${BATS_TEST_TMPDIR}/repo"
|
|
37
|
+
mkdir -p "${repo}/.claude"
|
|
38
|
+
printf '%s\n' '{"tribunal":{"enabled":true}}' > "${repo}/.claude/settings.json"
|
|
39
|
+
tribunal_config_load "$repo"
|
|
40
|
+
run tribunal_config_enabled
|
|
41
|
+
[ "$status" -eq 0 ]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@test "default judge_types is standard + adversarial" {
|
|
45
|
+
tribunal_config_load ""
|
|
46
|
+
local types
|
|
47
|
+
types=$(tribunal_config_get_json '.tribunal.session.judge_types')
|
|
48
|
+
[ "$types" = '["standard","adversarial"]' ]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@test "default gate_policy is majority" {
|
|
52
|
+
tribunal_config_load ""
|
|
53
|
+
local v
|
|
54
|
+
v=$(tribunal_config_get '.tribunal.session.gate_policy')
|
|
55
|
+
[ "$v" = "majority" ]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@test "judge model falls back to tribunal.judges.model when no per-type override" {
|
|
59
|
+
tribunal_config_load ""
|
|
60
|
+
local m
|
|
61
|
+
m=$(tribunal_config_judge_model "standard")
|
|
62
|
+
[ "$m" = "claude-opus-4-7" ]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@test "per-judge-type model override wins over fallback" {
|
|
66
|
+
mkdir -p "${HOME}/.claude"
|
|
67
|
+
printf '%s\n' '{"tribunal":{"judges":{"security":{"model":"claude-opus-4-7-deep"}}}}' > "${HOME}/.claude/settings.json"
|
|
68
|
+
tribunal_config_load ""
|
|
69
|
+
local m
|
|
70
|
+
m=$(tribunal_config_judge_model "security")
|
|
71
|
+
[ "$m" = "claude-opus-4-7-deep" ]
|
|
72
|
+
# Other types still fall through to the default.
|
|
73
|
+
m=$(tribunal_config_judge_model "standard")
|
|
74
|
+
[ "$m" = "claude-opus-4-7" ]
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@test "deep-merge preserves unset defaults" {
|
|
78
|
+
mkdir -p "${HOME}/.claude"
|
|
79
|
+
printf '%s\n' '{"tribunal":{"session":{"max_iterations":7}}}' > "${HOME}/.claude/settings.json"
|
|
80
|
+
tribunal_config_load ""
|
|
81
|
+
local mi gp
|
|
82
|
+
mi=$(tribunal_config_get '.tribunal.session.max_iterations')
|
|
83
|
+
gp=$(tribunal_config_get '.tribunal.session.gate_policy')
|
|
84
|
+
[ "$mi" = "7" ]
|
|
85
|
+
[ "$gp" = "majority" ]
|
|
86
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# Validates every emitted tribunal.* event against @onlooker-community/schema
|
|
4
|
+
# v2.1.0+. Builds a single event via the canonical emitter and asserts the
|
|
5
|
+
# resulting JSONL line passes validate().
|
|
6
|
+
|
|
7
|
+
setup() {
|
|
8
|
+
source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
|
|
9
|
+
setup_test_env
|
|
10
|
+
|
|
11
|
+
PLUGIN_ROOT="${REPO_ROOT}/plugins/tribunal"
|
|
12
|
+
export CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT"
|
|
13
|
+
export ONLOOKER_EVENTS_LOG="${ONLOOKER_DIR}/logs/onlooker-events.jsonl"
|
|
14
|
+
mkdir -p "$(dirname "$ONLOOKER_EVENTS_LOG")"
|
|
15
|
+
|
|
16
|
+
# tribunal-events.sh looks up onlooker-event.mjs relative to its plugin
|
|
17
|
+
# root, but tests set CLAUDE_PLUGIN_ROOT to plugins/tribunal — point the
|
|
18
|
+
# wrapper at the ecosystem copy directly so it does not have to walk.
|
|
19
|
+
export _ONLOOKER_EVENT_JS="${REPO_ROOT}/scripts/lib/onlooker-event.mjs"
|
|
20
|
+
|
|
21
|
+
# shellcheck disable=SC1091
|
|
22
|
+
source "${PLUGIN_ROOT}/scripts/lib/tribunal-events.sh"
|
|
23
|
+
|
|
24
|
+
export CLAUDE_SESSION_ID="bats-session-$$"
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
# Re-validate the latest event in the log against the schema.
|
|
28
|
+
_validate_latest_event() {
|
|
29
|
+
local last
|
|
30
|
+
last=$(tail -n 1 "$ONLOOKER_EVENTS_LOG")
|
|
31
|
+
[ -n "$last" ] || return 1
|
|
32
|
+
printf '%s' "$last" | ONLOOKER_DIR="$ONLOOKER_DIR" \
|
|
33
|
+
node "${REPO_ROOT}/scripts/lib/onlooker-event.mjs" validate >/dev/null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
TASK_ID="01J000000000000000000000TS"
|
|
37
|
+
ITER_ID="01J000000000000000000000IT"
|
|
38
|
+
JUDGE_ID="01J000000000000000000000JJ"
|
|
39
|
+
|
|
40
|
+
@test "session.start validates" {
|
|
41
|
+
local p
|
|
42
|
+
p=$(jq -n --arg t "$TASK_ID" '{
|
|
43
|
+
task_id: $t,
|
|
44
|
+
judge_types: ["standard","adversarial"],
|
|
45
|
+
gate_policy: "majority",
|
|
46
|
+
score_threshold: 0.75,
|
|
47
|
+
max_iterations: 3,
|
|
48
|
+
actor_model_id: "claude-sonnet-4-6",
|
|
49
|
+
judge_model_ids: ["claude-opus-4-7","claude-opus-4-7"],
|
|
50
|
+
meta_model_id: "claude-opus-4-7"
|
|
51
|
+
}')
|
|
52
|
+
tribunal_emit_event "tribunal.session.start" "$p"
|
|
53
|
+
run _validate_latest_event
|
|
54
|
+
[ "$status" -eq 0 ]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@test "iteration.start validates" {
|
|
58
|
+
local p
|
|
59
|
+
p=$(jq -n --arg t "$TASK_ID" --arg i "$ITER_ID" \
|
|
60
|
+
'{task_id: $t, iteration_id: $i, iteration_number: 0, trigger: "initial"}')
|
|
61
|
+
tribunal_emit_event "tribunal.iteration.start" "$p"
|
|
62
|
+
run _validate_latest_event
|
|
63
|
+
[ "$status" -eq 0 ]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@test "actor.start + actor.complete validate" {
|
|
67
|
+
local p
|
|
68
|
+
p=$(jq -n --arg t "$TASK_ID" --arg i "$ITER_ID" \
|
|
69
|
+
'{task_id: $t, iteration_id: $i, iteration_number: 0, actor_model_id: "claude-sonnet-4-6"}')
|
|
70
|
+
tribunal_emit_event "tribunal.actor.start" "$p"
|
|
71
|
+
_validate_latest_event
|
|
72
|
+
|
|
73
|
+
p=$(jq -n --arg t "$TASK_ID" --arg i "$ITER_ID" \
|
|
74
|
+
'{task_id: $t, success: true, duration_ms: 4200, iteration_id: $i, iteration_number: 0, artifact_kind: "patch", actor_model_id: "claude-sonnet-4-6"}')
|
|
75
|
+
tribunal_emit_event "tribunal.actor.complete" "$p"
|
|
76
|
+
run _validate_latest_event
|
|
77
|
+
[ "$status" -eq 0 ]
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@test "jury.empaneled validates" {
|
|
81
|
+
local p
|
|
82
|
+
p=$(jq -n --arg t "$TASK_ID" --arg i "$ITER_ID" --arg j "$JUDGE_ID" '{
|
|
83
|
+
task_id: $t,
|
|
84
|
+
iteration_id: $i,
|
|
85
|
+
judges: [
|
|
86
|
+
{judge_id: $j, judge_type: "standard", model_id: "claude-opus-4-7"},
|
|
87
|
+
{judge_id: ($j+"X"), judge_type: "adversarial", model_id: "claude-opus-4-7"}
|
|
88
|
+
],
|
|
89
|
+
panel_size: 2
|
|
90
|
+
}')
|
|
91
|
+
tribunal_emit_event "tribunal.jury.empaneled" "$p"
|
|
92
|
+
run _validate_latest_event
|
|
93
|
+
[ "$status" -eq 0 ]
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@test "judge.start + verdict validate" {
|
|
97
|
+
local p
|
|
98
|
+
p=$(jq -n --arg t "$TASK_ID" --arg i "$ITER_ID" --arg j "$JUDGE_ID" \
|
|
99
|
+
'{task_id: $t, iteration_id: $i, judge_id: $j, judge_type: "standard", judge_model_id: "claude-opus-4-7"}')
|
|
100
|
+
tribunal_emit_event "tribunal.judge.start" "$p"
|
|
101
|
+
_validate_latest_event
|
|
102
|
+
|
|
103
|
+
p=$(jq -n --arg t "$TASK_ID" --arg i "$ITER_ID" --arg j "$JUDGE_ID" '{
|
|
104
|
+
task_id: $t,
|
|
105
|
+
score: 0.82,
|
|
106
|
+
passed: true,
|
|
107
|
+
judge_type: "standard",
|
|
108
|
+
iteration_id: $i,
|
|
109
|
+
judge_id: $j,
|
|
110
|
+
judge_model_id: "claude-opus-4-7",
|
|
111
|
+
criteria_evaluated: ["correctness","completeness","clarity"],
|
|
112
|
+
strengths_count: 3,
|
|
113
|
+
weaknesses_count: 1,
|
|
114
|
+
confidence: 0.85,
|
|
115
|
+
feedback_summary: "looks fine"
|
|
116
|
+
}')
|
|
117
|
+
tribunal_emit_event "tribunal.verdict" "$p"
|
|
118
|
+
run _validate_latest_event
|
|
119
|
+
[ "$status" -eq 0 ]
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@test "meta.start + meta.complete validate (with bias_types)" {
|
|
123
|
+
local p
|
|
124
|
+
p=$(jq -n --arg t "$TASK_ID" --arg i "$ITER_ID" \
|
|
125
|
+
'{task_id: $t, iteration_id: $i, meta_model_id: "claude-opus-4-7", verdicts_reviewed: 2}')
|
|
126
|
+
tribunal_emit_event "tribunal.meta.start" "$p"
|
|
127
|
+
_validate_latest_event
|
|
128
|
+
|
|
129
|
+
p=$(jq -n --arg t "$TASK_ID" --arg i "$ITER_ID" '{
|
|
130
|
+
task_id: $t,
|
|
131
|
+
verdict_quality: "questionable",
|
|
132
|
+
bias_detected: true,
|
|
133
|
+
bias_types: ["verbosity","sycophancy"],
|
|
134
|
+
override_recommendation: "re-evaluate",
|
|
135
|
+
confidence: 0.7,
|
|
136
|
+
iteration_id: $i,
|
|
137
|
+
meta_model_id: "claude-opus-4-7"
|
|
138
|
+
}')
|
|
139
|
+
tribunal_emit_event "tribunal.meta.complete" "$p"
|
|
140
|
+
run _validate_latest_event
|
|
141
|
+
[ "$status" -eq 0 ]
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
@test "consensus.reached validates" {
|
|
145
|
+
local p
|
|
146
|
+
p=$(jq -n --arg t "$TASK_ID" --arg i "$ITER_ID" --arg j "$JUDGE_ID" '{
|
|
147
|
+
task_id: $t,
|
|
148
|
+
iteration_id: $i,
|
|
149
|
+
aggregated_score: 0.7,
|
|
150
|
+
passed: true,
|
|
151
|
+
aggregation_method: "weighted_mean",
|
|
152
|
+
judges: [{judge_id: $j, score: 0.8},{judge_id: ($j+"X"), score: 0.6}]
|
|
153
|
+
}')
|
|
154
|
+
tribunal_emit_event "tribunal.consensus.reached" "$p"
|
|
155
|
+
run _validate_latest_event
|
|
156
|
+
[ "$status" -eq 0 ]
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
@test "dissent.recorded validates with re-evaluate resolution" {
|
|
160
|
+
local p
|
|
161
|
+
p=$(jq -n --arg t "$TASK_ID" --arg i "$ITER_ID" --arg j "$JUDGE_ID" '{
|
|
162
|
+
task_id: $t,
|
|
163
|
+
iteration_id: $i,
|
|
164
|
+
disagreement_score: 0.5,
|
|
165
|
+
judges: [
|
|
166
|
+
{judge_id: $j, score: 0.85, passed: true},
|
|
167
|
+
{judge_id: ($j+"X"), score: 0.35, passed: false}
|
|
168
|
+
],
|
|
169
|
+
resolution: "re-evaluate"
|
|
170
|
+
}')
|
|
171
|
+
tribunal_emit_event "tribunal.dissent.recorded" "$p"
|
|
172
|
+
run _validate_latest_event
|
|
173
|
+
[ "$status" -eq 0 ]
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
@test "gate.passed and gate.blocked validate" {
|
|
177
|
+
local p
|
|
178
|
+
p=$(jq -n --arg t "$TASK_ID" --arg i "$ITER_ID" \
|
|
179
|
+
'{task_id: $t, iteration_id: $i, final_score: 0.82, iteration_number: 0, judges_consulted: 2}')
|
|
180
|
+
tribunal_emit_event "tribunal.gate.passed" "$p"
|
|
181
|
+
_validate_latest_event
|
|
182
|
+
|
|
183
|
+
p=$(jq -n --arg t "$TASK_ID" --arg i "$ITER_ID" '{
|
|
184
|
+
task_id: $t,
|
|
185
|
+
iteration_id: $i,
|
|
186
|
+
reason: "low_score",
|
|
187
|
+
final_score: 0.42,
|
|
188
|
+
iteration_number: 0,
|
|
189
|
+
will_retry: true,
|
|
190
|
+
retry_iteration_number: 1
|
|
191
|
+
}')
|
|
192
|
+
tribunal_emit_event "tribunal.gate.blocked" "$p"
|
|
193
|
+
run _validate_latest_event
|
|
194
|
+
[ "$status" -eq 0 ]
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
@test "session.complete validates with exhausted_iterations outcome" {
|
|
198
|
+
local p
|
|
199
|
+
p=$(jq -n --arg t "$TASK_ID" \
|
|
200
|
+
'{task_id: $t, outcome: "exhausted_iterations", final_score: 0.55, iterations_used: 3, total_duration_ms: 28000}')
|
|
201
|
+
tribunal_emit_event "tribunal.session.complete" "$p"
|
|
202
|
+
run _validate_latest_event
|
|
203
|
+
[ "$status" -eq 0 ]
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
@test "emission fails loudly on bogus event_type (schema rejects)" {
|
|
207
|
+
run tribunal_emit_event "tribunal.no.such.event" '{"task_id":"x"}'
|
|
208
|
+
[ "$status" -ne 0 ]
|
|
209
|
+
}
|