opencode-skills-collection 4.0.56 → 4.0.58

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "updatedAt": "2026-09-08T01:47:16.781Z",
3
+ "updatedAt": "2026-09-10T01:49:52.309Z",
4
4
  "entries": [
5
5
  "00-andruia-consultant",
6
6
  "007",
@@ -368,6 +368,7 @@
368
368
  "brand-guidelines-community",
369
369
  "brand-perception-psychologist",
370
370
  "brave-man",
371
+ "break-ai-fix-loops",
371
372
  "brendangregg-use-tsa",
372
373
  "brevo-automation",
373
374
  "broken-authentication",
@@ -1750,6 +1751,7 @@
1750
1751
  "skill-installer",
1751
1752
  "skill-issue",
1752
1753
  "skill-optimizer",
1754
+ "skill-porter",
1753
1755
  "skill-rails-upgrade",
1754
1756
  "skill-router",
1755
1757
  "skill-scanner",
@@ -78,6 +78,26 @@ Before changing anything:
78
78
  - Verify its managed-only diff, required checks, merge result, and the resulting `origin/main`.
79
79
  - If an unmanaged repair remains, use a topic PR; never patch `main` directly.
80
80
 
81
+
82
+ ### Reviewed fork bundle exceptions
83
+
84
+ `tools/config/reviewed-fork-skills.json` is a protected-base ledger for the two
85
+ explicitly reviewed fork contributions #1337 and #1413. Each entry binds the
86
+ base repository, fork repository, PR number, original full reviewed head and
87
+ complete Git skill-tree object. It permits only Python files under that skill's
88
+ `scripts/` subtree and its root `LICENSE`, with a read-only Git copy origin when
89
+ needed. It does not allow workflows, arbitrary script types, generated-file
90
+ mutations, unsafe modes, links, invalid paths/objects or oversized content.
91
+
92
+ Both CI intake and `merge:batch` load the ledger from their trusted evaluator
93
+ checkout, never the PR's repository directory. Any change anywhere in the skill
94
+ subtree invalidates the exception. A base-only merge may reuse identical content,
95
+ but the maintainer must inspect the new complete PR diff and attest its exact
96
+ current head with `--reviewed-head`. Evidence, source-only checks, truthful skill
97
+ review, immutable PR/workflow binding and strict branch protection all remain
98
+ mandatory. Missing or malformed ledger data fails closed. Further exceptions or
99
+ policy expansions need explicit maintainer authorization and protected review.
100
+
81
101
  ## Workflow Contract Change Gate
82
102
 
83
103
  When changing maintainer scripts, workflows, or policy, update the canonical skill, maintainer documentation, and regression tests in the same source PR. Add a negative test for every failure mode being fixed, run the relevant dry-run path, and reject any implementation/documentation mismatch. Source PRs must exclude generated registries and plugin mirrors; the protected canonical-sync PR owns that derived state, except for files intentionally staged by the scripted protected-release flow.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright GitHub, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,211 @@
1
+ ---
2
+ name: break-ai-fix-loops
3
+ description: "Stop ineffective AI coding repair loops with stable failure fingerprints, a three-attempt budget, real-path proof, negative controls, and tested rollback."
4
+ category: code-quality
5
+ risk: critical
6
+ source: community
7
+ source_repo: twoicewoo/awesome-copilot
8
+ source_type: community
9
+ date_added: "2026-09-04"
10
+ author: twoicewoo
11
+ tags: [ai-agents, debugging, verification, negative-control, rollback]
12
+ tools: [claude-code, codex-cli, copilot, cursor, gemini-cli]
13
+ license: "MIT"
14
+ license_source: "https://github.com/twoicewoo/awesome-copilot/blob/886bf799bb05501bfd1afa7aae9cc5a77dedb03e/LICENSE"
15
+ ---
16
+
17
+ # Break AI Fix Loops
18
+
19
+ Replace patch-and-retry behavior with a bounded, evidence-producing repair. Treat a changed patch as progress only when an observable state changes.
20
+
21
+ ## When to Use This Skill
22
+
23
+ - Use when an AI coding agent cycles through similar patches without changing the observed failure.
24
+ - Use when a focused test passes but the installed, deployed, UI, API, persistence, or other real execution path still fails.
25
+ - Use when a repair claim needs a verifier that can reject a known-bad state and a rollback that has actually restored the baseline on a disposable copy.
26
+ - Do not use for a one-shot, already-understood edit whose acceptance check directly exercises the complete claimed behavior.
27
+
28
+ ## Establish the repair contract
29
+
30
+ Before the first edit, record:
31
+
32
+ - the exact defect and the behavior that would disprove it;
33
+ - the revision, configuration, input, and execution path under test;
34
+ - the baseline command, literal result, and exit status;
35
+ - the strongest check that directly observes the claimed behavior;
36
+ - the rollback command and the state it must restore.
37
+
38
+ Save raw evidence before normalizing it. Redact credentials, tokens, cookies, personal data, and private URLs. Never put secrets into a fingerprint record or committed ledger.
39
+
40
+ If the defect cannot be reproduced, stop editing. Report `INCONCLUSIVE` with the missing observation instead of guessing at a fix.
41
+
42
+ ## Use a three-attempt budget
43
+
44
+ Allow at most **three repair attempts for one acceptance claim**. An attempt begins when code, configuration, dependencies, generated artifacts, or test expectations change. Inspections and read-only probes do not consume an attempt.
45
+
46
+ Do not reset the budget because the agent restarts, opens a new session, rewrites the same patch, changes models, clears a cache, or renames the hypothesis. A newly exposed downstream failure still belongs to the same three-attempt budget unless it is a separately accepted task.
47
+
48
+ For every attempt, write these fields before the next edit:
49
+
50
+ | Field | Required evidence |
51
+ | --- | --- |
52
+ | Hypothesis | One causal mechanism, not a restatement of the symptom |
53
+ | Prediction | An observation that would distinguish this hypothesis from the previous one |
54
+ | Change | Exact changed paths and a patch or before/after hash |
55
+ | Focused check | Exact command, input, literal output, and exit status |
56
+ | Real-path check | Direct observation, or `NOT_RUN` with a reason |
57
+ | Symptom fingerprint | Stable fingerprint described below |
58
+ | Decision | `ADVANCE`, `SHIFT_CAUSE`, `PROVEN`, or `STOP` |
59
+
60
+ Use [the evidence ledger](references/evidence-ledger.md) as a copyable record.
61
+
62
+ ## Fingerprint the observable failure
63
+
64
+ Fingerprint what the system did, not the agent's explanation. Build a canonical record from:
65
+
66
+ ```json
67
+ {
68
+ "schema_version": 1,
69
+ "command": "the exact verification command",
70
+ "input_digest": "digest or stable identifier of the tested input",
71
+ "exit_code": 1,
72
+ "failure_class": "stable-machine-readable-class",
73
+ "stable_excerpt": "the smallest decisive output with volatile values removed",
74
+ "real_path_state": "the directly observed state, or NOT_OBSERVED"
75
+ }
76
+ ```
77
+
78
+ Keep the unedited output beside this sanitized record. Remove timestamps, run IDs, ANSI codes, random ports, and temporary paths from `stable_excerpt` only when they do not affect the defect. Do not normalize away values that could distinguish two causes.
79
+
80
+ Optionally compute the canonical SHA-256 fingerprint from this skill directory:
81
+
82
+ ```bash
83
+ python3 scripts/fingerprint.py evidence/attempt-1.json
84
+ ```
85
+
86
+ The helper validates the record, rejects unknown fields, and prints the fingerprint. It does not execute commands or redact evidence.
87
+
88
+ The helper uses only the Python 3.9+ standard library. When changing it, run its bundled regression tests:
89
+
90
+ ```bash
91
+ PYTHONDONTWRITEBYTECODE=1 python3 -m unittest scripts/test_fingerprint.py -v
92
+ ```
93
+
94
+ The same fingerprint after a different patch means the observable failure did not move. A cosmetically different message with the same failure class, input, command, and real-path state also counts as a repeated failure when the changed text is only volatile data. Do not use a patch hash in the symptom fingerprint; record it separately so different edits cannot masquerade as different outcomes.
95
+
96
+ ## Shift the root-cause strategy
97
+
98
+ Set the decision to `SHIFT_CAUSE` immediately when any of these occurs:
99
+
100
+ - a symptom fingerprint repeats;
101
+ - the patch changes but the decisive state does not;
102
+ - a focused test passes while the real path still fails;
103
+ - a retry produces no new discriminating evidence.
104
+
105
+ Then stop editing and perform this sequence:
106
+
107
+ 1. List the attempted mechanisms and the observation that falsified or failed to distinguish each one.
108
+ 2. Identify the next unobserved owner boundary along the live path: input, dispatch, configuration, dependency, generated artifact, process, persistence, network, or presentation.
109
+ 3. Collect one new observation at that boundary with tracing, logging, inspection, or a minimal probe.
110
+ 4. Form a replacement hypothesis that predicts a different observation and targets a different causal mechanism.
111
+ 5. Resume only if the new evidence can discriminate the replacement hypothesis. Otherwise return `BLOCKED`.
112
+
113
+ Do not spend an attempt on the same mechanism with broader edits. Do not weaken the assertion, skip the failing path, add a silent fallback, or update expected output merely to obtain green tests.
114
+
115
+ ## Prove the real execution path
116
+
117
+ Match proof to the claim. Bind every result to the exact revision, configuration, and input.
118
+
119
+ | Claim | Required direct observation |
120
+ | --- | --- |
121
+ | CLI behavior | Invoke the installed or built entry point as a user would |
122
+ | API or integration | Send a real request and observe response plus the responsible service boundary |
123
+ | UI behavior | Perform the real interaction and observe UI state plus relevant network or console evidence |
124
+ | Persistence | Write, reload in a new read path or process, and observe the stored value |
125
+ | Deployment | Exercise the deployed revision and prove which revision served the result |
126
+ | Agent or tool action | Observe the actual tool call and its external state change, not the agent's narration |
127
+
128
+ A unit test, mock, type check, build, open port, process liveness check, or model-written summary is supporting evidence only when the claim crosses a boundary it does not exercise.
129
+
130
+ ## Make the verifier prove it can fail
131
+
132
+ After the modified path passes, run a negative control on a disposable copy:
133
+
134
+ 1. Copy the verified modified state to a separate worktree or directory.
135
+ 2. Reintroduce the original defect or substitute a known-bad input that violates the same acceptance claim.
136
+ 3. Run the **same primary verification command** with the same relevant configuration.
137
+ 4. Require a non-zero exit status caused by the intended assertion.
138
+ 5. Record the exact command, input, literal output, exit status, and failure classification.
139
+
140
+ An unrelated crash, missing dependency, timeout, syntax error, or test-discovery failure is not a valid negative control. If the known-bad state exits zero, the verifier is false-green: return `INCONCLUSIVE`, repair the verifier, and do not claim the product fix is proven.
141
+
142
+ Return to the untouched modified tree and rerun the primary verification after the negative control.
143
+
144
+ ## Test rollback on another copy
145
+
146
+ Never test rollback only by undoing the working repair. Instead:
147
+
148
+ 1. Copy the verified modified state to another disposable worktree or directory.
149
+ 2. Run the documented rollback command there.
150
+ 3. Verify changed paths and hashes match the recorded baseline.
151
+ 4. Run the baseline command and confirm the prior behavior or status is restored.
152
+ 5. Leave the primary modified tree unchanged.
153
+
154
+ A rollback script that parses, prints help, or exits zero without restoring behavior has not been tested.
155
+
156
+ ## Finish with an evidence status
157
+
158
+ Use exactly one status:
159
+
160
+ - `PROVEN`: baseline defect observed; responsible change identified; focused and real-path checks pass; the known-bad negative control exits non-zero for the intended reason; rollback succeeds on another copy; the primary tree remains modified and passing.
161
+ - `INCONCLUSIVE`: some useful evidence exists, but a decisive gate is missing, false-green, or ambiguous.
162
+ - `BLOCKED`: the three-attempt budget is exhausted, a repeated fingerprint has no new discriminator, or a named external condition prevents the next observation.
163
+
164
+ Report exact commands, inputs, literal results, exit statuses, fingerprints, changed paths, revision, and remaining gaps. A passing proxy check or the phrase "tests pass" is never a substitute for those fields.
165
+
166
+ ## Examples
167
+
168
+ ### Repeated patch with no state change
169
+
170
+ ```text
171
+ Attempt 1: patch hash changed; focused test passed; real path still shows disabled.
172
+ Fingerprint: 08b4...; decision: SHIFT_CAUSE.
173
+ Next action: stop editing and observe the configuration-to-process boundary.
174
+ ```
175
+
176
+ ### Valid negative control
177
+
178
+ ```text
179
+ Modified copy: primary verifier exits 0 and observes the expected UI state.
180
+ Known-bad disposable copy: the same verifier exits 1 on the intended assertion.
181
+ Rollback copy: baseline hashes match and the baseline command restores its prior result.
182
+ Decision: PROVEN.
183
+ ```
184
+
185
+ ## Limitations
186
+
187
+ - This workflow cannot prove a repair when the defect is not reproducible, the real execution path is inaccessible, or the primary verifier cannot observe the acceptance claim.
188
+ - A three-attempt budget exposes stagnation; it does not identify the correct architecture or replace domain expertise.
189
+ - A known-bad control demonstrates that one verifier catches one defect class. It does not prove complete test coverage.
190
+ - Rollback verification covers the recorded paths and baseline behavior only; external systems need their own provider-side readback.
191
+
192
+ ## Security & Safety Notes
193
+
194
+ - This skill can guide changes to code, configuration, dependencies, generated artifacts, and files, so its risk is `critical`.
195
+ - Confirm the repository, target environment, accepted paths, and approval boundary before modifying state. Ask before destructive, irreversible, production, financial, credential, or external-message actions.
196
+ - Keep negative controls and rollback trials on disposable copies. Never inject a known defect into the primary working tree or a live environment.
197
+ - Keep raw evidence private when it may contain credentials, personal data, internal URLs, or customer content. Commit only sanitized records.
198
+ - `scripts/fingerprint.py` is a Python standard-library helper that reads one local JSON record and prints a digest; it does not run commands, access the network, redact data, or modify the record.
199
+
200
+ ## Related Skills
201
+
202
+ - `systematic-debugging` focuses on root-cause investigation before a fix; use this skill when attempts must also be fingerprinted, bounded, falsified with a negative control, and made reversible.
203
+ - `verification-before-completion` gates success claims on fresh evidence; this skill adds repair-attempt accounting and rollback proof.
204
+ - `closed-loop-delivery` spans acceptance through delivery; this skill is the narrower anti-stagnation and verifier-falsification protocol for repair loops.
205
+ - `audit-agent-run-evidence` performs a read-only post-run audit; this skill governs the repair while it is happening.
206
+
207
+
208
+ ## Source and license
209
+
210
+ The upstream MIT copyright and permission notice is preserved in
211
+ [LICENSE](LICENSE), alongside the commit-pinned provenance above.
@@ -0,0 +1,90 @@
1
+ # Repair evidence ledger
2
+
3
+ Copy this template to a task-owned path. Do not commit runtime evidence unless the project requires it. Preserve full raw output separately and keep this ledger free of secrets and personal data.
4
+
5
+ ```markdown
6
+ # Repair ledger
7
+
8
+ ## Contract
9
+ - Acceptance claim:
10
+ - Defect-disproving behavior:
11
+ - Baseline revision:
12
+ - Baseline configuration:
13
+ - Baseline input and digest:
14
+ - Baseline command:
15
+ - Baseline literal output/result:
16
+ - Baseline exit status:
17
+ - Real execution path:
18
+ - Primary verification command:
19
+ - Rollback command:
20
+ - Expected restored behavior/status:
21
+
22
+ ## Attempts
23
+ | # | Hypothesis | Discriminating prediction | Changed paths / patch hash | Focused result + exit | Real-path result + exit | Symptom fingerprint | Decision |
24
+ | --- | --- | --- | --- | --- | --- | --- | --- |
25
+ | 1 | | | | | | | |
26
+ | 2 | | | | | | | |
27
+ | 3 | | | | | | | |
28
+
29
+ ## Root-cause shifts
30
+ ### Shift after attempt <n>
31
+ - Repeated fingerprint or unchanged state:
32
+ - Mechanisms already attempted:
33
+ - Evidence against each mechanism:
34
+ - Next unobserved owner boundary:
35
+ - New observation:
36
+ - Replacement hypothesis:
37
+ - Different predicted observation:
38
+
39
+ ## Modified proof
40
+ - Revision:
41
+ - Exact command:
42
+ - Input/configuration:
43
+ - Literal output/result:
44
+ - Exit status:
45
+ - Real-path observation:
46
+
47
+ ## Negative control on disposable copy
48
+ - Copy path or worktree:
49
+ - Known-bad mutation/input:
50
+ - Exact primary verification command:
51
+ - Literal output/result:
52
+ - Exit status (must be non-zero):
53
+ - Intended failure classification:
54
+ - Untouched modified tree rerun result:
55
+ - Untouched modified tree rerun exit status:
56
+
57
+ ## Rollback on another copy
58
+ - Copy path or worktree:
59
+ - Exact rollback command:
60
+ - Literal rollback output/result:
61
+ - Rollback exit status:
62
+ - Baseline hash comparison:
63
+ - Restored behavior/status:
64
+ - Baseline command rerun exit status:
65
+ - Primary modified tree status:
66
+
67
+ ## Decision
68
+ - Status: PROVEN | INCONCLUSIVE | BLOCKED
69
+ - Attempts consumed: <0-3>
70
+ - Decisive evidence:
71
+ - Remaining gap or next discriminating observation:
72
+ ```
73
+
74
+ ## Fingerprint record
75
+
76
+ Create one sanitized JSON record for every observed symptom:
77
+
78
+ ```json
79
+ {
80
+ "schema_version": 1,
81
+ "command": "npm test -- --runInBand path/to/regression.test.js",
82
+ "input_digest": "sha256:replace-with-real-input-digest",
83
+ "exit_code": 1,
84
+ "failure_class": "assertion-mismatch",
85
+ "stable_excerpt": "expected enabled; observed disabled",
86
+ "real_path_state": "settings page still shows disabled after reload"
87
+ }
88
+ ```
89
+
90
+ Keep the primary verification command unchanged across attempts unless the contract was wrong. If it changes, record why and preserve results from both commands; otherwise a changed verifier can hide an unchanged defect.
@@ -0,0 +1,122 @@
1
+ #!/usr/bin/env python3
2
+ """Create a stable symptom fingerprint from a validated JSON record."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import hashlib
8
+ import json
9
+ import re
10
+ import sys
11
+ from pathlib import Path
12
+ from typing import Any, Mapping
13
+
14
+
15
+ REQUIRED_FIELDS = (
16
+ "schema_version",
17
+ "command",
18
+ "input_digest",
19
+ "exit_code",
20
+ "failure_class",
21
+ "stable_excerpt",
22
+ "real_path_state",
23
+ )
24
+ FAILURE_CLASS_PATTERN = re.compile(r"^[a-z0-9][a-z0-9._:-]*$")
25
+
26
+
27
+ class RecordError(ValueError):
28
+ """Raised when a record cannot produce a trustworthy fingerprint."""
29
+
30
+
31
+ def _reject_duplicate_fields(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
32
+ """Build a JSON object while rejecting ambiguous duplicate field names."""
33
+ value: dict[str, Any] = {}
34
+ for field, field_value in pairs:
35
+ if field in value:
36
+ raise RecordError(f"duplicate field: {field}")
37
+ value[field] = field_value
38
+ return value
39
+
40
+
41
+ def _normalize_text(value: str) -> str:
42
+ """Normalize transport-only differences without deleting meaningful values."""
43
+ return value.replace("\r\n", "\n").replace("\r", "\n")
44
+
45
+
46
+ def canonicalize(record: Mapping[str, Any]) -> dict[str, Any]:
47
+ unknown = sorted(set(record) - set(REQUIRED_FIELDS))
48
+ missing = sorted(set(REQUIRED_FIELDS) - set(record))
49
+ if missing:
50
+ raise RecordError(f"missing field(s): {', '.join(missing)}")
51
+ if unknown:
52
+ raise RecordError(f"unknown field(s): {', '.join(unknown)}")
53
+
54
+ if (
55
+ isinstance(record["schema_version"], bool)
56
+ or not isinstance(record["schema_version"], int)
57
+ or record["schema_version"] != 1
58
+ ):
59
+ raise RecordError("schema_version must be 1")
60
+ if isinstance(record["exit_code"], bool) or not isinstance(record["exit_code"], int):
61
+ raise RecordError("exit_code must be an integer")
62
+
63
+ canonical: dict[str, Any] = {
64
+ "schema_version": 1,
65
+ "exit_code": record["exit_code"],
66
+ }
67
+ for field in REQUIRED_FIELDS:
68
+ if field in ("schema_version", "exit_code"):
69
+ continue
70
+ value = record[field]
71
+ if not isinstance(value, str):
72
+ raise RecordError(f"{field} must be a string")
73
+ value = _normalize_text(value)
74
+ if not value:
75
+ raise RecordError(f"{field} must not be empty")
76
+ canonical[field] = value
77
+
78
+ if not FAILURE_CLASS_PATTERN.fullmatch(canonical["failure_class"]):
79
+ raise RecordError(
80
+ "failure_class must use lowercase letters, numbers, dot, underscore, colon, or hyphen"
81
+ )
82
+ return canonical
83
+
84
+
85
+ def fingerprint(record: Mapping[str, Any]) -> str:
86
+ payload = json.dumps(
87
+ canonicalize(record), ensure_ascii=False, sort_keys=True, separators=(",", ":")
88
+ ).encode("utf-8")
89
+ return f"sha256:{hashlib.sha256(payload).hexdigest()}"
90
+
91
+
92
+ def load_record(path: str) -> Mapping[str, Any]:
93
+ try:
94
+ if path == "-":
95
+ value = json.load(sys.stdin, object_pairs_hook=_reject_duplicate_fields)
96
+ else:
97
+ with Path(path).open("r", encoding="utf-8") as handle:
98
+ value = json.load(handle, object_pairs_hook=_reject_duplicate_fields)
99
+ except (OSError, UnicodeDecodeError, json.JSONDecodeError) as error:
100
+ raise RecordError(str(error)) from error
101
+ if not isinstance(value, dict):
102
+ raise RecordError("record must be a JSON object")
103
+ return value
104
+
105
+
106
+ def main(argv: list[str] | None = None) -> int:
107
+ parser = argparse.ArgumentParser(
108
+ description="Validate a symptom record and print its canonical SHA-256 fingerprint."
109
+ )
110
+ parser.add_argument("record", help="JSON record path, or - to read standard input")
111
+ args = parser.parse_args(argv)
112
+
113
+ try:
114
+ print(fingerprint(load_record(args.record)))
115
+ except RecordError as error:
116
+ print(f"fingerprint: {error}", file=sys.stderr)
117
+ return 2
118
+ return 0
119
+
120
+
121
+ if __name__ == "__main__":
122
+ raise SystemExit(main())
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env python3
2
+ """Regression tests for fingerprint.py."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import importlib.util
7
+ import json
8
+ import subprocess
9
+ import sys
10
+ import tempfile
11
+ import unittest
12
+ from pathlib import Path
13
+
14
+
15
+ SCRIPT = Path(__file__).with_name("fingerprint.py")
16
+ SPEC = importlib.util.spec_from_file_location("fix_loop_fingerprint", SCRIPT)
17
+ assert SPEC and SPEC.loader
18
+ MODULE = importlib.util.module_from_spec(SPEC)
19
+ SPEC.loader.exec_module(MODULE)
20
+
21
+
22
+ def record() -> dict[str, object]:
23
+ return {
24
+ "schema_version": 1,
25
+ "command": "python -m pytest tests/test_regression.py",
26
+ "input_digest": "sha256:0123456789abcdef",
27
+ "exit_code": 1,
28
+ "failure_class": "assertion-mismatch",
29
+ "stable_excerpt": "expected enabled\nobserved disabled",
30
+ "real_path_state": "settings remain disabled after reload",
31
+ }
32
+
33
+
34
+ class FingerprintTests(unittest.TestCase):
35
+ def test_key_order_and_transport_newlines_do_not_change_fingerprint(self) -> None:
36
+ first = record()
37
+ second = dict(reversed(list(first.items())))
38
+ second["stable_excerpt"] = "expected enabled\r\nobserved disabled"
39
+ self.assertEqual(MODULE.fingerprint(first), MODULE.fingerprint(second))
40
+
41
+ def test_meaningful_boundary_blank_lines_change_fingerprint(self) -> None:
42
+ first = record()
43
+ second = record()
44
+ second["stable_excerpt"] = "\nexpected enabled\nobserved disabled\n"
45
+ self.assertNotEqual(MODULE.fingerprint(first), MODULE.fingerprint(second))
46
+
47
+ def test_meaningful_trailing_whitespace_changes_fingerprint(self) -> None:
48
+ first = record()
49
+ second = record()
50
+ second["stable_excerpt"] = "expected enabled \nobserved disabled"
51
+ self.assertNotEqual(MODULE.fingerprint(first), MODULE.fingerprint(second))
52
+
53
+ def test_observable_state_change_changes_fingerprint(self) -> None:
54
+ first = record()
55
+ second = record()
56
+ second["real_path_state"] = "settings are enabled after reload"
57
+ self.assertNotEqual(MODULE.fingerprint(first), MODULE.fingerprint(second))
58
+
59
+ def test_unknown_field_fails_closed(self) -> None:
60
+ value = record()
61
+ value["patch_hash"] = "must-be-recorded-outside-the-symptom-fingerprint"
62
+ with self.assertRaisesRegex(MODULE.RecordError, "unknown field"):
63
+ MODULE.fingerprint(value)
64
+
65
+ def test_missing_field_fails_closed(self) -> None:
66
+ value = record()
67
+ del value["input_digest"]
68
+ with self.assertRaisesRegex(MODULE.RecordError, "missing field"):
69
+ MODULE.fingerprint(value)
70
+
71
+ def test_boolean_schema_version_fails_closed(self) -> None:
72
+ value = record()
73
+ value["schema_version"] = True
74
+ with self.assertRaisesRegex(MODULE.RecordError, "schema_version must be 1"):
75
+ MODULE.fingerprint(value)
76
+
77
+ def test_cli_rejects_duplicate_fields_with_exit_two(self) -> None:
78
+ duplicate_record = (
79
+ '{"schema_version":1,"command":"pytest","input_digest":"sha256:abc",'
80
+ '"exit_code":1,"failure_class":"assertion-mismatch",'
81
+ '"stable_excerpt":"first","stable_excerpt":"second",'
82
+ '"real_path_state":"disabled"}'
83
+ )
84
+ with tempfile.TemporaryDirectory() as directory:
85
+ path = Path(directory, "duplicate.json")
86
+ path.write_text(duplicate_record, encoding="utf-8")
87
+ result = subprocess.run(
88
+ [sys.executable, str(SCRIPT), str(path)],
89
+ check=False,
90
+ capture_output=True,
91
+ text=True,
92
+ )
93
+ self.assertEqual(result.returncode, 2)
94
+ self.assertIn("duplicate field: stable_excerpt", result.stderr)
95
+ self.assertNotIn("Traceback", result.stderr)
96
+
97
+ def test_cli_rejects_invalid_utf8_with_exit_two(self) -> None:
98
+ with tempfile.TemporaryDirectory() as directory:
99
+ path = Path(directory, "invalid-utf8.json")
100
+ path.write_bytes(b"\xff")
101
+ result = subprocess.run(
102
+ [sys.executable, str(SCRIPT), str(path)],
103
+ check=False,
104
+ capture_output=True,
105
+ text=True,
106
+ )
107
+ self.assertEqual(result.returncode, 2)
108
+ self.assertIn("invalid start byte", result.stderr)
109
+ self.assertNotIn("Traceback", result.stderr)
110
+
111
+ def test_cli_rejects_invalid_record_with_exit_two(self) -> None:
112
+ with tempfile.TemporaryDirectory() as directory:
113
+ path = Path(directory, "invalid.json")
114
+ path.write_text(json.dumps({"schema_version": 1}), encoding="utf-8")
115
+ result = subprocess.run(
116
+ [sys.executable, str(SCRIPT), str(path)],
117
+ check=False,
118
+ capture_output=True,
119
+ text=True,
120
+ )
121
+ self.assertEqual(result.returncode, 2)
122
+ self.assertIn("missing field", result.stderr)
123
+
124
+
125
+ if __name__ == "__main__":
126
+ unittest.main()