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
|
@@ -194,6 +194,13 @@
|
|
|
194
194
|
]
|
|
195
195
|
}
|
|
196
196
|
},
|
|
197
|
+
"models_distribution": {
|
|
198
|
+
"type": "string",
|
|
199
|
+
"enum": [
|
|
200
|
+
"sequential",
|
|
201
|
+
"round_robin"
|
|
202
|
+
]
|
|
203
|
+
},
|
|
197
204
|
"fallback_models": {
|
|
198
205
|
"anyOf": [
|
|
199
206
|
{
|
|
@@ -746,6 +753,13 @@
|
|
|
746
753
|
]
|
|
747
754
|
}
|
|
748
755
|
},
|
|
756
|
+
"models_distribution": {
|
|
757
|
+
"type": "string",
|
|
758
|
+
"enum": [
|
|
759
|
+
"sequential",
|
|
760
|
+
"round_robin"
|
|
761
|
+
]
|
|
762
|
+
},
|
|
749
763
|
"fallback_models": {
|
|
750
764
|
"anyOf": [
|
|
751
765
|
{
|
|
@@ -1298,6 +1312,13 @@
|
|
|
1298
1312
|
]
|
|
1299
1313
|
}
|
|
1300
1314
|
},
|
|
1315
|
+
"models_distribution": {
|
|
1316
|
+
"type": "string",
|
|
1317
|
+
"enum": [
|
|
1318
|
+
"sequential",
|
|
1319
|
+
"round_robin"
|
|
1320
|
+
]
|
|
1321
|
+
},
|
|
1301
1322
|
"fallback_models": {
|
|
1302
1323
|
"anyOf": [
|
|
1303
1324
|
{
|
|
@@ -1850,6 +1871,13 @@
|
|
|
1850
1871
|
]
|
|
1851
1872
|
}
|
|
1852
1873
|
},
|
|
1874
|
+
"models_distribution": {
|
|
1875
|
+
"type": "string",
|
|
1876
|
+
"enum": [
|
|
1877
|
+
"sequential",
|
|
1878
|
+
"round_robin"
|
|
1879
|
+
]
|
|
1880
|
+
},
|
|
1853
1881
|
"fallback_models": {
|
|
1854
1882
|
"anyOf": [
|
|
1855
1883
|
{
|
|
@@ -2405,6 +2433,13 @@
|
|
|
2405
2433
|
]
|
|
2406
2434
|
}
|
|
2407
2435
|
},
|
|
2436
|
+
"models_distribution": {
|
|
2437
|
+
"type": "string",
|
|
2438
|
+
"enum": [
|
|
2439
|
+
"sequential",
|
|
2440
|
+
"round_robin"
|
|
2441
|
+
]
|
|
2442
|
+
},
|
|
2408
2443
|
"fallback_models": {
|
|
2409
2444
|
"anyOf": [
|
|
2410
2445
|
{
|
|
@@ -2957,6 +2992,13 @@
|
|
|
2957
2992
|
]
|
|
2958
2993
|
}
|
|
2959
2994
|
},
|
|
2995
|
+
"models_distribution": {
|
|
2996
|
+
"type": "string",
|
|
2997
|
+
"enum": [
|
|
2998
|
+
"sequential",
|
|
2999
|
+
"round_robin"
|
|
3000
|
+
]
|
|
3001
|
+
},
|
|
2960
3002
|
"fallback_models": {
|
|
2961
3003
|
"anyOf": [
|
|
2962
3004
|
{
|
|
@@ -3509,6 +3551,13 @@
|
|
|
3509
3551
|
]
|
|
3510
3552
|
}
|
|
3511
3553
|
},
|
|
3554
|
+
"models_distribution": {
|
|
3555
|
+
"type": "string",
|
|
3556
|
+
"enum": [
|
|
3557
|
+
"sequential",
|
|
3558
|
+
"round_robin"
|
|
3559
|
+
]
|
|
3560
|
+
},
|
|
3512
3561
|
"fallback_models": {
|
|
3513
3562
|
"anyOf": [
|
|
3514
3563
|
{
|
|
@@ -4061,6 +4110,13 @@
|
|
|
4061
4110
|
]
|
|
4062
4111
|
}
|
|
4063
4112
|
},
|
|
4113
|
+
"models_distribution": {
|
|
4114
|
+
"type": "string",
|
|
4115
|
+
"enum": [
|
|
4116
|
+
"sequential",
|
|
4117
|
+
"round_robin"
|
|
4118
|
+
]
|
|
4119
|
+
},
|
|
4064
4120
|
"fallback_models": {
|
|
4065
4121
|
"anyOf": [
|
|
4066
4122
|
{
|
|
@@ -4613,6 +4669,13 @@
|
|
|
4613
4669
|
]
|
|
4614
4670
|
}
|
|
4615
4671
|
},
|
|
4672
|
+
"models_distribution": {
|
|
4673
|
+
"type": "string",
|
|
4674
|
+
"enum": [
|
|
4675
|
+
"sequential",
|
|
4676
|
+
"round_robin"
|
|
4677
|
+
]
|
|
4678
|
+
},
|
|
4616
4679
|
"fallback_models": {
|
|
4617
4680
|
"anyOf": [
|
|
4618
4681
|
{
|
|
@@ -5165,6 +5228,13 @@
|
|
|
5165
5228
|
]
|
|
5166
5229
|
}
|
|
5167
5230
|
},
|
|
5231
|
+
"models_distribution": {
|
|
5232
|
+
"type": "string",
|
|
5233
|
+
"enum": [
|
|
5234
|
+
"sequential",
|
|
5235
|
+
"round_robin"
|
|
5236
|
+
]
|
|
5237
|
+
},
|
|
5168
5238
|
"fallback_models": {
|
|
5169
5239
|
"anyOf": [
|
|
5170
5240
|
{
|
|
@@ -5717,6 +5787,13 @@
|
|
|
5717
5787
|
]
|
|
5718
5788
|
}
|
|
5719
5789
|
},
|
|
5790
|
+
"models_distribution": {
|
|
5791
|
+
"type": "string",
|
|
5792
|
+
"enum": [
|
|
5793
|
+
"sequential",
|
|
5794
|
+
"round_robin"
|
|
5795
|
+
]
|
|
5796
|
+
},
|
|
5720
5797
|
"fallback_models": {
|
|
5721
5798
|
"anyOf": [
|
|
5722
5799
|
{
|
|
@@ -6269,6 +6346,13 @@
|
|
|
6269
6346
|
]
|
|
6270
6347
|
}
|
|
6271
6348
|
},
|
|
6349
|
+
"models_distribution": {
|
|
6350
|
+
"type": "string",
|
|
6351
|
+
"enum": [
|
|
6352
|
+
"sequential",
|
|
6353
|
+
"round_robin"
|
|
6354
|
+
]
|
|
6355
|
+
},
|
|
6272
6356
|
"fallback_models": {
|
|
6273
6357
|
"anyOf": [
|
|
6274
6358
|
{
|
|
@@ -6821,6 +6905,13 @@
|
|
|
6821
6905
|
]
|
|
6822
6906
|
}
|
|
6823
6907
|
},
|
|
6908
|
+
"models_distribution": {
|
|
6909
|
+
"type": "string",
|
|
6910
|
+
"enum": [
|
|
6911
|
+
"sequential",
|
|
6912
|
+
"round_robin"
|
|
6913
|
+
]
|
|
6914
|
+
},
|
|
6824
6915
|
"fallback_models": {
|
|
6825
6916
|
"anyOf": [
|
|
6826
6917
|
{
|
|
@@ -7373,6 +7464,13 @@
|
|
|
7373
7464
|
]
|
|
7374
7465
|
}
|
|
7375
7466
|
},
|
|
7467
|
+
"models_distribution": {
|
|
7468
|
+
"type": "string",
|
|
7469
|
+
"enum": [
|
|
7470
|
+
"sequential",
|
|
7471
|
+
"round_robin"
|
|
7472
|
+
]
|
|
7473
|
+
},
|
|
7376
7474
|
"fallback_models": {
|
|
7377
7475
|
"anyOf": [
|
|
7378
7476
|
{
|
|
@@ -7926,6 +8024,13 @@
|
|
|
7926
8024
|
]
|
|
7927
8025
|
}
|
|
7928
8026
|
},
|
|
8027
|
+
"models_distribution": {
|
|
8028
|
+
"type": "string",
|
|
8029
|
+
"enum": [
|
|
8030
|
+
"sequential",
|
|
8031
|
+
"round_robin"
|
|
8032
|
+
]
|
|
8033
|
+
},
|
|
7929
8034
|
"fallback_models": {
|
|
7930
8035
|
"anyOf": [
|
|
7931
8036
|
{
|
|
@@ -8487,6 +8592,13 @@
|
|
|
8487
8592
|
]
|
|
8488
8593
|
}
|
|
8489
8594
|
},
|
|
8595
|
+
"models_distribution": {
|
|
8596
|
+
"type": "string",
|
|
8597
|
+
"enum": [
|
|
8598
|
+
"sequential",
|
|
8599
|
+
"round_robin"
|
|
8600
|
+
]
|
|
8601
|
+
},
|
|
8490
8602
|
"fallback_models": {
|
|
8491
8603
|
"anyOf": [
|
|
8492
8604
|
{
|
|
@@ -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)))
|