@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,366 +0,0 @@
1
- // Tests for model-catalog — pure core + CLI round-trip (exit codes), no network.
2
- import { test } from 'node:test'
3
- import assert from 'node:assert/strict'
4
- import fs from 'node:fs'
5
- import os from 'node:os'
6
- import path from 'node:path'
7
- import { fileURLToPath } from 'node:url'
8
- import { spawnSync } from 'node:child_process'
9
-
10
- import {
11
- PRUNE_AFTER_DAYS, syncCatalog, bankEntry, readCatalog, writeCatalog, pathForHarness,
12
- EFFORTS, DEFAULT_EFFORT, TIERS, SLOT_ELIGIBLE_TIERS, normalizeTier, operatingPoints, tierAt, toTiers,
13
- } from './model-catalog.mjs'
14
-
15
- const SCRIPT = fileURLToPath(new URL('./bin/model-catalog.mjs', import.meta.url))
16
- const tmpFile = () => path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'model-catalog-test-')), 'catalog.json')
17
-
18
- const entry = (id, lastSeenAt, confirmedAt = '2026-01-01') => ({ id, family: 'x', tier: 'frontier', confirmedAt, ...(lastSeenAt ? { lastSeenAt } : {}) })
19
-
20
- // ---- syncCatalog: the rule-4 predicate in one place ----
21
-
22
- // @covers package:harness-runtime
23
- // @level unit
24
- test('sync bumps lastSeenAt for seen ids and keeps fresh unseen entries', () => {
25
- const { catalog, bumped, pruned } = syncCatalog(
26
- { models: [entry('a', '2026-05-01'), entry('b', '2026-06-01')] },
27
- ['a'],
28
- '2026-07-13',
29
- )
30
- assert.deepEqual(bumped, ['a'])
31
- assert.deepEqual(pruned, [])
32
- assert.equal(catalog.models.find((m) => m.id === 'a').lastSeenAt, '2026-07-13')
33
- assert.equal(catalog.models.find((m) => m.id === 'b').lastSeenAt, '2026-06-01') // unseen: untouched
34
- })
35
-
36
- test('sync prunes strictly after PRUNE_AFTER_DAYS — day 60 kept, day 61 pruned', () => {
37
- // 2026-05-14 is exactly 60 days before 2026-07-13; 2026-05-13 is 61.
38
- const { catalog, pruned } = syncCatalog(
39
- { models: [entry('kept', '2026-05-14'), entry('gone', '2026-05-13')] },
40
- [],
41
- '2026-07-13',
42
- )
43
- assert.equal(PRUNE_AFTER_DAYS, 60)
44
- assert.deepEqual(pruned, ['gone'])
45
- assert.deepEqual(catalog.models.map((m) => m.id), ['kept'])
46
- })
47
-
48
- test('sync falls back to confirmedAt when lastSeenAt is absent (pre-field entries)', () => {
49
- const { pruned } = syncCatalog(
50
- { models: [entry('old', null, '2026-01-01'), entry('new', null, '2026-07-01')] },
51
- [],
52
- '2026-07-13',
53
- )
54
- assert.deepEqual(pruned, ['old'])
55
- })
56
-
57
- test('sync bumps before pruning — a stale entry rediscovered this run survives', () => {
58
- const { catalog, pruned } = syncCatalog(
59
- { models: [entry('back', '2026-01-01')] },
60
- ['back'],
61
- '2026-07-13',
62
- )
63
- assert.deepEqual(pruned, [])
64
- assert.equal(catalog.models[0].lastSeenAt, '2026-07-13')
65
- })
66
-
67
- // ---- bankEntry: rule-2 classification writes ----
68
-
69
- test('bank appends a new entry stamped with confirmedAt + lastSeenAt', () => {
70
- const c = bankEntry({ models: [] }, { id: 'm', family: 'openai', tier: 'balanced-coder' }, '2026-07-13')
71
- assert.deepEqual(c.models, [{ id: 'm', family: 'openai', tiers: { high: 'balanced-coder' }, confirmedAt: '2026-07-13', lastSeenAt: '2026-07-13' }])
72
- })
73
-
74
- test('bank overwrites an existing id (human correction) and refreshes confirmedAt', () => {
75
- const c = bankEntry(
76
- { models: [entry('m', '2026-06-01')] },
77
- { id: 'm', family: 'openai', tier: 'balanced-coder' },
78
- '2026-07-13',
79
- )
80
- assert.equal(c.models.length, 1)
81
- assert.deepEqual(c.models[0].tiers, { high: 'balanced-coder' })
82
- assert.equal(c.models[0].confirmedAt, '2026-07-13')
83
- })
84
-
85
- test('bank rejects entries missing id/family/tier or with an unknown tier', () => {
86
- assert.throws(() => bankEntry({ models: [] }, { family: 'x', tier: 'frontier' }, '2026-07-13'))
87
- assert.throws(() => bankEntry({ models: [] }, { id: 'm', tier: 'frontier' }, '2026-07-13'))
88
- assert.throws(() => bankEntry({ models: [] }, { id: 'm', family: 'x' }, '2026-07-13')) // neither tier nor tiers
89
- assert.throws(() => bankEntry({ models: [] }, { id: 'm', family: 'x', tier: 'huge' }, '2026-07-13'))
90
- })
91
-
92
- // ---- operating points: tier = f(model, effort), banked once (2026-07-19) ----
93
-
94
- test('bank accepts a tiers map — one model, two operating points', () => {
95
- const c = bankEntry(
96
- { models: [] },
97
- { id: 'gpt-5.6-terra', family: 'openai', tiers: { high: 'balanced-coder', max: 'deep-reasoner' } },
98
- '2026-07-19',
99
- )
100
- assert.deepEqual(c.models[0].tiers, { high: 'balanced-coder', max: 'deep-reasoner' })
101
- assert.equal(c.models[0].tier, undefined) // the legacy single-point field never persists
102
- })
103
-
104
- test('a single tier UPSERTS one point; a full tiers map REPLACES all of them', () => {
105
- let c = bankEntry({ models: [] }, { id: 'terra', family: 'openai', tier: 'balanced-coder' }, '2026-07-19')
106
- assert.deepEqual(c.models[0].tiers, { high: 'balanced-coder' }) // no effort -> the high point
107
-
108
- // measured later, confirmed on its own: the existing point survives
109
- c = bankEntry(c, { id: 'terra', family: 'openai', tier: 'deep-reasoner', effort: 'max' }, '2026-07-19')
110
- assert.deepEqual(c.models[0].tiers, { high: 'balanced-coder', max: 'deep-reasoner' })
111
-
112
- // a full map is the human restating the whole picture — it wins outright
113
- c = bankEntry(c, { id: 'terra', family: 'openai', tiers: { max: 'deep-reasoner' } }, '2026-07-19')
114
- assert.deepEqual(c.models[0].tiers, { max: 'deep-reasoner' })
115
- })
116
-
117
- test('an upsert onto a LEGACY-shape entry leaves no stale tier/effort behind', () => {
118
- // Regression: the strip ran on the incoming entry only, so `existing.tier` was
119
- // spread back in with nothing to shadow it — the stored entry then claimed a tier
120
- // that could flatly contradict its own map. Reads went through toTiers and looked
121
- // fine; the FILE was wrong, and the file is what a human and a cross-harness
122
- // lookup (catalog rule 5) actually read.
123
- const legacy = { models: [{ id: 'm', family: 'f', tier: 'mid-tier', effort: 'high', confirmedAt: '2026-06-01' }] }
124
-
125
- const upserted = bankEntry(legacy, { id: 'm', family: 'f', tier: 'deep-reasoner', effort: 'max' }, '2026-07-19')
126
- assert.equal(upserted.models[0].tier, undefined)
127
- assert.equal(upserted.models[0].effort, undefined)
128
- assert.deepEqual(upserted.models[0].tiers, { high: 'balanced-coder', max: 'deep-reasoner' }) // legacy point preserved
129
-
130
- const replaced = bankEntry({ models: [{ id: 'm', family: 'f', tier: 'frontier' }] }, { id: 'm', family: 'f', tiers: { high: 'balanced-coder' } }, '2026-07-19')
131
- assert.equal(replaced.models[0].tier, undefined) // never "frontier" beside a balanced-coder map
132
- assert.deepEqual(replaced.models[0].tiers, { high: 'balanced-coder' })
133
- })
134
-
135
- test('bank refuses an entry carrying no operating points', () => {
136
- // `tiers: {}` passes a truthiness guard but classifies nothing: tierAt returns
137
- // null at every effort and no later sync ever flags it. Check the POINTS.
138
- for (const bad of [{}, [], null]) {
139
- assert.throws(
140
- () => bankEntry({ models: [] }, { id: 'a', family: 'f', tiers: bad }, '2026-07-19'),
141
- /tiers|operating points/,
142
- `tiers: ${JSON.stringify(bad)} must be rejected`,
143
- )
144
- }
145
- assert.throws(() => bankEntry({ models: [] }, { id: 'a', family: 'f', tiers: 'frontier' }, '2026-07-19'), /tiers/)
146
- })
147
-
148
- test('sync does not emit an undefined tier for an entry with no classification at all', () => {
149
- const { migrated } = syncCatalog({ models: [{ id: 'nocap', family: 'f', lastSeenAt: '2026-07-18' }] }, [], '2026-07-19')
150
- assert.ok(!migrated.some((m) => m.includes('undefined')), `migration line leaked undefined: ${migrated}`)
151
- })
152
-
153
- test('bank rejects a point below high — cheaper means a cheaper model, not a throttled one', () => {
154
- for (const bad of ['low', 'medium', 'none']) {
155
- assert.throws(
156
- () => bankEntry({ models: [] }, { id: 'm', family: 'x', tiers: { [bad]: 'balanced-coder' } }, '2026-07-19'),
157
- /effort/,
158
- `effort "${bad}" must be rejected`,
159
- )
160
- }
161
- assert.throws(() => bankEntry({ models: [] }, { id: 'm', family: 'x', tiers: { high: 'huge' } }, '2026-07-19'), /tier/)
162
- assert.deepEqual(EFFORTS, ['high', 'xhigh', 'max'])
163
- assert.equal(DEFAULT_EFFORT, 'high')
164
- })
165
-
166
- test('operatingPoints lists every banked point, strongest tier first', () => {
167
- const catalog = {
168
- models: [
169
- { id: 'terra', family: 'openai', tiers: { high: 'balanced-coder', max: 'deep-reasoner' } },
170
- { id: 'sonnet', family: 'anthropic', tiers: { high: 'balanced-coder' } },
171
- ],
172
- }
173
- // slotEligible rides along on every point (wired to SLOT_ELIGIBLE_TIERS in 2026-07-20's
174
- // audit fix); both bands here are eligible, the scoped-coder case lives in the seed tests.
175
- assert.deepEqual(operatingPoints(catalog, 'terra'), [
176
- { effort: 'max', tier: 'deep-reasoner', slotEligible: true },
177
- { effort: 'high', tier: 'balanced-coder', slotEligible: true },
178
- ])
179
- assert.deepEqual(operatingPoints(catalog, 'sonnet'), [{ effort: 'high', tier: 'balanced-coder', slotEligible: true }])
180
- assert.deepEqual(operatingPoints(catalog, 'unknown'), [])
181
- assert.deepEqual(operatingPoints(null, 'terra'), []) // absent catalog is normal
182
- })
183
-
184
- test('tierAt answers what a model reaches at a given effort, null where nothing is banked', () => {
185
- const catalog = { models: [{ id: 'terra', family: 'openai', tiers: { high: 'balanced-coder', max: 'deep-reasoner' } }] }
186
- assert.equal(tierAt(catalog, 'terra', 'max'), 'deep-reasoner')
187
- assert.equal(tierAt(catalog, 'terra', 'high'), 'balanced-coder')
188
- assert.equal(tierAt(catalog, 'terra', 'xhigh'), null) // not measured -> not claimed
189
- assert.equal(tierAt(catalog, 'nope', 'high'), null)
190
- })
191
-
192
- test('bank accepts deep-reasoner (the band below the current top end)', () => {
193
- const c = bankEntry({ models: [] }, { id: 'gpt-5.5', family: 'openai', tier: 'deep-reasoner' }, '2026-07-13')
194
- assert.deepEqual(c.models[0].tiers, { high: 'deep-reasoner' })
195
- })
196
-
197
- // ---- tier renames (2026-07-19): prior-frontier -> deep-reasoner, mid-tier -> balanced-coder ----
198
-
199
- test('bank accepts either legacy tier name and stores the current one', () => {
200
- const a = bankEntry({ models: [] }, { id: 'opus', family: 'anthropic', tier: 'prior-frontier' }, '2026-07-19')
201
- assert.deepEqual(a.models[0].tiers, { high: 'deep-reasoner' })
202
- const b = bankEntry({ models: [] }, { id: 'sonnet', family: 'anthropic', tier: 'mid-tier', effort: 'max' }, '2026-07-19')
203
- assert.deepEqual(b.models[0].tiers, { max: 'balanced-coder' })
204
- assert.deepEqual(toTiers({ tiers: { high: 'prior-frontier' } }), { high: 'deep-reasoner' }) // legacy names inside a map too
205
- assert.deepEqual(TIERS, ['frontier', 'deep-reasoner', 'balanced-coder', 'scoped-coder'])
206
- for (const t of TIERS) assert.equal(normalizeTier(t), t) // current names pass through
207
- })
208
-
209
- test('sync migrates legacy tier names AND the single-point shape, reporting both', () => {
210
- const { catalog, migrated } = syncCatalog(
211
- {
212
- models: [
213
- { id: 'opus', family: 'anthropic', tier: 'prior-frontier', lastSeenAt: '2026-07-18' },
214
- { id: 'terra', family: 'openai', tier: 'mid-tier', effort: 'max', lastSeenAt: '2026-07-18' },
215
- { id: 'fable', family: 'anthropic', tiers: { high: 'frontier' }, lastSeenAt: '2026-07-18' },
216
- ],
217
- },
218
- ['opus'],
219
- '2026-07-19',
220
- )
221
- // opus: renamed value + reshaped; terra: renamed value at its banked effort;
222
- // fable: already current -> absent from the report entirely.
223
- assert.deepEqual(migrated, [
224
- 'opus: {high:prior-frontier} -> {high:deep-reasoner}',
225
- 'terra: {max:mid-tier} -> {max:balanced-coder}',
226
- ])
227
- assert.deepEqual(catalog.models.find((m) => m.id === 'opus').tiers, { high: 'deep-reasoner' })
228
- assert.deepEqual(catalog.models.find((m) => m.id === 'terra').tiers, { max: 'balanced-coder' })
229
- assert.deepEqual(catalog.models.find((m) => m.id === 'fable').tiers, { high: 'frontier' })
230
- for (const m of catalog.models) {
231
- assert.equal(m.tier, undefined) // the legacy fields are gone, not merely shadowed
232
- assert.equal(m.effort, undefined)
233
- }
234
- })
235
-
236
- test('sync reshapes a current-name single-point entry without claiming a rename', () => {
237
- const { migrated } = syncCatalog(
238
- { models: [{ id: 'sonnet', family: 'anthropic', tier: 'balanced-coder', lastSeenAt: '2026-07-18' }] },
239
- [],
240
- '2026-07-19',
241
- )
242
- assert.deepEqual(migrated, ['sonnet: tier/effort -> tiers map {high:balanced-coder}'])
243
- })
244
-
245
- test('sync migrates a legacy entry even when it never saw it', () => {
246
- const { migrated } = syncCatalog(
247
- { models: [{ id: 'unseen', family: 'x', tier: 'prior-frontier', lastSeenAt: '2026-07-01' }] },
248
- [], // migration is not gated on discovery — it is a rename, not an observation
249
- '2026-07-19',
250
- )
251
- assert.deepEqual(migrated, ['unseen: {high:prior-frontier} -> {high:deep-reasoner}'])
252
- })
253
-
254
- // ---- CLI round-trip ----
255
-
256
- function run(args, input) {
257
- return spawnSync('node', [SCRIPT, ...args], { encoding: 'utf8', input })
258
- }
259
-
260
- test('CLI: read/sync exit 3 on an absent catalog (absent is normal)', () => {
261
- const file = tmpFile()
262
- assert.equal(run(['read', '--file', file]).status, 3)
263
- assert.equal(run(['sync', '--file', file, '--seen', 'a']).status, 3)
264
- })
265
-
266
- // @covers package:harness-runtime
267
- // @level integration
268
- test('CLI: bank creates the file; sync bumps + prunes and reports both', () => {
269
- const file = tmpFile()
270
- assert.equal(run(['bank', JSON.stringify({ id: 'a', family: 'x', tier: 'frontier' }), '--file', file, '--today', '2026-01-01']).status, 0)
271
- assert.equal(run(['bank', JSON.stringify({ id: 'b', family: 'y', tier: 'balanced-coder' }), '--file', file, '--today', '2026-07-01']).status, 0)
272
-
273
- const sync = run(['sync', '--file', file, '--seen', 'b', '--today', '2026-07-13'])
274
- assert.equal(sync.status, 0)
275
- assert.deepEqual(JSON.parse(sync.stdout), { bumped: ['b'], pruned: ['a'], migrated: [] }) // a: unseen since Jan — pruned
276
-
277
- const kept = readCatalog(file)
278
- assert.deepEqual(kept.models.map((m) => m.id), ['b'])
279
- assert.equal(kept.models[0].lastSeenAt, '2026-07-13')
280
- })
281
-
282
- test('CLI: list aliases read, help exits 0', () => {
283
- const file = tmpFile()
284
- assert.equal(run(['bank', JSON.stringify({ id: 'a', family: 'x', tier: 'frontier' }), '--file', file, '--today', '2026-07-13']).status, 0)
285
- const list = run(['list', '--file', file])
286
- assert.equal(list.status, 0)
287
- assert.equal(JSON.parse(list.stdout).models[0].id, 'a')
288
- assert.equal(run(['help']).status, 0)
289
- assert.equal(run(['bogus']).status, 2)
290
- })
291
-
292
- test('CLI: bank via --stdin round-trips through read', () => {
293
- const file = tmpFile()
294
- assert.equal(run(['bank', '--stdin', '--file', file, '--today', '2026-07-13'], JSON.stringify({ id: 's', family: 'x', tier: 'frontier' })).status, 0)
295
- const read = run(['read', '--file', file])
296
- assert.equal(read.status, 0)
297
- assert.equal(JSON.parse(read.stdout).models[0].id, 's')
298
- })
299
-
300
- // ---- per-harness catalogs (one file per harness, never shared) ----
301
-
302
- // @covers package:harness-runtime
303
- // @level unit
304
- test('pathForHarness maps the known harnesses and returns null for unknown ones', () => {
305
- assert.equal(pathForHarness('claude-code', { HOME: '/h' }), '/h/.claude/model-catalog.json')
306
- assert.equal(pathForHarness('codex', { HOME: '/h' }), '/h/.codex/model-catalog.json')
307
- assert.equal(pathForHarness('pi', { HOME: '/h' }), '/h/.pi/agent/model-catalog.json')
308
- assert.equal(pathForHarness('bogus', { HOME: '/h' }), null)
309
- })
310
-
311
- test('CLI: bank --harness stamps the file; a write from another harness is refused', () => {
312
- const file = tmpFile()
313
- assert.equal(run(['bank', JSON.stringify({ id: 'a', family: 'x', tier: 'frontier' }), '--file', file, '--harness', 'codex', '--today', '2026-07-13']).status, 0)
314
- assert.equal(readCatalog(file).harness, 'codex')
315
-
316
- // same file, different harness → refused (exit 2), catalog untouched
317
- assert.equal(run(['sync', '--file', file, '--harness', 'pi', '--seen', 'a', '--today', '2026-07-13']).status, 2)
318
- assert.equal(run(['bank', JSON.stringify({ id: 'b', family: 'y', tier: 'balanced-coder' }), '--file', file, '--harness', 'pi', '--today', '2026-07-13']).status, 2)
319
- assert.deepEqual(readCatalog(file).models.map((m) => m.id), ['a'])
320
-
321
- // matching harness → fine
322
- assert.equal(run(['sync', '--file', file, '--harness', 'codex', '--seen', 'a', '--today', '2026-07-13']).status, 0)
323
- })
324
-
325
- test('CLI: bank --harness stamps a legacy (pre-field) file at its next confirmed write', () => {
326
- const file = tmpFile()
327
- writeCatalog(file, { models: [entry('a', '2026-07-01')] }) // no harness stamp
328
- assert.equal(run(['bank', JSON.stringify({ id: 'b', family: 'y', tier: 'balanced-coder' }), '--file', file, '--harness', 'pi', '--today', '2026-07-13']).status, 0)
329
- assert.equal(readCatalog(file).harness, 'pi')
330
- })
331
-
332
- test('CLI: --harness with no location mapping errors, path --harness resolves', () => {
333
- assert.equal(run(['read', '--harness', 'bogus']).status, 2)
334
- const p = run(['path', '--harness', 'pi'])
335
- assert.equal(p.status, 0)
336
- assert.ok(p.stdout.trim().endsWith(path.join('.pi', 'agent', 'model-catalog.json')))
337
- })
338
-
339
- test('writeCatalog is atomic-ish: file parses after write, tmp file removed', () => {
340
- const file = tmpFile()
341
- writeCatalog(file, { models: [] })
342
- assert.deepEqual(JSON.parse(fs.readFileSync(file, 'utf8')), { models: [] })
343
- assert.ok(!fs.existsSync(`${file}.tmp`))
344
- })
345
-
346
- test('scoped-coder is a bankable classification but never a slot candidate', () => {
347
- // The band exists so a model that keeps turning up in discovery, but sits below the
348
- // implementer floor, can be answered ONCE instead of being tagged
349
- // "unconfirmed — please classify" every run. It must bank like any other tier...
350
- const c = bankEntry({ models: [] }, { id: 'gemini-3.5-flash', family: 'google', tier: 'scoped-coder' }, '2026-07-20')
351
- assert.deepEqual(c.models[0].tiers, { high: 'scoped-coder' })
352
-
353
- // ...and must never reach a slot.
354
- assert.ok(TIERS.includes('scoped-coder'), 'bankable')
355
- assert.ok(!SLOT_ELIGIBLE_TIERS.includes('scoped-coder'), 'not proposable for implementer/reviewer')
356
- assert.deepEqual(SLOT_ELIGIBLE_TIERS, ['frontier', 'deep-reasoner', 'balanced-coder'])
357
- })
358
-
359
- test('scoped-coder sorts below every slot-eligible band', () => {
360
- // operatingPoints sorts by TIERS.indexOf, so a mis-ordered enum would float a
361
- // scoped-coder above a balanced-coder in the proposal block and invite a bad pick.
362
- assert.equal(TIERS.indexOf('scoped-coder'), TIERS.length - 1)
363
- for (const t of SLOT_ELIGIBLE_TIERS) {
364
- assert.ok(TIERS.indexOf(t) < TIERS.indexOf('scoped-coder'), `${t} must outrank scoped-coder`)
365
- }
366
- })
@@ -1,280 +0,0 @@
1
- import { test } from 'node:test'
2
- import assert from 'node:assert/strict'
3
- import { execFileSync, spawnSync } from 'node:child_process'
4
- import { chmodSync, existsSync, mkdirSync, mkdtempSync, readdirSync, utimesSync, writeFileSync } from 'node:fs'
5
- import { tmpdir } from 'node:os'
6
- import { dirname, join } from 'node:path'
7
- import { fileURLToPath } from 'node:url'
8
-
9
- import { EVIDENCE_MAX_BYTES, planSweep } from './cleanup-runs.mjs'
10
-
11
- const script = join(dirname(fileURLToPath(import.meta.url)), 'cleanup-runs.mjs')
12
- const DAY_MS = 24 * 60 * 60 * 1000
13
-
14
- const git = (dir, ...args) => execFileSync('git', ['-C', dir, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] })
15
-
16
- function write(path, content = 'x') {
17
- mkdirSync(dirname(path), { recursive: true })
18
- writeFileSync(path, content)
19
- }
20
-
21
- // A clone of the remote at `dir`, with ignored installs, caches, and a test log.
22
- function clone(remote, dir) {
23
- git(dirname(dir), 'clone', '--quiet', remote, dir)
24
- git(dir, 'config', 'user.email', 't@example.com')
25
- git(dir, 'config', 'user.name', 'T')
26
- write(join(dir, 'node_modules', 'pkg', 'package.json'), '{}')
27
- write(join(dir, '.cache', 'sdk', 'system.img'), 'big')
28
- write(join(dir, 'test-results', 'journey.log'), 'emulator boot failed')
29
- write(join(dir, 'test-results', 'avd', 'Charge.avd', 'userdata.img'), 'big')
30
- return dir
31
- }
32
-
33
- function fixture() {
34
- const base = mkdtempSync(join(tmpdir(), 'cleanup-runs-'))
35
- const remote = join(base, 'remote.git')
36
- const seed = join(base, 'seed')
37
- execFileSync('git', ['init', '--quiet', '--bare', remote])
38
- execFileSync('git', ['init', '--quiet', seed])
39
- git(seed, 'config', 'user.email', 't@example.com')
40
- git(seed, 'config', 'user.name', 'T')
41
- write(join(seed, '.gitignore'), 'node_modules/\n.cache/\ntest-results/\n')
42
- write(join(seed, 'app.js'), 'v1')
43
- write(join(seed, 'docs', 'README.md'), '# tracked doc')
44
- git(seed, 'add', '.')
45
- git(seed, 'commit', '--quiet', '-m', 'seed')
46
- git(seed, 'push', '--quiet', remote, 'HEAD:main')
47
- git(remote, 'symbolic-ref', 'HEAD', 'refs/heads/main')
48
- const repo = join(base, 'repo')
49
- mkdirSync(repo)
50
- return { base, remote, repo, rush: join(repo, '.resolve-issues', 'rush') }
51
- }
52
-
53
- function makeRun(rush, name, idleDays) {
54
- const dir = join(rush, name)
55
- write(join(dir, 'task-plan.json'), '{}')
56
- const when = new Date(Date.now() - idleDays * DAY_MS)
57
- utimesSync(join(dir, 'task-plan.json'), when, when)
58
- return dir
59
- }
60
-
61
- const cli = (...args) => spawnSync(process.execPath, [script, ...args], { encoding: 'utf8' })
62
- const has = (dir, rel) => existsSync(join(dir, rel))
63
-
64
- test('an idle run keeps records and logs, and loses downloads, caches, and clean source', () => {
65
- const { remote, repo, rush } = fixture()
66
- const run = makeRun(rush, 'old', 10)
67
- write(join(run, 'exploration', 'findings.json'), '{}')
68
- write(join(run, 'reviews', '1131-r1', 'review.md'), 'ok')
69
- write(join(run, 'reviews', '1131-r1', 'sandbox', 'sdk', 'system.img'), 'big')
70
- write(join(run, 'reviews', '1131-r1', 'sandbox', 'worker.log'), 'review log')
71
- write(join(run, 'diagnoses', '1136', 'deep', 'nested', 'adb.txt'), 'adb not ready')
72
- write(join(run, '.tmp', 'resume-result.json'), '{}')
73
- write(join(run, 'jdk17.tar.gz'), 'big')
74
- mkdirSync(join(run, 'units'), { recursive: true })
75
- const ws = clone(remote, join(run, 'units', '1132'))
76
-
77
- const result = cli('--repo', repo, '--apply')
78
- assert.equal(result.status, 0, result.stderr)
79
-
80
- for (const kept of ['task-plan.json', 'exploration/findings.json', 'reviews/1131-r1/review.md', 'reviews/1131-r1/sandbox/worker.log', 'diagnoses/1136/deep/nested/adb.txt', '.tmp/resume-result.json']) {
81
- assert.ok(has(run, kept), `${kept} should be kept`)
82
- }
83
- for (const gone of ['reviews/1131-r1/sandbox/sdk', 'jdk17.tar.gz']) assert.ok(!has(run, gone), `${gone} should be removed`)
84
-
85
- // The clean clone keeps only its logs: no source, .git, installs, caches, or emulator disks.
86
- assert.ok(has(ws, 'test-results/journey.log'))
87
- for (const gone of ['app.js', 'docs', '.git', '.gitignore', 'node_modules', '.cache', 'test-results/avd']) assert.ok(!has(ws, gone), `clone ${gone} should be removed`)
88
- assert.deepEqual(readdirSync(ws), ['test-results'])
89
- assert.match(result.stdout, /sweep\s+units\/1132 \(clean and pushed; keeps 1 log file\(s\)/)
90
- assert.match(result.stdout.trim().split('\n').at(-1), /^cleanup-runs: 1 idle run\(s\), 0 active; .*1 clean clone\(s\) emptied.*log file\(s\) kept; freed/)
91
- })
92
-
93
- test('a clone with unpushed commits, edits, or a stash keeps its work and logs, and loses ignored downloads', () => {
94
- const { remote, repo, rush } = fixture()
95
- const run = makeRun(rush, 'old', 10)
96
- mkdirSync(join(run, 'units'), { recursive: true })
97
-
98
- const committed = clone(remote, join(run, 'units', 'committed'))
99
- write(join(committed, 'app.js'), 'v2')
100
- git(committed, 'commit', '--quiet', '-am', 'local only')
101
-
102
- const edited = clone(remote, join(run, 'units', 'edited'))
103
- write(join(edited, 'new.js'), 'untracked work')
104
-
105
- const stashed = clone(remote, join(run, 'units', 'stashed'))
106
- write(join(stashed, 'app.js'), 'stashed work')
107
- git(stashed, 'stash', '--quiet')
108
-
109
- const result = cli('--repo', repo, '--apply')
110
- assert.equal(result.status, 0, result.stderr)
111
-
112
- assert.equal(git(committed, 'rev-list', '--count', 'HEAD', '--not', '--remotes').trim(), '1')
113
- assert.ok(has(edited, 'new.js'))
114
- assert.equal(git(stashed, 'stash', 'list').split('\n').filter(Boolean).length, 1)
115
- for (const dir of [committed, edited, stashed]) {
116
- assert.ok(has(dir, 'app.js') && has(dir, 'docs/README.md'), `${dir} keeps tracked files`)
117
- assert.ok(has(dir, 'test-results/journey.log'), `${dir} keeps its log`)
118
- for (const gone of ['node_modules', '.cache', 'test-results/avd']) assert.ok(!has(dir, gone), `${dir} ${gone} should be stripped`)
119
- }
120
- assert.match(result.stdout, /strip\s+units\/committed \(keeps 1 unpushed commit\(s\) and 1 log file\(s\)/)
121
- })
122
-
123
- test('past the log window only records remain; clones with work still keep their work', () => {
124
- const { remote, repo, rush } = fixture()
125
- const run = makeRun(rush, 'ancient', 40)
126
- write(join(run, 'reviews', '1131-r1.json'), '{}')
127
- write(join(run, 'reviews', '1131-r1', 'sandbox', 'worker.log'), 'old log')
128
- mkdirSync(join(run, 'units'), { recursive: true })
129
- const clean = clone(remote, join(run, 'units', 'clean'))
130
- const busy = clone(remote, join(run, 'units', 'busy'))
131
- write(join(busy, 'app.js'), 'v2')
132
- git(busy, 'commit', '--quiet', '-am', 'local only')
133
-
134
- const result = cli('--repo', repo, '--log-days', '30', '--apply')
135
- assert.equal(result.status, 0, result.stderr)
136
- assert.match(result.stdout, /records only, past 30 days/)
137
- assert.ok(has(run, 'reviews/1131-r1.json'))
138
- assert.ok(!has(run, 'reviews/1131-r1'), 'the old log and its folder go')
139
- assert.ok(!existsSync(clean), 'a clean clone with no logs left is deleted')
140
- assert.ok(has(busy, 'app.js'))
141
- assert.ok(!has(busy, 'test-results'), 'its ignored log goes too')
142
- })
143
-
144
- test('text inside a tool cache is not evidence, and an oversized log is dropped', () => {
145
- const { repo, rush } = fixture()
146
- const run = makeRun(rush, 'old', 10)
147
- write(join(run, 'home', '.npm', '_cacache', 'index.json'), '{}')
148
- write(join(run, 'xdg-cache', 'pnpm', 'metadata.jsonl'), '{}')
149
- write(join(run, 'builds', 'Pixel.avd', 'config.txt'), 'x')
150
- write(join(run, 'logs', 'huge.log'), Buffer.alloc(EVIDENCE_MAX_BYTES + 1))
151
- write(join(run, 'logs', 'small.log'), 'ok')
152
-
153
- assert.equal(cli('--repo', repo, '--apply').status, 0)
154
- for (const gone of ['home', 'xdg-cache', 'builds', 'logs/huge.log']) assert.ok(!has(run, gone), `${gone} should be removed`)
155
- assert.ok(has(run, 'logs/small.log'))
156
- })
157
-
158
- test('a clone hidden inside a dot directory is still protected', () => {
159
- const { remote, repo, rush } = fixture()
160
- const run = makeRun(rush, 'old', 10)
161
- mkdirSync(join(run, '.tmp'), { recursive: true })
162
- const hidden = clone(remote, join(run, '.tmp', 'wip'))
163
- write(join(hidden, 'app.js'), 'v2')
164
- git(hidden, 'commit', '--quiet', '-am', 'local only')
165
-
166
- assert.equal(cli('--repo', repo, '--apply').status, 0)
167
- assert.ok(has(hidden, 'app.js'))
168
- assert.ok(!has(hidden, 'node_modules'))
169
- })
170
-
171
- test('a run used recently is left alone, and a dry run removes nothing', () => {
172
- const { remote, repo, rush } = fixture()
173
- const active = makeRun(rush, 'active', 1)
174
- write(join(active, 'units', 'scratch', 'big.img'), 'big')
175
- const idle = makeRun(rush, 'idle', 10)
176
- mkdirSync(join(idle, 'units'), { recursive: true })
177
- clone(remote, join(idle, 'units', 'a'))
178
-
179
- const dry = cli('--repo', repo)
180
- assert.equal(dry.status, 0, dry.stderr)
181
- assert.ok(has(idle, 'units/a/node_modules'))
182
- assert.match(dry.stdout.trim().split('\n').at(-1), /dry run, nothing removed.*Rerun with --apply/)
183
-
184
- assert.equal(cli('--repo', repo, '--older-than-days', '3', '--apply').status, 0)
185
- assert.ok(has(active, 'units/scratch/big.img'))
186
- assert.ok(!has(idle, 'units/a/node_modules'))
187
- })
188
-
189
- test('runs are staged by task-plan.json age', () => {
190
- const { repo, rush } = fixture()
191
- makeRun(rush, 'a-two', 2)
192
- makeRun(rush, 'b-ten', 10)
193
- makeRun(rush, 'c-forty', 40)
194
- const stages = planSweep({ repo, olderThanDays: 3, logDays: 30 }).runs.map((r) => [r.run, r.stage])
195
- assert.deepEqual(stages, [['a-two', 'active'], ['b-ten', 'keep-logs'], ['c-forty', 'records-only']])
196
- assert.deepEqual(planSweep({ repo: join(repo, 'missing') }).runs, [])
197
- })
198
-
199
- test('--merged empties a merged clean workspace to its logs, even with commits that look unpushed', () => {
200
- const { remote, rush } = fixture()
201
- const run = makeRun(rush, 'now', 0)
202
- mkdirSync(join(run, 'units'), { recursive: true })
203
- const ws = clone(remote, join(run, 'units', 'merged'))
204
- write(join(ws, 'app.js'), 'v2')
205
- git(ws, 'commit', '--quiet', '-am', 'squash-merged upstream')
206
-
207
- const result = cli('--merged', ws, '--apply')
208
- assert.equal(result.status, 0, result.stderr)
209
- assert.deepEqual(readdirSync(ws), ['test-results'])
210
- assert.ok(has(ws, 'test-results/journey.log'))
211
- })
212
-
213
- test('--merged keeps uncommitted edits and refuses paths outside .resolve-issues', () => {
214
- const { base, remote, rush } = fixture()
215
- const run = makeRun(rush, 'now', 0)
216
- mkdirSync(join(run, 'units'), { recursive: true })
217
- const ws = clone(remote, join(run, 'units', 'dirty'))
218
- write(join(ws, 'app.js'), 'uncommitted')
219
-
220
- assert.equal(cli('--merged', ws, '--apply').status, 0)
221
- assert.ok(has(ws, 'app.js') && has(ws, 'test-results/journey.log'))
222
- assert.ok(!has(ws, 'node_modules'))
223
-
224
- const outside = clone(remote, join(base, 'elsewhere'))
225
- const refused = cli('--merged', outside, '--apply')
226
- assert.equal(refused.status, 2)
227
- assert.match(refused.stderr, /only removes workspaces under a \.resolve-issues directory/)
228
- assert.ok(existsSync(outside))
229
- })
230
-
231
- test('a bad command exits 2 with usage', () => {
232
- for (const args of [['--older-than-days', 'soon'], ['--bogus'], ['--repo'], ['--log-days', '-1']]) {
233
- const result = cli(...args)
234
- assert.equal(result.status, 2)
235
- assert.match(result.stderr, /^usage: cleanup-runs\.mjs/)
236
- }
237
- const inverted = cli('--older-than-days', '10', '--log-days', '5')
238
- assert.equal(inverted.status, 2)
239
- assert.match(inverted.stderr, /--log-days must be at least --older-than-days/)
240
- })
241
-
242
- test('in a non-Git copy of a repository, only logs and text under evidence folders are kept', () => {
243
- const { repo, rush } = fixture()
244
- const run = makeRun(rush, 'old', 10)
245
- const copy = join(run, 'units', '1132', 'test-results', 'packet', 'build-x', 'workspace')
246
- write(join(copy, 'pnpm-lock.yaml'), 'lock')
247
- write(join(copy, 'package.json'), '{}')
248
- write(join(copy, 'docs', 'design.md'), '# copied source doc')
249
- write(join(copy, 'build.log'), 'gradle failed')
250
- write(join(copy, 'test-results', 'summary.json'), '{}')
251
- write(join(run, 'units', '1132', 'test-results', 'packet', 'gradle', 'caches', 'meta.json'), '{}')
252
- write(join(run, 'units', '1132', 'test-results', 'packet', 'result.json'), '{}')
253
-
254
- assert.equal(cli('--repo', repo, '--apply').status, 0)
255
- for (const kept of ['build.log', 'test-results/summary.json']) assert.ok(has(copy, kept), `${kept} should be kept`)
256
- for (const gone of ['package.json', 'pnpm-lock.yaml', 'docs']) assert.ok(!has(copy, gone), `${gone} should be removed`)
257
- assert.ok(!has(run, 'units/1132/test-results/packet/gradle'))
258
- assert.ok(has(run, 'units/1132/test-results/packet/result.json'))
259
- })
260
-
261
- test('read-only package stores are removed, not left to abort the run', () => {
262
- const { repo, rush } = fixture()
263
- const run = makeRun(rush, 'old', 10)
264
- const store = join(run, 'artifacts', 'pnpm-v11', 'v11')
265
- write(join(store, 'files', '61', 'abc'), 'blob')
266
- write(join(store, 'index.db'), 'db')
267
- for (const p of [join(store, 'files', '61', 'abc'), join(store, 'index.db')]) chmodSync(p, 0o444)
268
- write(join(run, 'artifacts', 'pnpm-tuple', 'store', 'blob'), 'x')
269
- write(join(run, 'artifacts', 'pnpm-tuple', 'install.log'), 'kept log')
270
- for (const d of [join(store, 'files', '61'), join(store, 'files'), store, join(run, 'artifacts', 'pnpm-tuple', 'store'), join(run, 'artifacts', 'pnpm-tuple')]) chmodSync(d, 0o555)
271
- write(join(run, 'zz', 'worker.log'), 'kept')
272
-
273
- const result = cli('--repo', repo, '--apply')
274
- assert.equal(result.status, 0, result.stderr)
275
- assert.ok(!has(run, 'artifacts/pnpm-v11'), 'the read-only store is gone')
276
- assert.ok(!has(run, 'artifacts/pnpm-tuple/store'), 'a store inside a read-only folder is gone')
277
- assert.ok(has(run, 'artifacts/pnpm-tuple/install.log'), 'its sibling log stays')
278
- assert.ok(has(run, 'zz/worker.log'), 'later paths are still processed')
279
- assert.doesNotMatch(result.stdout, /could not be removed/)
280
- })