lazycodex-ai 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.
Files changed (71) hide show
  1. package/dist/cli/index.js +29 -18
  2. package/dist/cli-node/index.js +29 -18
  3. package/package.json +2 -1
  4. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  5. package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +2 -0
  6. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  7. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  8. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  9. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  10. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  11. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  12. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  13. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  14. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +2 -2
  15. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  16. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  17. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  18. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  19. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  20. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  21. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  22. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  23. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  24. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  25. package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +1 -1
  26. package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
  27. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +5 -5
  28. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  29. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  30. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  31. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  32. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  33. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  34. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  35. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  36. package/packages/omo-codex/plugin/hooks/post-tool-use-recording-spawn-admission.json +1 -1
  37. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  38. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  39. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  40. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  41. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  42. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  43. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  44. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
  45. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  46. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  47. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  48. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  49. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  50. package/packages/omo-codex/plugin/package-lock.json +12 -12
  51. package/packages/omo-codex/plugin/package.json +1 -1
  52. package/packages/omo-codex/plugin/skills/browser/runtime/omowright/manifest.json +1 -1
  53. package/packages/omo-codex/plugin/skills/programming/SKILL.md +2 -0
  54. package/packages/omo-codex/plugin/skills/programming/references/testing/README.md +2 -2
  55. package/packages/omo-codex/plugin/skills/programming/scripts/typescript/check-test-junk-rules.ts +140 -0
  56. package/packages/omo-codex/plugin/skills/programming/scripts/typescript/check-test-junk.ts +148 -0
  57. package/packages/omo-codex/plugin/skills/programming/scripts/typescript/mutation-proof.ts +179 -0
  58. package/packages/omo-codex/plugin/skills/remove-ai-slops/SKILL.md +32 -20
  59. package/packages/omo-codex/plugin/skills/ulw-research/scripts/source-commands.mjs +3 -1
  60. package/packages/omo-codex/scripts/install-dist/install-local.mjs +4 -2
  61. package/packages/shared-skills/skills/browser/runtime/omowright/manifest.json +1 -1
  62. package/packages/shared-skills/skills/programming/SKILL.md +2 -0
  63. package/packages/shared-skills/skills/programming/references/testing/README.md +2 -2
  64. package/packages/shared-skills/skills/programming/scripts/typescript/check-test-junk-rules.ts +140 -0
  65. package/packages/shared-skills/skills/programming/scripts/typescript/check-test-junk.test.ts +125 -0
  66. package/packages/shared-skills/skills/programming/scripts/typescript/check-test-junk.ts +148 -0
  67. package/packages/shared-skills/skills/programming/scripts/typescript/mutation-proof.test.ts +43 -0
  68. package/packages/shared-skills/skills/programming/scripts/typescript/mutation-proof.ts +179 -0
  69. package/packages/shared-skills/skills/remove-ai-slops/SKILL.md +32 -20
  70. package/packages/shared-skills/skills/ulw-research/scripts/source-commands.mjs +3 -1
  71. package/packages/shared-skills/skills/ulw-research/scripts/source-integrity.test.ts +39 -2
@@ -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)))
@@ -0,0 +1,43 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test"
2
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"
3
+ import { tmpdir } from "node:os"
4
+ import { join } from "node:path"
5
+
6
+ import { MutationError, runMutation } from "./mutation-proof"
7
+
8
+ let root = ""
9
+ const OWNER = "export const add = (a: number, b: number): number => a + b\n"
10
+
11
+ describe("#given a production owner and a test that exercises it", () => {
12
+ beforeAll(() => {
13
+ root = mkdtempSync(join(process.env.RUN_TMP ?? tmpdir(), "mutation-proof-"))
14
+ writeFileSync(join(root, "owner.ts"), OWNER)
15
+ writeFileSync(
16
+ join(root, "owner.test.ts"),
17
+ 'import { expect, test } from "bun:test"\nimport { add } from "./owner"\ntest("adds", () => { expect(add(1, 2)).toBe(3) })\n',
18
+ )
19
+ })
20
+
21
+ afterAll(() => rmSync(root, { recursive: true, force: true }))
22
+
23
+ test("#when the owner is broken #then the test goes red and the owner is restored byte for byte", async () => {
24
+ const result = await runMutation({
25
+ cwd: root,
26
+ file: "owner.ts",
27
+ find: "a + b",
28
+ replace: "a - b",
29
+ tests: [{ label: "candidate", command: [process.execPath, "test", "owner.test.ts"] }],
30
+ })
31
+ expect(result.outcomes.map((outcome) => [outcome.label, outcome.verdict])).toEqual([["candidate", "red"]])
32
+ expect(result.restoredSha256).toBe(result.originalSha256)
33
+ expect(readFileSync(join(root, "owner.ts"), "utf8")).toBe(OWNER)
34
+ })
35
+
36
+ test("#when the find text is not unique or empty #then it refuses before touching the file", async () => {
37
+ for (const [find, kind] of [["a", "FIND_NOT_UNIQUE"], ["", "FIND_EMPTY"]] as const) {
38
+ const attempt = runMutation({ cwd: root, file: "owner.ts", find, replace: "x", tests: [] })
39
+ await expect(attempt).rejects.toMatchObject({ kind })
40
+ }
41
+ expect(readFileSync(join(root, "owner.ts"), "utf8")).toBe(OWNER)
42
+ })
43
+ })
@@ -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)))