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
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"type": "command",
|
|
8
8
|
"command": "node \"${PLUGIN_ROOT}/components/rules/dist/cli.js\" hook user-prompt-submit",
|
|
9
9
|
"timeout": 10,
|
|
10
|
-
"statusMessage": "(OmO 5.1.
|
|
10
|
+
"statusMessage": "(OmO 5.1.55) Loading Project Rules",
|
|
11
11
|
"commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -File \"${PLUGIN_ROOT}\\components\\bootstrap\\scripts\\node-dispatch.ps1\" \"${PLUGIN_ROOT}\\components\\rules\\dist\\cli.js\" hook user-prompt-submit"
|
|
12
12
|
}
|
|
13
13
|
]
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sisyphuslabs/omo-codex-plugin",
|
|
3
|
-
"version": "5.1.
|
|
3
|
+
"version": "5.1.55",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@sisyphuslabs/omo-codex-plugin",
|
|
9
|
-
"version": "5.1.
|
|
9
|
+
"version": "5.1.55",
|
|
10
10
|
"workspaces": [
|
|
11
11
|
"components/comment-checker",
|
|
12
12
|
"components/git-bash",
|
|
@@ -100,7 +100,7 @@
|
|
|
100
100
|
},
|
|
101
101
|
"components/comment-checker": {
|
|
102
102
|
"name": "@code-yeongyu/codex-comment-checker",
|
|
103
|
-
"version": "5.1.
|
|
103
|
+
"version": "5.1.55",
|
|
104
104
|
"license": "MIT",
|
|
105
105
|
"bin": {
|
|
106
106
|
"omo-comment-checker": "dist/cli.js"
|
|
@@ -122,7 +122,7 @@
|
|
|
122
122
|
},
|
|
123
123
|
"components/git-bash": {
|
|
124
124
|
"name": "@sisyphuslabs/codex-git-bash-hook",
|
|
125
|
-
"version": "5.1.
|
|
125
|
+
"version": "5.1.55",
|
|
126
126
|
"bin": {
|
|
127
127
|
"omo-git-bash-hook": "dist/cli.js"
|
|
128
128
|
},
|
|
@@ -136,7 +136,7 @@
|
|
|
136
136
|
},
|
|
137
137
|
"components/lazycodex-executor-verify": {
|
|
138
138
|
"name": "@code-yeongyu/codex-lazycodex-executor-verify",
|
|
139
|
-
"version": "5.1.
|
|
139
|
+
"version": "5.1.55",
|
|
140
140
|
"license": "MIT",
|
|
141
141
|
"bin": {
|
|
142
142
|
"lazycodex-executor-verify": "dist/cli.js"
|
|
@@ -154,7 +154,7 @@
|
|
|
154
154
|
},
|
|
155
155
|
"components/lsp": {
|
|
156
156
|
"name": "@code-yeongyu/codex-lsp",
|
|
157
|
-
"version": "5.1.
|
|
157
|
+
"version": "5.1.55",
|
|
158
158
|
"license": "MIT",
|
|
159
159
|
"dependencies": {
|
|
160
160
|
"@code-yeongyu/lsp-daemon": "file:../../../../lsp-daemon",
|
|
@@ -176,7 +176,7 @@
|
|
|
176
176
|
},
|
|
177
177
|
"components/rules": {
|
|
178
178
|
"name": "@code-yeongyu/codex-rules",
|
|
179
|
-
"version": "5.1.
|
|
179
|
+
"version": "5.1.55",
|
|
180
180
|
"license": "MIT",
|
|
181
181
|
"dependencies": {
|
|
182
182
|
"picomatch": "^4.0.7"
|
|
@@ -199,7 +199,7 @@
|
|
|
199
199
|
},
|
|
200
200
|
"components/teammode": {
|
|
201
201
|
"name": "@sisyphuslabs/codex-teammode",
|
|
202
|
-
"version": "5.1.
|
|
202
|
+
"version": "5.1.55",
|
|
203
203
|
"devDependencies": {
|
|
204
204
|
"@types/node": "^26.6.2",
|
|
205
205
|
"@vitest/coverage-v8": "5.0.1",
|
|
@@ -213,7 +213,7 @@
|
|
|
213
213
|
},
|
|
214
214
|
"components/telemetry": {
|
|
215
215
|
"name": "@code-yeongyu/codex-telemetry",
|
|
216
|
-
"version": "5.1.
|
|
216
|
+
"version": "5.1.55",
|
|
217
217
|
"license": "MIT",
|
|
218
218
|
"bin": {
|
|
219
219
|
"omo-telemetry": "dist/cli.js"
|
|
@@ -232,7 +232,7 @@
|
|
|
232
232
|
},
|
|
233
233
|
"components/ultrawork": {
|
|
234
234
|
"name": "@code-yeongyu/codex-ultrawork",
|
|
235
|
-
"version": "5.1.
|
|
235
|
+
"version": "5.1.55",
|
|
236
236
|
"license": "MIT",
|
|
237
237
|
"bin": {
|
|
238
238
|
"omo-ultrawork": "dist/cli.js"
|
|
@@ -251,7 +251,7 @@
|
|
|
251
251
|
},
|
|
252
252
|
"components/ulw-execute-continuation": {
|
|
253
253
|
"name": "@code-yeongyu/codex-ulw-execute-continuation",
|
|
254
|
-
"version": "5.1.
|
|
254
|
+
"version": "5.1.55",
|
|
255
255
|
"license": "MIT",
|
|
256
256
|
"bin": {
|
|
257
257
|
"omo-ulw-execute-continuation": "dist/cli.js"
|
|
@@ -269,7 +269,7 @@
|
|
|
269
269
|
},
|
|
270
270
|
"components/ulw-loop": {
|
|
271
271
|
"name": "@code-yeongyu/codex-ulw-loop",
|
|
272
|
-
"version": "5.1.
|
|
272
|
+
"version": "5.1.55",
|
|
273
273
|
"license": "MIT",
|
|
274
274
|
"bin": {
|
|
275
275
|
"omo-ulw-loop": "dist/cli.js",
|
|
@@ -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
|
|
package/packages/omo-codex/plugin/skills/programming/scripts/typescript/check-test-junk-rules.ts
ADDED
|
@@ -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,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)))
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Prove that a test can fail: apply one exact-match edit to a production file, run each test
|
|
4
|
+
* command against the mutated owner, then restore the original bytes and verify their hash.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* bun run scripts/typescript/mutation-proof.ts --file <path> --find <text> --replace <text> \
|
|
8
|
+
* --test '<label>=<argv...>' [--test ...] [--cwd <dir>] [--json]
|
|
9
|
+
*
|
|
10
|
+
* --test takes "label=command words"; the command is split on spaces and quoting is not
|
|
11
|
+
* supported, so select a test by a pattern without spaces or run the whole file.
|
|
12
|
+
*
|
|
13
|
+
* Verdicts per test: red (exit != 0, the test caught the mutation) or green (exit 0, it did not).
|
|
14
|
+
* The owner is restored on success, on a thrown error, and on SIGINT/SIGTERM.
|
|
15
|
+
* Exit codes: 0 ran and restored, 1 the find text is empty or did not match exactly once or the
|
|
16
|
+
* restore hash differed (the file was still restored), 2 usage, 130/143 interrupted and restored.
|
|
17
|
+
* Read references/testing/junk-classes.md "Proof rule" for how to read a candidate/keeper pair.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { createHash } from "node:crypto"
|
|
21
|
+
import { readFileSync, writeFileSync } from "node:fs"
|
|
22
|
+
import { resolve } from "node:path"
|
|
23
|
+
import process from "node:process"
|
|
24
|
+
import { parseArgs } from "node:util"
|
|
25
|
+
|
|
26
|
+
export type MutationTest = { readonly label: string; readonly command: readonly string[] }
|
|
27
|
+
|
|
28
|
+
export type MutationOutcome = {
|
|
29
|
+
readonly label: string
|
|
30
|
+
readonly command: readonly string[]
|
|
31
|
+
readonly exitCode: number
|
|
32
|
+
readonly verdict: "red" | "green"
|
|
33
|
+
readonly outputTail: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type MutationResult = {
|
|
37
|
+
readonly file: string
|
|
38
|
+
readonly find: string
|
|
39
|
+
readonly replace: string
|
|
40
|
+
readonly originalSha256: string
|
|
41
|
+
readonly restoredSha256: string
|
|
42
|
+
readonly outcomes: readonly MutationOutcome[]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class MutationError extends Error {
|
|
46
|
+
constructor(
|
|
47
|
+
readonly kind: "FIND_EMPTY" | "FIND_NOT_UNIQUE" | "RESTORE_MISMATCH",
|
|
48
|
+
message: string,
|
|
49
|
+
) {
|
|
50
|
+
super(message)
|
|
51
|
+
this.name = "MutationError"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const sha256 = (bytes: Uint8Array): string => createHash("sha256").update(bytes).digest("hex")
|
|
56
|
+
|
|
57
|
+
async function runCaptured(command: readonly string[], cwd: string): Promise<{ readonly exitCode: number; readonly outputTail: string }> {
|
|
58
|
+
const child = Bun.spawn([...command], { cwd, stdin: "ignore", stdout: "pipe", stderr: "pipe" })
|
|
59
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
60
|
+
new Response(child.stdout).text(),
|
|
61
|
+
new Response(child.stderr).text(),
|
|
62
|
+
child.exited,
|
|
63
|
+
])
|
|
64
|
+
const lines = `${stdout}${stderr}`.trimEnd().split("\n")
|
|
65
|
+
return { exitCode, outputTail: lines.slice(-30).join("\n") }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function runMutation(options: {
|
|
69
|
+
readonly cwd: string
|
|
70
|
+
readonly file: string
|
|
71
|
+
readonly find: string
|
|
72
|
+
readonly replace: string
|
|
73
|
+
readonly tests: readonly MutationTest[]
|
|
74
|
+
}): Promise<MutationResult> {
|
|
75
|
+
if (options.find.length === 0) throw new MutationError("FIND_EMPTY", `${options.file}: find text is empty`)
|
|
76
|
+
const full = resolve(options.cwd, options.file)
|
|
77
|
+
const original = readFileSync(full)
|
|
78
|
+
const text = original.toString("utf8")
|
|
79
|
+
const occurrences = text.split(options.find).length - 1
|
|
80
|
+
if (occurrences !== 1) {
|
|
81
|
+
throw new MutationError("FIND_NOT_UNIQUE", `${options.file}: find matched ${occurrences} times (need exactly 1)`)
|
|
82
|
+
}
|
|
83
|
+
const originalSha256 = sha256(original)
|
|
84
|
+
const outcomes: MutationOutcome[] = []
|
|
85
|
+
const restoreOnSignal = (signal: NodeJS.Signals): void => {
|
|
86
|
+
writeFileSync(full, original)
|
|
87
|
+
process.exit(signal === "SIGINT" ? 130 : 143)
|
|
88
|
+
}
|
|
89
|
+
process.on("SIGINT", restoreOnSignal)
|
|
90
|
+
process.on("SIGTERM", restoreOnSignal)
|
|
91
|
+
try {
|
|
92
|
+
writeFileSync(full, text.replace(options.find, () => options.replace))
|
|
93
|
+
for (const test of options.tests) {
|
|
94
|
+
const run = await runCaptured(test.command, options.cwd)
|
|
95
|
+
outcomes.push({
|
|
96
|
+
label: test.label,
|
|
97
|
+
command: test.command,
|
|
98
|
+
exitCode: run.exitCode,
|
|
99
|
+
verdict: run.exitCode === 0 ? "green" : "red",
|
|
100
|
+
outputTail: run.outputTail,
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
} finally {
|
|
104
|
+
writeFileSync(full, original)
|
|
105
|
+
process.off("SIGINT", restoreOnSignal)
|
|
106
|
+
process.off("SIGTERM", restoreOnSignal)
|
|
107
|
+
}
|
|
108
|
+
const restoredSha256 = sha256(readFileSync(full))
|
|
109
|
+
if (restoredSha256 !== originalSha256) {
|
|
110
|
+
throw new MutationError("RESTORE_MISMATCH", `${options.file} not restored byte for byte`)
|
|
111
|
+
}
|
|
112
|
+
return { file: options.file, find: options.find, replace: options.replace, originalSha256, restoredSha256, outcomes }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function parseTest(spec: string): MutationTest | undefined {
|
|
116
|
+
const separator = spec.indexOf("=")
|
|
117
|
+
if (separator <= 0) return undefined
|
|
118
|
+
const label = spec.slice(0, separator)
|
|
119
|
+
const command = spec
|
|
120
|
+
.slice(separator + 1)
|
|
121
|
+
.split(" ")
|
|
122
|
+
.filter((word) => word.length > 0)
|
|
123
|
+
return command.length === 0 ? undefined : { label, command }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function usage(): number {
|
|
127
|
+
console.error(
|
|
128
|
+
"usage: mutation-proof.ts --file <path> --find <text> --replace <text> --test '<label>=<argv...>' [--test ...] [--cwd <dir>] [--json]",
|
|
129
|
+
)
|
|
130
|
+
return 2
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function main(argv: readonly string[]): Promise<number> {
|
|
134
|
+
const { values } = parseArgs({
|
|
135
|
+
args: [...argv],
|
|
136
|
+
options: {
|
|
137
|
+
file: { type: "string" },
|
|
138
|
+
find: { type: "string" },
|
|
139
|
+
replace: { type: "string" },
|
|
140
|
+
test: { type: "string", multiple: true },
|
|
141
|
+
cwd: { type: "string", default: process.cwd() },
|
|
142
|
+
json: { type: "boolean", default: false },
|
|
143
|
+
},
|
|
144
|
+
strict: true,
|
|
145
|
+
})
|
|
146
|
+
if (values.file === undefined || values.find === undefined || values.replace === undefined || values.test === undefined) {
|
|
147
|
+
return usage()
|
|
148
|
+
}
|
|
149
|
+
const tests: MutationTest[] = []
|
|
150
|
+
for (const spec of values.test) {
|
|
151
|
+
const parsed = parseTest(spec)
|
|
152
|
+
if (parsed === undefined) return usage()
|
|
153
|
+
tests.push(parsed)
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
const result = await runMutation({
|
|
157
|
+
cwd: values.cwd ?? process.cwd(),
|
|
158
|
+
file: values.file,
|
|
159
|
+
find: values.find,
|
|
160
|
+
replace: values.replace,
|
|
161
|
+
tests,
|
|
162
|
+
})
|
|
163
|
+
if (values.json) {
|
|
164
|
+
console.log(JSON.stringify(result, null, 2))
|
|
165
|
+
} else {
|
|
166
|
+
for (const outcome of result.outcomes) console.log(`${outcome.label}: ${outcome.verdict} (exit ${outcome.exitCode})`)
|
|
167
|
+
console.log(`restored ${result.file} (sha256 ${result.restoredSha256.slice(0, 12)})`)
|
|
168
|
+
}
|
|
169
|
+
return 0
|
|
170
|
+
} catch (error) {
|
|
171
|
+
if (error instanceof MutationError) {
|
|
172
|
+
console.error(`mutation-proof: ${error.kind}: ${error.message}`)
|
|
173
|
+
return 1
|
|
174
|
+
}
|
|
175
|
+
throw error
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (import.meta.main) process.exit(await main(process.argv.slice(2)))
|