@1aboveio/skills 1.0.8 → 1.0.10

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 (214) hide show
  1. package/.codex-plugin/plugin.json +13 -0
  2. package/.codex-plugin/resources.json +1827 -0
  3. package/README.md +22 -0
  4. package/package.json +9 -2
  5. package/skills/app-dev/airflow/SKILL.md +1 -1
  6. package/skills/app-dev/airflow/release-check.mjs +3 -1
  7. package/skills/app-dev/android-foundation/SKILL.md +1 -1
  8. package/skills/app-dev/android-foundation/release-check.mjs +3 -1
  9. package/skills/app-dev/app-debug/SKILL.md +1 -1
  10. package/skills/app-dev/app-debug/release-check.mjs +3 -1
  11. package/skills/app-dev/better-auth/SKILL.md +1 -1
  12. package/skills/app-dev/better-auth/release-check.mjs +3 -1
  13. package/skills/app-dev/data-catalog/SKILL.md +1 -1
  14. package/skills/app-dev/data-catalog/release-check.mjs +3 -1
  15. package/skills/app-dev/data-quality/SKILL.md +1 -1
  16. package/skills/app-dev/data-quality/release-check.mjs +3 -1
  17. package/skills/app-dev/ios-foundation/SKILL.md +1 -1
  18. package/skills/app-dev/ios-foundation/release-check.mjs +3 -1
  19. package/skills/app-dev/monorepo/SKILL.md +1 -1
  20. package/skills/app-dev/monorepo/release-check.mjs +3 -1
  21. package/skills/app-dev/nextjs-fullstack/SKILL.md +1 -1
  22. package/skills/app-dev/nextjs-fullstack/release-check.mjs +3 -1
  23. package/skills/app-dev/prisma-setup/SKILL.md +1 -1
  24. package/skills/app-dev/prisma-setup/release-check.mjs +3 -1
  25. package/skills/app-dev/pyspark/SKILL.md +1 -1
  26. package/skills/app-dev/pyspark/release-check.mjs +3 -1
  27. package/skills/app-dev/python-backend/SKILL.md +1 -1
  28. package/skills/app-dev/python-backend/release-check.mjs +3 -1
  29. package/skills/app-dev/shadcn/SKILL.md +1 -1
  30. package/skills/app-dev/shadcn/release-check.mjs +3 -1
  31. package/skills/app-dev/zod-v4/SKILL.md +1 -1
  32. package/skills/app-dev/zod-v4/release-check.mjs +3 -1
  33. package/skills/cicd/cloud-build/SKILL.md +1 -1
  34. package/skills/cicd/cloud-build/release-check.mjs +3 -1
  35. package/skills/cicd/cloud-debug/SKILL.md +1 -1
  36. package/skills/cicd/cloud-debug/release-check.mjs +3 -1
  37. package/skills/cicd/cloud-deploy/SKILL.md +1 -1
  38. package/skills/cicd/cloud-deploy/release-check.mjs +3 -1
  39. package/skills/cicd/docker/SKILL.md +1 -1
  40. package/skills/cicd/docker/release-check.mjs +3 -1
  41. package/skills/cicd/github-actions/SKILL.md +1 -1
  42. package/skills/cicd/github-actions/release-check.mjs +3 -1
  43. package/skills/cicd/github-flow/SKILL.md +1 -1
  44. package/skills/cicd/github-flow/release-check.mjs +3 -1
  45. package/skills/cicd/google-cloud/SKILL.md +1 -1
  46. package/skills/cicd/google-cloud/release-check.mjs +3 -1
  47. package/skills/cicd/mergify/SKILL.md +1 -1
  48. package/skills/cicd/mergify/release-check.mjs +3 -1
  49. package/skills/cicd/podman/SKILL.md +1 -1
  50. package/skills/cicd/podman/release-check.mjs +3 -1
  51. package/skills/content/article-review/SKILL.md +1 -1
  52. package/skills/content/article-review/release-check.mjs +3 -1
  53. package/skills/content/article-to-image/SKILL.md +1 -1
  54. package/skills/content/article-to-image/release-check.mjs +3 -1
  55. package/skills/content/flowchart-svg/SKILL.md +1 -1
  56. package/skills/content/flowchart-svg/release-check.mjs +3 -1
  57. package/skills/content/image-gen/SKILL.md +1 -1
  58. package/skills/content/image-gen/release-check.mjs +3 -1
  59. package/skills/content/mermaid/SKILL.md +1 -1
  60. package/skills/content/mermaid/release-check.mjs +3 -1
  61. package/skills/content/wechat-article-writer/SKILL.md +1 -1
  62. package/skills/content/wechat-article-writer/release-check.mjs +3 -1
  63. package/skills/content/wechat-publisher/SKILL.md +1 -1
  64. package/skills/content/wechat-publisher/release-check.mjs +3 -1
  65. package/skills/content/wechat-search/SKILL.md +1 -1
  66. package/skills/content/wechat-search/release-check.mjs +3 -1
  67. package/skills/content/xai-search/SKILL.md +88 -13
  68. package/skills/content/xai-search/release-check.mjs +3 -1
  69. package/skills/content/xai-search/scripts/xai_search.sh +125 -149
  70. package/skills/core/distill-writing/SKILL.md +1 -1
  71. package/skills/core/distill-writing/release-check.mjs +3 -1
  72. package/skills/core/doc-to-md/SKILL.md +1 -1
  73. package/skills/core/doc-to-md/release-check.mjs +3 -1
  74. package/skills/core/editorial-review/SKILL.md +1 -1
  75. package/skills/core/editorial-review/release-check.mjs +3 -1
  76. package/skills/core/house-style/SKILL.md +1 -1
  77. package/skills/core/house-style/release-check.mjs +3 -1
  78. package/skills/core/plain-language/SKILL.md +1 -1
  79. package/skills/core/plain-language/release-check.mjs +3 -1
  80. package/skills/core/polish-cn/SKILL.md +1 -1
  81. package/skills/core/polish-cn/release-check.mjs +3 -1
  82. package/skills/core/reg-parser/SKILL.md +1 -1
  83. package/skills/core/reg-parser/release-check.mjs +3 -1
  84. package/skills/core/terminology/SKILL.md +1 -1
  85. package/skills/core/terminology/release-check.mjs +3 -1
  86. package/skills/core/write-doc/SKILL.md +1 -1
  87. package/skills/core/write-doc/release-check.mjs +3 -1
  88. package/skills/engineering/better-review/SKILL.md +1 -1
  89. package/skills/engineering/better-review/release-check.mjs +3 -1
  90. package/skills/engineering/contributing/SKILL.md +1 -1
  91. package/skills/engineering/contributing/release-check.mjs +3 -1
  92. package/skills/engineering/e2e-test/SKILL.md +1 -1
  93. package/skills/engineering/e2e-test/release-check.mjs +3 -1
  94. package/skills/engineering/engineering-runtime/SKILL.md +1 -1
  95. package/skills/engineering/engineering-runtime/coherence/workflow.json +13 -13
  96. package/skills/engineering/engineering-runtime/release-check.mjs +3 -1
  97. package/skills/engineering/engineering-runtime/scripts/installation-resolver.mjs +191 -0
  98. package/skills/engineering/engineering-runtime/scripts/workflow-coherence.mjs +51 -10
  99. package/skills/engineering/ensure-coverage/SKILL.md +1 -1
  100. package/skills/engineering/ensure-coverage/release-check.mjs +3 -1
  101. package/skills/engineering/golden-path/SKILL.md +1 -1
  102. package/skills/engineering/golden-path/release-check.mjs +3 -1
  103. package/skills/engineering/harness-runtime/SKILL.md +1 -1
  104. package/skills/engineering/harness-runtime/release-check.mjs +3 -1
  105. package/skills/engineering/resolve-issues/SKILL.md +1 -1
  106. package/skills/engineering/resolve-issues/references/preflight.md +7 -0
  107. package/skills/engineering/resolve-issues/references/shared-modules.md +27 -0
  108. package/skills/engineering/resolve-issues/references/task-plan.md +5 -1
  109. package/skills/engineering/resolve-issues/release-check.mjs +3 -1
  110. package/skills/engineering/resolve-issues/scripts/active-installation.mjs +32 -0
  111. package/skills/engineering/resolve-issues/scripts/install-hooks.mjs +3 -0
  112. package/skills/engineering/resolve-issues/scripts/rehydrate.mjs +17 -1
  113. package/skills/engineering/resolve-release/SKILL.md +1 -1
  114. package/skills/engineering/resolve-release/release-check.mjs +3 -1
  115. package/skills/engineering/rush-issues/SKILL.md +1 -1
  116. package/skills/engineering/rush-issues/release-check.mjs +3 -1
  117. package/skills/engineering/rush-release/SKILL.md +1 -1
  118. package/skills/engineering/rush-release/release-check.mjs +3 -1
  119. package/skills/engineering/test-quality/SKILL.md +1 -1
  120. package/skills/engineering/test-quality/release-check.mjs +3 -1
  121. package/skills/risk-compliance/compliance-audit/SKILL.md +1 -1
  122. package/skills/risk-compliance/compliance-audit/release-check.mjs +3 -1
  123. package/skills/risk-compliance/contract-compare/SKILL.md +1 -1
  124. package/skills/risk-compliance/contract-compare/release-check.mjs +3 -1
  125. package/skills/risk-compliance/contract-review/SKILL.md +1 -1
  126. package/skills/risk-compliance/contract-review/release-check.mjs +3 -1
  127. package/skills/risk-compliance/doc-authenticity/SKILL.md +1 -1
  128. package/skills/risk-compliance/doc-authenticity/release-check.mjs +3 -1
  129. package/skills/risk-compliance/fraud-analysis/SKILL.md +1 -1
  130. package/skills/risk-compliance/fraud-analysis/release-check.mjs +3 -1
  131. package/skills/risk-compliance/payment-analysis/SKILL.md +1 -1
  132. package/skills/risk-compliance/payment-analysis/release-check.mjs +3 -1
  133. package/skills/risk-compliance/similar-domain/SKILL.md +1 -1
  134. package/skills/risk-compliance/similar-domain/release-check.mjs +3 -1
  135. package/skills/risk-compliance/tld-detection/SKILL.md +1 -1
  136. package/skills/risk-compliance/tld-detection/release-check.mjs +3 -1
  137. package/skills/app-dev/android-foundation/scripts/device-run.test.mjs +0 -436
  138. package/skills/app-dev/data-catalog/scripts/data-catalog.test.mjs +0 -36
  139. package/skills/app-dev/data-quality/scripts/data-quality.test.mjs +0 -12
  140. package/skills/cicd/github-actions/scripts/ci-run-audit.test.mjs +0 -637
  141. package/skills/cicd/github-flow/scripts/flow.test.mjs +0 -114
  142. package/skills/cicd/github-flow/scripts/persist-conventions.test.mjs +0 -108
  143. package/skills/cicd/mergify/scripts/audit.test.mjs +0 -201
  144. package/skills/cicd/mergify/scripts/entrypoints.test.mjs +0 -106
  145. package/skills/cicd/mergify/scripts/pr-620-mergify-comments.fixture.json +0 -30
  146. package/skills/cicd/mergify/scripts/stack-push.test.mjs +0 -233
  147. package/skills/cicd/mergify/scripts/watch-pr-delivery.test.mjs +0 -1826
  148. package/skills/content/image-gen/scripts/credentials.test.mjs +0 -212
  149. package/skills/content/image-gen/scripts/options.test.mjs +0 -245
  150. package/skills/content/xai-search/tests/smoke_test.sh +0 -36
  151. package/skills/core/polish-cn/scripts/polish-cn.test.mjs +0 -244
  152. package/skills/core/reg-parser/scripts/reg-parser.test.mjs +0 -165
  153. package/skills/engineering/better-review/scripts/pr-context.test.mjs +0 -168
  154. package/skills/engineering/better-review/scripts/rereview-scope.test.mjs +0 -587
  155. package/skills/engineering/better-review/scripts/scan-diff.test.mjs +0 -414
  156. package/skills/engineering/better-review/scripts/vendor-sync.test.mjs +0 -96
  157. package/skills/engineering/contributing/scripts/persist-conventions.test.mjs +0 -118
  158. package/skills/engineering/e2e-test/scripts/presentation-checks.test.mjs +0 -106
  159. package/skills/engineering/e2e-test/scripts/presentation-perceivability.test.mjs +0 -117
  160. package/skills/engineering/e2e-test/scripts/presentation-reachability.test.mjs +0 -69
  161. package/skills/engineering/e2e-test/scripts/presentation-render-health.test.mjs +0 -98
  162. package/skills/engineering/e2e-test/scripts/presentation-sweep.test.mjs +0 -71
  163. package/skills/engineering/e2e-test/scripts/presentation-temporal.test.mjs +0 -59
  164. package/skills/engineering/e2e-test/scripts/project-route-manifest.test.mjs +0 -76
  165. package/skills/engineering/engineering-runtime/engineering-runtime.test.mjs +0 -68
  166. package/skills/engineering/engineering-runtime/scripts/invocation-policy.test.mjs +0 -95
  167. package/skills/engineering/engineering-runtime/scripts/main-module.test.mjs +0 -288
  168. package/skills/engineering/engineering-runtime/scripts/required-check-plan.test.mjs +0 -96
  169. package/skills/engineering/ensure-coverage/scripts/adapters/nextjs-inventory.test.mjs +0 -96
  170. package/skills/engineering/ensure-coverage/scripts/check-quarantine-expiry.test.mjs +0 -47
  171. package/skills/engineering/ensure-coverage/scripts/ci-audit.test.mjs +0 -86
  172. package/skills/engineering/ensure-coverage/scripts/coverage-ledger.test.mjs +0 -447
  173. package/skills/engineering/ensure-coverage/scripts/design-parity.test.mjs +0 -552
  174. package/skills/engineering/ensure-coverage/scripts/lint-tests.test.mjs +0 -289
  175. package/skills/engineering/ensure-coverage/scripts/mock-policy-config.test.mjs +0 -119
  176. package/skills/engineering/golden-path/scripts/golden-path.test.mjs +0 -1457
  177. package/skills/engineering/harness-runtime/discover-models.test.mjs +0 -363
  178. package/skills/engineering/harness-runtime/harness-runtime.test.mjs +0 -967
  179. package/skills/engineering/harness-runtime/model-catalog-seed.test.mjs +0 -829
  180. package/skills/engineering/harness-runtime/model-catalog.test.mjs +0 -366
  181. package/skills/engineering/resolve-issues/scripts/cleanup-runs.test.mjs +0 -280
  182. package/skills/engineering/resolve-issues/scripts/dropbox.test.mjs +0 -346
  183. package/skills/engineering/resolve-issues/scripts/findings-reuse.test.mjs +0 -88
  184. package/skills/engineering/resolve-issues/scripts/github-reviews.test.mjs +0 -264
  185. package/skills/engineering/resolve-issues/scripts/install-hooks.test.mjs +0 -79
  186. package/skills/engineering/resolve-issues/scripts/issue-tree.test.mjs +0 -107
  187. package/skills/engineering/resolve-issues/scripts/manual-invocation.test.mjs +0 -14
  188. package/skills/engineering/resolve-issues/scripts/next-actions-capacity.test.mjs +0 -149
  189. package/skills/engineering/resolve-issues/scripts/next-actions.test.mjs +0 -915
  190. package/skills/engineering/resolve-issues/scripts/plan-sync.test.mjs +0 -156
  191. package/skills/engineering/resolve-issues/scripts/preflight-models.test.mjs +0 -448
  192. package/skills/engineering/resolve-issues/scripts/ready-check.test.mjs +0 -217
  193. package/skills/engineering/resolve-issues/scripts/rehydrate.test.mjs +0 -91
  194. package/skills/engineering/resolve-issues/scripts/review-verdict.test.mjs +0 -1060
  195. package/skills/engineering/resolve-issues/scripts/size-check.test.mjs +0 -458
  196. package/skills/engineering/resolve-release/scripts/apply.test.mjs +0 -99
  197. package/skills/engineering/resolve-release/scripts/dark-flags.test.mjs +0 -100
  198. package/skills/engineering/resolve-release/scripts/flow.test.mjs +0 -340
  199. package/skills/engineering/resolve-release/scripts/green-head.test.mjs +0 -101
  200. package/skills/engineering/resolve-release/scripts/npm-version-absence.test.mjs +0 -426
  201. package/skills/engineering/resolve-release/scripts/plan-npm.test.mjs +0 -659
  202. package/skills/engineering/resolve-release/scripts/plan-toml-boundary.test.mjs +0 -317
  203. package/skills/engineering/resolve-release/scripts/plan-uv.test.mjs +0 -291
  204. package/skills/engineering/resolve-release/scripts/plan.test.mjs +0 -428
  205. package/skills/engineering/test-quality/scripts/lint-tests.test.mjs +0 -591
  206. package/skills/engineering/test-quality/scripts/mock-policy-config.test.mjs +0 -119
  207. package/skills/engineering/test-quality/scripts/persist-conventions.test.mjs +0 -104
  208. package/skills/risk-compliance/compliance-audit/scripts/compliance-audit.test.mjs +0 -14
  209. package/skills/risk-compliance/doc-authenticity/manual-invocation.test.mjs +0 -12
  210. package/skills/risk-compliance/doc-authenticity/scripts/validate_verdict.test.mjs +0 -212
  211. package/skills/risk-compliance/tld-detection/scripts/detect_ladders.test.mjs +0 -14
  212. package/skills/risk-compliance/tld-detection/scripts/detect_streams.test.mjs +0 -14
  213. package/skills/risk-compliance/tld-detection/tests/test_detect_ladders.py +0 -134
  214. package/skills/risk-compliance/tld-detection/tests/test_detect_streams.py +0 -520
