@1aboveio/skills 1.0.8 → 1.0.10

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