@1aboveio/skills 1.0.7 → 1.0.9

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 (224) 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 +92 -34
  8. package/skills/app-dev/android-foundation/assets/android-ci-job.yml +13 -7
  9. package/skills/app-dev/android-foundation/references/ci.md +23 -13
  10. package/skills/app-dev/android-foundation/references/delivery-focus.md +29 -0
  11. package/skills/app-dev/android-foundation/references/diagnosis.md +136 -0
  12. package/skills/app-dev/android-foundation/references/gotchas.md +20 -11
  13. package/skills/app-dev/android-foundation/references/setup.md +15 -6
  14. package/skills/app-dev/android-foundation/release-check.mjs +3 -1
  15. package/skills/app-dev/android-foundation/scripts/device-run.sh +148 -42
  16. package/skills/app-dev/app-debug/SKILL.md +1 -1
  17. package/skills/app-dev/app-debug/release-check.mjs +3 -1
  18. package/skills/app-dev/better-auth/SKILL.md +1 -1
  19. package/skills/app-dev/better-auth/release-check.mjs +3 -1
  20. package/skills/app-dev/data-catalog/SKILL.md +1 -1
  21. package/skills/app-dev/data-catalog/release-check.mjs +3 -1
  22. package/skills/app-dev/data-quality/SKILL.md +1 -1
  23. package/skills/app-dev/data-quality/release-check.mjs +3 -1
  24. package/skills/app-dev/ios-foundation/SKILL.md +1 -1
  25. package/skills/app-dev/ios-foundation/release-check.mjs +3 -1
  26. package/skills/app-dev/monorepo/SKILL.md +1 -1
  27. package/skills/app-dev/monorepo/release-check.mjs +3 -1
  28. package/skills/app-dev/nextjs-fullstack/SKILL.md +1 -1
  29. package/skills/app-dev/nextjs-fullstack/release-check.mjs +3 -1
  30. package/skills/app-dev/prisma-setup/SKILL.md +1 -1
  31. package/skills/app-dev/prisma-setup/release-check.mjs +3 -1
  32. package/skills/app-dev/pyspark/SKILL.md +1 -1
  33. package/skills/app-dev/pyspark/release-check.mjs +3 -1
  34. package/skills/app-dev/python-backend/SKILL.md +1 -1
  35. package/skills/app-dev/python-backend/release-check.mjs +3 -1
  36. package/skills/app-dev/shadcn/SKILL.md +1 -1
  37. package/skills/app-dev/shadcn/release-check.mjs +3 -1
  38. package/skills/app-dev/zod-v4/SKILL.md +1 -1
  39. package/skills/app-dev/zod-v4/release-check.mjs +3 -1
  40. package/skills/cicd/cloud-build/SKILL.md +1 -1
  41. package/skills/cicd/cloud-build/release-check.mjs +3 -1
  42. package/skills/cicd/cloud-debug/SKILL.md +1 -1
  43. package/skills/cicd/cloud-debug/release-check.mjs +3 -1
  44. package/skills/cicd/cloud-deploy/SKILL.md +1 -1
  45. package/skills/cicd/cloud-deploy/release-check.mjs +3 -1
  46. package/skills/cicd/docker/SKILL.md +1 -1
  47. package/skills/cicd/docker/release-check.mjs +3 -1
  48. package/skills/cicd/github-actions/SKILL.md +1 -1
  49. package/skills/cicd/github-actions/release-check.mjs +3 -1
  50. package/skills/cicd/github-flow/SKILL.md +1 -1
  51. package/skills/cicd/github-flow/release-check.mjs +3 -1
  52. package/skills/cicd/google-cloud/SKILL.md +1 -1
  53. package/skills/cicd/google-cloud/release-check.mjs +3 -1
  54. package/skills/cicd/mergify/SKILL.md +1 -1
  55. package/skills/cicd/mergify/release-check.mjs +3 -1
  56. package/skills/cicd/podman/SKILL.md +1 -1
  57. package/skills/cicd/podman/release-check.mjs +3 -1
  58. package/skills/content/article-review/SKILL.md +1 -1
  59. package/skills/content/article-review/release-check.mjs +3 -1
  60. package/skills/content/article-to-image/SKILL.md +1 -1
  61. package/skills/content/article-to-image/release-check.mjs +3 -1
  62. package/skills/content/flowchart-svg/SKILL.md +1 -1
  63. package/skills/content/flowchart-svg/release-check.mjs +3 -1
  64. package/skills/content/image-gen/SKILL.md +1 -1
  65. package/skills/content/image-gen/release-check.mjs +3 -1
  66. package/skills/content/mermaid/SKILL.md +1 -1
  67. package/skills/content/mermaid/release-check.mjs +3 -1
  68. package/skills/content/wechat-article-writer/SKILL.md +1 -1
  69. package/skills/content/wechat-article-writer/release-check.mjs +3 -1
  70. package/skills/content/wechat-publisher/SKILL.md +1 -1
  71. package/skills/content/wechat-publisher/release-check.mjs +3 -1
  72. package/skills/content/wechat-search/SKILL.md +1 -1
  73. package/skills/content/wechat-search/release-check.mjs +3 -1
  74. package/skills/content/xai-search/SKILL.md +88 -13
  75. package/skills/content/xai-search/release-check.mjs +3 -1
  76. package/skills/content/xai-search/scripts/xai_search.sh +125 -149
  77. package/skills/core/distill-writing/SKILL.md +1 -1
  78. package/skills/core/distill-writing/release-check.mjs +3 -1
  79. package/skills/core/doc-to-md/SKILL.md +1 -1
  80. package/skills/core/doc-to-md/release-check.mjs +3 -1
  81. package/skills/core/editorial-review/SKILL.md +1 -1
  82. package/skills/core/editorial-review/release-check.mjs +3 -1
  83. package/skills/core/house-style/SKILL.md +1 -1
  84. package/skills/core/house-style/release-check.mjs +3 -1
  85. package/skills/core/plain-language/SKILL.md +1 -1
  86. package/skills/core/plain-language/release-check.mjs +3 -1
  87. package/skills/core/polish-cn/SKILL.md +1 -1
  88. package/skills/core/polish-cn/release-check.mjs +3 -1
  89. package/skills/core/reg-parser/SKILL.md +1 -1
  90. package/skills/core/reg-parser/release-check.mjs +3 -1
  91. package/skills/core/terminology/SKILL.md +1 -1
  92. package/skills/core/terminology/release-check.mjs +3 -1
  93. package/skills/core/write-doc/SKILL.md +1 -1
  94. package/skills/core/write-doc/release-check.mjs +3 -1
  95. package/skills/engineering/better-review/SKILL.md +1 -1
  96. package/skills/engineering/better-review/release-check.mjs +3 -1
  97. package/skills/engineering/contributing/SKILL.md +1 -1
  98. package/skills/engineering/contributing/release-check.mjs +3 -1
  99. package/skills/engineering/e2e-test/SKILL.md +1 -1
  100. package/skills/engineering/e2e-test/release-check.mjs +3 -1
  101. package/skills/engineering/engineering-runtime/SKILL.md +1 -1
  102. package/skills/engineering/engineering-runtime/coherence/workflow.json +13 -13
  103. package/skills/engineering/engineering-runtime/release-check.mjs +3 -1
  104. package/skills/engineering/engineering-runtime/scripts/installation-resolver.mjs +191 -0
  105. package/skills/engineering/engineering-runtime/scripts/workflow-coherence.mjs +51 -10
  106. package/skills/engineering/ensure-coverage/SKILL.md +1 -1
  107. package/skills/engineering/ensure-coverage/release-check.mjs +3 -1
  108. package/skills/engineering/golden-path/SKILL.md +1 -1
  109. package/skills/engineering/golden-path/release-check.mjs +3 -1
  110. package/skills/engineering/harness-runtime/SKILL.md +1 -1
  111. package/skills/engineering/harness-runtime/release-check.mjs +3 -1
  112. package/skills/engineering/resolve-issues/SKILL.md +11 -20
  113. package/skills/engineering/resolve-issues/references/delivery-progress.md +63 -0
  114. package/skills/engineering/resolve-issues/references/expire.md +24 -11
  115. package/skills/engineering/resolve-issues/references/implementation.md +9 -1
  116. package/skills/engineering/resolve-issues/references/preflight.md +7 -0
  117. package/skills/engineering/resolve-issues/references/shared-modules.md +27 -0
  118. package/skills/engineering/resolve-issues/references/task-plan.md +12 -5
  119. package/skills/engineering/resolve-issues/release-check.mjs +3 -1
  120. package/skills/engineering/resolve-issues/scripts/active-installation.mjs +32 -0
  121. package/skills/engineering/resolve-issues/scripts/install-hooks.mjs +3 -0
  122. package/skills/engineering/resolve-issues/scripts/rehydrate.mjs +17 -1
  123. package/skills/engineering/resolve-release/SKILL.md +1 -1
  124. package/skills/engineering/resolve-release/release-check.mjs +3 -1
  125. package/skills/engineering/rush-issues/SKILL.md +5 -2
  126. package/skills/engineering/rush-issues/release-check.mjs +3 -1
  127. package/skills/engineering/rush-release/SKILL.md +1 -1
  128. package/skills/engineering/rush-release/release-check.mjs +3 -1
  129. package/skills/engineering/test-quality/SKILL.md +1 -1
  130. package/skills/engineering/test-quality/release-check.mjs +3 -1
  131. package/skills/risk-compliance/compliance-audit/SKILL.md +1 -1
  132. package/skills/risk-compliance/compliance-audit/release-check.mjs +3 -1
  133. package/skills/risk-compliance/contract-compare/SKILL.md +1 -1
  134. package/skills/risk-compliance/contract-compare/release-check.mjs +3 -1
  135. package/skills/risk-compliance/contract-review/SKILL.md +1 -1
  136. package/skills/risk-compliance/contract-review/release-check.mjs +3 -1
  137. package/skills/risk-compliance/doc-authenticity/SKILL.md +1 -1
  138. package/skills/risk-compliance/doc-authenticity/release-check.mjs +3 -1
  139. package/skills/risk-compliance/fraud-analysis/SKILL.md +1 -1
  140. package/skills/risk-compliance/fraud-analysis/release-check.mjs +3 -1
  141. package/skills/risk-compliance/payment-analysis/SKILL.md +1 -1
  142. package/skills/risk-compliance/payment-analysis/release-check.mjs +3 -1
  143. package/skills/risk-compliance/similar-domain/SKILL.md +1 -1
  144. package/skills/risk-compliance/similar-domain/release-check.mjs +3 -1
  145. package/skills/risk-compliance/tld-detection/SKILL.md +1 -1
  146. package/skills/risk-compliance/tld-detection/release-check.mjs +3 -1
  147. package/skills/app-dev/android-foundation/scripts/device-run.test.mjs +0 -172
  148. package/skills/app-dev/data-catalog/scripts/data-catalog.test.mjs +0 -36
  149. package/skills/app-dev/data-quality/scripts/data-quality.test.mjs +0 -12
  150. package/skills/cicd/github-actions/scripts/ci-run-audit.test.mjs +0 -637
  151. package/skills/cicd/github-flow/scripts/flow.test.mjs +0 -114
  152. package/skills/cicd/github-flow/scripts/persist-conventions.test.mjs +0 -108
  153. package/skills/cicd/mergify/scripts/audit.test.mjs +0 -201
  154. package/skills/cicd/mergify/scripts/entrypoints.test.mjs +0 -106
  155. package/skills/cicd/mergify/scripts/pr-620-mergify-comments.fixture.json +0 -30
  156. package/skills/cicd/mergify/scripts/stack-push.test.mjs +0 -233
  157. package/skills/cicd/mergify/scripts/watch-pr-delivery.test.mjs +0 -1826
  158. package/skills/content/image-gen/scripts/credentials.test.mjs +0 -212
  159. package/skills/content/image-gen/scripts/options.test.mjs +0 -245
  160. package/skills/content/xai-search/tests/smoke_test.sh +0 -36
  161. package/skills/core/polish-cn/scripts/polish-cn.test.mjs +0 -244
  162. package/skills/core/reg-parser/scripts/reg-parser.test.mjs +0 -165
  163. package/skills/engineering/better-review/scripts/pr-context.test.mjs +0 -168
  164. package/skills/engineering/better-review/scripts/rereview-scope.test.mjs +0 -587
  165. package/skills/engineering/better-review/scripts/scan-diff.test.mjs +0 -414
  166. package/skills/engineering/better-review/scripts/vendor-sync.test.mjs +0 -96
  167. package/skills/engineering/contributing/scripts/persist-conventions.test.mjs +0 -118
  168. package/skills/engineering/e2e-test/scripts/presentation-checks.test.mjs +0 -106
  169. package/skills/engineering/e2e-test/scripts/presentation-perceivability.test.mjs +0 -117
  170. package/skills/engineering/e2e-test/scripts/presentation-reachability.test.mjs +0 -69
  171. package/skills/engineering/e2e-test/scripts/presentation-render-health.test.mjs +0 -98
  172. package/skills/engineering/e2e-test/scripts/presentation-sweep.test.mjs +0 -71
  173. package/skills/engineering/e2e-test/scripts/presentation-temporal.test.mjs +0 -59
  174. package/skills/engineering/e2e-test/scripts/project-route-manifest.test.mjs +0 -76
  175. package/skills/engineering/engineering-runtime/engineering-runtime.test.mjs +0 -68
  176. package/skills/engineering/engineering-runtime/scripts/invocation-policy.test.mjs +0 -95
  177. package/skills/engineering/engineering-runtime/scripts/main-module.test.mjs +0 -288
  178. package/skills/engineering/engineering-runtime/scripts/required-check-plan.test.mjs +0 -96
  179. package/skills/engineering/ensure-coverage/scripts/adapters/nextjs-inventory.test.mjs +0 -96
  180. package/skills/engineering/ensure-coverage/scripts/check-quarantine-expiry.test.mjs +0 -47
  181. package/skills/engineering/ensure-coverage/scripts/ci-audit.test.mjs +0 -86
  182. package/skills/engineering/ensure-coverage/scripts/coverage-ledger.test.mjs +0 -447
  183. package/skills/engineering/ensure-coverage/scripts/design-parity.test.mjs +0 -552
  184. package/skills/engineering/ensure-coverage/scripts/lint-tests.test.mjs +0 -289
  185. package/skills/engineering/ensure-coverage/scripts/mock-policy-config.test.mjs +0 -119
  186. package/skills/engineering/golden-path/scripts/golden-path.test.mjs +0 -1457
  187. package/skills/engineering/harness-runtime/discover-models.test.mjs +0 -363
  188. package/skills/engineering/harness-runtime/harness-runtime.test.mjs +0 -967
  189. package/skills/engineering/harness-runtime/model-catalog-seed.test.mjs +0 -829
  190. package/skills/engineering/harness-runtime/model-catalog.test.mjs +0 -366
  191. package/skills/engineering/resolve-issues/scripts/cleanup-runs.test.mjs +0 -280
  192. package/skills/engineering/resolve-issues/scripts/dropbox.test.mjs +0 -346
  193. package/skills/engineering/resolve-issues/scripts/findings-reuse.test.mjs +0 -88
  194. package/skills/engineering/resolve-issues/scripts/github-reviews.test.mjs +0 -264
  195. package/skills/engineering/resolve-issues/scripts/install-hooks.test.mjs +0 -79
  196. package/skills/engineering/resolve-issues/scripts/issue-tree.test.mjs +0 -107
  197. package/skills/engineering/resolve-issues/scripts/manual-invocation.test.mjs +0 -14
  198. package/skills/engineering/resolve-issues/scripts/next-actions-capacity.test.mjs +0 -149
  199. package/skills/engineering/resolve-issues/scripts/next-actions.test.mjs +0 -915
  200. package/skills/engineering/resolve-issues/scripts/plan-sync.test.mjs +0 -156
  201. package/skills/engineering/resolve-issues/scripts/preflight-models.test.mjs +0 -448
  202. package/skills/engineering/resolve-issues/scripts/ready-check.test.mjs +0 -217
  203. package/skills/engineering/resolve-issues/scripts/rehydrate.test.mjs +0 -91
  204. package/skills/engineering/resolve-issues/scripts/review-verdict.test.mjs +0 -1060
  205. package/skills/engineering/resolve-issues/scripts/size-check.test.mjs +0 -458
  206. package/skills/engineering/resolve-release/scripts/apply.test.mjs +0 -99
  207. package/skills/engineering/resolve-release/scripts/dark-flags.test.mjs +0 -100
  208. package/skills/engineering/resolve-release/scripts/flow.test.mjs +0 -340
  209. package/skills/engineering/resolve-release/scripts/green-head.test.mjs +0 -101
  210. package/skills/engineering/resolve-release/scripts/npm-version-absence.test.mjs +0 -426
  211. package/skills/engineering/resolve-release/scripts/plan-npm.test.mjs +0 -659
  212. package/skills/engineering/resolve-release/scripts/plan-toml-boundary.test.mjs +0 -317
  213. package/skills/engineering/resolve-release/scripts/plan-uv.test.mjs +0 -291
  214. package/skills/engineering/resolve-release/scripts/plan.test.mjs +0 -428
  215. package/skills/engineering/test-quality/scripts/lint-tests.test.mjs +0 -591
  216. package/skills/engineering/test-quality/scripts/mock-policy-config.test.mjs +0 -119
  217. package/skills/engineering/test-quality/scripts/persist-conventions.test.mjs +0 -104
  218. package/skills/risk-compliance/compliance-audit/scripts/compliance-audit.test.mjs +0 -14
  219. package/skills/risk-compliance/doc-authenticity/manual-invocation.test.mjs +0 -12
  220. package/skills/risk-compliance/doc-authenticity/scripts/validate_verdict.test.mjs +0 -212
  221. package/skills/risk-compliance/tld-detection/scripts/detect_ladders.test.mjs +0 -14
  222. package/skills/risk-compliance/tld-detection/scripts/detect_streams.test.mjs +0 -14
  223. package/skills/risk-compliance/tld-detection/tests/test_detect_ladders.py +0 -134
  224. 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
- })