@@ -1,591 +0,0 @@
1
- // Tests for the lint-tests engine (pure core, no filesystem/git).
2
- import { test } from 'node:test'
3
- import assert from 'node:assert/strict'
4
-
5
- import { scanText, scanDiff, selectRules, RULES, ALL_RULES, isTestFile, stripComment, runCli, findDuplicateFiles, splitShares } from './lint-tests.mjs'
6
- import { resolveMockPolicy } from './mock-policy-config.mjs'
7
- import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
8
- import { execSync } from 'node:child_process'
9
- import { tmpdir } from 'node:os'
10
- import { join } from 'node:path'
11
-
12
- const ALL_IDS = [
13
- 'conditional-skip', 'config-read', 'count-pin', 'duplicate-file', 'mock-internal-seam', 'no-empty-test',
14
- 'no-fixed-timeout', 'no-skip', 'no-swallowed-assertion', 'redundant-matcher', 'text-oracle', 'utility-class',
15
- ]
16
-
17
- test('default rules = every rule, gates and leads', () => {
18
- assert.deepEqual(selectRules().map((r) => r.id).sort(), ALL_IDS)
19
- assert.equal(RULES.length, 4) // the four authoring gates; the rest live in ALL_RULES
20
- })
21
-
22
- test('every rule names its principle and a gate or lead severity', () => {
23
- for (const rule of ALL_RULES) {
24
- assert.ok([1, 2, 3, 4, 5, 6, 7].includes(rule.principle), rule.id)
25
- assert.ok(['gate', 'lead'].includes(rule.severity), rule.id)
26
- }
27
- const gates = ALL_RULES.filter((r) => r.severity === 'gate').map((r) => r.id).sort()
28
- assert.deepEqual(gates, ['mock-internal-seam', 'no-empty-test', 'no-fixed-timeout', 'no-skip', 'no-swallowed-assertion'])
29
- })
30
-
31
- test('no-skip rule flags skip/only/xfail (carries rule + kind)', () => {
32
- // non-empty bodies so this isolates no-skip from the no-empty-test rule
33
- const f = scanText('a.spec.ts', 'test.skip("x", () => { go() })\ndescribe.only("y", () => { go() })')
34
- assert.deepEqual(f.map((x) => [x.rule, x.kind]), [['no-skip', 'skip'], ['no-skip', 'only']])
35
- })
36
-
37
- test('no-fixed-timeout flags waitForTimeout / numeric cy.wait / time.sleep', () => {
38
- const f = scanText('a.spec.ts', [
39
- 'await page.waitForTimeout(500)',
40
- "cy.wait(1000)",
41
- 'time.sleep(2)',
42
- ].join('\n'))
43
- assert.deepEqual(f.map((x) => x.kind), ['waitForTimeout', 'cy.wait', 'time.sleep'])
44
- assert.ok(f.every((x) => x.rule === 'no-fixed-timeout'))
45
- })
46
-
47
- test('no-fixed-timeout does NOT flag an aliased cy.wait (not a fixed sleep)', () => {
48
- assert.deepEqual(scanText('a.spec.ts', "cy.wait('@getUser')"), [])
49
- })
50
-
51
- test('--rules selection runs only the chosen rule', () => {
52
- const text = 'test.skip("x", () => {})\nawait page.waitForTimeout(500)'
53
- const onlySkip = scanText('a.spec.ts', text, ['no-skip'])
54
- assert.deepEqual(onlySkip.map((x) => x.rule), ['no-skip'])
55
- const onlyTimeout = scanText('a.spec.ts', text, ['no-fixed-timeout'])
56
- assert.deepEqual(onlyTimeout.map((x) => x.rule), ['no-fixed-timeout'])
57
- })
58
-
59
- test('a single line can trip two different rules', () => {
60
- // contrived but valid: a skipped test on the same line as a timeout call
61
- const f = scanText('a.spec.ts', 'test.skip("x", async () => { await page.waitForTimeout(1) })')
62
- assert.deepEqual(f.map((x) => x.rule).sort(), ['no-fixed-timeout', 'no-skip'])
63
- })
64
-
65
- test('scanDiff flags only ADDED violations in test files, with new-file line numbers', () => {
66
- const diff = [
67
- '+++ b/e2e/x.spec.ts',
68
- '@@ -10,2 +10,3 @@',
69
- ' context',
70
- '+await page.waitForTimeout(300)',
71
- '-old line',
72
- ].join('\n')
73
- const f = scanDiff(diff)
74
- assert.equal(f.length, 1)
75
- assert.equal(f[0].rule, 'no-fixed-timeout')
76
- assert.equal(f[0].line, 11)
77
- })
78
-
79
- test('scanDiff ignores violations added to NON-test files', () => {
80
- const diff = ['+++ b/src/util.ts', '@@ -1,1 +1,2 @@', '+await sleepThing.waitForTimeout(5)'].join('\n')
81
- assert.deepEqual(scanDiff(diff), [])
82
- })
83
-
84
- test('isTestFile ignores data files under a tests directory', () => {
85
- assert.equal(isTestFile('backend/tests/data/cybs_a100.parquet'), false)
86
- assert.equal(isTestFile('tests/fixtures/sample.json'), false)
87
- assert.equal(isTestFile('backend/internal/service/user_test.go'), true)
88
- })
89
-
90
- test('isTestFile is shared and recognizes spec/test paths, rejects app code', () => {
91
- for (const p of ['e2e/a.spec.ts', 'src/x.test.tsx', 'tests/e2e/b.spec.js', 'api/test_views.py', 'pkg/foo_test.py', '__tests__/c.ts']) {
92
- assert.equal(isTestFile(p), true, p)
93
- }
94
- for (const p of ['src/app/page.tsx', 'lib/util.ts', 'README.md']) {
95
- assert.equal(isTestFile(p), false, p)
96
- }
97
- })
98
-
99
- // ---- no-skip rule: richer cases (folded in from the former skip-lint suite) ----
100
-
101
- test('conditional runtime skips are a lead, declared skipped tests a gate', () => {
102
- const f = scanText('a.spec.ts', 'if (!user) test.skip();\ntest.skip(true, "https://github.com/o/r/issues/1")\ntest.skip(\'broken\', () => {})', ['no-skip', 'conditional-skip'])
103
- assert.deepEqual(f.map((x) => `${x.rule}:${x.line}`), ['conditional-skip:1', 'conditional-skip:2', 'no-skip:3'])
104
- })
105
-
106
- test('Python: unconditional skip is a gate; skipif, runtime skip, importorskip and xfail are leads', () => {
107
- const text = [
108
- '@pytest.mark.skip(reason="x")',
109
- '@pytest.mark.skipif(sys.platform == "win32", reason="posix only")',
110
- '@unittest.skip("x")',
111
- ' pytest.skip("x")',
112
- '@pytest.mark.xfail(strict=True, reason="bug #12")',
113
- 'np = pytest.importorskip("numpy")',
114
- '@unittest.skipIf(sys.version_info < (3, 11), "needs 3.11")',
115
- ].join('\n')
116
- assert.deepEqual(scanText('test_x.py', text, ['no-skip', 'conditional-skip']).map((f) => `${f.rule}/${f.kind}`), [
117
- 'no-skip/skip', 'conditional-skip/skipif', 'no-skip/skip', 'conditional-skip/runtime-skip',
118
- 'conditional-skip/xfail', 'conditional-skip/runtime-skip', 'conditional-skip/skipif',
119
- ])
120
- })
121
-
122
- test('Go and node runtime skips are leads; test.todo is a gate', () => {
123
- const text = ['\tif testing.Short() { t.Skip("slow") }', " t.skip('needs docker')", "test.todo('handles refunds')"].join('\n')
124
- assert.deepEqual(scanText('x_test.go', text, ['no-skip', 'conditional-skip']).map((f) => `${f.rule}:${f.line}`),
125
- ['conditional-skip:1', 'conditional-skip:2', 'no-skip:3'])
126
- })
127
-
128
- test('no-empty-test flags a Python test whose body is only pass or ...', () => {
129
- const text = ['def test_refund():\n pass', 'def test_a(): pass', 'async def test_b() -> None: ...', 'def test_c(): assert f() == 1'].join('\n')
130
- assert.deepEqual(scanText('test_x.py', text, ['no-empty-test']).map((f) => f.line), [3, 4])
131
- })
132
-
133
- test('no-fixed-timeout flags Go time.Sleep', () => {
134
- assert.equal(scanText('a_test.go', '\ttime.Sleep(100 * time.Millisecond)', ['no-fixed-timeout']).length, 1)
135
- })
136
-
137
- test('no-skip does not flag a commented-out or doc mention', () => {
138
- const text = [
139
- '// never use test.skip() — banned',
140
- '# doc: test.skip is not allowed',
141
- 'const xs = items.skip(2) // unrelated array method',
142
- ].join('\n')
143
- assert.deepEqual(scanText('a.spec.ts', text), [])
144
- })
145
-
146
- test('no-skip does not flag .only/.skip method calls that are not test forms', () => {
147
- assert.deepEqual(scanText('a.spec.ts', 'arr.skip(1); foo.only;'), [])
148
- })
149
-
150
- // ---- no-empty-test rule: the decidable floor of the Missing-Assertion smell ----
151
-
152
- test('no-empty-test flags empty arrow/function/playwright/.each/.concurrent bodies', () => {
153
- const cases = [
154
- "test('a', () => {})",
155
- 'it("b", async () => {})',
156
- "test('c', function () {})",
157
- "test('d', async ({ page }) => {})",
158
- "it.each([1,2])('e', () => {})",
159
- "test.concurrent('f', () => {})",
160
- ]
161
- for (const c of cases) {
162
- const f = scanText('a.spec.ts', c, ['no-empty-test'])
163
- assert.equal(f.length, 1, `should flag: ${c}`)
164
- assert.equal(f[0].kind, 'empty-test')
165
- }
166
- })
167
-
168
- test('no-empty-test does NOT flag a test that has a body', () => {
169
- const cases = [
170
- "test('a', () => { expect(1).toBe(1) })",
171
- "it('b', async () => { await verifyDashboard(page) })", // asserts via helper — not empty, not flagged
172
- "test('c', () => { doThing() })",
173
- ]
174
- for (const c of cases) assert.deepEqual(scanText('a.spec.ts', c, ['no-empty-test']), [], c)
175
- })
176
-
177
- test('no-empty-test does NOT flag describe/suite, look-alike identifiers, or 1-arg calls', () => {
178
- const text = [
179
- "describe('group', () => {})", // empty suite is not a fake test
180
- "emit('evt', () => {})", // 'it' inside emit — word boundary protects
181
- "submit('x', () => {})", // 'it' inside submit
182
- "it('pending')", // no callback — not an empty-bodied test
183
- 'const t = () => {}', // not a test call
184
- ].join('\n')
185
- assert.deepEqual(scanText('a.spec.ts', text, ['no-empty-test']), [])
186
- })
187
-
188
- test('no-empty-test leaves a multi-line empty body to the review lead (no false block)', () => {
189
- // The opening line has no closing brace, so the single-line gate does not fire.
190
- assert.deepEqual(scanText('a.spec.ts', "it('x', () => {\n})", ['no-empty-test']), [])
191
- })
192
-
193
- // ---- no-swallowed-assertion rule: the vacuous-green form behind #237 ----
194
-
195
- test('no-swallowed-assertion flags a matcher whose rejection is eaten by .catch()', () => {
196
- const cases = [
197
- "await expect(page.locator('#cta')).not.toBeVisible().catch(() => {})", // the #237 form
198
- "await expect(btn).toHaveText('Sign in').catch(() => null)",
199
- 'await expect(x).toBe(1).catch(() => {})',
200
- ]
201
- for (const c of cases) {
202
- const f = scanText('a.spec.ts', c, ['no-swallowed-assertion'])
203
- assert.equal(f.length, 1, `should flag: ${c}`)
204
- assert.equal(f[0].kind, 'swallowed-catch')
205
- assert.equal(f[0].rule, 'no-swallowed-assertion')
206
- }
207
- })
208
-
209
- test('no-swallowed-assertion does NOT flag a legitimate assertion or an unrelated .catch', () => {
210
- const cases = [
211
- "await expect(page.locator('#cta')).toBeVisible()", // real assertion, no swallow
212
- "const v = await fetchThing().catch(() => fallback)", // .catch on setup, not a matcher
213
- "promise.then(ok).catch(handleError)", // plain promise handling
214
- ]
215
- for (const c of cases) assert.deepEqual(scanText('a.spec.ts', c, ['no-swallowed-assertion']), [], c)
216
- })
217
-
218
- test('no-swallowed-assertion is a default gate and flags it in a PR diff', () => {
219
- const diff = [
220
- '+++ b/e2e/login.sweep.spec.ts',
221
- '@@ -1,1 +1,2 @@',
222
- ' context',
223
- "+await expect(page.locator('#cta')).not.toBeVisible().catch(() => {})",
224
- ].join('\n')
225
- const f = scanDiff(diff) // default rule set, no explicit --rules
226
- assert.equal(f.length, 1)
227
- assert.equal(f[0].rule, 'no-swallowed-assertion')
228
- })
229
-
230
- test('stripComment removes // and # trailing comments', () => {
231
- assert.equal(stripComment('code // test.skip()').includes('test.skip'), false)
232
- assert.equal(stripComment('code # pytest.skip').includes('pytest.skip'), false)
233
- })
234
-
235
- test('scanDiff ignores a pre-existing skip on an unchanged (context) line', () => {
236
- const diff = ['+++ b/e2e/x.spec.ts', '@@ -1,2 +1,3 @@', ' test.skip("inherited", () => {})', '+test("added clean", () => { ok() })'].join('\n')
237
- assert.deepEqual(scanDiff(diff), [])
238
- })
239
-
240
- // ---- mock-internal-seam rule: the zero-tolerance, default-on over-mock gate ----
241
-
242
- const SEAM_CTX = { policy: resolveMockPolicy({ firstPartyPackages: ['@agentier/'] }) }
243
-
244
- test('mock-internal-seam is default-on: in ALL_RULES and in the default gate set', () => {
245
- assert.ok(ALL_RULES.some((r) => r.id === 'mock-internal-seam'))
246
- // zero-tolerance: it runs by default, no opt-in needed
247
- assert.ok(selectRules().some((r) => r.id === 'mock-internal-seam'))
248
- })
249
-
250
- test('mock-internal-seam cannot be opted out via config (enabled:false is ignored)', () => {
251
- // the gate is defaultOn:true; selectRules does not consult policy.enabled
252
- assert.ok(selectRules().some((r) => r.id === 'mock-internal-seam'))
253
- // and naming a subset still resolves it from the registry
254
- assert.deepEqual(selectRules(['mock-internal-seam']).map((r) => r.id), ['mock-internal-seam'])
255
- })
256
-
257
- test('mock-internal-seam flags a first-party alias mock (the studio incident)', () => {
258
- const f = scanText('a.test.ts', "vi." + "mock('@/lib/org-allowlist-client')", ['mock-internal-seam'], SEAM_CTX)
259
- assert.equal(f.length, 1)
260
- assert.equal(f[0].rule, 'mock-internal-seam')
261
- assert.equal(f[0].detail, '@/lib/org-allowlist-client')
262
- })
263
-
264
- test('mock-internal-seam exempts stdlib and third-party packages', () => {
265
- const text = ["vi.mock('node:fs')", "jest.mock('react')", "vi.mock('@playwright/test')"].join('\n')
266
- assert.deepEqual(scanText('a.test.ts', text, ['mock-internal-seam'], SEAM_CTX), [])
267
- })
268
-
269
- test('mock-internal-seam ignores an inline @mock-ok comment (no waivers)', () => {
270
- const text = "vi." + "mock('@/lib/cp') // @mock-ok: covered by cp.real.test.ts"
271
- const f = scanText('a.test.ts', text, ['mock-internal-seam'], SEAM_CTX)
272
- assert.equal(f.length, 1) // still flagged — zero-tolerance, no escape hatch
273
- assert.equal(f[0].detail, '@/lib/cp')
274
- })
275
-
276
- test('mock-internal-seam flags a declared first-party workspace package', () => {
277
- const f = scanText('a.test.ts', "vi.mock('@agentier/shared')", ['mock-internal-seam'], SEAM_CTX)
278
- assert.equal(f.length, 1)
279
- assert.equal(f[0].detail, '@agentier/shared')
280
- })
281
-
282
- test('mock-internal-seam fires with NO ctx policy (never silently inert; @/ defaults apply)', () => {
283
- // zero-tolerance: omitting config must not disable the gate — it falls back
284
- // to DEFAULT_MOCK_POLICY, which flags conventional @/ first-party mocks.
285
- const f = scanText('a.test.ts', "vi." + "mock('@/lib/x')", ['mock-internal-seam'])
286
- assert.equal(f.length, 1)
287
- assert.equal(f[0].detail, '@/lib/x')
288
- })
289
-
290
- test('mock-internal-seam flags an ADDED first-party mock in a PR diff (PR lane)', () => {
291
- const diff = ['+++ b/src/__tests__/x.test.ts', '@@ -1,1 +1,2 @@', " import x", "+vi." + "mock('@/lib/cp')"].join('\n')
292
- const f = scanDiff(diff, ['mock-internal-seam'], SEAM_CTX)
293
- assert.equal(f.length, 1)
294
- assert.equal(f[0].rule, 'mock-internal-seam')
295
- })
296
-
297
- test('mock-internal-seam flags an INHERITED (un-added) mock in a whole-tree scan (no grandfathering)', () => {
298
- // whole-tree lane: scanText sees every line, not just diff additions, so the
299
- // 781-style inherited debt is flagged until removed.
300
- const f = scanText('src/__tests__/x.test.ts', "import x\nvi.mock('@/lib/cp')\n", ['mock-internal-seam'], SEAM_CTX)
301
- assert.equal(f.length, 1)
302
- assert.equal(f[0].detail, '@/lib/cp')
303
- })
304
-
305
- test('mock-internal-seam does not fire on a commented-out mock line', () => {
306
- assert.deepEqual(scanText('a.test.ts', "// vi.mock('@/lib/x')", ['mock-internal-seam'], SEAM_CTX), [])
307
- })
308
-
309
- test('--help prints usage and exits 0 without scanning the tree', () => {
310
- const lines = []
311
- const orig = console.log
312
- console.log = (s) => lines.push(String(s))
313
- try {
314
- assert.equal(runCli(['--help']), 0)
315
- } finally {
316
- console.log = orig
317
- }
318
- assert.match(lines.join('\n'), /usage: lint-tests\.mjs/)
319
- })
320
-
321
- // ---- test-quality rules ----
322
-
323
- // Fixture text for first-party mocks, built at runtime so repository-wide over-mock scans do not
324
- // read this file's own fixtures as real mocks.
325
- const seamMocks = (...targets) => targets.map((target) => `vi.mock('${target}')`).join('\n') + '\n'
326
-
327
- const ids = (findings) => findings.map((f) => `${f.rule}:${f.line}`)
328
-
329
- test('redundant-matcher flags a matcher after a query that already throws when missing', () => {
330
- const text = [
331
- "expect(screen.getByText('Saved')).toBeDefined()",
332
- "expect(within(row).getByRole('button', { name: /edit/i })).toBeTruthy()",
333
- "expect(wrapper.get('[data-test=title]').exists()).toBe(true)",
334
- ].join('\n')
335
- assert.deepEqual(ids(scanText('a.test.ts', text, ['redundant-matcher'])), ['redundant-matcher:1', 'redundant-matcher:2', 'redundant-matcher:3'])
336
- })
337
-
338
- test('redundant-matcher leaves queryBy, find and real matchers alone', () => {
339
- const text = [
340
- "expect(screen.queryByText('Saved')).toBeNull()",
341
- "expect(screen.getByText('Saved')).toBeVisible()",
342
- "expect(wrapper.find('.x').exists()).toBe(false)",
343
- "expect(items.find((i) => i.id === 1)).toBeDefined()",
344
- ].join('\n')
345
- assert.deepEqual(scanText('a.test.ts', text, ['redundant-matcher']), [])
346
- })
347
-
348
- test('text-oracle flags a checked-in doc or source read and text-matched', () => {
349
- const text = [
350
- "const doc = readFileSync(join(root, 'docs/runbook.md'), 'utf8')",
351
- "const src = readFileSync('src/auth.ts', 'utf8')",
352
- "assert.match(doc, /Never force-publish/)",
353
- "expect(src).toMatch(/expiresIn:\\s*600/)",
354
- ].join('\n')
355
- const found = scanText('x.test.mjs', text, ['text-oracle'])
356
- assert.deepEqual(ids(found), ['text-oracle:1']) // one lead per file, at the first read
357
- assert.equal(found[0].severity, 'lead')
358
- assert.equal(found[0].detail, 'docs/runbook.md, src/auth.ts')
359
- })
360
-
361
- test('text-oracle flags a Python read_text with an `in` assertion', () => {
362
- const text = [
363
- "content = (ROOT / 'dags' / 'mw_dag.py').read_text()",
364
- "assert 'start >> fee_settings' in content",
365
- ].join('\n')
366
- // the path literal on line 1 is 'mw_dag.py'
367
- assert.deepEqual(ids(scanText('tests/test_dag.py', text, ['text-oracle'])), ['text-oracle:1'])
368
- })
369
-
370
- test('text-oracle ignores executed scripts, fixtures, written temp files and match-free reads', () => {
371
- const runs = [
372
- "const out = execFileSync('node', ['scripts/build.mjs'])",
373
- "const cfg = readFileSync('fixtures/sample.md', 'utf8')",
374
- "writeFileSync(join(tmp, 'out.md'), render(doc))",
375
- "const got = readFileSync(join(tmp, 'out.md'), 'utf8')",
376
- "import { run } from './run.mjs'",
377
- "fs.copyFileSync(join(repoRoot, 'scripts/e2e-db.mjs'), script)",
378
- "expect(got).toContain('# Title')",
379
- ].join('\n')
380
- assert.deepEqual(scanText('x.test.mjs', runs, ['text-oracle']), [])
381
- const bound = [
382
- "const cli = path.join(__dirname, '../bin/cli.ts')",
383
- "const out = spawnSync('node', [cli]).stdout.toString()",
384
- "const log = readFileSync(logPath, 'utf8')",
385
- "expect(out).toContain('usage')",
386
- ].join('\n')
387
- assert.deepEqual(scanText('x.test.mjs', bound, ['text-oracle']), [])
388
- const noMatch = "const src = readFileSync('src/a.ts', 'utf8')\nexpect(parse(src)).toEqual({ ok: true })"
389
- assert.deepEqual(scanText('x.test.mjs', noMatch, ['text-oracle']), [])
390
- })
391
-
392
- test('config-read flags a parsed config file whatever the assertion', () => {
393
- const text = [
394
- "const config = yaml.load(fs.readFileSync(path.join(repo, '.mergify.yml'), 'utf8'))",
395
- "const wf = readFileSync('.github/workflows/ci.yml', 'utf8')",
396
- "const pkg = JSON.parse(readFileSync('package.json', 'utf8'))",
397
- "const docker = readFileSync('Dockerfile.prod', 'utf8')",
398
- "assert.equal(config.queue_rules[0].batch_size, 3)",
399
- ].join('\n')
400
- const found = scanText('x.test.mjs', text, ['config-read'])
401
- assert.deepEqual(ids(found), ['config-read:1']) // one lead per file, at the first read
402
- assert.equal(found[0].severity, 'lead')
403
- assert.equal(found[0].detail, '.mergify.yml, .github/workflows/ci.yml, package.json, Dockerfile.prod')
404
- const py = "with open('cloudbuild.yaml') as f:\n steps = yaml.safe_load(f)['steps']\nassert steps[0]['name'] == 'gcr.io/cloud-builders/docker'"
405
- assert.deepEqual(ids(scanText('tests/test_build.py', py, ['config-read'])), ['config-read:1'])
406
- })
407
-
408
- test('config-read ignores fixtures, written files, executed configs, data JSON and read-free tests', () => {
409
- const text = [
410
- "const cfg = readFileSync('fixtures/coverage.config.json', 'utf8')",
411
- "writeFileSync(join(tmp, 'coverage.config.json'), JSON.stringify(policy))",
412
- "const got = readFileSync(join(tmp, 'coverage.config.json'), 'utf8')",
413
- "execFileSync('node', ['cli.mjs', '--config', 'app.config.json'])",
414
- "expect(got).toEqual(cfg)",
415
- ].join('\n')
416
- assert.deepEqual(scanText('x.test.mjs', text, ['config-read']), [])
417
- const data = "const run = JSON.parse(readFileSync(join(dir, 'run-state.json'), 'utf8'))\nconst rates = readFileSync('data/rates.json', 'utf8')\nexpect(run.status).toBe('green')"
418
- assert.deepEqual(scanText('x.test.mjs', data, ['config-read']), []) // JSON as data, not config
419
- const noRead = "expect(loadConfig({ env: 'prod' }).port).toBe(8080) // config.yaml defaults"
420
- assert.deepEqual(scanText('x.test.mjs', noRead, ['config-read']), [])
421
- })
422
-
423
- test('a text-matched config file is config-read, not text-oracle', () => {
424
- const text = "const wf = readFileSync('.github/workflows/ci.yml', 'utf8')\nexpect(wf).toContain('postgres:17')"
425
- assert.deepEqual(ids(scanText('x.test.mjs', text, ['text-oracle', 'config-read'])), ['config-read:1'])
426
- })
427
-
428
- test('count-pin flags exact sizes of five or more and skips format lengths', () => {
429
- const text = [
430
- 'expect(STATES).toHaveLength(11)',
431
- 'assert.equal(members.length, 19)',
432
- 'require.Len(t, knownLogEvents, 28)',
433
- 'assert len(rows) == 12',
434
- 'expect(digest).toHaveLength(64)',
435
- 'expect(list).toHaveLength(2)',
436
- 'expect(id.length).toBe(36)',
437
- ].join('\n')
438
- assert.deepEqual(ids(scanText('a.test.ts', text, ['count-pin'])), ['count-pin:1', 'count-pin:2', 'count-pin:3', 'count-pin:4'])
439
- })
440
-
441
- test('utility-class flags Tailwind tokens on class assertions, not semantic state classes', () => {
442
- const text = [
443
- "expect(el).toHaveClass('-mx-6', 'rounded-none', 'sm:shadow-sm')",
444
- "expect(wrapper.classes()).toContain('h-[min(62vh,36rem)]')",
445
- "expect(button.className).toContain('min-h-11')",
446
- "expect(cell.classes()).toContain('health-unknown')",
447
- "expect(el).toHaveClass('is-active')",
448
- ].join('\n')
449
- assert.deepEqual(ids(scanText('a.test.ts', text, ['utility-class'])), ['utility-class:1', 'utility-class:2', 'utility-class:3'])
450
- })
451
-
452
- test('findDuplicateFiles groups whitespace-identical files and ignores tiny ones', () => {
453
- const body = Array.from({ length: 12 }, (_, i) => ` it('case ${i}', () => expect(f(${i})).toBe(${i}))`).join('\n')
454
- const found = findDuplicateFiles([
455
- { file: 'a/x.test.ts', text: body },
456
- { file: 'b/x.test.ts', text: body.replace(/ {2}/g, '\t') },
457
- { file: 'c/y.test.ts', text: body + '\nit("extra", () => {})' },
458
- { file: 'd/tiny.test.ts', text: 'it("a", () => {})' },
459
- { file: 'e/tiny.test.ts', text: 'it("a", () => {})' },
460
- ])
461
- assert.deepEqual(found.map((f) => [f.file, f.detail]), [['a/x.test.ts', 'b/x.test.ts'], ['b/x.test.ts', 'a/x.test.ts']])
462
- assert.equal(found[0].severity, 'lead')
463
- })
464
-
465
- test('scanDiff runs file-level rules on the full file but reports only added lines', () => {
466
- const file = [
467
- "const old = readFileSync('docs/old.md', 'utf8')",
468
- "const doc = readFileSync('docs/new.md', 'utf8')",
469
- "expect(doc).toContain('x')",
470
- ].join('\n')
471
- const diff = [
472
- 'diff --git a/x.test.mjs b/x.test.mjs',
473
- '--- a/x.test.mjs',
474
- '+++ b/x.test.mjs',
475
- '@@ -1,1 +1,3 @@',
476
- " const old = readFileSync('docs/old.md', 'utf8')",
477
- "+const doc = readFileSync('docs/new.md', 'utf8')",
478
- "+expect(doc).toContain('x')",
479
- ].join('\n')
480
- assert.deepEqual(ids(scanDiff(diff, ['text-oracle'], undefined, () => file)), ['text-oracle:2'])
481
- assert.deepEqual(scanDiff(diff, ['text-oracle']), []) // no reader → file-level rules skipped
482
- })
483
-
484
- test('lead findings never change the exit code; gate findings do', () => {
485
- const dir = mkdtempSync(join(tmpdir(), 'lint-tests-'))
486
- const lead = join(dir, 'lead.test.ts')
487
- const gate = join(dir, 'gate.test.ts')
488
- writeFileSync(lead, 'expect(STATES).toHaveLength(11)\n')
489
- writeFileSync(gate, seamMocks('@/lib/db'))
490
- const orig = console.log
491
- console.log = () => {}
492
- try {
493
- assert.equal(runCli(['--files', lead]), 0)
494
- assert.equal(runCli(['--files', gate]), 1)
495
- } finally {
496
- console.log = orig
497
- rmSync(dir, { recursive: true, force: true })
498
- }
499
- })
500
-
501
- // ---- CLI: fail closed, working-tree diff, exclude ----
502
-
503
- function quietly(fn) {
504
- const out = []
505
- const [log, err] = [console.log, console.error]
506
- console.log = (s) => out.push(String(s))
507
- console.error = (s) => out.push(String(s))
508
- try { return { code: fn(), out: out.join('\n') } } finally { console.log = log; console.error = err }
509
- }
510
-
511
- test('the CLI rejects unknown flags, unknown rules, and missing files with exit 2', () => {
512
- const dir = mkdtempSync(join(tmpdir(), 'lint-cli-'))
513
- try {
514
- assert.equal(quietly(() => runCli(['--file', 'a.test.ts'], { cwd: dir })).code, 2)
515
- const typo = quietly(() => runCli(['--rules', 'cannot-fial'], { cwd: dir }))
516
- assert.equal(typo.code, 2)
517
- assert.match(typo.out, /unknown rule\(s\): cannot-fial/)
518
- assert.equal(quietly(() => runCli(['--files', 'nope.test.ts'], { cwd: dir })).code, 2)
519
- assert.equal(quietly(() => runCli(['--diff'], { cwd: dir })).code, 2)
520
- } finally {
521
- rmSync(dir, { recursive: true, force: true })
522
- }
523
- })
524
-
525
- test('--exclude drops files by path segment or prefix', () => {
526
- const dir = mkdtempSync(join(tmpdir(), 'lint-cli-'))
527
- try {
528
- for (const d of ['src', 'dist', 'tests/fixtures']) mkdirSync(join(dir, d), { recursive: true })
529
- for (const f of ['src/a.test.ts', 'dist/a.test.ts', 'tests/fixtures/b.test.ts']) writeFileSync(join(dir, f), seamMocks('@/lib/db'))
530
- const all = JSON.parse(quietly(() => runCli(['--json'], { cwd: dir })).out)
531
- assert.equal(all.gates, 3)
532
- const some = JSON.parse(quietly(() => runCli(['--json', '--exclude', 'dist,fixtures'], { cwd: dir })).out)
533
- assert.deepEqual(some.findings.map((f) => f.file), ['src/a.test.ts'])
534
- } finally {
535
- rmSync(dir, { recursive: true, force: true })
536
- }
537
- })
538
-
539
- test('--diff sees staged, unstaged, and untracked test files, with repo-root paths from a subdirectory', () => {
540
- const dir = mkdtempSync(join(tmpdir(), 'lint-diff-'))
541
- const git = (args) => execSync(`git ${args}`, { cwd: dir, stdio: 'pipe', env: { ...process.env, GIT_AUTHOR_NAME: 't', GIT_AUTHOR_EMAIL: 't@t', GIT_COMMITTER_NAME: 't', GIT_COMMITTER_EMAIL: 't@t' } })
542
- try {
543
- git('init -q -b main')
544
- mkdirSync(join(dir, 'pkg/tests'), { recursive: true })
545
- writeFileSync(join(dir, 'pkg/tests/old.test.ts'), seamMocks('@/lib/old'))
546
- git('add -A')
547
- git('commit -q -m base')
548
- git('checkout -q -b work')
549
- writeFileSync(join(dir, 'pkg/tests/old.test.ts'), seamMocks('@/lib/old', '@/lib/unstaged'))
550
- writeFileSync(join(dir, 'pkg/tests/staged.test.ts'), seamMocks('@/lib/staged'))
551
- git('add pkg/tests/staged.test.ts')
552
- writeFileSync(join(dir, 'pkg/tests/new.test.ts'), "const doc = readFileSync('docs/a.md', 'utf8')\nexpect(doc).toContain('x')\n")
553
- const res = JSON.parse(quietly(() => runCli(['--json', '--diff', 'main'], { cwd: join(dir, 'pkg') })).out)
554
- assert.deepEqual(res.findings.map((f) => `${f.file}:${f.line}:${f.rule}`).sort(), [
555
- 'pkg/tests/new.test.ts:1:text-oracle',
556
- 'pkg/tests/old.test.ts:2:mock-internal-seam',
557
- 'pkg/tests/staged.test.ts:1:mock-internal-seam',
558
- ])
559
- } finally {
560
- rmSync(dir, { recursive: true, force: true })
561
- }
562
- })
563
-
564
- // ---- --shares: split flagged files for parallel reviewers ----
565
-
566
- test('splitShares balances finding counts, keeps each file whole, and never makes empty shares', () => {
567
- const at = (file, n) => Array.from({ length: n }, () => ({ file }))
568
- const findings = [...at('a', 5), ...at('b', 3), ...at('c', 3), ...at('d', 2), ...at('e', 1)]
569
- const shares = splitShares(findings, 3)
570
- assert.deepEqual(shares.map((s) => s.findings), [5, 5, 4])
571
- assert.deepEqual(shares.flatMap((s) => s.files).sort(), ['a', 'b', 'c', 'd', 'e'])
572
- assert.equal(splitShares(at('a', 9), 4).length, 1, 'one flagged file is one share')
573
- assert.deepEqual(splitShares([], 4), [])
574
- })
575
-
576
- test('--shares adds the split to --json output and rejects a non-positive count', () => {
577
- const dir = mkdtempSync(join(tmpdir(), 'lint-tests-'))
578
- try {
579
- writeFileSync(join(dir, 'a.test.ts'), 'expect(STATES).toHaveLength(11)\nexpect(X).toHaveLength(12)\n')
580
- writeFileSync(join(dir, 'b.test.ts'), 'expect(Y).toHaveLength(13)\n')
581
- writeFileSync(join(dir, 'clean.test.ts'), 'expect(add(1, 2)).toBe(3)\n')
582
- const run = quietly(() => runCli(['--json', '--shares', '2'], { cwd: dir }))
583
- assert.equal(run.code, 0)
584
- assert.deepEqual(JSON.parse(run.out).shares, [{ findings: 2, files: ['a.test.ts'] }, { findings: 1, files: ['b.test.ts'] }])
585
- assert.equal(JSON.parse(quietly(() => runCli(['--json'], { cwd: dir })).out).shares, undefined)
586
- assert.equal(quietly(() => runCli(['--shares', '0'], { cwd: dir })).code, 2)
587
- assert.equal(quietly(() => runCli(['--shares', 'x'], { cwd: dir })).code, 2)
588
- } finally {
589
- rmSync(dir, { recursive: true, force: true })
590
- }
591
- })