@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,119 +0,0 @@
1
- // Tests for the mock-policy classifier (pure; one fs case via a temp dir).
2
- import { test } from 'node:test'
3
- import assert from 'node:assert/strict'
4
- import fs from 'node:fs'
5
- import os from 'node:os'
6
- import path from 'node:path'
7
-
8
- import {
9
- DEFAULT_MOCK_POLICY,
10
- resolveMockPolicy,
11
- loadMockPolicy,
12
- loadMockPolicyFromFile,
13
- extractMockTarget,
14
- classifyMockTarget,
15
- } from './mock-policy-config.mjs'
16
-
17
- test('default policy is zero-tolerance: default-on, no waiver tag', () => {
18
- assert.equal(DEFAULT_MOCK_POLICY.enabled, true)
19
- // the inline-waiver escape hatch is gone
20
- assert.equal(DEFAULT_MOCK_POLICY.waiverTag, undefined)
21
- })
22
-
23
- test('extractMockTarget pulls the specifier from vi/jest/vitest mock+doMock', () => {
24
- assert.equal(extractMockTarget("vi." + "mock('@/lib/x')"), '@/lib/x')
25
- assert.equal(extractMockTarget('jest.' + 'mock("pkg")'), 'pkg')
26
- assert.equal(extractMockTarget('vi.' + 'doMock(`@/a/b`)'), '@/a/b')
27
- assert.equal(extractMockTarget('vitest.' + 'mock("./rel")'), './rel')
28
- assert.equal(extractMockTarget('const x = mockThing()'), null) // bare mock() is not a module mock
29
- assert.equal(extractMockTarget('expect(x).toBe(1)'), null)
30
- })
31
-
32
- test('classify: first-party alias @/ and ~/ are internal seams (the incident case)', () => {
33
- const p = resolveMockPolicy({})
34
- assert.equal(classifyMockTarget('@/lib/org-allowlist-client', p).internal, true)
35
- assert.equal(classifyMockTarget('~/services/cp', p).internal, true)
36
- })
37
-
38
- test('classify: stdlib and third-party packages are external (exempt)', () => {
39
- const p = resolveMockPolicy({})
40
- assert.equal(classifyMockTarget('node:fs', p).internal, false)
41
- assert.equal(classifyMockTarget('react', p).internal, false)
42
- assert.equal(classifyMockTarget('@playwright/test', p).internal, false)
43
- })
44
-
45
- test('classify: relative import into source is internal; into a test util/fixture is not', () => {
46
- const p = resolveMockPolicy({})
47
- assert.equal(classifyMockTarget('../services/payments', p).internal, true)
48
- assert.equal(classifyMockTarget('./helpers/operator-session-store', p).internal, false)
49
- assert.equal(classifyMockTarget('../__mocks__/cp', p).internal, false)
50
- assert.equal(classifyMockTarget('../fixtures/data', p).internal, false)
51
- })
52
-
53
- test('classify: declared first-party workspace package is internal', () => {
54
- const p = resolveMockPolicy({ firstPartyPackages: ['@acme/'] })
55
- assert.equal(classifyMockTarget('@acme/db', p).internal, true)
56
- assert.equal(classifyMockTarget('@acme', p).internal, true)
57
- assert.equal(classifyMockTarget('@other/db', p).internal, false)
58
- })
59
-
60
- test('classify: declared externals allowlist exempts a first-party-looking target', () => {
61
- const p = resolveMockPolicy({ externals: ['^@/generated/'] })
62
- assert.equal(classifyMockTarget('@/generated/openapi', p).internal, false) // exempted
63
- assert.equal(classifyMockTarget('@/lib/x', p).internal, true) // still flagged
64
- })
65
-
66
- test('compiled _externalsRe is non-enumerable: does not leak into JSON / enumeration', () => {
67
- const p = resolveMockPolicy({ externals: ['^@/generated/'] })
68
- // the compiled RegExp cache works...
69
- assert.equal(classifyMockTarget('@/generated/openapi', p).internal, false)
70
- // ...but is hidden from enumeration and serialization
71
- assert.ok(!Object.keys(p).includes('_externalsRe'))
72
- assert.equal(JSON.parse(JSON.stringify(p))._externalsRe, undefined)
73
- })
74
-
75
- test('loadMockPolicy reads coverage.config.json topology, defaults to on with no config', () => {
76
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mpc-'))
77
- // no config → zero-tolerance default (on), conventional topology
78
- assert.equal(loadMockPolicy(dir).enabled, true)
79
- // with config → declared topology is merged in (config declares topology, not on/off)
80
- fs.writeFileSync(
81
- path.join(dir, 'coverage.config.json'),
82
- JSON.stringify({ mockPolicy: { firstPartyPackages: ['@acme/'] } }),
83
- )
84
- const p = loadMockPolicy(dir)
85
- assert.equal(p.enabled, true)
86
- assert.deepEqual(p.firstPartyPackages, ['@acme/'])
87
- // inherited default array still present
88
- assert.ok(p.firstPartyAliasPrefixes.includes('@/'))
89
- })
90
-
91
- test('loadMockPolicy never throws on malformed config (and stays on)', () => {
92
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mpc-'))
93
- fs.writeFileSync(path.join(dir, 'coverage.config.json'), '{ not json')
94
- assert.equal(loadMockPolicy(dir).enabled, true)
95
- })
96
-
97
- test('loadMockPolicyFromFile reads the NAMED file (finding #1: not just its dirname)', () => {
98
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mpc-'))
99
- const file = path.join(dir, 'team-coverage.json') // deliberately non-standard name
100
- fs.writeFileSync(file, JSON.stringify({ mockPolicy: { enabled: true, externals: ['^@/generated/'] } }))
101
- const p = loadMockPolicyFromFile(file)
102
- assert.equal(p.enabled, true)
103
- // the declared externals must actually apply (proof the file was read, not defaults)
104
- assert.equal(classifyMockTarget('@/generated/openapi', p).internal, false)
105
- assert.equal(classifyMockTarget('@/lib/auth', p).internal, true)
106
- })
107
-
108
- test('loadMockPolicyFromFile falls back to defaults when the named file is missing', () => {
109
- // finding #1: a declared config that does not apply must not look like a clean pass.
110
- // (It warns on stderr; here we assert the zero-tolerance default — gate stays ON.)
111
- const p = loadMockPolicyFromFile(path.join(os.tmpdir(), 'definitely-absent-config-xyz.json'))
112
- assert.equal(p.enabled, true)
113
- })
114
-
115
- test('loadMockPolicyFromFile with no argument discovers coverage.config.json in cwd', () => {
116
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mpc-'))
117
- fs.writeFileSync(path.join(dir, 'coverage.config.json'), JSON.stringify({ mockPolicy: { enabled: true } }))
118
- assert.equal(loadMockPolicyFromFile(undefined, dir).enabled, true)
119
- })
@@ -1,104 +0,0 @@
1
- // Tests for persist-conventions: the real CLI against real files in a temp repo.
2
- import { test } from 'node:test'
3
- import assert from 'node:assert/strict'
4
- import { spawnSync } from 'node:child_process'
5
- import { existsSync, lstatSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
6
- import { tmpdir } from 'node:os'
7
- import { join } from 'node:path'
8
- import { fileURLToPath } from 'node:url'
9
-
10
- import { markers } from './persist-conventions-core.mjs'
11
-
12
- const CLI = fileURLToPath(new URL('./persist-conventions.mjs', import.meta.url))
13
- const SKILL = readFileSync(fileURLToPath(new URL('../SKILL.md', import.meta.url)), 'utf8')
14
- const TEMPLATE = readFileSync(fileURLToPath(new URL('../references/testing-conventions.md', import.meta.url)), 'utf8')
15
- const [START, END] = markers('testing')
16
-
17
- function withRepo(fn) {
18
- const dir = mkdtempSync(join(tmpdir(), 'persist-conventions-'))
19
- try {
20
- return fn(dir, (name) => readFileSync(join(dir, name), 'utf8'))
21
- } finally {
22
- rmSync(dir, { recursive: true, force: true })
23
- }
24
- }
25
-
26
- function run(dir, ...args) {
27
- const res = spawnSync(process.execPath, [CLI, '--repo-root', dir, ...args], { encoding: 'utf8' })
28
- return { code: res.status, out: res.stdout }
29
- }
30
-
31
- test('the persisted conventions carry one rule per SKILL.md principle, in order', () => {
32
- const fromSkill = [...SKILL.matchAll(/^\| (\d) \| \[/gm)].map((m) => Number(m[1]))
33
- const fromTemplate = [...TEMPLATE.matchAll(/^(\d+)\. /gm)].map((m) => Number(m[1]))
34
- assert.ok(fromSkill.length >= 5, 'principle table not found in SKILL.md')
35
- assert.deepEqual(fromTemplate, fromSkill)
36
- })
37
-
38
- test('without --write it previews and writes nothing', () => {
39
- withRepo((dir) => {
40
- const { code, out } = run(dir)
41
- assert.equal(code, 0)
42
- assert.match(out, /CONTRIBUTING\.md: would create/)
43
- assert.match(out, /re-run with --write to write$/m)
44
- assert.equal(existsSync(join(dir, 'CONTRIBUTING.md')), false)
45
- assert.equal(existsSync(join(dir, 'AGENTS.md')), false)
46
- })
47
- })
48
-
49
- test('--write creates CONTRIBUTING.md and AGENTS.md, links CLAUDE.md to it, and a re-run changes nothing', () => {
50
- withRepo((dir, read) => {
51
- const first = run(dir, '--write')
52
- assert.equal(first.code, 0)
53
- assert.match(first.out, /CONTRIBUTING\.md: created/)
54
- assert.match(first.out, /AGENTS\.md: created/)
55
- assert.match(first.out, /CLAUDE\.md: linked to AGENTS\.md/)
56
- const conventions = read('CONTRIBUTING.md')
57
- assert.ok(conventions.startsWith(`# Contributing\n\n${START}`))
58
- assert.ok(conventions.includes('## Testing conventions'))
59
- assert.ok(conventions.includes("6. Don't test config files"))
60
- assert.match(read('AGENTS.md'), /\[CONTRIBUTING\.md\]\(CONTRIBUTING\.md\)/)
61
-
62
- assert.ok(lstatSync(join(dir, 'CLAUDE.md')).isSymbolicLink())
63
-
64
- const before = [conventions, read('AGENTS.md'), read('CLAUDE.md')]
65
- assert.match(run(dir, '--write').out, /0 file\(s\) written, 3 unchanged$/m)
66
- assert.deepEqual([read('CONTRIBUTING.md'), read('AGENTS.md'), read('CLAUDE.md')], before)
67
- })
68
- })
69
-
70
- test('a stale section is replaced and the rest of each file is kept', () => {
71
- withRepo((dir, read) => {
72
- writeFileSync(join(dir, 'CONTRIBUTING.md'), `# Contributing\n\nUse tabs.\n\n${START}\nold principles\n${END}\n\n## Naming\n\ncamelCase.\n`)
73
- writeFileSync(join(dir, 'AGENTS.md'), '# Agents\n\nRun `pnpm test`.\n')
74
- run(dir, '--write')
75
- const conventions = read('CONTRIBUTING.md')
76
- assert.ok(conventions.startsWith('# Contributing\n\nUse tabs.\n\n'))
77
- assert.ok(conventions.endsWith(`${END}\n\n## Naming\n\ncamelCase.\n`))
78
- assert.ok(!conventions.includes('old principles'))
79
- assert.equal(conventions.split(START).length, 2) // exactly one section
80
- assert.ok(read('AGENTS.md').startsWith('# Agents\n\nRun `pnpm test`.\n\n## Contributing'))
81
- })
82
- })
83
-
84
- test('an AGENTS.md that already names CONTRIBUTING.md is left alone; OLD_CONTRIBUTING.md is not a mention', () => {
85
- withRepo((dir, read) => {
86
- const agents = '# Agents\n\nSee CONTRIBUTING.md.\n'
87
- writeFileSync(join(dir, 'AGENTS.md'), agents)
88
- assert.match(run(dir, '--write').out, /AGENTS\.md: unchanged/)
89
- assert.equal(read('AGENTS.md'), agents)
90
- })
91
- withRepo((dir, read) => {
92
- writeFileSync(join(dir, 'AGENTS.md'), '# Agents\n\nSee OLD_CONTRIBUTING.md.\n')
93
- assert.match(run(dir, '--write').out, /AGENTS\.md: updated/)
94
- assert.match(read('AGENTS.md'), /\[CONTRIBUTING\.md\]\(CONTRIBUTING\.md\)/)
95
- })
96
- })
97
-
98
- test('an unknown argument or a missing repo root is a usage error', () => {
99
- withRepo((dir) => {
100
- assert.equal(run(dir, '--force').code, 2)
101
- assert.equal(run(join(dir, 'missing')).code, 2)
102
- assert.equal(existsSync(join(dir, 'CONTRIBUTING.md')), false)
103
- })
104
- })
@@ -1,14 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import { spawnSync } from 'node:child_process';
3
- import { dirname, join } from 'node:path';
4
- import { fileURLToPath } from 'node:url';
5
- import test from 'node:test';
6
-
7
- const scriptsDir = dirname(fileURLToPath(import.meta.url));
8
-
9
- for (const suite of ['test_validate_tracker.py', 'test_export_tracker_xlsx.py']) {
10
- test(`compliance-audit ${suite}`, () => {
11
- const result = spawnSync('python3', [join(scriptsDir, suite)], { encoding: 'utf8' });
12
- assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
13
- });
14
- }
@@ -1,12 +0,0 @@
1
- import { test } from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import { dirname } from 'node:path';
4
- import { fileURLToPath } from 'node:url';
5
-
6
- import { manualOnlyPolicyFindings } from '../../engineering/engineering-runtime/scripts/invocation-policy.mjs';
7
-
8
- const SKILL_ROOT = dirname(fileURLToPath(import.meta.url));
9
-
10
- test('doc-authenticity remains explicit-invocation only across supported harnesses', () => {
11
- assert.deepEqual(manualOnlyPolicyFindings(SKILL_ROOT), []);
12
- });
@@ -1,212 +0,0 @@
1
- // @covers doc-authenticity:routed-verdict-contract
2
- // @level integration
3
- import assert from 'node:assert/strict';
4
- import { spawnSync } from 'node:child_process';
5
- import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
6
- import { tmpdir } from 'node:os';
7
- import { dirname, join } from 'node:path';
8
- import { test } from 'node:test';
9
- import { fileURLToPath } from 'node:url';
10
-
11
- const SCRIPTS = dirname(fileURLToPath(import.meta.url));
12
- const VALIDATOR = join(SCRIPTS, 'validate_verdict.py');
13
- const VALIDITY_IDS = {
14
- bank_statement: [...Array.from({ length: 5 }, (_, i) => `C${i + 1}`), ...Array.from({ length: 5 }, (_, i) => `D${i + 1}`)],
15
- invoice: Array.from({ length: 8 }, (_, i) => `N${i + 1}`),
16
- agreement: Array.from({ length: 8 }, (_, i) => `A${i + 1}`),
17
- conversation_log: [],
18
- other: [],
19
- };
20
- const CONTENT_IDS = new Set([
21
- ...Array.from({ length: 5 }, (_, i) => `C${i + 1}`),
22
- 'N2', 'N3', 'N4', 'N7',
23
- 'A2', 'A4', 'A5', 'A7',
24
- ]);
25
-
26
- function check(id) {
27
- const authenticity = id.startsWith('F') || id.startsWith('I');
28
- return {
29
- id,
30
- name: `Check ${id}`,
31
- axis: authenticity ? 'authenticity' : 'validity',
32
- dimension: authenticity ? 'file_forensics' : CONTENT_IDS.has(id) ? 'content' : 'domain',
33
- result: 'PASS',
34
- decisive: false,
35
- evidence: `Concrete evidence for ${id}.`,
36
- };
37
- }
38
-
39
- function verdict(fileType, docType) {
40
- const prefix = fileType === 'pdf' ? 'F' : 'I';
41
- const authIds = Array.from({ length: 9 }, (_, i) => `${prefix}${i + 1}`);
42
- const validityIds = VALIDITY_IDS[docType];
43
- return {
44
- schema_version: '2.2',
45
- artifact: {
46
- path: `/tmp/${fileType}-${docType}`,
47
- file_type: fileType,
48
- media_type: fileType === 'pdf' ? 'application/pdf' : 'image/jpeg',
49
- size_bytes: 1234,
50
- sha256: 'a'.repeat(64),
51
- extension_magic_match: true,
52
- doc_type: docType,
53
- detail: 'route fixture',
54
- claimed_issuer: null,
55
- supplied_claim: 'Test document.',
56
- },
57
- authenticity: {
58
- verdict: 'NO_EVIDENCE_OF_MODIFICATION',
59
- confidence: 'MEDIUM',
60
- summary: 'No modification evidence.',
61
- },
62
- validity: {
63
- verdict: validityIds.length ? 'VALID' : 'INCONCLUSIVE',
64
- confidence: 'LOW',
65
- summary: 'Validity route result.',
66
- },
67
- checks: [...authIds, ...validityIds].map(check),
68
- summary: 'Route test.',
69
- checked_at: '2026-09-03T13:30:00+08:00',
70
- };
71
- }
72
-
73
- function runValidator(directory, name, value) {
74
- const path = join(directory, `${name}.json`);
75
- writeFileSync(path, `${JSON.stringify(value)}\n`);
76
- return spawnSync('python3', [VALIDATOR, path], { encoding: 'utf8' });
77
- }
78
-
79
- test('all file-type and document-type routes require the correct check set', () => {
80
- const directory = mkdtempSync(join(tmpdir(), 'doc-auth-routes-'));
81
- try {
82
- for (const fileType of ['pdf', 'image']) {
83
- for (const docType of Object.keys(VALIDITY_IDS)) {
84
- const result = runValidator(directory, `${fileType}-${docType}`, verdict(fileType, docType));
85
- assert.equal(result.status, 0, `${fileType}/${docType}: ${result.stdout}${result.stderr}`);
86
- assert.match(result.stdout, new RegExp(`valid: ${fileType}/${docType} route`));
87
- }
88
- }
89
- } finally {
90
- rmSync(directory, { recursive: true, force: true });
91
- }
92
- });
93
-
94
- test('conversation logs and other documents cannot claim specialized validity', () => {
95
- const directory = mkdtempSync(join(tmpdir(), 'doc-auth-no-validity-'));
96
- try {
97
- for (const docType of ['conversation_log', 'other']) {
98
- const value = verdict('image', docType);
99
- value.validity.verdict = 'VALID';
100
- const result = runValidator(directory, docType, value);
101
- assert.notEqual(result.status, 0);
102
- assert.match(result.stdout, /validity verdict must be INCONCLUSIVE/);
103
- }
104
- } finally {
105
- rmSync(directory, { recursive: true, force: true });
106
- }
107
- });
108
-
109
- test('missing and cross-profile validity checks fail closed', () => {
110
- const directory = mkdtempSync(join(tmpdir(), 'doc-auth-invalid-route-'));
111
- try {
112
- const missing = verdict('pdf', 'invoice');
113
- missing.checks = missing.checks.filter(({ id }) => id !== 'N8');
114
- const missingResult = runValidator(directory, 'missing-invoice-check', missing);
115
- assert.notEqual(missingResult.status, 0);
116
- assert.match(missingResult.stdout, /missing checks: \['N8'\]/);
117
-
118
- const mixed = verdict('image', 'agreement');
119
- mixed.checks.push(check('N1'));
120
- const mixedResult = runValidator(directory, 'mixed-agreement-check', mixed);
121
- assert.notEqual(mixedResult.status, 0);
122
- assert.match(mixedResult.stdout, /checks do not belong to the selected route: \['N1'\]/);
123
- } finally {
124
- rmSync(directory, { recursive: true, force: true });
125
- }
126
- });
127
-
128
- test('artifact audit fields and checked_at timezone are mandatory', () => {
129
- const directory = mkdtempSync(join(tmpdir(), 'doc-auth-artifact-'));
130
- try {
131
- const missingAudit = verdict('pdf', 'invoice');
132
- delete missingAudit.artifact.sha256;
133
- const missingResult = runValidator(directory, 'missing-hash', missingAudit);
134
- assert.notEqual(missingResult.status, 0);
135
- assert.match(missingResult.stdout, /artifact missing keys: \['sha256'\]/);
136
-
137
- const badTimestamp = verdict('pdf', 'invoice');
138
- badTimestamp.checked_at = '2026-99-99T13:30:00';
139
- const timestampResult = runValidator(directory, 'bad-timestamp', badTimestamp);
140
- assert.notEqual(timestampResult.status, 0);
141
- assert.match(timestampResult.stdout, /valid ISO 8601 timestamp with timezone/);
142
- } finally {
143
- rmSync(directory, { recursive: true, force: true });
144
- }
145
- });
146
-
147
- test('authenticity verdict must match decisive, FAIL, FLAG, and NA thresholds', () => {
148
- const directory = mkdtempSync(join(tmpdir(), 'doc-auth-rubric-'));
149
- try {
150
- const tooManyFlags = verdict('pdf', 'other');
151
- for (const id of ['F1', 'F2', 'F3']) tooManyFlags.checks.find((item) => item.id === id).result = 'FLAG';
152
- const flagsResult = runValidator(directory, 'flags', tooManyFlags);
153
- assert.notEqual(flagsResult.status, 0);
154
- assert.match(flagsResult.stdout, /authenticity verdict must be SUSPICIOUS/);
155
-
156
- const unavailable = verdict('pdf', 'other');
157
- for (const id of ['F1', 'F2', 'F3', 'F4', 'F5']) unavailable.checks.find((item) => item.id === id).result = 'NA';
158
- const unavailableResult = runValidator(directory, 'unavailable', unavailable);
159
- assert.notEqual(unavailableResult.status, 0);
160
- assert.match(unavailableResult.stdout, /authenticity verdict must be INCONCLUSIVE/);
161
-
162
- const decisive = verdict('image', 'other');
163
- decisive.authenticity.verdict = 'LIKELY_MODIFIED';
164
- const splice = decisive.checks.find((item) => item.id === 'I7');
165
- splice.result = 'FAIL';
166
- splice.decisive = true;
167
- const decisiveResult = runValidator(directory, 'decisive', decisive);
168
- assert.equal(decisiveResult.status, 0, decisiveResult.stdout);
169
- } finally {
170
- rmSync(directory, { recursive: true, force: true });
171
- }
172
- });
173
-
174
- test('validity verdict must match decisive, FAIL, FLAG, and NA thresholds', () => {
175
- const directory = mkdtempSync(join(tmpdir(), 'doc-validity-rubric-'));
176
- try {
177
- const unavailable = verdict('pdf', 'invoice');
178
- unavailable.validity.verdict = 'INCONCLUSIVE';
179
- for (const id of ['N1', 'N2', 'N3', 'N4']) unavailable.checks.find((item) => item.id === id).result = 'NA';
180
- const unavailableResult = runValidator(directory, 'unavailable-validity', unavailable);
181
- assert.equal(unavailableResult.status, 0, unavailableResult.stdout);
182
-
183
- const decisive = verdict('pdf', 'agreement');
184
- decisive.validity.verdict = 'INVALID';
185
- const conflict = decisive.checks.find((item) => item.id === 'A4');
186
- conflict.result = 'FAIL';
187
- conflict.decisive = true;
188
- const decisiveResult = runValidator(directory, 'decisive-validity', decisive);
189
- assert.equal(decisiveResult.status, 0, decisiveResult.stdout);
190
-
191
- const invalidDecisive = verdict('pdf', 'invoice');
192
- invalidDecisive.checks.find((item) => item.id === 'N1').decisive = true;
193
- const invalidResult = runValidator(directory, 'decisive-pass', invalidDecisive);
194
- assert.notEqual(invalidResult.status, 0);
195
- assert.match(invalidResult.stdout, /only a FAIL can be decisive/);
196
- } finally {
197
- rmSync(directory, { recursive: true, force: true });
198
- }
199
- });
200
-
201
- test('the schema advertises every routed document type and check family', () => {
202
- const schema = JSON.parse(readFileSync(join(SCRIPTS, '..', 'references', 'verdict.schema.json'), 'utf8'));
203
- assert.deepEqual(
204
- schema.properties.artifact.properties.doc_type.enum,
205
- ['bank_statement', 'conversation_log', 'invoice', 'agreement', 'other'],
206
- );
207
- assert.equal(schema.properties.schema_version.const, '2.2');
208
- assert.ok(schema.properties.artifact.required.includes('sha256'));
209
- assert.ok(schema.$defs.check.required.includes('decisive'));
210
- assert.match(schema.$defs.check.properties.id.pattern, /N\[1-8\]/);
211
- assert.match(schema.$defs.check.properties.id.pattern, /A\[1-8\]/);
212
- });
@@ -1,14 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import { spawnSync } from 'node:child_process';
3
- import { dirname, join } from 'node:path';
4
- import { fileURLToPath } from 'node:url';
5
- import test from 'node:test';
6
-
7
- const skillDir = join(dirname(fileURLToPath(import.meta.url)), '..');
8
-
9
- test('TLD ladder detector Python contract', () => {
10
- const result = spawnSync('python3', [join(skillDir, 'tests', 'test_detect_ladders.py'), '-v'], {
11
- encoding: 'utf8',
12
- });
13
- assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
14
- });
@@ -1,14 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import { spawnSync } from 'node:child_process';
3
- import { dirname, join } from 'node:path';
4
- import { fileURLToPath } from 'node:url';
5
- import test from 'node:test';
6
-
7
- const skillDir = join(dirname(fileURLToPath(import.meta.url)), '..');
8
-
9
- test('TLD stream inference Python contract', () => {
10
- const result = spawnSync('python3', [join(skillDir, 'tests', 'test_detect_streams.py'), '-v'], {
11
- encoding: 'utf8',
12
- });
13
- assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
14
- });
@@ -1,134 +0,0 @@
1
- from collections import Counter
2
- import csv
3
- from decimal import Decimal
4
- import importlib.util
5
- import json
6
- from pathlib import Path
7
- import subprocess
8
- import sys
9
- import tempfile
10
- import unittest
11
-
12
-
13
- sys.dont_write_bytecode = True
14
- SCRIPT = Path(__file__).parents[1] / "scripts" / "detect_ladders.py"
15
- SPEC = importlib.util.spec_from_file_location("detect_ladders", SCRIPT)
16
- MODULE = importlib.util.module_from_spec(SPEC)
17
- assert SPEC.loader is not None
18
- sys.modules[SPEC.name] = MODULE
19
- SPEC.loader.exec_module(MODULE)
20
-
21
-
22
- def pair(amount: str) -> tuple[str, Decimal]:
23
- return "USD", Decimal(amount)
24
-
25
-
26
- class DetectLaddersTest(unittest.TestCase):
27
- def detect(self, values: dict[str, int], cap: str = "0.15"):
28
- counts = Counter({pair(amount): count for amount, count in values.items()})
29
- return MODULE.detect_ladders(
30
- counts,
31
- sum(counts.values()),
32
- MODULE.LadderConfig({"USD": Decimal(cap)}),
33
- )[0]
34
-
35
- def test_detects_tight_exact_ladder(self):
36
- ladders = self.detect({"28.03": 131, "56.04": 117, "112.09": 65, "224.19": 27, "70.00": 100})
37
- amounts = {amount for _, amount in ladders[0]["pairs"]}
38
- self.assertEqual(amounts, {Decimal("28.03"), Decimal("56.04"), Decimal("112.09"), Decimal("224.19")})
39
-
40
- def test_keeps_multiple_exact_anchors_at_one_level(self):
41
- ladders = self.detect({"14.41": 34, "14.52": 25, "28.94": 43, "57.96": 29, "80.00": 100})
42
- levels = next(ladder["levels"] for ladder in ladders if pair("28.94") in ladder["pairs"])
43
- self.assertEqual(levels[-1], {pair("14.41"), pair("14.52")})
44
-
45
- def test_rejects_upper_amount_outside_absolute_cap(self):
46
- ladders = self.detect({"14.41": 34, "28.94": 43, "57.96": 29, "116.85": 29, "80.00": 100})
47
- ladder = next(ladder for ladder in ladders if pair("14.41") in ladder["pairs"])
48
- self.assertNotIn(pair("116.85"), ladder["pairs"])
49
-
50
- def test_requires_explicit_currency_cap(self):
51
- with self.assertRaisesRegex(ValueError, "missing absolute residual cap"):
52
- MODULE.detect_ladders(Counter({pair("10"): 10}), 10, MODULE.LadderConfig({}))
53
-
54
- def test_ignores_currency_below_rung_pool_threshold(self):
55
- counts = Counter({pair("10"): 1000, ("EUR", Decimal("9.99")): 1})
56
- ladders, _ = MODULE.detect_ladders(counts, 1001, MODULE.LadderConfig({"USD": Decimal("0.15")}))
57
- self.assertEqual(ladders, [])
58
-
59
- def test_sensitivity_reports_coverage_by_cap(self):
60
- counts = Counter({pair("14.41"): 34, pair("28.94"): 43, pair("57.96"): 29})
61
- rows = MODULE.sensitivity_analysis(
62
- counts,
63
- sum(counts.values()),
64
- MODULE.LadderConfig({"USD": Decimal("0.15")}),
65
- "USD",
66
- [Decimal("0.10"), Decimal("0.15")],
67
- )
68
- self.assertEqual([row["ladder_count"] for row in rows], [0, 1])
69
-
70
- def test_keeps_below_threshold_family_out_of_clusters(self):
71
- counts = Counter({pair("10"): 32, pair("20"): 32, pair("40"): 32})
72
- ladders, metadata = MODULE.detect_ladders(
73
- counts,
74
- 96,
75
- MODULE.LadderConfig({"USD": Decimal("0.15")}),
76
- )
77
- self.assertEqual(ladders, [])
78
- self.assertEqual(metadata["below_threshold"][0]["attempts"], 96)
79
-
80
- def test_same_pair_can_belong_to_different_partitions(self):
81
- entries = []
82
- amounts = (("10", 40), ("20", 40), ("40", 40))
83
- for partition in ("alpha", "beta"):
84
- for amount, count in amounts:
85
- entries.extend([(partition, pair(amount))] * count)
86
- _, ladders, _ = MODULE.detect_partitioned_ladders(
87
- entries,
88
- MODULE.LadderConfig({"USD": Decimal("0.15")}),
89
- )
90
- self.assertEqual(len(ladders), 2)
91
- self.assertEqual({ladder["partition"] for ladder in ladders}, {"alpha", "beta"})
92
-
93
- def test_requires_at_least_three_levels(self):
94
- self.assertEqual(self.detect({"10": 60, "20": 60, "70": 100}), [])
95
-
96
- def test_rejects_non_two_x_relationships(self):
97
- self.assertEqual(self.detect({"10": 40, "30": 40, "90": 40}), [])
98
-
99
- def test_cli_keeps_whole_descriptor_despite_country_and_pattern_labels(self):
100
- with tempfile.TemporaryDirectory() as directory:
101
- csv_path = Path(directory) / "clean.csv"
102
- with csv_path.open("w", encoding="utf-8", newline="") as handle:
103
- writer = csv.DictWriter(handle, fieldnames=["descriptor", "country", "currency", "amount", "payment_pattern"])
104
- writer.writeheader()
105
- for country in ("US", "GB"):
106
- for amount in ("10", "20", "40"):
107
- writer.writerows(
108
- {"descriptor": "ACME", "country": country, "currency": "USD", "amount": amount,
109
- "payment_pattern": "Direct top-up pattern" if country == "US" else "E-commerce pattern"}
110
- for _ in range(20)
111
- )
112
- result = subprocess.run(
113
- [
114
- sys.executable,
115
- str(SCRIPT),
116
- str(csv_path),
117
- "--descriptor-column", "descriptor",
118
- "--bin-country-column", "country",
119
- "--currency-column", "currency",
120
- "--amount-column", "amount",
121
- "--cap", "USD=0.15",
122
- ],
123
- check=True,
124
- capture_output=True,
125
- text=True,
126
- )
127
- payload = json.loads(result.stdout)
128
- self.assertEqual(len(payload["ladders"]), 1)
129
- self.assertEqual(payload["ladders"][0]["partition_attempts"], 120)
130
- self.assertEqual(payload["ladders"][0]["bin_country_components"], {"US": 60, "GB": 60})
131
-
132
-
133
- if __name__ == "__main__":
134
- unittest.main()