@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,317 +0,0 @@
1
- import { test } from 'node:test'
2
- import assert from 'node:assert/strict'
3
- import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
4
- import { tmpdir } from 'node:os'
5
- import { join } from 'node:path'
6
- import { spawnSync } from 'node:child_process'
7
- import { planRelease } from './plan.mjs'
8
-
9
- function project(version) {
10
- return `[project]\nname = "forex-service"\nversion = "${version}"\nrequires-python = ">=3.11"\ndependencies = ["anyio>=4.6"]\n\n[tool.boundary]\nlabel = "unchanged"\nretry-limit = 12345\n`
11
- }
12
-
13
- function lock(version) {
14
- return `version = 1\nrevision = 3\nrequires-python = ">=3.11"\n\n[[package]]\nname = "anyio"\nversion = "4.6.0"\nsource = { registry = "https://pypi.org/simple" }\nsdist = { url = "https://files.pythonhosted.org/anyio.tar.gz", hash = "sha256:${'a'.repeat(64)}", size = 12345 }\n\n[[package]]\nname = "forex-service"\nversion = "${version}"\nsource = { editable = "." }\ndependencies = [{ name = "anyio" }]\n\n[package.metadata]\nrequires-dist = [{ name = "anyio", specifier = ">=4.6" }]\n`
15
- }
16
-
17
- const identityFiles = ['pyproject.toml', 'uv.lock']
18
- const originalCode = 'def usable_rate(age):\n return True\n'
19
- const fixedCode = 'def usable_rate(age):\n return age < 60\n'
20
-
21
- function fixture(t, transform = (files) => files) {
22
- const base = mkdtempSync(join(tmpdir(), 'prepared-toml-boundary-'))
23
- t.after(() => rmSync(base, { recursive: true, force: true }))
24
- const cwd = join(base, 'repo')
25
- const remote = join(base, 'origin.git')
26
- const home = join(base, 'home')
27
- for (const directory of [cwd, remote, home]) mkdirSync(directory)
28
- // Whitelist only the executable search path; no inherited credentials, git config or hooks.
29
- const env = {
30
- PATH: process.env.PATH, HOME: home, TMPDIR: base, LC_ALL: 'C', TZ: 'UTC',
31
- GIT_CONFIG_NOSYSTEM: '1', GIT_CONFIG_GLOBAL: '/dev/null', GIT_ATTR_NOSYSTEM: '1',
32
- GIT_ALLOW_PROTOCOL: 'file', GIT_TERMINAL_PROMPT: '0',
33
- GIT_CONFIG_COUNT: '1', GIT_CONFIG_KEY_0: 'core.hooksPath', GIT_CONFIG_VALUE_0: '/dev/null',
34
- GIT_AUTHOR_DATE: '2026-08-24T12:00:00Z', GIT_COMMITTER_DATE: '2026-08-24T12:00:00Z',
35
- }
36
- const gitBytes = (directory, ...args) => {
37
- const result = spawnSync('git', args, { cwd: directory, env, timeout: 10_000, maxBuffer: 16 * 1024 * 1024 })
38
- assert.equal(result.status, 0, result.stderr?.toString('utf8'))
39
- return result.stdout
40
- }
41
- const git = (...args) => gitBytes(cwd, ...args).toString('utf8').trim()
42
- git('init', '--template=', '-b', 'main')
43
- git('config', 'user.email', 'fixture@example.invalid')
44
- git('config', 'user.name', 'Offline Fixture')
45
- git('config', 'commit.gpgsign', 'false')
46
- git('config', 'tag.gpgsign', 'false')
47
- gitBytes(remote, 'init', '--bare', '--template=')
48
- git('remote', 'add', 'origin', 'https://github.com/example/forex-service.git')
49
- git('config', `url.${remote}.insteadOf`, 'https://github.com/example/forex-service.git')
50
-
51
- const snapshots = {}
52
- const commitStage = (stage, version, message) => {
53
- const files = transform({ 'pyproject.toml': project(version), 'uv.lock': lock(version) }, stage)
54
- snapshots[stage] = Object.fromEntries(identityFiles.map((file) => [file, Buffer.from(files[file])]))
55
- for (const file of identityFiles) writeFileSync(join(cwd, file), snapshots[stage][file])
56
- writeFileSync(join(cwd, 'CHANGELOG.md'), `# Changelog\n\n## [${version}] - 2026-08-24\n\n- Updated rates.\n`)
57
- writeFileSync(join(cwd, 'rates.py'), stage === 'target' ? fixedCode : originalCode)
58
- git('add', '.')
59
- git('commit', '-m', message)
60
- const sha = git('rev-parse', 'HEAD')
61
- // Read blobs without a text encoding: invalid octets must survive the real Git round trip.
62
- for (const file of identityFiles) {
63
- assert.deepEqual(gitBytes(cwd, 'show', `${sha}:${file}`), snapshots[stage][file], `${stage}:${file} fixture bytes`)
64
- }
65
- return sha
66
- }
67
-
68
- const parentSha = commitStage('parent', '0.6.2', 'chore: existing Python metadata')
69
- git('tag', 'v0.6.2')
70
- git('switch', '-c', 'metadata')
71
- const cutSha = commitStage('cut', '0.7.0', 'chore(release): 0.7.0')
72
- git('switch', 'main')
73
- git('merge', '--no-ff', 'metadata', '-m', 'Merge reviewed metadata')
74
- const mergeSha = git('rev-parse', 'HEAD')
75
- git('tag', '-a', 'v0.7.0-rc.1', '-m', 'Failed candidate')
76
- git('push', 'origin', 'main', '--tags')
77
- const sha = commitStage('target', '0.7.0', 'fix: reject stale cached rates')
78
- const preparedMetadata = {
79
- schema: 'prepared-release-metadata/v1', repository: 'example/forex-service', version: '0.7.0', metadataPr: 42,
80
- cutSha, mergeSha,
81
- previousAttempt: {
82
- receiptId: 'failed-offline-attempt', receiptSha256: 'a'.repeat(64),
83
- candidateTag: 'v0.7.0-rc.1', candidateSha: mergeSha,
84
- },
85
- }
86
- const plan = () => {
87
- // planRelease is synchronous; its own git/Python children receive the same offline environment.
88
- const inherited = process.env
89
- process.env = env
90
- try {
91
- return planRelease({ cwd, sha, preparedMetadata, cloudRun: true, now: new Date('2026-08-24T12:00:00Z') })
92
- } finally {
93
- process.env = inherited
94
- }
95
- }
96
- return { cwd, env, sha, parentSha, cutSha, mergeSha, preparedMetadata, git, gitBytes: (...args) => gitBytes(cwd, ...args), plan }
97
- }
98
-
99
- function assertTomlRefusal(f, file, reason) {
100
- assert.throws(f.plan, (error) => error instanceof Error && error.message.includes(file) && reason.test(error.message),
101
- `prepared reuse must refuse ${file} at the TOML boundary`)
102
- }
103
-
104
- function invalidOctet(text, octet) {
105
- // Put a raw invalid UTF-8 octet inside a quoted TOML string, not an escape or U+FFFD.
106
- return Buffer.concat([Buffer.from(text), Buffer.from('\n[tool.raw]\nlabel = "'), Buffer.from([octet]), Buffer.from('"\n')])
107
- }
108
-
109
- for (const [file, from, to] of [
110
- ['uv.lock', 'size = 12345', 'size = 12345.0'],
111
- ['pyproject.toml', 'retry-limit = 12345', 'retry-limit = 12345.0'],
112
- ]) {
113
- test(`prepared reuse rejects historical integer-to-float non-version change in ${file}`, (t) => {
114
- const f = fixture(t, (files, stage) => {
115
- if (stage !== 'parent') files[file] = files[file].replace(from, to)
116
- return files
117
- })
118
- assertTomlRefusal(f, file, /metadata.only|non.version|type|integer|float/i)
119
- })
120
- }
121
-
122
- for (const file of identityFiles) {
123
- for (const invalidStage of ['parent', 'cut', 'target']) {
124
- test(`prepared reuse rejects invalid UTF-8 ${file} at ${invalidStage}`, (t) => {
125
- const f = fixture(t, (files, stage) => {
126
- files[file] = stage === invalidStage
127
- ? invalidOctet(files[file], 0xfe)
128
- : `${files[file]}\n[tool.raw]\nlabel = "\uFFFD"\n`
129
- return files
130
- })
131
- assertTomlRefusal(f, file, /UTF.?8|encoding|TOML/i)
132
- })
133
- }
134
-
135
- test(`prepared reuse rejects distinct historical 0xfe-to-0xff blob collision in ${file}`, (t) => {
136
- const f = fixture(t, (files, stage) => {
137
- files[file] = invalidOctet(files[file], stage === 'parent' ? 0xfe : 0xff)
138
- return files
139
- })
140
- const before = f.gitBytes('show', `${f.parentSha}:${file}`)
141
- const after = f.gitBytes('show', `${f.cutSha}:${file}`)
142
- assert.equal(before.includes(Buffer.from([0xfe])), true)
143
- assert.equal(after.includes(Buffer.from([0xff])), true)
144
- assert.notDeepEqual(before, after)
145
- assertTomlRefusal(f, file, /UTF.?8|encoding|TOML/i)
146
- })
147
- }
148
-
149
- test('prepared reuse accepts unchanged valid UTF-8 non-ASCII TOML strings', (t) => {
150
- const f = fixture(t, (files) => {
151
- for (const file of identityFiles) files[file] += '\n[tool.unicode]\nlabel = "caf\u00e9 \u6771\u4eac"\n'
152
- return files
153
- })
154
- const plan = f.plan()
155
- assert.equal(plan.releaseSourceSha, f.sha)
156
- assert.equal(plan.nextVersion, '0.7.0')
157
- assert.equal(plan.bump, 'none')
158
- assert.deepEqual(plan.versionFiles, identityFiles)
159
- })
160
-
161
- test('prepared reuse accepts unchanged legitimate fractional and integral-valued TOML floats', (t) => {
162
- const f = fixture(t, (files) => {
163
- files['pyproject.toml'] += '\n[tool.tuning]\nbackoff = 1.25\nthreshold = 12345.0\n'
164
- return files
165
- })
166
- const plan = f.plan()
167
- assert.equal(plan.releaseSourceSha, f.sha)
168
- assert.equal(plan.nextVersion, '0.7.0')
169
- assert.equal(plan.metadata.mode, 'reuse-prepared')
170
- })
171
-
172
- test('prepared reuse rejects a coordinated target project and editable-root rename at the retained version', (t) => {
173
- const f = fixture(t, (files, stage) => {
174
- if (stage === 'target') {
175
- for (const file of identityFiles) files[file] = files[file].replace('forex-service', 'renamed-service')
176
- }
177
- return files
178
- })
179
- assert.throws(f.plan, /name|identity/i, 'the frozen target must retain the original project identity')
180
- })
181
-
182
- for (const stageToChange of ['parent', 'cut', 'target']) {
183
- test(`prepared reuse requires an integer uv lock format at ${stageToChange}`, (t) => {
184
- const f = fixture(t, (files, stage) => {
185
- if (stage === stageToChange) files['uv.lock'] = files['uv.lock'].replace('version = 1\n', 'version = 1.0\n')
186
- return files
187
- })
188
- assertTomlRefusal(f, 'uv.lock', /format|integer|type/i)
189
- })
190
- }
191
-
192
- for (const [before, after] of [['editable', 'virtual'], ['virtual', 'editable']]) {
193
- test(`prepared reuse rejects target root mode change from ${before} to ${after}`, (t) => {
194
- const f = fixture(t, (files, stage) => {
195
- files['uv.lock'] = files['uv.lock'].replace('editable = "."', `${stage === 'target' ? after : before} = "."`)
196
- return files
197
- })
198
- assertTomlRefusal(f, 'uv.lock', /source|mode|identity/i)
199
- })
200
- }
201
-
202
- for (const [label, before, after] of [
203
- ['nested list scalar', '[{ nested = [12345] }]', '[{ nested = [12345.0] }]'],
204
- ['tag-shaped user data', '{ type = "int", value = "12345" }', '12345'],
205
- ]) {
206
- test(`prepared comparison preserves ${label} types`, (t) => {
207
- const f = fixture(t, (files, stage) => {
208
- files['pyproject.toml'] += `\n[tool.typed]\nvalue = ${stage === 'parent' ? before : after}\n`
209
- return files
210
- })
211
- assertTomlRefusal(f, 'pyproject.toml', /metadata.only|non.version|type/i)
212
- })
213
- }
214
-
215
- test('prepared comparison accepts unchanged nested scalars and reordered TOML keys', (t) => {
216
- const f = fixture(t, (files, stage) => {
217
- const entries = ['enabled = true', 'values = [12345.0, 1.25, -0.0, 12345, "int"]', 'tag = { type = "int", value = "12345" }']
218
- files['pyproject.toml'] += `\n[tool.typed]\n${(stage === 'parent' ? entries : entries.reverse()).join('\n')}\n`
219
- return files
220
- })
221
- assert.equal(f.plan().releaseSourceSha, f.sha)
222
- })
223
-
224
- function runCli(f, env = f.env) {
225
- const input = join(f.cwd, '.git', 'prepared.json')
226
- writeFileSync(input, JSON.stringify(f.preparedMetadata))
227
- return spawnSync(process.execPath, [join(import.meta.dirname, 'plan.mjs'), '--sha', f.sha,
228
- '--prepared-metadata', input, '--json'], { cwd: f.cwd, env, encoding: 'utf8', timeout: 15_000 })
229
- }
230
-
231
- test('invalid UTF-8 CLI refusal is sanitized and leaves no plan or repository changes', (t) => {
232
- const marker = 'private-metadata-must-not-appear'
233
- const f = fixture(t, (files, stage) => {
234
- if (stage === 'target') files['pyproject.toml'] = invalidOctet(`${files['pyproject.toml']}\n# ${marker}`, 0xfe)
235
- return files
236
- })
237
- const before = f.git('show-ref')
238
- const projectBytes = readFileSync(join(f.cwd, 'pyproject.toml'))
239
- const result = runCli(f)
240
- assert.equal(result.status, 1)
241
- assert.equal(result.stdout, '')
242
- assert.match(result.stderr, /pyproject.toml.*(?:TOML|UTF.?8)/i)
243
- assert.doesNotMatch(result.stderr, new RegExp(`${marker}|Traceback|UnicodeDecodeError`))
244
- assert.equal(f.git('show-ref'), before)
245
- assert.equal(f.git('status', '--porcelain'), '')
246
- assert.deepEqual(readFileSync(join(f.cwd, 'pyproject.toml')), projectBytes)
247
- })
248
-
249
- test('prepared metadata Git acquisition refuses oversized blobs without printing their content', (t) => {
250
- const marker = 'private-oversized-metadata'
251
- const f = fixture(t, (files, stage) => {
252
- if (stage === 'target') files['pyproject.toml'] += `\n# ${marker}${'x'.repeat(8 * 1024 * 1024)}\n`
253
- return files
254
- })
255
- const result = runCli(f)
256
- assert.equal(result.status, 1)
257
- assert.equal(result.stdout, '')
258
- assert.match(result.stderr, /pyproject.toml.*(?:limit|bounded|acquisition)/i)
259
- assert.doesNotMatch(result.stderr, new RegExp(marker))
260
- assert.equal(f.git('status', '--porcelain'), '')
261
- })
262
-
263
- for (const file of identityFiles) {
264
- test(`prepared metadata Git acquisition accepts an exact 8 MiB ${file} blob`, (t) => {
265
- const limit = 8 * 1024 * 1024
266
- const f = fixture(t, (files, stage) => {
267
- if (stage === 'target') files[file] += `\n#${'x'.repeat(limit - Buffer.byteLength(files[file]) - 3)}\n`
268
- return files
269
- })
270
- assert.equal(f.gitBytes('show', `${f.sha}:${file}`).length, limit)
271
- const result = runCli(f)
272
- assert.equal(result.status, 0, result.stderr)
273
- assert.equal(JSON.parse(result.stdout).releaseSourceSha, f.sha)
274
- })
275
- }
276
-
277
- for (const [label, action] of [
278
- ['process error', 'process.stderr.write("private-git-error"); process.exit(1)'],
279
- ['timeout', 'setTimeout(() => process.exit(0), 30_000)'],
280
- ]) {
281
- test(`prepared metadata Git acquisition handles ${label} without leaking output`, (t) => {
282
- const f = fixture(t)
283
- const realGit = spawnSync('which', ['git'], { env: f.env, encoding: 'utf8' }).stdout.trim()
284
- const bin = join(f.cwd, '.git', 'git-bin')
285
- mkdirSync(bin)
286
- // Substitute only the external Git process for the bounded metadata-blob read.
287
- writeFileSync(join(bin, 'git'), `#!${process.execPath}
288
- const args = process.argv.slice(2)
289
- if (args[0] === 'show' && args[1].endsWith(':pyproject.toml')) {
290
- ${action}
291
- } else {
292
- const { spawnSync } = require('node:child_process')
293
- const result = spawnSync(${JSON.stringify(realGit)}, args, { stdio: 'inherit' })
294
- process.exit(result.status ?? 1)
295
- }
296
- `, { mode: 0o755 })
297
- const result = runCli(f, { ...f.env, PATH: `${bin}:${f.env.PATH}` })
298
- assert.equal(result.status, 1)
299
- assert.equal(result.stdout, '')
300
- assert.match(result.stderr, /pyproject.toml Git blob acquisition failed.*bounded/)
301
- assert.doesNotMatch(result.stderr, /private-git-error|Traceback/)
302
- assert.equal(f.git('status', '--porcelain'), '')
303
- })
304
- }
305
-
306
- test('prepared reuse permits an unrelated critical code fix after the metadata merge', (t) => {
307
- const f = fixture(t)
308
- assert.equal(f.gitBytes('show', `${f.cutSha}:rates.py`).toString('utf8'), originalCode)
309
- assert.equal(f.gitBytes('show', `${f.sha}:rates.py`).toString('utf8'), fixedCode)
310
- assert.notEqual(f.git('rev-parse', `${f.mergeSha}^{tree}`), f.git('rev-parse', `${f.sha}^{tree}`))
311
- const plan = f.plan()
312
- assert.equal(plan.releaseSourceSha, f.sha)
313
- assert.notEqual(plan.releaseSourceSha, f.mergeSha)
314
- assert.equal(plan.nextVersion, '0.7.0')
315
- assert.equal(plan.metadata.cutSha, f.cutSha)
316
- assert.equal(plan.metadata.mergeSha, f.mergeSha)
317
- })
@@ -1,291 +0,0 @@
1
- import { test } from 'node:test'
2
- import assert from 'node:assert/strict'
3
- import { mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
4
- import { tmpdir } from 'node:os'
5
- import { join } from 'node:path'
6
- import { spawnSync } from 'node:child_process'
7
- import { planRelease } from './plan.mjs'
8
-
9
- function git(cwd, ...args) {
10
- const result = spawnSync('git', args, { cwd, encoding: 'utf8' })
11
- assert.equal(result.status, 0, result.stderr)
12
- return result.stdout.trim()
13
- }
14
-
15
- function project(version) {
16
- return `[project]\nname = "forex-service"\nversion = "${version}"\nrequires-python = ">=3.11"\ndependencies = ["anyio>=4.6"]\n\n[tool.other]\nversion = "not-the-project"\n`
17
- }
18
-
19
- function root(version, mode = 'editable') {
20
- return `[[package]]\nname = "forex-service"\nversion = "${version}"\nsource = { ${mode} = "." }\ndependencies = [{ name = "anyio" }]\n\n[package.metadata]\nrequires-dist = [{ name = "anyio", specifier = ">=4.6" }]\n`
21
- }
22
-
23
- function lock(version, mode = 'editable') {
24
- // Minimal uv v1/revision 3 lock: the dependency precedes the project root.
25
- return `version = 1\nrevision = 3\nrequires-python = ">=3.11"\n\n[[package]]\nname = "anyio"\nversion = "4.6.0"\nsource = { registry = "https://pypi.org/simple" }\nsdist = { url = "https://files.pythonhosted.org/anyio.tar.gz", hash = "sha256:${'a'.repeat(64)}", size = 12345 }\nwheels = [{ url = "https://files.pythonhosted.org/anyio.whl", hash = "sha256:${'b'.repeat(64)}", size = 23456 }]\n\n${root(version, mode)}`
26
- }
27
-
28
- function fixture(t, { beforeLock = lock('0.4.0'), cutLock = lock('0.7.0'), targetLock = cutLock,
29
- cutProject = project('0.7.0'), targetProject = cutProject } = {}) {
30
- const cwd = mkdtempSync(join(tmpdir(), 'prepared-python-uv-'))
31
- const remote = mkdtempSync(join(tmpdir(), 'prepared-python-origin-'))
32
- t.after(() => { rmSync(cwd, { recursive: true, force: true }); rmSync(remote, { recursive: true, force: true }) })
33
- git(cwd, 'init', '-b', 'main')
34
- git(cwd, 'config', 'user.email', 'dev@example.com')
35
- git(cwd, 'config', 'user.name', 'Dev')
36
- git(cwd, 'config', 'commit.gpgsign', 'false')
37
- git(remote, 'init', '--bare')
38
- git(cwd, 'remote', 'add', 'origin', 'https://github.com/example/forex-service.git')
39
- git(cwd, 'config', `url.${remote}.insteadOf`, 'https://github.com/example/forex-service.git')
40
- writeFileSync(join(cwd, 'pyproject.toml'), project('0.6.2'))
41
- writeFileSync(join(cwd, 'uv.lock'), beforeLock)
42
- writeFileSync(join(cwd, 'CHANGELOG.md'), '# Changelog\n\n## [0.6.2] - 2026-08-01\n\n- Previous release.\n')
43
- git(cwd, 'add', '.')
44
- git(cwd, 'commit', '-m', 'chore: existing Python project with stale root lock')
45
- git(cwd, 'tag', 'v0.6.2')
46
- git(cwd, 'switch', '-c', 'metadata')
47
- writeFileSync(join(cwd, 'pyproject.toml'), cutProject)
48
- writeFileSync(join(cwd, 'uv.lock'), cutLock)
49
- writeFileSync(join(cwd, 'CHANGELOG.md'), '# Changelog\n\n## [0.7.0] - 2026-08-24\n\n- Updated rates.\n')
50
- git(cwd, 'add', '.')
51
- git(cwd, 'commit', '-m', 'chore(release): 0.7.0')
52
- const cutSha = git(cwd, 'rev-parse', 'HEAD')
53
- git(cwd, 'switch', 'main')
54
- git(cwd, 'merge', '--no-ff', 'metadata', '-m', 'Merge metadata')
55
- const mergeSha = git(cwd, 'rev-parse', 'HEAD')
56
- git(cwd, 'tag', '-a', 'v0.7.0-rc.1', '-m', 'Failed candidate')
57
- git(cwd, 'push', 'origin', 'main', '--tags')
58
- writeFileSync(join(cwd, 'uv.lock'), targetLock)
59
- writeFileSync(join(cwd, 'pyproject.toml'), targetProject)
60
- writeFileSync(join(cwd, 'rates.py'), 'fixed = True\n')
61
- git(cwd, 'add', '.')
62
- git(cwd, 'commit', '-m', 'fix: candidate failure')
63
- const sha = git(cwd, 'rev-parse', 'HEAD')
64
- const preparedMetadata = {
65
- schema: 'prepared-release-metadata/v1', repository: 'example/forex-service', version: '0.7.0', metadataPr: 42,
66
- cutSha, mergeSha,
67
- previousAttempt: { receiptId: 'failed-forex-attempt', receiptSha256: 'a'.repeat(64), candidateTag: 'v0.7.0-rc.1', candidateSha: mergeSha },
68
- }
69
- const plan = (options = {}) => planRelease({ cwd, sha, preparedMetadata, cloudRun: true, ...options })
70
- return { cwd, sha, preparedMetadata, plan }
71
- }
72
-
73
- test('Python-only prepared reuse accepts editable uv root-only version repair without writes', (t) => {
74
- const f = fixture(t)
75
- const receipt = join(f.cwd, '.git', 'resolve-release-authorization.json')
76
- writeFileSync(receipt, 'do not reset historical authority')
77
- const status = git(f.cwd, 'status', '--porcelain')
78
- const plan = f.plan()
79
- assert.deepEqual(plan.versionFiles, ['pyproject.toml', 'uv.lock'])
80
- assert.equal(plan.currentVersion, '0.7.0')
81
- assert.equal(plan.nextVersion, '0.7.0')
82
- assert.equal(plan.bump, 'none')
83
- assert.equal(plan.candidateTag, 'v0.7.0-rc.2')
84
- assert.equal(plan.releaseSourceSha, f.sha)
85
- assert.notEqual(plan.receiptId, 'failed-forex-attempt')
86
- assert.equal(readFileSync(receipt, 'utf8'), 'do not reset historical authority')
87
- assert.equal(git(f.cwd, 'status', '--porcelain'), status)
88
- assert.equal(git(f.cwd, 'rev-parse', 'HEAD'), f.sha)
89
- })
90
-
91
- test('Python-only prepared reuse accepts actual Forex virtual root version repair without writes', (t) => {
92
- const f = fixture(t, { beforeLock: lock('0.4.0', 'virtual'), cutLock: lock('0.7.0', 'virtual') })
93
- const receipt = join(f.cwd, '.git', 'resolve-release-authorization.json')
94
- writeFileSync(receipt, 'do not reset historical authority')
95
- const refs = git(f.cwd, 'show-ref')
96
- const projectBytes = readFileSync(join(f.cwd, 'pyproject.toml'))
97
- const lockBytes = readFileSync(join(f.cwd, 'uv.lock'))
98
- const plan = f.plan()
99
- assert.deepEqual(plan.versionFiles, ['pyproject.toml', 'uv.lock'])
100
- assert.equal(plan.currentVersion, '0.7.0')
101
- assert.equal(plan.nextVersion, '0.7.0')
102
- assert.equal(plan.bump, 'none')
103
- assert.equal(plan.candidateTag, 'v0.7.0-rc.2')
104
- assert.equal(plan.tag, 'v0.7.0')
105
- assert.equal(plan.releaseSourceSha, f.sha)
106
- assert.equal(plan.metadata.mode, 'reuse-prepared')
107
- assert.notEqual(plan.receiptId, 'failed-forex-attempt')
108
- assert.equal(readFileSync(receipt, 'utf8'), 'do not reset historical authority')
109
- assert.deepEqual(readFileSync(join(f.cwd, 'pyproject.toml')), projectBytes)
110
- assert.deepEqual(readFileSync(join(f.cwd, 'uv.lock')), lockBytes)
111
- assert.equal(git(f.cwd, 'show-ref'), refs)
112
- assert.equal(git(f.cwd, 'status', '--porcelain'), '')
113
- assert.equal(git(f.cwd, 'rev-parse', 'HEAD'), f.sha)
114
- })
115
-
116
- test('Python-only prepared reuse refuses a stale uv root even when the cut did not edit the lock', (t) => {
117
- const f = fixture(t, { cutLock: lock('0.4.0') })
118
- assert.throws(() => f.plan(), /uv.lock.*version.*does not match/)
119
- })
120
-
121
- const invalidRoots = [
122
- ['missing root', (text, version, mode) => text.replace(root(version, mode), ''), /uv.lock.*exactly one/],
123
- ['wrong name', (text) => text.replace('name = "forex-service"', 'name = "foreign-service"'), /uv.lock.*name/],
124
- ['parent path source', (text, version, mode) => text.replace(`${mode} = "."`, `${mode} = ".."`), /uv.lock.*source/],
125
- ['foreign path source', (text, version, mode) => text.replace(`${mode} = "."`, `${mode} = "../foreign"`), /uv.lock.*source/],
126
- ['noncanonical root path', (text, version, mode) => text.replace(`${mode} = "."`, `${mode} = "./"`), /uv.lock.*source/],
127
- ['registry-only source', (text, version, mode) => text.replace(`${mode} = "."`, 'registry = "https://pypi.org/simple"'), /uv.lock.*source/],
128
- ['mixed root source', (text, version, mode) => text.replace(`${mode} = "."`, 'editable = ".", virtual = "."'), /uv.lock.*source/],
129
- ['extra registry field', (text, version, mode) => text.replace(`${mode} = "."`, `${mode} = ".", registry = "https://pypi.org/simple"`), /uv.lock.*source/],
130
- ['unknown source field', (text, version, mode) => text.replace(`${mode} = "."`, `${mode} = ".", unexpected = true`), /uv.lock.*source/],
131
- ['duplicate root', (text, version, mode) => `${text}\n${root(version, mode)}`, /uv.lock.*exactly one/],
132
- ['opposite mode duplicate root', (text, version, mode) => `${text}\n${root(version, mode === 'editable' ? 'virtual' : 'editable')}`, /uv.lock.*exactly one/],
133
- ['second differently named root', (text, version, mode) => `${text}\n${root(version, mode).replace('forex-service', 'foreign-service')}`, /uv.lock.*exactly one/],
134
- ['empty version', (text, version) => text.replace(`version = "${version}"`, 'version = ""'), /uv.lock.*version/],
135
- ]
136
-
137
- for (const mode of ['editable', 'virtual']) {
138
- for (const stage of ['beforeLock', 'cutLock', 'targetLock']) {
139
- test(`prepared reuse validates ${mode} uv root identity at ${stage}`, async (t) => {
140
- for (const [label, change, error] of invalidRoots) {
141
- await t.test(label, (t) => {
142
- const version = stage === 'beforeLock' ? '0.4.0' : '0.7.0'
143
- const f = fixture(t, {
144
- beforeLock: lock('0.4.0', mode), cutLock: lock('0.7.0', mode),
145
- [stage]: change(lock(version, mode), version, mode),
146
- })
147
- assert.throws(() => f.plan(), error)
148
- })
149
- }
150
- })
151
- }
152
- }
153
-
154
- test('historical uv comparison refuses source mode flips even though both root shapes are valid', async (t) => {
155
- for (const [before, cut] of [['editable', 'virtual'], ['virtual', 'editable']]) {
156
- await t.test(`${before} to ${cut}`, (t) => {
157
- const f = fixture(t, { beforeLock: lock('0.4.0', before), cutLock: lock('0.7.0', cut) })
158
- assert.throws(() => f.plan(), /metadata-only: uv.lock changes more than version identity/)
159
- assert.equal(git(f.cwd, 'status', '--porcelain'), '')
160
- })
161
- }
162
- })
163
-
164
- test('prepared reuse refuses stale uv root or Python project versions at the frozen target', async (t) => {
165
- for (const [label, options, error] of [
166
- ['stale lock', { targetLock: lock('0.4.0') }, /uv.lock.*version.*does not match/],
167
- ['stale project', { targetProject: project('0.6.2') }, /pyproject.toml version.*does not match/],
168
- ['empty project name', { targetProject: project('0.7.0').replace('name = "forex-service"', 'name = ""') }, /project name is missing/],
169
- ['foreign project', { targetProject: project('0.7.0').replace('forex-service', 'foreign-service') }, /uv.lock.*name/],
170
- ['missing actual project version', { targetProject: `${project('0.7.0').replace('version = "0.7.0"\n', '')}\n[tool.poetry]\nversion = "0.7.0"\n` }, /project version must be static/],
171
- ]) {
172
- await t.test(label, (t) => {
173
- const f = fixture(t, options)
174
- assert.throws(() => f.plan(), error)
175
- })
176
- }
177
- })
178
-
179
- test('historical uv comparison excludes only the identified root version, never dependency data', async (t) => {
180
- const changes = [
181
- ['lock revision', (text) => text.replace('revision = 3', 'revision = 4')],
182
- ['Python requirement', (text) => text.replace('>=3.11', '>=3.12')],
183
- ['dependency version', (text) => text.replace('4.6.0', '4.7.0')],
184
- ['dependency source', (text) => text.replace('https://pypi.org/simple', 'https://foreign.example/simple')],
185
- ['sdist hash', (text) => text.replace('a'.repeat(64), 'c'.repeat(64))],
186
- ['wheel size', (text) => text.replace('size = 23456', 'size = 23457')],
187
- ['root dependency edge', (text) => text.replace('dependencies = [{ name = "anyio" }]', 'dependencies = []')],
188
- ['root dependency requirement', (text) => text.replace('specifier = ">=4.6"', 'specifier = ">=4.7"')],
189
- ['extra dependency', (text) => `${text}\n[[package]]\nname = "unsafe"\nversion = "9.0.0"\nsource = { registry = "https://pypi.org/simple" }\n`],
190
- ]
191
- for (const [label, change] of changes) {
192
- await t.test(label, (t) => {
193
- const f = fixture(t, { cutLock: change(lock('0.7.0')) })
194
- assert.throws(() => f.plan(), /metadata-only: uv.lock changes more than version identity/)
195
- })
196
- }
197
- })
198
-
199
- test('historical Python metadata comparison still refuses non-version changes', async (t) => {
200
- for (const [label, change] of [
201
- ['project dependencies', (text) => text.replace('anyio>=4.6', 'anyio>=4.7')],
202
- ['tool version', (text) => text.replace('not-the-project', 'changed-tool-version')],
203
- ]) {
204
- await t.test(label, (t) => {
205
- const f = fixture(t, { cutProject: change(project('0.7.0')) })
206
- assert.throws(() => f.plan(), /metadata-only: pyproject.toml changes more than version identity/)
207
- })
208
- }
209
- })
210
-
211
- test('prepared reuse allows reviewed dependency fixes after the historical metadata cut', (t) => {
212
- const f = fixture(t, {
213
- targetLock: lock('0.7.0').replace('4.6.0', '4.7.0').replace('specifier = ">=4.6"', 'specifier = ">=4.7"'),
214
- targetProject: project('0.7.0').replace('anyio>=4.6', 'anyio>=4.7'),
215
- })
216
- assert.equal(f.plan().releaseSourceSha, f.sha)
217
- })
218
-
219
- test('TOML inspection accepts equivalent syntax and refuses invalid or unsupported lock data', async (t) => {
220
- await t.test('single quotes, inline comments, and table syntax', (t) => {
221
- const f = fixture(t, {
222
- cutProject: project('0.7.0').replace('version = "0.7.0"', "version = '0.7.0' # prepared version"),
223
- cutLock: lock('0.7.0').replace('source = { editable = "." }\n', '').replace('[package.metadata]', "[package.source]\neditable = '.'\n\n[package.metadata]"),
224
- })
225
- assert.equal(f.plan().nextVersion, '0.7.0')
226
- })
227
- for (const [label, cutLock, error] of [
228
- ['duplicate TOML key', `${lock('0.7.0')}\nrequires-dist = []\n`, /uv.lock TOML inspection failed/],
229
- ['unsupported format', lock('0.7.0').replace('version = 1', 'version = 2'), /uv.lock.*unsupported lock format/],
230
- ['missing packages', 'version = 1\npackage = []\n', /uv.lock.*exactly one/],
231
- ['unsafe integer', lock('0.7.0').replace('size = 12345', 'size = 9007199254740992'), /uv.lock TOML inspection failed/],
232
- ['nonfinite TOML number', lock('0.7.0').replace('size = 12345', 'size = inf'), /uv.lock TOML inspection failed/],
233
- ]) {
234
- await t.test(label, (t) => {
235
- const f = fixture(t, { cutLock })
236
- assert.throws(() => f.plan(), error)
237
- })
238
- }
239
- })
240
-
241
- test('Python parser unavailability fails closed through the public CLI without a plan or receipt reset', (t) => {
242
- const f = fixture(t)
243
- const bin = join(f.cwd, '.git', 'only-git')
244
- mkdirSync(bin)
245
- symlinkSync(spawnSync('which', ['git'], { encoding: 'utf8' }).stdout.trim(), join(bin, 'git'))
246
- const receipt = join(f.cwd, '.git', 'resolve-release-authorization.json')
247
- writeFileSync(receipt, 'historical authority')
248
- const input = join(f.cwd, '.git', 'prepared.json')
249
- writeFileSync(input, JSON.stringify(f.preparedMetadata))
250
- const result = spawnSync(process.execPath, [join(import.meta.dirname, 'plan.mjs'), '--sha', f.sha,
251
- '--prepared-metadata', input, '--json'], { cwd: f.cwd, encoding: 'utf8', env: { ...process.env, PATH: bin } })
252
- assert.equal(result.status, 1)
253
- assert.equal(result.stdout, '')
254
- assert.match(result.stderr, /Python 3.11\+ with tomllib/)
255
- assert.equal(readFileSync(receipt, 'utf8'), 'historical authority')
256
- })
257
-
258
- test('external TOML parser errors, invalid output, output overflow and timeout all fail closed', async (t) => {
259
- for (const [label, parser, error] of [
260
- ['process failure', 'process.exit(1)', /TOML inspection failed/],
261
- ['invalid JSON', "process.stdout.write('{')", /parser returned invalid JSON/],
262
- ['output overflow', "process.stdout.write('x'.repeat(9 * 1024 * 1024))", /TOML inspection failed/],
263
- ['timeout', 'setTimeout(() => process.exit(0), 30_000)', /TOML inspection failed/],
264
- ]) {
265
- await t.test(label, (t) => {
266
- const f = fixture(t)
267
- const bin = join(f.cwd, '.git', 'parser-bin')
268
- mkdirSync(bin)
269
- // Substitute only the external Python boundary; git objects and planner stay real.
270
- writeFileSync(join(bin, 'python3'), `#!${process.execPath}\nprocess.stdin.resume()\n${parser}\n`, { mode: 0o755 })
271
- const input = join(f.cwd, '.git', 'prepared.json')
272
- writeFileSync(input, JSON.stringify(f.preparedMetadata))
273
- const result = spawnSync(process.execPath, [join(import.meta.dirname, 'plan.mjs'), '--sha', f.sha,
274
- '--prepared-metadata', input, '--json'], { cwd: f.cwd, encoding: 'utf8',
275
- env: { ...process.env, PATH: `${bin}:${process.env.PATH}` }, timeout: 15_000 })
276
- assert.equal(result.status, 1)
277
- assert.equal(result.stdout, '')
278
- assert.match(result.stderr, error)
279
- assert.equal(git(f.cwd, 'status', '--porcelain'), '')
280
- })
281
- }
282
- })
283
-
284
- test('normal Python planning remains unchanged and does not acquire uv identity write permissions', (t) => {
285
- const f = fixture(t)
286
- const plan = f.plan({ preparedMetadata: null, cloudRun: false })
287
- assert.deepEqual(plan.versionFiles, ['pyproject.toml'])
288
- assert.equal(plan.currentVersion, '0.7.0')
289
- assert.equal(plan.bump, 'patch')
290
- assert.equal(plan.nextVersion, '0.7.1')
291
- })