@1aboveio/skills 1.0.8 → 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.
- package/.codex-plugin/plugin.json +13 -0
- package/.codex-plugin/resources.json +1827 -0
- package/README.md +22 -0
- package/package.json +9 -2
- package/skills/app-dev/airflow/SKILL.md +1 -1
- package/skills/app-dev/airflow/release-check.mjs +3 -1
- package/skills/app-dev/android-foundation/SKILL.md +1 -1
- package/skills/app-dev/android-foundation/release-check.mjs +3 -1
- package/skills/app-dev/app-debug/SKILL.md +1 -1
- package/skills/app-dev/app-debug/release-check.mjs +3 -1
- package/skills/app-dev/better-auth/SKILL.md +1 -1
- package/skills/app-dev/better-auth/release-check.mjs +3 -1
- package/skills/app-dev/data-catalog/SKILL.md +1 -1
- package/skills/app-dev/data-catalog/release-check.mjs +3 -1
- package/skills/app-dev/data-quality/SKILL.md +1 -1
- package/skills/app-dev/data-quality/release-check.mjs +3 -1
- package/skills/app-dev/ios-foundation/SKILL.md +1 -1
- package/skills/app-dev/ios-foundation/release-check.mjs +3 -1
- package/skills/app-dev/monorepo/SKILL.md +1 -1
- package/skills/app-dev/monorepo/release-check.mjs +3 -1
- package/skills/app-dev/nextjs-fullstack/SKILL.md +1 -1
- package/skills/app-dev/nextjs-fullstack/release-check.mjs +3 -1
- package/skills/app-dev/prisma-setup/SKILL.md +1 -1
- package/skills/app-dev/prisma-setup/release-check.mjs +3 -1
- package/skills/app-dev/pyspark/SKILL.md +1 -1
- package/skills/app-dev/pyspark/release-check.mjs +3 -1
- package/skills/app-dev/python-backend/SKILL.md +1 -1
- package/skills/app-dev/python-backend/release-check.mjs +3 -1
- package/skills/app-dev/shadcn/SKILL.md +1 -1
- package/skills/app-dev/shadcn/release-check.mjs +3 -1
- package/skills/app-dev/zod-v4/SKILL.md +1 -1
- package/skills/app-dev/zod-v4/release-check.mjs +3 -1
- package/skills/cicd/cloud-build/SKILL.md +1 -1
- package/skills/cicd/cloud-build/release-check.mjs +3 -1
- package/skills/cicd/cloud-debug/SKILL.md +1 -1
- package/skills/cicd/cloud-debug/release-check.mjs +3 -1
- package/skills/cicd/cloud-deploy/SKILL.md +1 -1
- package/skills/cicd/cloud-deploy/release-check.mjs +3 -1
- package/skills/cicd/docker/SKILL.md +1 -1
- package/skills/cicd/docker/release-check.mjs +3 -1
- package/skills/cicd/github-actions/SKILL.md +1 -1
- package/skills/cicd/github-actions/release-check.mjs +3 -1
- package/skills/cicd/github-flow/SKILL.md +1 -1
- package/skills/cicd/github-flow/release-check.mjs +3 -1
- package/skills/cicd/google-cloud/SKILL.md +1 -1
- package/skills/cicd/google-cloud/release-check.mjs +3 -1
- package/skills/cicd/mergify/SKILL.md +1 -1
- package/skills/cicd/mergify/release-check.mjs +3 -1
- package/skills/cicd/podman/SKILL.md +1 -1
- package/skills/cicd/podman/release-check.mjs +3 -1
- package/skills/content/article-review/SKILL.md +1 -1
- package/skills/content/article-review/release-check.mjs +3 -1
- package/skills/content/article-to-image/SKILL.md +1 -1
- package/skills/content/article-to-image/release-check.mjs +3 -1
- package/skills/content/flowchart-svg/SKILL.md +1 -1
- package/skills/content/flowchart-svg/release-check.mjs +3 -1
- package/skills/content/image-gen/SKILL.md +1 -1
- package/skills/content/image-gen/release-check.mjs +3 -1
- package/skills/content/mermaid/SKILL.md +1 -1
- package/skills/content/mermaid/release-check.mjs +3 -1
- package/skills/content/wechat-article-writer/SKILL.md +1 -1
- package/skills/content/wechat-article-writer/release-check.mjs +3 -1
- package/skills/content/wechat-publisher/SKILL.md +1 -1
- package/skills/content/wechat-publisher/release-check.mjs +3 -1
- package/skills/content/wechat-search/SKILL.md +1 -1
- package/skills/content/wechat-search/release-check.mjs +3 -1
- package/skills/content/xai-search/SKILL.md +88 -13
- package/skills/content/xai-search/release-check.mjs +3 -1
- package/skills/content/xai-search/scripts/xai_search.sh +125 -149
- package/skills/core/distill-writing/SKILL.md +1 -1
- package/skills/core/distill-writing/release-check.mjs +3 -1
- package/skills/core/doc-to-md/SKILL.md +1 -1
- package/skills/core/doc-to-md/release-check.mjs +3 -1
- package/skills/core/editorial-review/SKILL.md +1 -1
- package/skills/core/editorial-review/release-check.mjs +3 -1
- package/skills/core/house-style/SKILL.md +1 -1
- package/skills/core/house-style/release-check.mjs +3 -1
- package/skills/core/plain-language/SKILL.md +1 -1
- package/skills/core/plain-language/release-check.mjs +3 -1
- package/skills/core/polish-cn/SKILL.md +1 -1
- package/skills/core/polish-cn/release-check.mjs +3 -1
- package/skills/core/reg-parser/SKILL.md +1 -1
- package/skills/core/reg-parser/release-check.mjs +3 -1
- package/skills/core/terminology/SKILL.md +1 -1
- package/skills/core/terminology/release-check.mjs +3 -1
- package/skills/core/write-doc/SKILL.md +1 -1
- package/skills/core/write-doc/release-check.mjs +3 -1
- package/skills/engineering/better-review/SKILL.md +1 -1
- package/skills/engineering/better-review/release-check.mjs +3 -1
- package/skills/engineering/contributing/SKILL.md +1 -1
- package/skills/engineering/contributing/release-check.mjs +3 -1
- package/skills/engineering/e2e-test/SKILL.md +1 -1
- package/skills/engineering/e2e-test/release-check.mjs +3 -1
- package/skills/engineering/engineering-runtime/SKILL.md +1 -1
- package/skills/engineering/engineering-runtime/coherence/workflow.json +13 -13
- package/skills/engineering/engineering-runtime/release-check.mjs +3 -1
- package/skills/engineering/engineering-runtime/scripts/installation-resolver.mjs +191 -0
- package/skills/engineering/engineering-runtime/scripts/workflow-coherence.mjs +51 -10
- package/skills/engineering/ensure-coverage/SKILL.md +1 -1
- package/skills/engineering/ensure-coverage/release-check.mjs +3 -1
- package/skills/engineering/golden-path/SKILL.md +1 -1
- package/skills/engineering/golden-path/release-check.mjs +3 -1
- package/skills/engineering/harness-runtime/SKILL.md +1 -1
- package/skills/engineering/harness-runtime/release-check.mjs +3 -1
- package/skills/engineering/resolve-issues/SKILL.md +1 -1
- package/skills/engineering/resolve-issues/references/preflight.md +7 -0
- package/skills/engineering/resolve-issues/references/shared-modules.md +27 -0
- package/skills/engineering/resolve-issues/references/task-plan.md +5 -1
- package/skills/engineering/resolve-issues/release-check.mjs +3 -1
- package/skills/engineering/resolve-issues/scripts/active-installation.mjs +32 -0
- package/skills/engineering/resolve-issues/scripts/install-hooks.mjs +3 -0
- package/skills/engineering/resolve-issues/scripts/rehydrate.mjs +17 -1
- package/skills/engineering/resolve-release/SKILL.md +1 -1
- package/skills/engineering/resolve-release/release-check.mjs +3 -1
- package/skills/engineering/rush-issues/SKILL.md +1 -1
- package/skills/engineering/rush-issues/release-check.mjs +3 -1
- package/skills/engineering/rush-release/SKILL.md +1 -1
- package/skills/engineering/rush-release/release-check.mjs +3 -1
- package/skills/engineering/test-quality/SKILL.md +1 -1
- package/skills/engineering/test-quality/release-check.mjs +3 -1
- package/skills/risk-compliance/compliance-audit/SKILL.md +1 -1
- package/skills/risk-compliance/compliance-audit/release-check.mjs +3 -1
- package/skills/risk-compliance/contract-compare/SKILL.md +1 -1
- package/skills/risk-compliance/contract-compare/release-check.mjs +3 -1
- package/skills/risk-compliance/contract-review/SKILL.md +1 -1
- package/skills/risk-compliance/contract-review/release-check.mjs +3 -1
- package/skills/risk-compliance/doc-authenticity/SKILL.md +1 -1
- package/skills/risk-compliance/doc-authenticity/release-check.mjs +3 -1
- package/skills/risk-compliance/fraud-analysis/SKILL.md +1 -1
- package/skills/risk-compliance/fraud-analysis/release-check.mjs +3 -1
- package/skills/risk-compliance/payment-analysis/SKILL.md +1 -1
- package/skills/risk-compliance/payment-analysis/release-check.mjs +3 -1
- package/skills/risk-compliance/similar-domain/SKILL.md +1 -1
- package/skills/risk-compliance/similar-domain/release-check.mjs +3 -1
- package/skills/risk-compliance/tld-detection/SKILL.md +1 -1
- package/skills/risk-compliance/tld-detection/release-check.mjs +3 -1
- package/skills/app-dev/android-foundation/scripts/device-run.test.mjs +0 -436
- package/skills/app-dev/data-catalog/scripts/data-catalog.test.mjs +0 -36
- package/skills/app-dev/data-quality/scripts/data-quality.test.mjs +0 -12
- package/skills/cicd/github-actions/scripts/ci-run-audit.test.mjs +0 -637
- package/skills/cicd/github-flow/scripts/flow.test.mjs +0 -114
- package/skills/cicd/github-flow/scripts/persist-conventions.test.mjs +0 -108
- package/skills/cicd/mergify/scripts/audit.test.mjs +0 -201
- package/skills/cicd/mergify/scripts/entrypoints.test.mjs +0 -106
- package/skills/cicd/mergify/scripts/pr-620-mergify-comments.fixture.json +0 -30
- package/skills/cicd/mergify/scripts/stack-push.test.mjs +0 -233
- package/skills/cicd/mergify/scripts/watch-pr-delivery.test.mjs +0 -1826
- package/skills/content/image-gen/scripts/credentials.test.mjs +0 -212
- package/skills/content/image-gen/scripts/options.test.mjs +0 -245
- package/skills/content/xai-search/tests/smoke_test.sh +0 -36
- package/skills/core/polish-cn/scripts/polish-cn.test.mjs +0 -244
- package/skills/core/reg-parser/scripts/reg-parser.test.mjs +0 -165
- package/skills/engineering/better-review/scripts/pr-context.test.mjs +0 -168
- package/skills/engineering/better-review/scripts/rereview-scope.test.mjs +0 -587
- package/skills/engineering/better-review/scripts/scan-diff.test.mjs +0 -414
- package/skills/engineering/better-review/scripts/vendor-sync.test.mjs +0 -96
- package/skills/engineering/contributing/scripts/persist-conventions.test.mjs +0 -118
- package/skills/engineering/e2e-test/scripts/presentation-checks.test.mjs +0 -106
- package/skills/engineering/e2e-test/scripts/presentation-perceivability.test.mjs +0 -117
- package/skills/engineering/e2e-test/scripts/presentation-reachability.test.mjs +0 -69
- package/skills/engineering/e2e-test/scripts/presentation-render-health.test.mjs +0 -98
- package/skills/engineering/e2e-test/scripts/presentation-sweep.test.mjs +0 -71
- package/skills/engineering/e2e-test/scripts/presentation-temporal.test.mjs +0 -59
- package/skills/engineering/e2e-test/scripts/project-route-manifest.test.mjs +0 -76
- package/skills/engineering/engineering-runtime/engineering-runtime.test.mjs +0 -68
- package/skills/engineering/engineering-runtime/scripts/invocation-policy.test.mjs +0 -95
- package/skills/engineering/engineering-runtime/scripts/main-module.test.mjs +0 -288
- package/skills/engineering/engineering-runtime/scripts/required-check-plan.test.mjs +0 -96
- package/skills/engineering/ensure-coverage/scripts/adapters/nextjs-inventory.test.mjs +0 -96
- package/skills/engineering/ensure-coverage/scripts/check-quarantine-expiry.test.mjs +0 -47
- package/skills/engineering/ensure-coverage/scripts/ci-audit.test.mjs +0 -86
- package/skills/engineering/ensure-coverage/scripts/coverage-ledger.test.mjs +0 -447
- package/skills/engineering/ensure-coverage/scripts/design-parity.test.mjs +0 -552
- package/skills/engineering/ensure-coverage/scripts/lint-tests.test.mjs +0 -289
- package/skills/engineering/ensure-coverage/scripts/mock-policy-config.test.mjs +0 -119
- package/skills/engineering/golden-path/scripts/golden-path.test.mjs +0 -1457
- package/skills/engineering/harness-runtime/discover-models.test.mjs +0 -363
- package/skills/engineering/harness-runtime/harness-runtime.test.mjs +0 -967
- package/skills/engineering/harness-runtime/model-catalog-seed.test.mjs +0 -829
- package/skills/engineering/harness-runtime/model-catalog.test.mjs +0 -366
- package/skills/engineering/resolve-issues/scripts/cleanup-runs.test.mjs +0 -280
- package/skills/engineering/resolve-issues/scripts/dropbox.test.mjs +0 -346
- package/skills/engineering/resolve-issues/scripts/findings-reuse.test.mjs +0 -88
- package/skills/engineering/resolve-issues/scripts/github-reviews.test.mjs +0 -264
- package/skills/engineering/resolve-issues/scripts/install-hooks.test.mjs +0 -79
- package/skills/engineering/resolve-issues/scripts/issue-tree.test.mjs +0 -107
- package/skills/engineering/resolve-issues/scripts/manual-invocation.test.mjs +0 -14
- package/skills/engineering/resolve-issues/scripts/next-actions-capacity.test.mjs +0 -149
- package/skills/engineering/resolve-issues/scripts/next-actions.test.mjs +0 -915
- package/skills/engineering/resolve-issues/scripts/plan-sync.test.mjs +0 -156
- package/skills/engineering/resolve-issues/scripts/preflight-models.test.mjs +0 -448
- package/skills/engineering/resolve-issues/scripts/ready-check.test.mjs +0 -217
- package/skills/engineering/resolve-issues/scripts/rehydrate.test.mjs +0 -91
- package/skills/engineering/resolve-issues/scripts/review-verdict.test.mjs +0 -1060
- package/skills/engineering/resolve-issues/scripts/size-check.test.mjs +0 -458
- package/skills/engineering/resolve-release/scripts/apply.test.mjs +0 -99
- package/skills/engineering/resolve-release/scripts/dark-flags.test.mjs +0 -100
- package/skills/engineering/resolve-release/scripts/flow.test.mjs +0 -340
- package/skills/engineering/resolve-release/scripts/green-head.test.mjs +0 -101
- package/skills/engineering/resolve-release/scripts/npm-version-absence.test.mjs +0 -426
- package/skills/engineering/resolve-release/scripts/plan-npm.test.mjs +0 -659
- package/skills/engineering/resolve-release/scripts/plan-toml-boundary.test.mjs +0 -317
- package/skills/engineering/resolve-release/scripts/plan-uv.test.mjs +0 -291
- package/skills/engineering/resolve-release/scripts/plan.test.mjs +0 -428
- package/skills/engineering/test-quality/scripts/lint-tests.test.mjs +0 -591
- package/skills/engineering/test-quality/scripts/mock-policy-config.test.mjs +0 -119
- package/skills/engineering/test-quality/scripts/persist-conventions.test.mjs +0 -104
- package/skills/risk-compliance/compliance-audit/scripts/compliance-audit.test.mjs +0 -14
- package/skills/risk-compliance/doc-authenticity/manual-invocation.test.mjs +0 -12
- package/skills/risk-compliance/doc-authenticity/scripts/validate_verdict.test.mjs +0 -212
- package/skills/risk-compliance/tld-detection/scripts/detect_ladders.test.mjs +0 -14
- package/skills/risk-compliance/tld-detection/scripts/detect_streams.test.mjs +0 -14
- package/skills/risk-compliance/tld-detection/tests/test_detect_ladders.py +0 -134
- 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
|
-
})
|