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