oh-my-opencode 5.1.54 → 5.1.55
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/dist/cli/index.js +29 -18
- package/dist/cli-node/index.js +29 -18
- package/dist/config/schema/agent-overrides.d.ts +64 -0
- package/dist/config/schema/categories.d.ts +8 -0
- package/dist/config/schema/oh-my-opencode-config.d.ts +64 -0
- package/dist/index.js +120 -48
- package/dist/oh-my-opencode.schema.json +112 -0
- package/dist/skills/browser/runtime/omowright/manifest.json +1 -1
- package/dist/skills/programming/SKILL.md +2 -0
- package/dist/skills/programming/references/testing/README.md +2 -2
- package/dist/skills/programming/scripts/typescript/check-test-junk-rules.ts +140 -0
- package/dist/skills/programming/scripts/typescript/check-test-junk.test.ts +125 -0
- package/dist/skills/programming/scripts/typescript/check-test-junk.ts +148 -0
- package/dist/skills/programming/scripts/typescript/mutation-proof.test.ts +43 -0
- package/dist/skills/programming/scripts/typescript/mutation-proof.ts +179 -0
- package/dist/skills/remove-ai-slops/SKILL.md +32 -20
- package/dist/skills/ulw-research/scripts/source-commands.mjs +3 -1
- package/dist/skills/ulw-research/scripts/source-integrity.test.ts +39 -2
- package/dist/tools/delegate-task/models-distribution.d.ts +1 -0
- package/dist/tui.js +28 -17
- package/package.json +16 -14
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +2 -0
- package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
- package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
- package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
- package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
- package/packages/omo-codex/plugin/components/rules/package.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +5 -5
- package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-recording-spawn-admission.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/package-lock.json +12 -12
- package/packages/omo-codex/plugin/package.json +1 -1
- package/packages/omo-codex/plugin/skills/browser/runtime/omowright/manifest.json +1 -1
- package/packages/omo-codex/plugin/skills/programming/SKILL.md +2 -0
- package/packages/omo-codex/plugin/skills/programming/references/testing/README.md +2 -2
- package/packages/omo-codex/plugin/skills/programming/scripts/typescript/check-test-junk-rules.ts +140 -0
- package/packages/omo-codex/plugin/skills/programming/scripts/typescript/check-test-junk.ts +148 -0
- package/packages/omo-codex/plugin/skills/programming/scripts/typescript/mutation-proof.ts +179 -0
- package/packages/omo-codex/plugin/skills/remove-ai-slops/SKILL.md +32 -20
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/source-commands.mjs +3 -1
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +4 -2
- package/packages/shared-skills/skills/browser/runtime/omowright/manifest.json +1 -1
- package/packages/shared-skills/skills/programming/SKILL.md +2 -0
- package/packages/shared-skills/skills/programming/references/testing/README.md +2 -2
- package/packages/shared-skills/skills/programming/scripts/typescript/check-test-junk-rules.ts +140 -0
- package/packages/shared-skills/skills/programming/scripts/typescript/check-test-junk.test.ts +125 -0
- package/packages/shared-skills/skills/programming/scripts/typescript/check-test-junk.ts +148 -0
- package/packages/shared-skills/skills/programming/scripts/typescript/mutation-proof.test.ts +43 -0
- package/packages/shared-skills/skills/programming/scripts/typescript/mutation-proof.ts +179 -0
- package/packages/shared-skills/skills/remove-ai-slops/SKILL.md +32 -20
- package/packages/shared-skills/skills/ulw-research/scripts/source-commands.mjs +3 -1
- package/packages/shared-skills/skills/ulw-research/scripts/source-integrity.test.ts +39 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: remove-ai-slops
|
|
3
|
-
description: "Removes AI-generated code smells from branch changes or an explicit file list behind regression tests. Use when the user asks to clean up, deslop,
|
|
3
|
+
description: "Removes AI-generated code smells from branch changes or an explicit file list behind regression tests, including junk tests (prose pins, source greps, copied inventories, mock echoes), each proven by its class's rule before a cut. Use when the user asks to clean up, deslop, remove AI-slop patterns, or prune tests from recent changes."
|
|
4
4
|
argument-hint: "[file ...]"
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -45,7 +45,7 @@ The core safety invariant: **behavior is locked by green tests before a single l
|
|
|
45
45
|
|
|
46
46
|
## Categories (what counts as slop)
|
|
47
47
|
|
|
48
|
-
The agent looks for these
|
|
48
|
+
The agent looks for these ten categories. The first three are stylistic, the next three are structural, the next two are about hidden cost, the ninth is tests, and the last is size.
|
|
49
49
|
|
|
50
50
|
### Stylistic
|
|
51
51
|
1. **Obvious comments** — comments restating code, trivial docstrings, section dividers, commented-out code, vague TODOs/Notes.
|
|
@@ -86,10 +86,13 @@ The agent looks for these nine categories. The first three are stylistic, the ne
|
|
|
86
86
|
|
|
87
87
|
**Hard rule**: only apply when behavior equivalence is obvious. Do NOT change algorithms with subtle correctness implications. Do NOT micro-optimize hot paths without a benchmark. If in doubt, SKIP.
|
|
88
88
|
|
|
89
|
-
###
|
|
90
|
-
9. **
|
|
89
|
+
### Tests
|
|
90
|
+
9. **Tests: missing or junk** — tests are code in scope like any other file, judged by the one table in the `programming` skill, `references/testing/junk-classes.md` (junk classes, retention bar, R/F/C/D marks, proof rule). Two directions:
|
|
91
|
+
- **Missing**: a behavior in the changed files that no test guards, where the repository keeps tests for that kind of behavior and a regression would otherwise pass unnoticed. ADD the narrowest test that passes the four-question authoring gate (`references/testing/README.md`); name its owner contract. A PROSE file (prompt, `SKILL.md`, rule, markdown) has no behavioral seam: cover only a machine-consumed value (parsed field, sentinel a runtime greps, a doc JSON sample through its real validator) or leave it to review.
|
|
92
|
+
- **Junk**: mark every in-scope test declaration `R` / `F` / `C` / `D`. A `D` or `C` needs the proof the table names for its class BEFORE the cut: for a behavior class (cannot fail, mock echo, source grep, duplicate contract, layer replay, implementation mirror, fabricated postcondition, input transcript) a mutation of the production owner run against the candidate and its named keeper on the unmodified test tree; for a prose pin the assertion line plus the repo's prose rule; for a change certificate the revert that turns it red; for a seam keeper the `knip`/grep showing no production caller; for a copied inventory the generator or consumer that owns it; for a capability restatement the delivery test that owns the promise (none: `F`, not `D`); for near-duplicate cases the one table that keeps every old assertion (typecheck after). A misnamed test is re-marked by what its assertions check, never cut for its name. A mutation proves `D` only when the keeper goes red and the candidate stays green (cannot fail) or both go red (redundant); a candidate that is the ONLY red is `R`, or `C` into the keeper and re-run. Mutations run in Phase 4b, serially, by the orchestrator, never by a batch worker. A test-only production seam (export, flag, getter, reset hook) whose last test was deleted goes with it.
|
|
93
|
+
- KEEP: everything under the retention bar (public API, protocol, config, migration, storage, security, platform, shipped-copy equality, Node-compat suites, issue regressions, machine-consumed text). A test that is red on the unchanged base is a product bug to report, never a deletion. Static or slow is never a reason to delete.
|
|
91
94
|
|
|
92
|
-
###
|
|
95
|
+
### Size
|
|
93
96
|
10. **Oversized modules** — any source file exceeding **250 pure LOC** (non-blank, non-comment lines). This is an architectural defect, not a style preference. Measure: `awk '!/^[[:space:]]*$/ && !/^[[:space:]]*(#|\/\/)/' <file> | wc -l`.
|
|
94
97
|
|
|
95
98
|
**When found, do NOT just flag it. Execute a full modular refactoring:**
|
|
@@ -147,16 +150,17 @@ For each in-scope source file:
|
|
|
147
150
|
|
|
148
151
|
1. Identify the public/observable behavior the file exposes (exported functions, HTTP handlers, CLI commands, classes used elsewhere).
|
|
149
152
|
2. Check whether existing tests cover that behavior. Use `git grep` / project test conventions to find related test files.
|
|
150
|
-
3. **If behavior
|
|
151
|
-
4. Run the test suite (or at minimum the relevant tests).
|
|
153
|
+
3. **If a behavior the cleanup could change is uncovered, write the narrowest test that passes the authoring gate (`programming/references/testing/README.md`) BEFORE editing the file**, and name the behavior it guards. It asserts observable outputs at the real boundary, never the implementation shape the cleanup is about to change, and never a value derived from the code under test. A PROSE file (prompt/`SKILL.md`/rule/markdown) is exempt: its wording is not behavior; skip the test and rely on review, or assert only a machine-consumed value.
|
|
154
|
+
4. Run the test suite (or at minimum the relevant tests). A test that is red on the unchanged base is recorded as a bug (baseline-red list), excluded from every proof, and neither fixed in this pass nor deleted; everything else must be **green** before any cleanup begins.
|
|
155
|
+
5. Run the `programming` skill's static detector over every in-scope test file, the ones written in step 3 included: `bun run <programming skill dir>/scripts/typescript/check-test-junk.ts <test files>` (exit 1 lists `file:line` per junk class it can see: source grep, prose pin, fixed sleep, `.only`/`.skip`, self-comparison, lower-bound cardinality). Its findings are category-9 candidates for the Tests Ledger (marked `R`/`F`/`C`/`D` by the table once its proof runs), never automatic cuts, and a test from step 3 that it flags is rewritten before the pass continues.
|
|
152
156
|
|
|
153
|
-
|
|
157
|
+
Only a broken runner stops the pass: if the suite cannot run at all, STOP and report. Do not proceed with cleanup on unverified ground.
|
|
154
158
|
|
|
155
159
|
### Phase 3: Cleanup plan — existence first, then smells
|
|
156
160
|
|
|
157
161
|
The largest, safest deletion is code that should not have existed. **Before categorizing smells, run the deletion ladder on each changed unit:**
|
|
158
162
|
|
|
159
|
-
- **Delete entirely** — the behavior is not needed (YAGNI, speculative, dead on arrival).
|
|
163
|
+
- **Delete entirely** — the behavior is not needed (YAGNI, speculative, dead on arrival); for a test, its contract is already owned by a keeper or it has none (category 9, with the proof).
|
|
160
164
|
- **Reuse** — an existing helper or pattern in this repo already does it; replace the reimplementation with a call to it.
|
|
161
165
|
- **Platform / stdlib / native / dependency** — the language stdlib, the runtime, or an already-installed dependency already does it (a hand-rolled date picker → `<input type="date">`, a custom query parser → `URLSearchParams`, a bespoke debounce → the util already imported).
|
|
162
166
|
- **Simplify in place** — it must exist; make it smaller.
|
|
@@ -183,11 +187,11 @@ File: src/bar.py
|
|
|
183
187
|
|
|
184
188
|
**Intentional shortcuts:** if the plan deliberately keeps a bounded simplification (a naive scan fine under N rows, a global lock, an O(n²) path), mark it in-code with a `debt:` comment naming the ceiling and the upgrade trigger (in omo, prefix with `// @allow` so the comment-checker treats it as intentional), and list it under "Remaining Risks / Deferred" in the report. That section is the debt ledger — a simplification with a known ceiling and no marker is indistinguishable from a bug.
|
|
185
189
|
|
|
186
|
-
Order rule (safest → riskiest): comments → dead code → defensive → duplication → complexity → abstraction/boundary → performance → tests → oversized-modules. This minimizes blast radius of any one change.
|
|
190
|
+
Order rule (safest → riskiest): comments → dead code → defensive → duplication → complexity → abstraction/boundary → performance → tests (`F` rows; `D`/`C` wait for Phase 4b) → oversized-modules. This minimizes blast radius of any one change.
|
|
187
191
|
|
|
188
192
|
### Phase 4: Parallel slop removal via `deep-low` agents in batches of 5
|
|
189
193
|
|
|
190
|
-
Files are processed by `deep-low` category agents with the `$omo:remove-ai-slops` skill loaded, **batched 5 at a time in parallel**. The executable skill name is `remove-ai-slops`. The `deep-low` category gives the agent enough thoroughness to correctly evaluate the
|
|
194
|
+
Files are processed by `deep-low` category agents with the `$omo:remove-ai-slops` skill loaded, **batched 5 at a time in parallel**. The executable skill name is `remove-ai-slops`. The `deep-low` category gives the agent enough thoroughness to correctly evaluate the 10 categories and respect the KEEP rules without slipping into surface fixes; the 5-wide batch is the sweet spot — more than 5 creates result-merging noise and context contention, fewer wastes parallelism.
|
|
191
195
|
|
|
192
196
|
**Batching protocol** (strict):
|
|
193
197
|
|
|
@@ -213,9 +217,9 @@ Remove AI slops from: {file_path}
|
|
|
213
217
|
|
|
214
218
|
First run the deletion ladder from Phase 3 on this file (delete entirely / reuse existing repo code / platform-stdlib-native / simplify in place); only code that must exist proceeds to smell removal.
|
|
215
219
|
|
|
216
|
-
Then evaluate EVERY category defined in this skill's "Categories (what counts as slop)" section, applying that section's KEEP and REFACTOR rules verbatim — the Categories section you have loaded is canonical, do not work from a restated subset.
|
|
220
|
+
Then evaluate EVERY category defined in this skill's "Categories (what counts as slop)" section, applying that section's KEEP and REFACTOR rules verbatim — the Categories section you have loaded is canonical, do not work from a restated subset. For a test file, do NOT cut yet: mark every declaration R/F/C/D against `programming/references/testing/junk-classes.md` and return one ledger row per D/C/F (`| test | mark | keeper or reason | proposed proof |`), where the proposed proof names the class's proof kind and, for a mutation, the owner file, the exact find/replace, and the keeper to run. This worker applies `F` rows in the test file it owns and reports them in the ledger as applied; `D`/`C` rows are returned uncut; it never mutates any file to prove a test - the orchestrator runs the proofs in Phase 4b.
|
|
217
221
|
|
|
218
|
-
Apply changes in this order (safest → riskiest): comments → dead code → defensive → duplication → complexity → abstraction/boundary → performance → oversized-modules.
|
|
222
|
+
Apply changes in this order (safest → riskiest): comments → dead code → defensive → duplication → complexity → abstraction/boundary → performance → tests (`F` rows; `D`/`C` wait for Phase 4b) → oversized-modules.
|
|
219
223
|
|
|
220
224
|
Hard constraints:
|
|
221
225
|
- Behavior MUST be preserved. When equivalence is not obvious, SKIP.
|
|
@@ -225,11 +229,15 @@ Hard constraints:
|
|
|
225
229
|
- Diff stays minimal and scoped to slop removal.
|
|
226
230
|
|
|
227
231
|
Report changes grouped by category. For each change, give before/after, why-slop, why-safe.
|
|
228
|
-
For each skipped issue, give reason.
|
|
232
|
+
For each skipped issue, give reason. For a test file, end with the ledger rows.
|
|
229
233
|
"""
|
|
230
234
|
)
|
|
231
235
|
```
|
|
232
236
|
|
|
237
|
+
### Phase 4b: Prove the test cuts, serially, then cut
|
|
238
|
+
|
|
239
|
+
After the last batch finishes and no worker is running, the orchestrator takes the collected ledger rows. For each row whose proof is a mutation, run the `programming` skill's `bun run <programming skill dir>/scripts/typescript/mutation-proof.ts --file <owner> --find <text> --replace <text> --test 'candidate=<argv>' --test 'keeper=<argv>'` (add `--json` for the ledger; each `--test` value is split on spaces with no quoting, so a filter that contains a space needs a wrapper script or a file-path target instead): it applies the find/replace to the production owner, runs the candidate and the keeper on the unmodified test tree, records both verdicts, and restores the owner byte for byte (sha256-checked) before the next row. One mutation at a time, never while another worker or test run is live, and never two on the same file; a mutation touches a file outside the diff only transiently and is never committed. Rows proven per the table become the cuts; a D or C whose proof fails or was not run is an R. Non-mutation proofs (prose pin, change certificate, seam keeper, copied inventory, capability restatement, near-duplicate cases) are checked here too, from the row's evidence.
|
|
240
|
+
|
|
233
241
|
**Batch failure handling**: a `multi_agent_v1.wait_agent` timeout only means no new mailbox update arrived, not that a `deep-low` agent failed. For long passes, require each child to send `WORKING: <file> - <current phase>` and `BLOCKED: <reason>` only when it cannot progress. Treat a running child as alive. Mark a file for retry only when the child is completed without the deliverable, ack-only after followup, explicitly `BLOCKED:`, or no longer running. Do NOT block the remaining 4 in that batch; collect successful results and retry the failed file once later. If retry also fails, escalate that file under "Issues Found & Fixed" in the final report.
|
|
234
242
|
|
|
235
243
|
### Phase 5: Verify with quality gates + critical review
|
|
@@ -284,8 +292,8 @@ Files: [N files]
|
|
|
284
292
|
|
|
285
293
|
Behavior Lock:
|
|
286
294
|
- Existing coverage: [N files already covered]
|
|
287
|
-
- Tests added: [M
|
|
288
|
-
- Baseline status: GREEN
|
|
295
|
+
- Tests added: [M gate-passing tests at path/to/test_X.py, each naming its contract]
|
|
296
|
+
- Baseline status: GREEN apart from the baseline-red list (recorded as bugs, excluded from proofs, not deleted): [none] OR [list]
|
|
289
297
|
|
|
290
298
|
Cleanup Plan:
|
|
291
299
|
- path/to/file1.ts: [ladder: 1 delete (native) + simplify-in-place] → [dead code → complexity → performance]
|
|
@@ -303,6 +311,12 @@ Per-File Results (each cut shows what replaces it):
|
|
|
303
311
|
- Obvious comments: 5 removed → nothing
|
|
304
312
|
- Over-defensive: 1 simplified (redundant isinstance on typed param)
|
|
305
313
|
|
|
314
|
+
Tests Ledger (category 9; one row per test declaration marked D/C/F, proofs run in Phase 4b):
|
|
315
|
+
| Test | Mark | Keeper / reason | Proof |
|
|
316
|
+
|---|---|---|---|
|
|
317
|
+
| `foo.test.ts > reads the source for "export"` | D | source grep; `foo.test.ts > exports parse()` owns the contract | mutation parse.ts `return tokens` -> `return []`: candidate green, keeper red; restored sha-equal |
|
|
318
|
+
| `skill.test.ts > mentions the gate` | D | prose pin on SKILL.md | assertion line + repo prose rule (no mutation applies) |
|
|
319
|
+
|
|
306
320
|
Quality Gates:
|
|
307
321
|
- Regression tests: PASS (12 tests, 0 failed)
|
|
308
322
|
- Lint: PASS
|
|
@@ -355,7 +369,5 @@ Final Status: CLEAN | ISSUES FIXED | REQUIRES ATTENTION
|
|
|
355
369
|
## Quality Assurance
|
|
356
370
|
|
|
357
371
|
- NEVER remove code that serves a functional purpose.
|
|
358
|
-
-
|
|
359
|
-
-
|
|
360
|
-
- If uncertain about a change, err on the side of keeping the original code.
|
|
361
|
-
- The default action when in doubt is SKIP, not GUESS.
|
|
372
|
+
- Preserve every proven contract: a test leaves only with its proof receipt (category 9), and a missing contract gets a gate-passing test, never a change certificate.
|
|
373
|
+
- When in doubt, SKIP and keep the original; never GUESS.
|
|
@@ -143,7 +143,9 @@ function declareOverKeptRaw(sourcesDir, id, input) {
|
|
|
143
143
|
const dir = join(sourcesDir, String(id))
|
|
144
144
|
if (input.kind !== "converted-only" || !existsSync(join(dir, "raw.bin")) || !existsSync(join(dir, "meta.json"))) return
|
|
145
145
|
const before = JSON.parse(readFileSync(join(dir, "meta.json"), "utf8"))
|
|
146
|
-
|
|
146
|
+
const raw = readFileSync(join(dir, "raw.bin"))
|
|
147
|
+
if (sha256Of(raw) !== before.sha256) throw new CliError(`snapshot_hash_mismatch: ${id} raw.bin no longer matches its pinned hash`)
|
|
148
|
+
Object.assign(input, { url: before.url, raw, kind: before.kind, observed_at: before.observed_at, declared: true })
|
|
147
149
|
delete input.path
|
|
148
150
|
}
|
|
149
151
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// omo-codex-install:
|
|
2
|
+
// omo-codex-install:433a9648567283696cca4884fe6998c6505e9bb7c6d463cc7df59f23eff71db3:9a2769cbf171ab2cdfdc3886d3c883f0a3f6956ad73617d310ab592eea1abb82
|
|
3
3
|
var __esm = (fn, res, err) => () => {
|
|
4
4
|
if (fn)
|
|
5
5
|
try {
|
|
@@ -10205,7 +10205,7 @@ var package_default;
|
|
|
10205
10205
|
var init_package = __esm(() => {
|
|
10206
10206
|
package_default = {
|
|
10207
10207
|
name: "@oh-my-opencode/omo-codex",
|
|
10208
|
-
version: "5.1.
|
|
10208
|
+
version: "5.1.55",
|
|
10209
10209
|
type: "module",
|
|
10210
10210
|
private: true,
|
|
10211
10211
|
description: "Codex harness adapter for oh-my-openagent. Vendored Codex plugin namespace (omo) + TypeScript installer + telemetry.",
|
|
@@ -19647,6 +19647,7 @@ var OmoAgentDefInputSchema = object({
|
|
|
19647
19647
|
prompt: string2().optional(),
|
|
19648
19648
|
model: string2().optional(),
|
|
19649
19649
|
models: array(OmoAgentModelEntrySchema).optional(),
|
|
19650
|
+
models_distribution: _enum(["sequential", "round_robin"]).optional(),
|
|
19650
19651
|
reasoning: OmoReasoningSchema.optional(),
|
|
19651
19652
|
variant: string2().optional(),
|
|
19652
19653
|
reasoningEffort: OmoReasoningEffortSchema.optional(),
|
|
@@ -19673,6 +19674,7 @@ var OmoCategoryConfigObjectSchema = object({
|
|
|
19673
19674
|
description: string2().optional(),
|
|
19674
19675
|
model: string2().optional(),
|
|
19675
19676
|
models: array(union([string2(), OmoFallbackModelObjectSchema])).optional(),
|
|
19677
|
+
models_distribution: _enum(["sequential", "round_robin"]).optional(),
|
|
19676
19678
|
reasoning: OmoReasoningSchema.optional(),
|
|
19677
19679
|
temperature: number2().min(0).max(2).optional(),
|
|
19678
19680
|
top_p: number2().min(0).max(1).optional(),
|
|
@@ -199,6 +199,8 @@ uv run scripts/python/check-no-excuse-rules.py <changed paths>
|
|
|
199
199
|
bash scripts/rust/check-no-excuse-rules.sh <changed paths>
|
|
200
200
|
# TypeScript
|
|
201
201
|
bun run scripts/typescript/check-no-excuse-rules.ts <changed paths>
|
|
202
|
+
# Any language, test files you touched: the junk classes a regex can see (candidates for "test your test")
|
|
203
|
+
bun run scripts/typescript/check-test-junk.ts <changed test paths>
|
|
202
204
|
```
|
|
203
205
|
|
|
204
206
|
### Step 2 — interpret
|
|
@@ -56,12 +56,12 @@ When delegating test-writing, hand the child the behavior the test must distingu
|
|
|
56
56
|
|
|
57
57
|
## After writing: test your test
|
|
58
58
|
|
|
59
|
-
A test earns its place by failing. Before the PR, for each new or reshaped test:
|
|
59
|
+
A test earns its place by failing. Before the PR, run `bun run scripts/typescript/check-test-junk.ts <test files>` for the classes a regex can see (source grep, prose pin, fixed sleep, `.only`/`.skip`, self-comparison, lower bound), then for each new or reshaped test:
|
|
60
60
|
|
|
61
61
|
1. **Break the owner** (flip the condition, drop the branch, return a constant, skip the write) and run the test. Confirm the mutated bytes are actually in the file before reading the result; a no-op edit "passes" identically to the original.
|
|
62
62
|
2. **Red for the intended reason?** Read the failure message: a loose pattern can match the base's generic error by coincidence (a false red), and a fake that simulates the failure by a different mechanism than the guard reads fails with the fix present.
|
|
63
63
|
3. **Still green?** Then one of: the test is vacuous (fix the assertion); the mutant is equivalent (a `try/catch`, fallback, or normalization maps it back to the same outcome: retarget the mutation at the observable claim); a second rule already produces the same outcome (make the input exercise only the rule under test and assert the recorded reason); or the mutated code is dead (delete the code, not the test's teeth).
|
|
64
|
-
4. **Restore byte for byte.** Never commit a mutation.
|
|
64
|
+
4. **Restore byte for byte.** Never commit a mutation. `bun run scripts/typescript/mutation-proof.ts --file <owner> --find <text> --replace <text> --test 'candidate=bun test <file>' --test 'keeper=bun test <file>'` does steps 1, 3 and 4 in one run and prints red/green per test.
|
|
65
65
|
|
|
66
66
|
Assertion shapes that survive this step:
|
|
67
67
|
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { extname } from "node:path"
|
|
2
|
+
|
|
3
|
+
export type RuleId =
|
|
4
|
+
| "source-grep"
|
|
5
|
+
| "prose-pin"
|
|
6
|
+
| "fixed-sleep"
|
|
7
|
+
| "focused"
|
|
8
|
+
| "skipped"
|
|
9
|
+
| "self-compare"
|
|
10
|
+
| "lower-bound"
|
|
11
|
+
|
|
12
|
+
export type Language = "typescript" | "python" | "rust" | "go"
|
|
13
|
+
|
|
14
|
+
export type ReadKind = "prose" | "source"
|
|
15
|
+
|
|
16
|
+
export type LineRule = {
|
|
17
|
+
readonly rule: RuleId
|
|
18
|
+
readonly languages: readonly Language[]
|
|
19
|
+
readonly pattern: RegExp
|
|
20
|
+
readonly message: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const RULE_IDS: readonly RuleId[] = [
|
|
24
|
+
"source-grep",
|
|
25
|
+
"prose-pin",
|
|
26
|
+
"fixed-sleep",
|
|
27
|
+
"focused",
|
|
28
|
+
"skipped",
|
|
29
|
+
"self-compare",
|
|
30
|
+
"lower-bound",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
export function languageOf(file: string): Language | undefined {
|
|
34
|
+
switch (extname(file)) {
|
|
35
|
+
case ".ts":
|
|
36
|
+
case ".tsx":
|
|
37
|
+
case ".mts":
|
|
38
|
+
case ".cts":
|
|
39
|
+
case ".js":
|
|
40
|
+
case ".jsx":
|
|
41
|
+
case ".mjs":
|
|
42
|
+
case ".cjs":
|
|
43
|
+
return "typescript"
|
|
44
|
+
case ".py":
|
|
45
|
+
return "python"
|
|
46
|
+
case ".rs":
|
|
47
|
+
return "rust"
|
|
48
|
+
case ".go":
|
|
49
|
+
return "go"
|
|
50
|
+
default:
|
|
51
|
+
return undefined
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const SLEEP_MESSAGE = "fixed sleep: wait on the event or state change, not on the clock"
|
|
56
|
+
const SKIP_MESSAGE = "skipped test: fix the test or delete it with its proof; never park it"
|
|
57
|
+
const LOWER_BOUND_MESSAGE = "lower bound on a cardinality: removing one item stays green; pin the exact set"
|
|
58
|
+
|
|
59
|
+
export const LINE_RULES: readonly LineRule[] = [
|
|
60
|
+
{
|
|
61
|
+
rule: "fixed-sleep",
|
|
62
|
+
languages: ["typescript"],
|
|
63
|
+
pattern: /\bsetTimeout\s*\(\s*(resolve|res|r|done|cb)\s*,\s*[1-9]|\bawait\s+(Bun\.)?sleep\s*\(\s*[1-9]|new Promise\s*\(\s*\(?\s*\w*\s*\)?\s*=>\s*setTimeout\s*\(\s*\w+\s*,\s*[1-9]/,
|
|
64
|
+
message: SLEEP_MESSAGE,
|
|
65
|
+
},
|
|
66
|
+
{ rule: "fixed-sleep", languages: ["python"], pattern: /\btime\.sleep\s*\(|\bawait\s+(asyncio|anyio|trio)\.sleep\s*\(\s*[0-9]/, message: SLEEP_MESSAGE },
|
|
67
|
+
{ rule: "fixed-sleep", languages: ["rust"], pattern: /\bthread::sleep\s*\(|\btokio::time::sleep\s*\(/, message: SLEEP_MESSAGE },
|
|
68
|
+
{ rule: "fixed-sleep", languages: ["go"], pattern: /\btime\.Sleep\s*\(/, message: SLEEP_MESSAGE },
|
|
69
|
+
{
|
|
70
|
+
rule: "focused",
|
|
71
|
+
languages: ["typescript"],
|
|
72
|
+
pattern: /\b(test|it|describe)\.only\s*\(/,
|
|
73
|
+
message: "focused test: the rest of the suite is skipped silently",
|
|
74
|
+
},
|
|
75
|
+
{ rule: "skipped", languages: ["typescript"], pattern: /\b(test|it|describe)\.skip\s*\(|\b(xit|xdescribe|xtest)\s*\(/, message: SKIP_MESSAGE },
|
|
76
|
+
{ rule: "skipped", languages: ["python"], pattern: /@pytest\.mark\.skip\b|\bpytest\.skip\s*\(|\bunittest\.skip\b/, message: SKIP_MESSAGE },
|
|
77
|
+
{ rule: "skipped", languages: ["rust"], pattern: /#\[ignore\b/, message: SKIP_MESSAGE },
|
|
78
|
+
{ rule: "skipped", languages: ["go"], pattern: /\bt\.Skip(f|Now)?\s*\(/, message: SKIP_MESSAGE },
|
|
79
|
+
{
|
|
80
|
+
rule: "lower-bound",
|
|
81
|
+
languages: ["typescript"],
|
|
82
|
+
pattern: /\.(length|size|count)\s*\)\s*\.(toBeGreaterThan\s*\(\s*[1-9]|toBeGreaterThanOrEqual\s*\(\s*([2-9]|\d{2,}))/,
|
|
83
|
+
message: LOWER_BOUND_MESSAGE,
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
rule: "lower-bound",
|
|
87
|
+
languages: ["python"],
|
|
88
|
+
pattern: /\bassert\s+len\s*\(.+\)\s*(>\s*[1-9]|>=\s*([2-9]|\d{2,}))/,
|
|
89
|
+
message: LOWER_BOUND_MESSAGE,
|
|
90
|
+
},
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
export const SELF_COMPARE: Readonly<Record<Language, RegExp>> = {
|
|
94
|
+
typescript: /\bexpect\s*\(\s*([^()]+?)\s*\)\s*\.(toBe|toEqual|toStrictEqual)\s*\(\s*\1\s*\)/,
|
|
95
|
+
python: /\bassert\s+([A-Za-z_][\w.]*)\s*==\s*\1\s*(#|$)/,
|
|
96
|
+
rust: /\bassert_eq!\s*\(\s*([^,()]+?)\s*,\s*\1\s*\)/,
|
|
97
|
+
go: /\b(Equal|Exactly)\s*\(\s*t\s*,\s*([^,()]+?)\s*,\s*\2\s*\)/,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const SOURCE_EXTENSIONS = /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs|py|rs|go)$/
|
|
101
|
+
const PROSE_EXTENSIONS = /\.(md|mdc|txt|prompt)$/
|
|
102
|
+
const READ_ASSIGNMENT = /\b(?:const|let|var)\s+(\w+)\s*=\s*(?:await\s+)?(?:\w+\.)?(?:readFileSync|readFile|readTextSync|Bun\.file|open|read_text|read)\s*\(([^\n]*)$/
|
|
103
|
+
const PYTHON_READ_ASSIGNMENT = /^\s*(\w+)\s*=\s*(?:\w+\.)?(?:open|Path|read_text)\s*\(([^\n]*)$/
|
|
104
|
+
const STRING_LITERAL = /["'`]([^"'`]+)["'`]/g
|
|
105
|
+
|
|
106
|
+
function readKindOf(callArguments: string): ReadKind | undefined {
|
|
107
|
+
for (const literal of callArguments.matchAll(STRING_LITERAL)) {
|
|
108
|
+
const value = literal[1] ?? ""
|
|
109
|
+
if (PROSE_EXTENSIONS.test(value)) return "prose"
|
|
110
|
+
if (SOURCE_EXTENSIONS.test(value)) return "source"
|
|
111
|
+
}
|
|
112
|
+
return undefined
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function collectReadVariables(lines: readonly string[], language: Language): ReadonlyMap<string, ReadKind> {
|
|
116
|
+
const variables = new Map<string, ReadKind>()
|
|
117
|
+
const pattern = language === "python" ? PYTHON_READ_ASSIGNMENT : READ_ASSIGNMENT
|
|
118
|
+
for (const line of lines) {
|
|
119
|
+
const match = pattern.exec(line)
|
|
120
|
+
if (match === null) continue
|
|
121
|
+
const kind = readKindOf(match[2] ?? "")
|
|
122
|
+
if (kind !== undefined && match[1] !== undefined) variables.set(match[1], kind)
|
|
123
|
+
}
|
|
124
|
+
return variables
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function greppedVariable(line: string, language: Language): string | undefined {
|
|
128
|
+
if (language === "python") {
|
|
129
|
+
return /\bassert\s+(?:not\s+)?["'][^"']*["']\s+(?:not\s+)?in\s+(\w+)/.exec(line)?.[1]
|
|
130
|
+
}
|
|
131
|
+
return (
|
|
132
|
+
/\bexpect\s*\(\s*(\w+)\s*\)\s*(?:\.not)?\.(?:toContain|toMatch|toMatchSnapshot|toMatchInlineSnapshot|toInclude|toStartWith|toEndWith)\s*\(/.exec(line)?.[1] ??
|
|
133
|
+
/\bexpect\s*\(\s*(\w+)\.(?:includes|match|indexOf|search)\s*\(/.exec(line)?.[1]
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export const READ_RULE_MESSAGES: Readonly<Record<ReadKind, { readonly rule: RuleId; readonly message: string }>> = {
|
|
138
|
+
prose: { rule: "prose-pin", message: "wording of a prompt/skill/doc is pinned; assert a machine-consumed value or write no test" },
|
|
139
|
+
source: { rule: "source-grep", message: "a source file is grepped as text; drive the module and assert behavior" },
|
|
140
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test"
|
|
2
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"
|
|
3
|
+
import { tmpdir } from "node:os"
|
|
4
|
+
import { join } from "node:path"
|
|
5
|
+
|
|
6
|
+
import type { Finding, RuleId } from "./check-test-junk"
|
|
7
|
+
|
|
8
|
+
const script = join(import.meta.dir, "check-test-junk.ts")
|
|
9
|
+
let root = ""
|
|
10
|
+
|
|
11
|
+
async function run(args: readonly string[]): Promise<{ readonly code: number; readonly findings: readonly Finding[] }> {
|
|
12
|
+
const child = Bun.spawn([process.execPath, script, "--json", ...args], { cwd: root, stdout: "pipe", stderr: "pipe" })
|
|
13
|
+
const [stdout, code] = await Promise.all([new Response(child.stdout).text(), child.exited])
|
|
14
|
+
const parsed: { readonly findings: readonly Finding[] } = JSON.parse(stdout)
|
|
15
|
+
return { code, findings: parsed.findings }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const JUNK_TS = `import { readFileSync } from "node:fs"
|
|
19
|
+
test("source grep", () => {
|
|
20
|
+
const source = readFileSync("src/parser.ts", "utf8")
|
|
21
|
+
expect(source).toContain("export function parse")
|
|
22
|
+
})
|
|
23
|
+
test("prose pin", () => {
|
|
24
|
+
const skill = readFileSync("SKILL.md", "utf8")
|
|
25
|
+
expect(skill).toContain("You are a senior engineer")
|
|
26
|
+
})
|
|
27
|
+
test("sleeps", async () => {
|
|
28
|
+
await new Promise((resolve) => setTimeout(resolve, 500))
|
|
29
|
+
})
|
|
30
|
+
test.only("focused", () => {})
|
|
31
|
+
it.skip("parked", () => {})
|
|
32
|
+
test("self compare", () => {
|
|
33
|
+
expect(config.prompt).toEqual(config.prompt)
|
|
34
|
+
})
|
|
35
|
+
test("lower bound", () => {
|
|
36
|
+
expect(masked.length).toBeGreaterThanOrEqual(3)
|
|
37
|
+
})
|
|
38
|
+
test("opted out", async () => {
|
|
39
|
+
await new Promise((resolve) => setTimeout(resolve, 50)) // test-junk-ok: fixed-sleep debounce window is the behavior under test
|
|
40
|
+
})
|
|
41
|
+
`
|
|
42
|
+
|
|
43
|
+
const CLEAN_TS = `import { readFileSync } from "node:fs"
|
|
44
|
+
test("parses a date when the input has a timezone", () => {
|
|
45
|
+
expect(parse("2026-01-01T00:00:00Z")).toEqual({ year: 2026 })
|
|
46
|
+
})
|
|
47
|
+
test("the packaged skill equals its source", () => {
|
|
48
|
+
const source = readFileSync("skills/x/SKILL.md", "utf8")
|
|
49
|
+
const packaged = readFileSync("dist/skills/x/SKILL.md", "utf8")
|
|
50
|
+
expect(packaged).toBe(source)
|
|
51
|
+
})
|
|
52
|
+
test("the bundle keeps the runtime entry", () => {
|
|
53
|
+
const bundle = readFileSync("dist/index.js", "utf8")
|
|
54
|
+
expect(bundle.length).toBeGreaterThan(0)
|
|
55
|
+
expect(parseExports(bundle)).toEqual(["main"])
|
|
56
|
+
})
|
|
57
|
+
test("yields one tick before asserting", async () => {
|
|
58
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
59
|
+
expect(flushed).toBe(true)
|
|
60
|
+
})
|
|
61
|
+
`
|
|
62
|
+
|
|
63
|
+
const JUNK_PY = `import time
|
|
64
|
+
def test_sleeps():
|
|
65
|
+
time.sleep(1)
|
|
66
|
+
def test_lower():
|
|
67
|
+
assert len(items) >= 2
|
|
68
|
+
def test_lower_nested():
|
|
69
|
+
assert len(select(items)) > 1
|
|
70
|
+
def test_prose():
|
|
71
|
+
body = Path("SKILL.md").read_text()
|
|
72
|
+
assert "You are" in body
|
|
73
|
+
`
|
|
74
|
+
|
|
75
|
+
const JUNK_GO = `func TestSleeps(t *testing.T) {
|
|
76
|
+
\ttime.Sleep(100 * time.Millisecond)
|
|
77
|
+
\tt.Skip("flaky")
|
|
78
|
+
}
|
|
79
|
+
`
|
|
80
|
+
|
|
81
|
+
describe("#given test files with mechanically detectable junk", () => {
|
|
82
|
+
beforeAll(() => {
|
|
83
|
+
root = mkdtempSync(join(process.env.RUN_TMP ?? tmpdir(), "check-test-junk-"))
|
|
84
|
+
mkdirSync(join(root, "src"), { recursive: true })
|
|
85
|
+
writeFileSync(join(root, "src", "junk.test.ts"), JUNK_TS)
|
|
86
|
+
writeFileSync(join(root, "src", "clean.test.ts"), CLEAN_TS)
|
|
87
|
+
writeFileSync(join(root, "src", "test_junk.py"), JUNK_PY)
|
|
88
|
+
writeFileSync(join(root, "src", "junk_test.go"), JUNK_GO)
|
|
89
|
+
writeFileSync(join(root, "src", "parser.ts"), "export const parse = () => readFileSync('x.ts')\n")
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
afterAll(() => rmSync(root, { recursive: true, force: true }))
|
|
93
|
+
|
|
94
|
+
test("#when the TypeScript fixture is scanned #then every class is reported at its line and the opted-out line is not", async () => {
|
|
95
|
+
const { code, findings } = await run(["src/junk.test.ts"])
|
|
96
|
+
const byRule = new Map<RuleId, number[]>()
|
|
97
|
+
for (const finding of findings) byRule.set(finding.rule, [...(byRule.get(finding.rule) ?? []), finding.line])
|
|
98
|
+
expect(code).toBe(1)
|
|
99
|
+
expect(byRule.get("source-grep")).toEqual([4])
|
|
100
|
+
expect(byRule.get("prose-pin")).toEqual([8])
|
|
101
|
+
expect(byRule.get("fixed-sleep")).toEqual([11])
|
|
102
|
+
expect(byRule.get("focused")).toEqual([13])
|
|
103
|
+
expect(byRule.get("skipped")).toEqual([14])
|
|
104
|
+
expect(byRule.get("self-compare")).toEqual([16])
|
|
105
|
+
expect(byRule.get("lower-bound")).toEqual([19])
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test("#when Python and Go fixtures are scanned #then their sleep, skip and lower-bound forms are reported", async () => {
|
|
109
|
+
const { findings } = await run(["src/test_junk.py", "src/junk_test.go"])
|
|
110
|
+
expect(findings.map((finding) => [finding.file.split("/").at(-1), finding.line, finding.rule])).toEqual([
|
|
111
|
+
["test_junk.py", 3, "fixed-sleep"],
|
|
112
|
+
["test_junk.py", 5, "lower-bound"],
|
|
113
|
+
["test_junk.py", 7, "lower-bound"],
|
|
114
|
+
["test_junk.py", 10, "prose-pin"],
|
|
115
|
+
["junk_test.go", 2, "fixed-sleep"],
|
|
116
|
+
["junk_test.go", 3, "skipped"],
|
|
117
|
+
])
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
test("#when a clean test file and a production file are scanned #then nothing is reported and the exit code is 0", async () => {
|
|
121
|
+
const { code, findings } = await run(["src/clean.test.ts", "src/parser.ts"])
|
|
122
|
+
expect(findings).toEqual([])
|
|
123
|
+
expect(code).toBe(0)
|
|
124
|
+
})
|
|
125
|
+
})
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Flag test files for the junk classes a regex can see (the rest need a reader):
|
|
4
|
+
*
|
|
5
|
+
* source-grep - a value read from a source file (.ts/.js/.py/.rs/.go/...) is grepped
|
|
6
|
+
* (toContain / toMatch / includes / `in`); byte equality between two reads is not flagged
|
|
7
|
+
* prose-pin - a value read from a prompt/skill/markdown file has its wording pinned
|
|
8
|
+
* (toContain / not.toContain / toMatch / toMatchSnapshot / `in`)
|
|
9
|
+
* fixed-sleep - setTimeout-as-wait, sleep(N), time.sleep, thread::sleep, time.Sleep
|
|
10
|
+
* focused - test.only / it.only / describe.only (a focused test silently skips the suite)
|
|
11
|
+
* skipped - .skip / xit / xdescribe / @pytest.mark.skip / #[ignore] / t.Skip
|
|
12
|
+
* self-compare - expect(x).toBe(x) / assert x == x / assert_eq!(x, x)
|
|
13
|
+
* lower-bound - toBeGreaterThan(N>=1) / toBeGreaterThanOrEqual(N>=2) on a length/size/count
|
|
14
|
+
* (removing one item stays green; a plain non-empty check is not flagged)
|
|
15
|
+
*
|
|
16
|
+
* Opt out per line with a trailing comment naming the rule and the reason:
|
|
17
|
+
* // test-junk-ok: fixed-sleep debounce window is the behavior under test
|
|
18
|
+
*
|
|
19
|
+
* Usage:
|
|
20
|
+
* bun run scripts/typescript/check-test-junk.ts [--json] <file-or-dir>...
|
|
21
|
+
*
|
|
22
|
+
* Exit codes: 0 no findings, 1 findings, 2 input error.
|
|
23
|
+
* Each class is explained in references/testing/junk-classes.md; a finding here is a candidate
|
|
24
|
+
* to prove with references/testing/README.md "test your test", not an automatic deletion.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { readdirSync, readFileSync, statSync } from "node:fs"
|
|
28
|
+
import { basename, extname, join } from "node:path"
|
|
29
|
+
import process from "node:process"
|
|
30
|
+
|
|
31
|
+
import {
|
|
32
|
+
collectReadVariables,
|
|
33
|
+
greppedVariable,
|
|
34
|
+
languageOf,
|
|
35
|
+
LINE_RULES,
|
|
36
|
+
READ_RULE_MESSAGES,
|
|
37
|
+
RULE_IDS,
|
|
38
|
+
SELF_COMPARE,
|
|
39
|
+
type RuleId,
|
|
40
|
+
} from "./check-test-junk-rules"
|
|
41
|
+
|
|
42
|
+
export type { RuleId } from "./check-test-junk-rules"
|
|
43
|
+
|
|
44
|
+
export type Finding = {
|
|
45
|
+
readonly file: string
|
|
46
|
+
readonly line: number
|
|
47
|
+
readonly rule: RuleId
|
|
48
|
+
readonly message: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const EXCLUDED_DIRS: ReadonlySet<string> = new Set([".git", "node_modules", "dist", "build", "target", "__pycache__", ".venv", "vendor"])
|
|
52
|
+
|
|
53
|
+
const TEST_FILE_PATTERNS: readonly RegExp[] = [
|
|
54
|
+
/\.(test|spec)\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/,
|
|
55
|
+
/_test\.go$/,
|
|
56
|
+
/(^|\/)test_[^/]*\.py$/,
|
|
57
|
+
/(^|\/)[^/]*_test\.py$/,
|
|
58
|
+
/\.rs$/,
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
const OPT_OUT = /test-junk-ok:\s*([a-z-]+)/
|
|
62
|
+
|
|
63
|
+
function isTestFile(file: string): boolean {
|
|
64
|
+
if (!TEST_FILE_PATTERNS.some((pattern) => pattern.test(file))) return false
|
|
65
|
+
// Rust unit tests live inside source files; only files declaring a test module count.
|
|
66
|
+
return extname(file) !== ".rs" || /#\[cfg\(test\)\]|#\[test\]/.test(readFileSync(file, "utf8"))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function* walk(root: string): Generator<string> {
|
|
70
|
+
if (statSync(root).isFile()) {
|
|
71
|
+
yield root
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
75
|
+
if (entry.isDirectory()) {
|
|
76
|
+
if (!EXCLUDED_DIRS.has(entry.name)) yield* walk(join(root, entry.name))
|
|
77
|
+
} else if (entry.isFile()) {
|
|
78
|
+
yield join(root, entry.name)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function optedOut(line: string, rule: RuleId): boolean {
|
|
84
|
+
const match = OPT_OUT.exec(line)
|
|
85
|
+
return match !== null && match[1] === rule
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function scanFile(file: string): readonly Finding[] {
|
|
89
|
+
const language = languageOf(file)
|
|
90
|
+
if (language === undefined) return []
|
|
91
|
+
const lines = readFileSync(file, "utf8").split("\n")
|
|
92
|
+
const readVariables = collectReadVariables(lines, language)
|
|
93
|
+
const findings: Finding[] = []
|
|
94
|
+
const report = (line: number, rule: RuleId, message: string): void => {
|
|
95
|
+
if (!optedOut(lines[line - 1] ?? "", rule)) findings.push({ file, line, rule, message })
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
lines.forEach((source, index) => {
|
|
99
|
+
const line = index + 1
|
|
100
|
+
for (const rule of LINE_RULES) {
|
|
101
|
+
if (rule.languages.includes(language) && rule.pattern.test(source)) report(line, rule.rule, rule.message)
|
|
102
|
+
}
|
|
103
|
+
if (SELF_COMPARE[language].test(source)) {
|
|
104
|
+
report(line, "self-compare", "the expected value is the value under test; derive it from the input")
|
|
105
|
+
}
|
|
106
|
+
const grepped = greppedVariable(source, language)
|
|
107
|
+
const kind = grepped === undefined ? undefined : readVariables.get(grepped)
|
|
108
|
+
if (kind !== undefined) report(line, READ_RULE_MESSAGES[kind].rule, READ_RULE_MESSAGES[kind].message)
|
|
109
|
+
})
|
|
110
|
+
return findings
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function scan(paths: readonly string[]): readonly Finding[] {
|
|
114
|
+
const findings: Finding[] = []
|
|
115
|
+
for (const root of paths) {
|
|
116
|
+
for (const file of walk(root)) {
|
|
117
|
+
if (isTestFile(file)) findings.push(...scanFile(file))
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return findings
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function main(argv: readonly string[]): number {
|
|
124
|
+
const json = argv.includes("--json")
|
|
125
|
+
const paths = argv.filter((arg) => arg !== "--json")
|
|
126
|
+
if (paths.length === 0) {
|
|
127
|
+
console.error(`usage: ${basename(import.meta.path)} [--json] <file-or-dir>...`)
|
|
128
|
+
return 2
|
|
129
|
+
}
|
|
130
|
+
for (const path of paths) {
|
|
131
|
+
try {
|
|
132
|
+
statSync(path)
|
|
133
|
+
} catch {
|
|
134
|
+
console.error(`error: ${path} does not exist`)
|
|
135
|
+
return 2
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const findings = scan(paths)
|
|
139
|
+
if (json) {
|
|
140
|
+
console.log(JSON.stringify({ findings, rules: RULE_IDS }, null, 2))
|
|
141
|
+
} else {
|
|
142
|
+
for (const finding of findings) console.log(`${finding.file}:${finding.line} ${finding.rule} ${finding.message}`)
|
|
143
|
+
console.log(findings.length === 0 ? "check-test-junk: no findings" : `check-test-junk: ${findings.length} finding(s)`)
|
|
144
|
+
}
|
|
145
|
+
return findings.length === 0 ? 0 : 1
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (import.meta.main) process.exit(main(process.argv.slice(2)))
|