@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,520 +0,0 @@
1
- import csv
2
- from copy import deepcopy
3
- import importlib.util
4
- import json
5
- from pathlib import Path
6
- import random
7
- import subprocess
8
- import sys
9
- import tempfile
10
- from types import SimpleNamespace
11
- import unittest
12
-
13
-
14
- sys.dont_write_bytecode = True
15
- SCRIPT = Path(__file__).parents[1] / "scripts" / "detect_streams.py"
16
- SPEC = importlib.util.spec_from_file_location("detect_streams", SCRIPT)
17
- MODULE = importlib.util.module_from_spec(SPEC)
18
- assert SPEC.loader is not None
19
- sys.modules[SPEC.name] = MODULE
20
- SPEC.loader.exec_module(MODULE)
21
-
22
- DAYS = [f"2026-08-{d:02d}" for d in range(1, 21)]
23
- FIELDS = ["amount", "currency", "ts", "descriptor", "country", "response", "account", "card_type", "avs", "auth"]
24
-
25
-
26
- def rates_cache() -> dict:
27
- """USD base; EUR drifts 1.15 -> 1.17, AUD 0.70 -> 0.72 over the window (quoted as units per USD)."""
28
- rates = {}
29
- for i, day in enumerate(DAYS):
30
- eur = 1.15 + 0.001 * i
31
- aud = 0.70 + 0.001 * i
32
- rates[day] = {"EUR": 1 / eur, "AUD": 1 / aud, "GBP": 1 / 1.34, "JPY": 147.0}
33
- return {"base": "USD", "rates": rates}
34
-
35
-
36
- def synthetic_rows() -> list[dict]:
37
- rng = random.Random(3)
38
- rates = rates_cache()["rates"]
39
- rows = []
40
- acc = 0
41
-
42
- def row(amount, day, ctry, resp="A001", account=None, card="Debit", avs="Postal code not checked", auth="None", desc="HLP*shop", hour=None):
43
- nonlocal acc
44
- acc += 1
45
- h = hour if hour is not None else rng.randint(0, 23)
46
- return {"amount": f"{amount:.2f}", "currency": "USD", "ts": f"{day} {h:02d}:{rng.randint(0, 59):02d}:{rng.randint(0, 59):02d}",
47
- "descriptor": desc, "country": ctry, "response": resp, "account": account or f"4{acc:05d}******{acc % 9999:04d}",
48
- "card_type": card, "avs": avs, "auth": auth}
49
-
50
- # Euro-wallet players (Germany): presets 10/20/50 at live rate with 0-1% markup, some retries
51
- for _ in range(400):
52
- day = rng.choice(DAYS)
53
- preset = rng.choice([10, 10, 10, 20, 20, 50])
54
- usd = preset / rates[day]["EUR"] * (1 + rng.uniform(0, 0.01))
55
- rows.append(row(usd, day, "Germany", resp=rng.choice(["A001", "A001", "D102"])))
56
- # Australian players: A$30 / A$50 live
57
- for _ in range(300):
58
- day = rng.choice(DAYS)
59
- preset = rng.choice([30, 30, 50])
60
- usd = preset / rates[day]["AUD"] * (1 + rng.uniform(0, 0.01))
61
- rows.append(row(usd, day, "Australia"))
62
- # US cards: euro presets 24/48 at ONE frozen rate for six days, one attempt per card, lunchtime
63
- frozen = 1.1679
64
- for _ in range(300):
65
- day = rng.choice(DAYS[10:16])
66
- preset = rng.choice([24, 24, 48])
67
- rows.append(row(round(preset * frozen, 2), day, "United States of America", resp=rng.choice(["A001", "D102"]), avs="Postal code matched", hour=rng.choice([16, 17, 18])))
68
- # Canada: retail list x 1.06 with US-style billing address
69
- for _ in range(250):
70
- day = rng.choice(DAYS)
71
- price = rng.choice([79.99, 84.99, 89.99, 134.99, 59.95, 239.98, 25.98, 63.98, 137.99, 199.99])
72
- rows.append(row(round(price * 1.06, 2), day, "Canada", avs="Postal code matched", card="Credit"))
73
- # Japan: off-menu typed amounts
74
- for _ in range(200):
75
- day = rng.choice(DAYS)
76
- rows.append(row(rng.uniform(15, 400), day, "Japan"))
77
- return rows
78
-
79
-
80
- class RatesTest(unittest.TestCase):
81
- def test_lookup_uses_query_date_and_bounded_prior_day_carry(self):
82
- cache = {"base": "USD", "rates": {
83
- "2026-08-01": {"EUR": 0.8},
84
- "2026-08-04": {"EUR": 0.82},
85
- }}
86
- cases = [
87
- ("before_cache", "2026-07-31", 7, None, {}),
88
- ("exact_first", "2026-08-01", 7, 0.8, {}),
89
- ("gap", "2026-08-03", 7, 0.8, {"EUR": 1}),
90
- ("exact_last", "2026-08-04", 7, 0.82, {}),
91
- ("after_cache", "2026-08-05", 7, 0.82, {"EUR": 1}),
92
- ("max_carry", "2026-08-11", 7, 0.82, {"EUR": 1}),
93
- ("beyond_carry", "2026-08-12", 7, None, {}),
94
- ("far_after_cache", "2027-08-04", 7, None, {}),
95
- ("configured_limit", "2026-08-06", 1, None, {}),
96
- ("no_carry_missing", "2026-08-05", 0, None, {}),
97
- ("no_carry_exact", "2026-08-04", 0, 0.82, {}),
98
- ]
99
- for name, day, carry, expected, carried in cases:
100
- with self.subTest(name=name):
101
- rates = MODULE.Rates(cache, carry=carry)
102
- self.assertEqual(rates.rate(day, "EUR"), expected)
103
- self.assertEqual(dict(rates.carried), carried)
104
-
105
- def test_missing_currency_searches_prior_quotes_not_just_cache_dates(self):
106
- rates = MODULE.Rates({"base": "USD", "rates": {
107
- "2026-08-01": {"EUR": 0.8},
108
- "2026-08-08": {"GBP": 0.6},
109
- }}, carry=7)
110
- self.assertEqual(rates.rate("2026-08-08", "EUR"), 0.8)
111
- self.assertIsNone(rates.rate("2026-08-09", "EUR"))
112
- self.assertIsNone(rates.rate("2026-08-08", "CHF"))
113
- self.assertEqual(dict(rates.carried), {"EUR": 1})
114
-
115
- def test_identity_and_usd_pegs_do_not_require_dated_quotes(self):
116
- rates = MODULE.Rates({"base": "USD", "rates": {}}, carry=0)
117
- self.assertEqual(rates.rate("2026-08-01", "USD"), 1.0)
118
- self.assertEqual(rates.rate("2026-08-01", "SAR"), 3.75)
119
- self.assertIsNone(rates.rate("2026-08-01", "EUR"))
120
- self.assertEqual(dict(rates.carried), {})
121
-
122
-
123
- class RateConsistencyTest(unittest.TestCase):
124
- def setUp(self):
125
- self.opt = SimpleNamespace(peak_tol=0.012, frozen_days=4,
126
- frozen_min_per_day=3, frozen_market_move=0.001)
127
-
128
- def consistency(self, quotes, charges, wallet="EUR", center=24):
129
- cache = {"base": "USD", "rates": dict(zip(DAYS, quotes))}
130
- rows = [
131
- {"day": day, "currency": currency, "amount": amount}
132
- for day, daily_charges in zip(DAYS, charges)
133
- for currency, amount, count in daily_charges
134
- for _ in range(count)
135
- ]
136
- return MODULE.rate_consistency(rows, MODULE.Rates(cache), wallet,
137
- [(center, len(rows))], self.opt)
138
-
139
- def test_non_usd_source_freezes_when_cross_rate_moves_but_wallet_quote_does_not(self):
140
- quotes = [{"EUR": 0.8, "GBP": gbp} for gbp in (0.6, 0.6012, 0.6024, 0.6036)]
141
- result = self.consistency(quotes, [[("GBP", 18, 3)]] * 4)
142
- self.assertEqual(result["frozen_attempts"], 12)
143
- self.assertEqual(result["tested_attempts"], 12)
144
- self.assertEqual(result["frozen_peaks"], 1)
145
- self.assertTrue(result["detail"][0]["frozen"])
146
- # EUR per GBP changes from 4/3 to 2000/1509, about 0.5964%.
147
- self.assertEqual(result["detail"][0]["market_move"], 0.006)
148
-
149
- def test_matching_source_and_wallet_is_not_frozen_by_usd_quote_movement(self):
150
- quotes = [{"EUR": eur} for eur in (0.8, 0.804, 0.808, 0.812)]
151
- result = self.consistency(quotes, [[("EUR", 24, 3)]] * 4)
152
- self.assertEqual(result["frozen_attempts"], 0)
153
- self.assertEqual(result["tested_attempts"], 12)
154
- self.assertEqual(result["live_peaks"], 1)
155
- self.assertFalse(result["detail"][0]["frozen"])
156
- self.assertEqual(result["detail"][0]["market_move"], 0.0)
157
-
158
- def test_parallel_source_and_wallet_quotes_have_no_cross_rate_movement(self):
159
- quotes = [{"EUR": eur, "GBP": gbp} for eur, gbp in
160
- ((0.8, 0.6), (0.804, 0.603), (0.808, 0.606), (0.812, 0.609))]
161
- result = self.consistency(quotes, [[("GBP", 18, 3)]] * 4)
162
- self.assertEqual(result["frozen_attempts"], 0)
163
- self.assertEqual(result["tested_attempts"], 12)
164
- self.assertFalse(result["detail"][0]["frozen"])
165
- self.assertEqual(result["detail"][0]["market_move"], 0.0)
166
-
167
- def test_live_non_usd_source_tracks_cross_rate(self):
168
- quotes = [{"EUR": 0.8, "GBP": gbp} for gbp in (0.6, 0.6012, 0.6024, 0.6036)]
169
- charges = [[("GBP", amount, 3)] for amount in (18, 18.036, 18.072, 18.108)]
170
- result = self.consistency(quotes, charges)
171
- self.assertEqual(result["frozen_attempts"], 0)
172
- self.assertEqual(result["tested_attempts"], 12)
173
- self.assertFalse(result["detail"][0]["frozen"])
174
- self.assertEqual(result["detail"][0]["market_move"], 0.006)
175
-
176
- def test_base_currency_wallet_can_freeze_against_non_base_source(self):
177
- quotes = [{"EUR": eur} for eur in (0.8, 0.8016, 0.8032, 0.8048)]
178
- result = self.consistency(quotes, [[("EUR", 24, 3)]] * 4, wallet="USD", center=30)
179
- self.assertEqual(result["frozen_attempts"], 12)
180
- self.assertEqual(result["tested_attempts"], 12)
181
- self.assertTrue(result["detail"][0]["frozen"])
182
- self.assertEqual(result["detail"][0]["market_move"], 0.006)
183
-
184
- def test_mixed_sources_keep_modes_movements_and_attempts_separate(self):
185
- quotes = [{"EUR": 0.8, "GBP": gbp} for gbp in (0.6, 0.6012, 0.6024, 0.6036)]
186
- result = self.consistency(quotes, [[("EUR", 24, 6), ("GBP", 18, 3)]] * 4)
187
- self.assertEqual(result["tested_attempts"], 36)
188
- self.assertEqual(result["frozen_attempts"], 12)
189
- self.assertEqual(result["frozen_peaks"], 1)
190
- self.assertEqual(result["live_peaks"], 1)
191
- self.assertEqual(len(result["detail"]), 2)
192
- details = {d["source_currency"]: d for d in result["detail"]}
193
- self.assertFalse(details["EUR"]["frozen"])
194
- self.assertEqual(details["EUR"]["modal_amounts"], [24])
195
- self.assertEqual(details["EUR"]["market_move"], 0.0)
196
- self.assertEqual(details["EUR"]["attempts"], 24)
197
- self.assertTrue(details["GBP"]["frozen"])
198
- self.assertEqual(details["GBP"]["modal_amounts"], [18])
199
- self.assertEqual(details["GBP"]["market_move"], 0.006)
200
- self.assertEqual(details["GBP"]["attempts"], 12)
201
-
202
- def test_mixed_sources_cannot_pool_days_or_daily_minimum_for_testing(self):
203
- quotes = [{"EUR": 0.8, "GBP": 0.6}] * 4
204
- cases = [
205
- [[("EUR", 24, 2), ("GBP", 18, 2)]] * 4,
206
- [[("EUR", 24, 3)]] * 2 + [[("GBP", 18, 3)]] * 2,
207
- ]
208
- for charges in cases:
209
- with self.subTest(charges=charges):
210
- result = self.consistency(quotes, charges)
211
- self.assertEqual(result["tested_attempts"], 0)
212
- self.assertEqual(result["frozen_attempts"], 0)
213
- self.assertEqual(result["detail"], [])
214
-
215
-
216
- class PaymentPatternPresentationTest(unittest.TestCase):
217
- def run_cli(self, rows, candidates=("EUR",), cache=None, extra=()):
218
- with tempfile.TemporaryDirectory() as tmp:
219
- root = Path(tmp)
220
- csv_path = root / "auth.csv"
221
- with csv_path.open("w", newline="") as fh:
222
- writer = csv.DictWriter(fh, fieldnames=FIELDS)
223
- writer.writeheader()
224
- writer.writerows(rows)
225
- rates_path = root / "rates.json"
226
- rates_path.write_text(json.dumps(cache or rates_cache()))
227
- markdown_path = root / "streams.md"
228
- cmd = [sys.executable, str(SCRIPT), str(csv_path), "--rates", str(rates_path),
229
- "--amount-column", "amount", "--currency-column", "currency", "--date-column", "ts",
230
- "--descriptor-column", "descriptor", "--bin-country-column", "country",
231
- "--candidate-currencies", *candidates, "--markdown", str(markdown_path), *extra]
232
- result = subprocess.run(cmd, capture_output=True, text=True)
233
- self.assertEqual(result.returncode, 0, result.stderr)
234
- report = json.loads(result.stdout)
235
- markdown = markdown_path.read_text()
236
- before = deepcopy(report)
237
- self.assertEqual(MODULE.to_markdown(report), markdown)
238
- self.assertEqual(report, before)
239
- descriptor = report["descriptors"][0]
240
- self.assertEqual(descriptor["attempts"], len(rows))
241
- self.assertEqual(sum(s["attempts"] for s in descriptor["streams"]), len(rows))
242
- for stream in descriptor["streams"] + [s for g in descriptor["groups"] for s in g["substreams"]]:
243
- p = stream["presentation"]
244
- self.assertEqual(sum(p["pattern_counts"].values()), stream["attempts"])
245
- self.assertEqual(sum(p["charge_currencies"].values()), stream["attempts"])
246
- self.assertEqual(sum(part["attempts"] for part in p["by_charge_currency"]), stream["attempts"])
247
- return descriptor, markdown
248
-
249
- def rows(self, charges, country="United Kingdom"):
250
- return [{"amount": f"{amount:.2f}", "currency": currency, "ts": f"{day} 12:00:00",
251
- "descriptor": "pattern-fixture", "country": country}
252
- for day, daily in zip(DAYS, charges)
253
- for currency, amount, count in daily
254
- for _ in range(count)]
255
-
256
- def markdown_row(self, markdown, raw, country=None):
257
- prefix = f"| {raw} |" if country is None else f"| {country} |"
258
- matches = [line for line in markdown.splitlines() if line.startswith(prefix) and f"| {raw} |" in line]
259
- self.assertEqual(len(matches), 1, matches)
260
- return [cell.strip() for cell in matches[0].split("|")[1:-1]]
261
-
262
- def test_direct_usd_and_eur_are_based_on_charge_currency_not_legal_tender(self):
263
- for currency in ("USD", "EUR"):
264
- with self.subTest(currency=currency):
265
- rows = self.rows([[(currency, 24, 20)]] * 4)
266
- descriptor, markdown = self.run_cli(rows, candidates=(currency,))
267
- group = descriptor["groups"][0]
268
- stream = group["substreams"][0]
269
- self.assertEqual(group["legal_tender"], "GBP")
270
- self.assertEqual(stream["stream"], f"{currency} menu-12 live")
271
- self.assertEqual(stream["attempts"], 80)
272
- p = stream["presentation"]
273
- self.assertEqual(p["pattern_counts"], {"Direct top-up pattern": 80})
274
- self.assertEqual(p["charge_currencies"], {currency: 80})
275
- self.assertEqual(p["inferred_top_up_currencies"], [currency])
276
- self.assertEqual(p["pricing_characteristics"], ["fixed amount"])
277
- self.assertEqual(p["by_charge_currency"][0]["frozen_fx_attempts"], 0)
278
- cells = self.markdown_row(markdown, stream["stream"], group["country"])
279
- self.assertIn(f"Direct top-up pattern: 80 [{currency}: 80]", cells[3])
280
- self.assertEqual(cells[4:7], [currency, f"{currency}: 80", "fixed amount"])
281
- self.assertNotIn("frozen FX", cells[3])
282
- self.assertNotIn("floating", cells[3])
283
-
284
- def test_fx_currency_and_no_frozen_qualifier_without_positive_cross_rate_test(self):
285
- rows = self.rows([[('USD', 24 / rates_cache()["rates"][day]["EUR"], 20)] for day in DAYS[:4]])
286
- descriptor, markdown = self.run_cli(rows)
287
- stream = descriptor["groups"][0]["substreams"][0]
288
- self.assertEqual(stream["stream"], "EUR menu-12 live")
289
- self.assertEqual(stream["presentation"]["pattern_counts"], {"FX-converted top-up pattern": 80})
290
- self.assertEqual(stream["presentation"]["charge_currencies"], {"USD": 80})
291
- self.assertEqual(stream["presentation"]["pricing_characteristics"], [])
292
- cells = self.markdown_row(markdown, stream["stream"])
293
- self.assertIn("FX-converted top-up pattern: 80 [USD: 80]", cells[1])
294
- self.assertEqual(cells[2:4], ["EUR", "USD: 80"])
295
- self.assertEqual(cells[4], "n/a")
296
- self.assertNotIn("frozen FX", cells[1])
297
- self.assertNotIn("floating", cells[1])
298
-
299
- def test_repetition_without_cross_rate_tests_has_no_frozen_qualifier(self):
300
- charge = 24 / rates_cache()["rates"][DAYS[0]]["EUR"]
301
- rows = self.rows([[('USD', charge, 80)]])
302
- descriptor, markdown = self.run_cli(rows)
303
- stream = descriptor["groups"][0]["substreams"][0]
304
- self.assertEqual(stream["stream"], "EUR menu-12 live")
305
- self.assertEqual(stream["rate_consistency"]["tested_attempts"], 0)
306
- self.assertEqual(stream["rate_consistency"]["detail"], [])
307
- self.assertEqual(stream["presentation"]["by_charge_currency"][0]["frozen_fx_attempts"], 0)
308
- cells = self.markdown_row(markdown, stream["stream"])
309
- self.assertIn("FX-converted top-up pattern: 80 [USD: 80]", cells[1])
310
- self.assertEqual(cells[4], "n/a")
311
- self.assertNotIn("frozen", cells[1])
312
- self.assertNotIn("floating", cells[1])
313
-
314
- def test_mixed_sources_show_both_types_and_only_tested_source_is_frozen(self):
315
- cache = {"base": "USD", "rates": {day: {"EUR": 0.8, "GBP": 0.6 + i * 0.0012}
316
- for i, day in enumerate(DAYS[:4])}}
317
- rows = self.rows([[('EUR', 24, 12), ('GBP', 18, 8)]] * 4)
318
- descriptor, markdown = self.run_cli(rows, cache=cache)
319
- stream = descriptor["groups"][0]["substreams"][0]
320
- self.assertEqual(stream["stream"], "EUR menu-12 live")
321
- self.assertEqual(len(descriptor["streams"]), 1)
322
- self.assertEqual(len(descriptor["groups"][0]["substreams"]), 1)
323
- p = stream["presentation"]
324
- self.assertEqual(p["pattern_counts"], {"Direct top-up pattern": 48, "FX-converted top-up pattern": 32})
325
- self.assertEqual(p["charge_currencies"], {"EUR": 48, "GBP": 32})
326
- parts = {part["charge_currency"]: part for part in p["by_charge_currency"]}
327
- self.assertEqual(parts["EUR"]["frozen_fx_attempts"], 0)
328
- self.assertEqual(parts["GBP"]["frozen_fx_attempts"], 32)
329
- self.assertEqual(parts["EUR"]["inferred_top_up_currency"], "EUR")
330
- self.assertEqual(parts["GBP"]["inferred_top_up_currency"], "EUR")
331
- cells = self.markdown_row(markdown, stream["stream"], "United Kingdom")
332
- self.assertIn("Direct top-up pattern: 48 [EUR: 48]", cells[3])
333
- self.assertIn("FX-converted top-up pattern: 32 [GBP: 32 (32 frozen FX tested attempts)]", cells[3])
334
- self.assertEqual(cells[4:6], ["EUR", "EUR: 48, GBP: 32"])
335
- self.assertEqual(descriptor["streams"][0]["presentation"], p)
336
-
337
- def test_summary_aggregates_same_raw_stream_without_splitting_payment_patterns(self):
338
- direct = self.rows([[('EUR', 24, 20)]] * 4, country="Germany")
339
- fx = self.rows([[('USD', 24 / rates_cache()["rates"][day]["EUR"], 20)] for day in DAYS[:4]])
340
- descriptor, markdown = self.run_cli(direct + fx)
341
- self.assertEqual(len(descriptor["groups"]), 2)
342
- self.assertEqual(len(descriptor["streams"]), 1)
343
- stream = descriptor["streams"][0]
344
- self.assertEqual(stream["stream"], "EUR menu-12 live")
345
- self.assertEqual(stream["attempts"], 160)
346
- self.assertEqual(stream["share_of_descriptor"], 1.0)
347
- self.assertEqual(stream["presentation"]["pattern_counts"], {"Direct top-up pattern": 80, "FX-converted top-up pattern": 80})
348
- self.assertEqual(stream["presentation"]["charge_currencies"], {"EUR": 80, "USD": 80})
349
- cells = self.markdown_row(markdown, stream["stream"])
350
- self.assertIn("Direct top-up pattern: 80 [EUR: 80]", cells[1])
351
- self.assertIn("FX-converted top-up pattern: 80 [USD: 80]", cells[1])
352
-
353
- def test_summary_detects_fixed_charges_across_country_dates(self):
354
- germany = self.rows([[('EUR', 24, 80)]], country="Germany")
355
- uk = self.rows([[], [('EUR', 24, 80)]], country="United Kingdom")
356
- descriptor, markdown = self.run_cli(germany + uk, candidates=("EUR",))
357
- self.assertEqual(len(descriptor["streams"]), 1)
358
- summary = descriptor["streams"][0]
359
- self.assertEqual(summary["attempts"], 160)
360
- self.assertEqual(summary["presentation"]["pattern_counts"], {"Direct top-up pattern": 160})
361
- self.assertEqual(summary["presentation"]["pricing_characteristics"], ["fixed amount"])
362
- for group in descriptor["groups"]:
363
- self.assertEqual(group["substreams"][0]["presentation"]["pricing_characteristics"], [])
364
- self.assertEqual(self.markdown_row(markdown, summary["stream"])[4], "fixed amount")
365
-
366
- def test_menu_none_is_other_repeated_amount_not_inferred_top_up(self):
367
- rows = self.rows([[('EUR', 17.37, 20)]] * 4, country="UNKNOWN")
368
- descriptor, markdown = self.run_cli(rows)
369
- stream = descriptor["groups"][0]["substreams"][0]
370
- self.assertEqual(stream["stream"], "EUR menu-none live")
371
- self.assertEqual(stream["wallet"], "EUR")
372
- p = stream["presentation"]
373
- self.assertEqual(p["pattern_counts"], {"Other pattern": 80})
374
- self.assertEqual(p["inferred_top_up_currencies"], [])
375
- self.assertIsNone(p["by_charge_currency"][0]["inferred_top_up_currency"])
376
- cells = self.markdown_row(markdown, stream["stream"], "UNKNOWN")
377
- self.assertIn("Other pattern: 80 [EUR: 80]", cells[3])
378
- self.assertIn("unidentified repeated-amount pattern", cells[3])
379
- self.assertEqual(cells[4:7], ["n/a", "EUR: 80", "fixed amount"])
380
-
381
- def test_off_menu_and_small_groups_render_other_with_real_charge_currencies(self):
382
- for count, extra, raw in ((20, ("--max-substreams", "0"), "off-menu"), (2, (), "small groups")):
383
- with self.subTest(raw=raw):
384
- rows = self.rows([[('EUR', 24, count), ('USD', 30, count)]] * 4)
385
- descriptor, markdown = self.run_cli(rows, extra=extra)
386
- stream = descriptor["streams"][0]
387
- self.assertEqual(stream["stream"], raw)
388
- self.assertEqual(stream["presentation"]["pattern_counts"], {"Other pattern": len(rows)})
389
- self.assertEqual(stream["presentation"]["inferred_top_up_currencies"], [])
390
- self.assertEqual(stream["presentation"]["charge_currencies"], {"EUR": count * 4, "USD": count * 4})
391
- cells = self.markdown_row(markdown, raw)
392
- self.assertIn(f"Other pattern: {len(rows)}", cells[1])
393
- self.assertEqual(cells[2], "n/a")
394
- self.assertEqual(cells[3], f"EUR: {count * 4}, USD: {count * 4}")
395
- self.assertNotIn("top-up pattern", cells[1])
396
-
397
- def test_retail_renders_ecommerce_without_tax_fee_or_wallet_claim(self):
398
- rows = [r for r in synthetic_rows() if r["country"] == "Canada"]
399
- descriptor, markdown = self.run_cli(rows)
400
- stream = descriptor["groups"][0]["substreams"][0]
401
- self.assertTrue(stream["stream"].startswith("retail x"))
402
- self.assertAlmostEqual(stream["retail"]["constant"], 1.06, places=3)
403
- p = stream["presentation"]
404
- self.assertEqual(p["pattern_counts"], {"E-commerce pattern": stream["attempts"]})
405
- self.assertEqual(p["inferred_top_up_currencies"], [])
406
- self.assertEqual(p["charge_currencies"], {"USD": stream["attempts"]})
407
- cells = self.markdown_row(markdown, stream["stream"], "Canada")
408
- self.assertIn(f"E-commerce pattern: {stream['attempts']} [USD: {stream['attempts']}]", cells[3])
409
- self.assertEqual(cells[4], "n/a")
410
- self.assertNotIn("tax", markdown.lower())
411
- self.assertNotIn("fee", markdown.lower())
412
-
413
-
414
- class DetectStreamsTest(unittest.TestCase):
415
- @classmethod
416
- def setUpClass(cls):
417
- cls.tmp = tempfile.TemporaryDirectory()
418
- root = Path(cls.tmp.name)
419
- cls.csv = root / "auth.csv"
420
- with cls.csv.open("w", newline="") as fh:
421
- w = csv.DictWriter(fh, fieldnames=FIELDS)
422
- w.writeheader()
423
- w.writerows(synthetic_rows())
424
- cls.rates = root / "rates.json"
425
- cls.rates.write_text(json.dumps(rates_cache()))
426
- cls.out = root / "streams.json"
427
- cmd = [sys.executable, str(SCRIPT), str(cls.csv), "--rates", str(cls.rates), "--amount-column", "amount", "--currency-column", "currency",
428
- "--date-column", "ts", "--descriptor-column", "descriptor", "--bin-country-column", "country", "--response-column", "response",
429
- "--account-column", "account", "--card-type-column", "card_type", "--avs-column", "avs", "--auth-column", "auth",
430
- "--output", str(cls.out), "--markdown", str(root / "streams.md")]
431
- result = subprocess.run(cmd, capture_output=True, text=True)
432
- assert result.returncode == 0, result.stderr
433
- cls.report = json.loads(cls.out.read_text())
434
- cls.groups = {g["country"]: g for g in cls.report["descriptors"][0]["groups"]}
435
-
436
- @classmethod
437
- def main_sub(cls, country):
438
- return max(cls.groups[country]["substreams"], key=lambda s: s["attempts"])
439
-
440
- @classmethod
441
- def tearDownClass(cls):
442
- cls.tmp.cleanup()
443
-
444
- def test_euro_wallet_inferred_for_germany_with_10_scheme_live(self):
445
- g = self.main_sub("Germany")
446
- self.assertEqual(g["wallet"], "EUR")
447
- self.assertEqual(g["scheme"], "10")
448
- self.assertFalse(g["frozen"])
449
- peaks = [c for c, _ in g["peaks"][:3]]
450
- self.assertTrue(all(any(abs(c - m) <= 0.012 * m for m in (10, 20, 50)) for c in peaks), peaks)
451
-
452
- def test_aud_wallet_inferred_for_australia(self):
453
- g = self.main_sub("Australia")
454
- self.assertEqual(g["wallet"], "AUD")
455
- self.assertTrue(any(abs(c - 30) <= 0.4 for c, _ in g["peaks"]))
456
-
457
- def test_us_cards_on_euro_menu_are_flagged_frozen(self):
458
- g = self.main_sub("United States of America")
459
- self.assertEqual(g["wallet"], "EUR")
460
- self.assertEqual(g["scheme"], "12")
461
- self.assertTrue(g["frozen"], g["rate_consistency"])
462
- self.assertIn("frozen", g["stream"])
463
- self.assertLess(g["behaviour"]["repeat_card_share"], 0.05)
464
-
465
- def test_retail_constant_detected_for_canada(self):
466
- g = self.main_sub("Canada")
467
- self.assertTrue(g["stream"].startswith("retail"), g["stream"])
468
- self.assertAlmostEqual(g["retail"]["constant"], 1.06, places=3)
469
-
470
- def test_japan_is_off_menu(self):
471
- self.assertEqual(self.main_sub("Japan")["stream"], "off-menu")
472
-
473
- def test_streams_summarised_per_descriptor(self):
474
- labels = {s["stream"] for s in self.report["descriptors"][0]["streams"]}
475
- self.assertTrue(any(l.startswith("EUR menu-10 live") for l in labels), labels)
476
- self.assertTrue(any(l.startswith("EUR menu-12 frozen") for l in labels), labels)
477
- self.assertTrue(any(l.startswith("retail") for l in labels), labels)
478
-
479
- def test_same_currency_charges_produce_live_stream_through_cli(self):
480
- rows = [
481
- {"amount": "24.00", "currency": "EUR", "ts": f"{day} 12:00:00",
482
- "descriptor": "EUR-only", "country": "Germany"}
483
- for day in DAYS[:4]
484
- for _ in range(20)
485
- ]
486
- with tempfile.TemporaryDirectory() as tmp:
487
- root = Path(tmp)
488
- csv_path = root / "auth.csv"
489
- with csv_path.open("w", newline="") as fh:
490
- writer = csv.DictWriter(fh, fieldnames=FIELDS)
491
- writer.writeheader()
492
- writer.writerows(rows)
493
- rates_path = root / "rates.json"
494
- rates_path.write_text(json.dumps(rates_cache()))
495
- cmd = [sys.executable, str(SCRIPT), str(csv_path), "--rates", str(rates_path),
496
- "--amount-column", "amount", "--currency-column", "currency", "--date-column", "ts",
497
- "--descriptor-column", "descriptor", "--bin-country-column", "country",
498
- "--candidate-currencies", "EUR"]
499
- result = subprocess.run(cmd, capture_output=True, text=True)
500
- self.assertEqual(result.returncode, 0, result.stderr)
501
- report = json.loads(result.stdout)
502
- descriptor = report["descriptors"][0]
503
- self.assertEqual(descriptor["streams"][0]["stream"], "EUR menu-12 live")
504
- stream = descriptor["groups"][0]["substreams"][0]
505
- self.assertEqual(stream["attempts"], len(rows))
506
- self.assertFalse(stream["frozen"])
507
- self.assertEqual(stream["rate_consistency"]["tested_attempts"], len(rows))
508
- self.assertEqual(stream["rate_consistency"]["frozen_attempts"], 0)
509
- self.assertEqual(stream["rate_consistency"]["detail"][0]["market_move"], 0.0)
510
-
511
- def test_peaks_disappear_under_jitter_control(self):
512
- vals = [10.0] * 50 + [20.0] * 30
513
- cov, _ = MODULE.coverage(vals, 0.012, 0.02, 10)
514
- self.assertGreater(cov, 0.99)
515
- ctrl = MODULE.coverage_against(MODULE.jitter(vals, random.Random(1), 0.02, 0.05), [10.0, 20.0], 0.012)
516
- self.assertLess(ctrl, 0.05)
517
-
518
-
519
- if __name__ == "__main__":
520
- unittest.main()