@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,637 +0,0 @@
1
- // Tests exercise pure decisions, the gh provider boundary and the real saved-data CLI.
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 { spawnSync } from 'node:child_process'
8
- import { fileURLToPath } from 'node:url'
9
-
10
- import { analyze, render, lane, DEFAULT_IGNORE, fetchRuns, fetchRun, profileRun, renderProfile } from './ci-run-audit-core.mjs'
11
-
12
- const T0 = Date.parse('2026-09-01T00:00:00Z')
13
- const at = (m) => new Date(T0 + m * 60000).toISOString()
14
-
15
- // A job that starts at `start` minutes and runs `mins`, with optional setup minutes.
16
- function job(name, conclusion, start, mins, setup = 0) {
17
- const steps = []
18
- if (setup) steps.push({ name: 'Set up job', conclusion: 'success', started: at(start), completed: at(start + setup) })
19
- steps.push({ name: `Run ${name}`, conclusion, started: at(start + setup), completed: at(start + mins) })
20
- return { name, conclusion, created: at(start), started: at(start), completed: at(start + mins), steps }
21
- }
22
- let id = 0
23
- const runOf = (o) => ({ id: ++id, workflow: 'CI', event: 'pull_request', branch: 'feat/x', defaultBranch: 'main', created: at(o.start ?? 0), ...o })
24
-
25
- test('lane: queue branches, PRs, default-branch pushes, and everything else', () => {
26
- assert.equal(lane({ branch: 'mergify/merge-queue/abc', event: 'pull_request' }), 'queue')
27
- assert.equal(lane({ branch: 'gh-readonly-queue/main/pr-1', event: 'merge_group' }), 'queue')
28
- assert.equal(lane({ branch: 'feat/x', event: 'pull_request' }), 'pr')
29
- assert.equal(lane({ branch: 'trunk', defaultBranch: 'trunk', event: 'push' }), 'main')
30
- assert.equal(lane({ branch: 'main', event: 'schedule' }), 'schedule')
31
- })
32
-
33
- test('re-proved: a job passing again on the same tree counts, by lane pair; a new tree does not', () => {
34
- const a = analyze([
35
- runOf({ sha: 's1', tree: 't1', start: 0, jobs: [job('test', 'success', 0, 4)] }),
36
- runOf({ sha: 'q1', tree: 't1', branch: 'mergify/merge-queue/1', start: 10, jobs: [job('test', 'success', 10, 4)] }),
37
- runOf({ sha: 'm1', tree: 't1', event: 'push', branch: 'main', start: 20, jobs: [job('test', 'success', 20, 4)] }),
38
- runOf({ sha: 's2', tree: 't2', start: 30, jobs: [job('test', 'success', 30, 4)] }),
39
- ])
40
- assert.equal(a.reproved.passMin, 16)
41
- assert.equal(a.reproved.reprovedMin, 8)
42
- assert.deepEqual(a.reproved.byLanePair, { 'pr→queue': 4, 'pr→main': 4 })
43
- assert.equal(a.reproved.byJob.test, 8)
44
- })
45
-
46
- test('re-proved: scheduled repeats are deliberate, reported apart and not counted as waste', () => {
47
- const a = analyze([
48
- runOf({ sha: 'm1', tree: 't1', event: 'schedule', branch: 'main', start: 0, jobs: [job('nightly', 'success', 0, 10)] }),
49
- runOf({ sha: 'm1', tree: 't1', event: 'schedule', branch: 'main', start: 1440, jobs: [job('nightly', 'success', 1440, 10)] }),
50
- ])
51
- assert.equal(a.reproved.reprovedMin, 0)
52
- assert.equal(a.reproved.scheduledRepeatMin, 10)
53
- })
54
-
55
- test('re-proved: a failed first attempt does not make the later pass a repeat', () => {
56
- const a = analyze([
57
- runOf({ sha: 's1', tree: 't1', start: 0, jobs: [job('test', 'failure', 0, 4)] }),
58
- runOf({ sha: 's1', tree: 't1', start: 10, jobs: [job('test', 'success', 10, 4)] }),
59
- ])
60
- assert.equal(a.reproved.reprovedMin, 0)
61
- })
62
-
63
- test('yield: sole catches, shared failures, and fail-then-pass flakes are told apart', () => {
64
- const a = analyze([
65
- // t1: unit alone fails -> a sole catch for unit
66
- runOf({ sha: 's1', tree: 't1', jobs: [job('unit', 'failure', 0, 2), job('lint', 'success', 0, 1)] }),
67
- // t2: unit and lint both fail -> shared, no sole catch
68
- runOf({ sha: 's2', tree: 't2', jobs: [job('unit', 'failure', 0, 2), job('lint', 'failure', 0, 1)] }),
69
- // t3: lint fails then passes on the same tree -> flaky, not a real failure
70
- runOf({ sha: 's3', tree: 't3', jobs: [job('unit', 'success', 0, 2), job('lint', 'failure', 0, 1)] }),
71
- runOf({ sha: 's3', tree: 't3', start: 5, jobs: [job('lint', 'success', 5, 1)] }),
72
- ], { minTrees: 3 })
73
- const row = (n) => a.yield.find((e) => e.job === n)
74
- assert.deepEqual([row('unit').trees, row('unit').failed, row('unit').sole, row('unit').flaky], [3, 2, 1, 0])
75
- assert.deepEqual([row('lint').trees, row('lint').failed, row('lint').sole, row('lint').flaky], [3, 1, 0, 1])
76
- assert.deepEqual(a.noSoleCatch.map((e) => e.job), ['lint'])
77
- })
78
-
79
- test('yield: umbrella jobs are ignored, so they never make a real failure look shared', () => {
80
- const a = analyze([
81
- runOf({ sha: 's1', tree: 't1', jobs: [job('unit', 'failure', 0, 2), job('all-gates', 'failure', 2, 0.1)] }),
82
- ], { minTrees: 1 })
83
- assert.equal(a.yield.find((e) => e.job === 'unit').sole, 1)
84
- assert.equal(a.yield.find((e) => e.job === 'all-gates'), undefined)
85
- })
86
-
87
- test('DEFAULT_IGNORE drops roll-ups but keeps real checks that happen to say "gate"', () => {
88
- for (const n of ['all-gates', 'CI', 'PR admission gate', 'Merge queue gate', 'Post-deploy validation verdict', 'CI timing report']) assert.ok(DEFAULT_IGNORE.test(n), n)
89
- for (const n of ['smoke-gate', 'route-docs-gate', 'PR Gate (install + builds + syntax)', 'Smoke Gate (golden paths)', 'typecheck']) assert.ok(!DEFAULT_IGNORE.test(n), n)
90
- })
91
-
92
- test('overlap: a job whose every failure (>=3) coincided with another job failing', () => {
93
- const runs = [1, 2, 3].map((i) => runOf({ sha: `s${i}`, tree: `t${i}`, jobs: [job('sweep', 'failure', 0, 5), job('smoke', 'failure', 0, 5)] }))
94
- runs.push(runOf({ sha: 's4', tree: 't4', jobs: [job('sweep', 'success', 0, 5), job('smoke', 'failure', 0, 5)] }))
95
- const a = analyze(runs)
96
- assert.deepEqual(a.overlap.find((o) => o.job === 'sweep'), { job: 'sweep', alwaysWith: 'smoke', failures: 3, otherFailures: 4 })
97
- assert.equal(a.overlap.find((o) => o.job === 'smoke'), undefined, 'smoke failed once without sweep')
98
- })
99
-
100
- test('setup share: flags jobs that are mostly setup, not ones that are mostly work', () => {
101
- const runs = [1, 2, 3].map((i) => runOf({ sha: `s${i}`, tree: `t${i}`, jobs: [job('ledger', 'success', 0, 1, 0.95), job('e2e', 'success', 0, 10, 2)] }))
102
- const a = analyze(runs)
103
- assert.deepEqual(a.setup.map((e) => e.job), ['ledger'])
104
- assert.ok(Math.abs(a.setup[0].setupShare - 0.95) < 1e-9)
105
- })
106
-
107
- test('history keeps same-named workflows and checks separate in yield and overlap', () => {
108
- const runs = [1, 2, 3].flatMap((i) => [
109
- runOf({ workflowId: 1, tree: `t${i}`, jobs: [job('unit', 'failure', 0, 2), job('lint', 'failure', 0, 1)] }),
110
- runOf({ workflowId: 2, tree: `t${i}`, jobs: [job('unit', 'success', 0, 2), job('lint', 'success', 0, 1)] }),
111
- ])
112
- const a = analyze(runs, { minTrees: 3 })
113
- assert.equal(a.yield.length, 4)
114
- const unit = a.yield.find((e) => e.job === 'CI [1] / unit')
115
- assert.deepEqual([unit.trees, unit.failed, unit.flaky], [3, 3, 0])
116
- assert.deepEqual(unit.coFail, { 'CI [1] / lint': 3 })
117
- assert.deepEqual(a.overlap.find((e) => e.job === unit.job), {
118
- job: unit.job, alwaysWith: 'CI [1] / lint', failures: 3, otherFailures: 3,
119
- })
120
- assert.equal(a.yield.find((e) => e.job === 'CI [2] / unit').failed, 0)
121
- assert.match(render(a), /CI \[1\] \/ unit/)
122
- })
123
-
124
- test('history keeps setup samples separate by workflow identity', () => {
125
- const runs = [1, 2, 3].flatMap((i) => [
126
- runOf({ workflowId: 1, tree: `t${i}`, jobs: [job('ledger', 'success', 0, 1, 0.95)] }),
127
- runOf({ workflowId: 2, tree: `t${i}`, jobs: [job('ledger', 'success', 0, 10, 2)] }),
128
- ])
129
- const a = analyze(runs)
130
- assert.equal(a.setup.length, 1)
131
- assert.equal(a.setup[0].job, 'CI [1] / ledger')
132
- assert.equal(a.setup[0].runs, 3)
133
- assert.ok(Math.abs(a.setup[0].setupShare - 0.95) < 1e-9)
134
- })
135
-
136
- test('history preserves failure overlap across distinct workflows on the same code states', () => {
137
- const runs = [1, 2, 3].flatMap((i) => [
138
- runOf({ workflow: 'Unit CI', workflowId: 1, tree: `t${i}`, jobs: [job('unit', 'failure', 0, 2)] }),
139
- runOf({ workflow: 'Integration CI', workflowId: 2, tree: `t${i}`, jobs: [job('integration', 'failure', 0, 2)] }),
140
- ])
141
- const a = analyze(runs, { minTrees: 3 })
142
- for (const e of a.yield) {
143
- assert.equal(e.failed, 3)
144
- assert.equal(e.sole, 0)
145
- assert.equal(e.flaky, 0)
146
- }
147
- assert.deepEqual(a.yield.find((e) => e.job === 'unit').coFail, { integration: 3 })
148
- assert.deepEqual(a.overlap.find((e) => e.job === 'unit'), {
149
- job: 'unit', alwaysWith: 'integration', failures: 3, otherFailures: 3,
150
- })
151
- })
152
-
153
- test('workflow-path fallback disambiguates repeat candidates and last finishers', () => {
154
- const a = analyze([
155
- runOf({ workflowPath: '.github/workflows/a.yml', sha: 'a', tree: 't', jobs: [job('unit', 'success', 0, 1)] }),
156
- runOf({ workflowPath: '.github/workflows/b.yml', sha: 'b', tree: 't', jobs: [job('unit', 'success', 0, 2)] }),
157
- runOf({ workflowPath: '.github/workflows/a.yml', sha: 'q', tree: 't', start: 10,
158
- branch: 'mergify/merge-queue/1', jobs: [job('unit', 'success', 10, 1)] }),
159
- ])
160
- assert.equal(a.yield.length, 2)
161
- assert.deepEqual(a.reproved.byJob, { 'CI [.github/workflows/a.yml] / unit': 1 })
162
- assert.deepEqual(a.criticalPath.lastJob, {
163
- 'CI [.github/workflows/a.yml] / unit': 1,
164
- 'CI [.github/workflows/b.yml] / unit': 1,
165
- })
166
- })
167
-
168
- test('history and profile both exclude unknown or inconsistent setup measurements', () => {
169
- const variants = [
170
- { name: 'missing end', steps: [{ name: 'Test', started: at(0), completed: null }] },
171
- { name: 'negative step', steps: [{ name: 'Test', started: at(2), completed: at(1) }] },
172
- { name: 'out of bounds', steps: [{ name: 'Test', started: at(9), completed: at(11) }] },
173
- { name: 'overlap', steps: [{ name: 'Install deps', started: at(0), completed: at(4) }, { name: 'Test', started: at(3), completed: at(5) }] },
174
- { name: 'no steps', steps: [] },
175
- { name: 'untimed job', completed: null, steps: [{ name: 'Test', started: at(0), completed: at(5) }] },
176
- ]
177
- const runs = [1, 2, 3].map((i) => runOf({ tree: `t${i}`, jobs: [
178
- ...variants.map((j) => ({ conclusion: 'success', created: at(0), started: at(0), completed: at(10), ...j })),
179
- job('measured', 'success', 0, 1, 0.95),
180
- ] }))
181
- const a = analyze(runs)
182
- assert.deepEqual(a.setup.map((e) => e.job), ['measured'])
183
- assert.equal(a.dataGaps.timings.filter((g) => g.reason.startsWith('setup/work')).length, 18)
184
- for (const j of profileRun(runs, runs[0].id).jobs.slice(0, variants.length)) {
185
- assert.equal(j.workS, null, j.name)
186
- assert.equal(j.setupS, null, j.name)
187
- }
188
- assert.match(render(a), /18 unavailable timing measurement/)
189
- })
190
-
191
- test('history scheduling keeps zero, excludes negative/missing/skipped intervals, and reports no samples as unknown', () => {
192
- const r = runOf({ jobs: [
193
- job('zero', 'success', 0, 1),
194
- { ...job('delay', 'success', 2, 1), created: at(0) },
195
- { ...job('negative', 'success', 0, 1), created: at(2) },
196
- { ...job('missing', 'success', 0, 1), created: null },
197
- { ...job('skipped', 'skipped', 99, 0), created: at(0) },
198
- ] })
199
- const a = analyze([r])
200
- assert.deepEqual(a.queue, { samples: 2, p50: 1, p90: 0 })
201
- assert.equal(a.dataGaps.timings.filter((g) => g.reason === 'created-to-start interval unavailable').length, 2)
202
- const unknown = analyze([runOf({ jobs: [r.jobs[2]] })])
203
- assert.deepEqual(unknown.queue, { samples: 0, p50: null, p90: null })
204
- assert.match(render(unknown), /0 measured job\(s\), p50 unknown, p90 unknown/)
205
- })
206
-
207
- test('history does not invent PR waits from missing SHAs or invalid creation intervals', () => {
208
- const a = analyze([
209
- runOf({ sha: null, start: 0, jobs: [job('test', 'success', 0, 1)] }),
210
- runOf({ sha: null, start: 10, jobs: [job('test', 'success', 10, 3)] }),
211
- runOf({ sha: 'invalid', start: 20, jobs: [job('test', 'success', 0, 1)] }),
212
- runOf({ sha: 'missing', created: null, jobs: [job('test', 'success', 0, 1)] }),
213
- ])
214
- assert.equal(a.criticalPath.prCommits, 2)
215
- assert.equal(a.criticalPath.p50, 2)
216
- assert.equal(a.dataGaps.timings.filter((g) => g.reason === 'PR creation-to-completion interval unavailable').length, 2)
217
- const unknown = analyze([])
218
- assert.deepEqual(unknown.criticalPath, { prCommits: 0, p50: null, p90: null, lastJob: {} })
219
- assert.match(render(unknown), /wait p50 \*\*unknown\*\*, p90 \*\*unknown\*\*/)
220
- })
221
-
222
- test('critical path: PR wait runs from the first run created to the last job done; queue runs excluded', () => {
223
- const a = analyze([
224
- runOf({ sha: 's1', tree: 't1', start: 0, jobs: [job('lint', 'success', 0, 1), job('unit', 'success', 0, 6)] }),
225
- runOf({ sha: 's1', tree: 't1', workflow: 'Other', start: 1, jobs: [job('smoke', 'success', 1, 3)] }),
226
- runOf({ sha: 'q1', tree: 't1', branch: 'mergify/merge-queue/1', start: 10, jobs: [job('unit', 'success', 10, 30)] }),
227
- ])
228
- assert.equal(a.criticalPath.prCommits, 1)
229
- assert.equal(a.criticalPath.p50, 6)
230
- assert.deepEqual(a.criticalPath.lastJob, { unit: 1 })
231
- })
232
-
233
- test('setup share: time between steps counts as overhead, not as work', () => {
234
- // A 1-minute job whose only work step lasts 3s; the rest is runner start and gaps.
235
- const j = (start) => ({ name: 'ledger', conclusion: 'success', created: at(start), started: at(start), completed: at(start + 1),
236
- steps: [{ name: 'Run node coverage-ledger.mjs validate', conclusion: 'success', started: at(start + 0.5), completed: at(start + 0.55) }] })
237
- const a = analyze([1, 2, 3].map((i) => runOf({ sha: `s${i}`, tree: `t${i}`, jobs: [j(0)] })))
238
- assert.equal(a.setup[0]?.job, 'ledger')
239
- assert.ok(a.setup[0].setupShare > 0.94)
240
- })
241
-
242
- test('critical path: a roll-up job finishing last is skipped for the last real job', () => {
243
- const a = analyze([
244
- runOf({ sha: 's1', tree: 't1', jobs: [job('unit', 'success', 0, 6), job('all-gates', 'success', 6, 0.2)] }),
245
- ])
246
- assert.deepEqual(a.criticalPath.lastJob, { unit: 1 })
247
- assert.equal(a.criticalPath.p50, 6)
248
- })
249
-
250
- test('stuck: a job past 6h is reported and kept out of the time figures', () => {
251
- const a = analyze([
252
- runOf({ sha: 's1', tree: 't1', jobs: [job('typecheck', 'cancelled', 0, 1440), job('unit', 'success', 0, 3)] }),
253
- ])
254
- assert.equal(a.stuck.length, 1)
255
- assert.equal(a.stuck[0].job, 'typecheck')
256
- assert.equal(a.criticalPath.p50, 3)
257
- })
258
-
259
- test('render: says the sample is too small instead of silently listing no zero-yield jobs', () => {
260
- const md = render(analyze([runOf({ sha: 's1', tree: 't1', jobs: [job('unit', 'success', 0, 3)] })]))
261
- assert.match(md, /too few to call any job zero-yield/)
262
- const big = render(analyze([runOf({ sha: 's1', tree: 't1', jobs: [job('unit', 'success', 0, 3)] })], { minTrees: 1 }))
263
- assert.doesNotMatch(big, /too few/)
264
- assert.match(big, /No sole catch.*`unit`/)
265
- })
266
-
267
- test('render: every section is present and the report calls itself report-only', () => {
268
- const md = render(analyze([runOf({ sha: 's1', tree: 't1', jobs: [job('unit', 'success', 0, 3)] })]), 'o/r')
269
- for (const h of ['# CI run audit — o/r', '## 1. Re-proved trees', '## 2. Catch yield', '## 3. Overlap', '## 4. Setup share', '## 5. Critical path', '## 6. Stuck jobs']) assert.match(md, new RegExp(h.replace(/[.()]/g, '\\$&')))
270
- assert.match(md, /Report-only/)
271
- })
272
-
273
- test('unknown trees never prove repeats, SHA fallbacks are separate from verified trees and missing SHAs do not collapse', () => {
274
- const runs = [
275
- runOf({ sha: 'same', tree: null, jobs: [job('unit', 'failure', 0, 2)] }),
276
- runOf({ sha: 'same', tree: null, start: 5, jobs: [job('unit', 'success', 5, 2)] }),
277
- runOf({ sha: 'different', tree: 'same', start: 10, jobs: [job('unit', 'failure', 10, 2)] }),
278
- runOf({ sha: null, tree: null, start: 15, jobs: [job('unit', 'failure', 15, 2)] }),
279
- runOf({ sha: null, tree: null, start: 20, jobs: [job('unit', 'success', 20, 2)] }),
280
- ]
281
- const a = analyze(runs)
282
- assert.equal(a.reproved.reprovedMin, 0)
283
- assert.deepEqual([a.yield[0].trees, a.yield[0].failed, a.yield[0].flaky], [4, 2, 1])
284
- assert.deepEqual(a.dataGaps.unknownTrees.map((r) => r.runId), [runs[0].id, runs[1].id, runs[3].id, runs[4].id])
285
- assert.match(render(a), /no verified tree; excluded from identical-tree repeat evidence/)
286
- })
287
-
288
- test('metadata repeat identity uses workflow ID instead of a nonunique display name', () => {
289
- const a = analyze([
290
- runOf({ workflowId: 1, sha: 'a', tree: 't', jobs: [job('unit', 'success', 0, 2)] }),
291
- runOf({ workflowId: 2, sha: 'b', tree: 't', start: 5, jobs: [job('unit', 'success', 5, 2)] }),
292
- runOf({ workflowId: 1, sha: 'c', tree: 't', start: 10, jobs: [job('unit', 'success', 10, 2)] }),
293
- ])
294
- assert.equal(a.reproved.reprovedMin, 2)
295
- })
296
-
297
- test('same PR head across different bases is a metadata candidate with an explicit tested-checkout gap', () => {
298
- const runs = [
299
- runOf({ id: 61, sha: 'same-head', tree: 'head-tree', baseSha: 'base-a', jobs: [job('unit', 'success', 0, 2)] }),
300
- runOf({ id: 62, sha: 'same-head', tree: 'head-tree', baseSha: 'base-b', start: 5, jobs: [job('unit', 'success', 5, 2)] }),
301
- ]
302
- const a = analyze(runs)
303
- assert.equal(a.reproved.reprovedMin, 2)
304
- assert.equal(a.reproved.identity, 'run-metadata-tree-candidates')
305
- assert.equal(a.reproved.testedCheckoutVerified, false)
306
- assert.deepEqual(a.dataGaps.testedCheckouts.map((r) => r.runId), [61, 62])
307
- assert.match(render(a), /not verified repeated tested checkouts/)
308
- assert.match(render(a), /PR synthetic merges and explicit checkout refs may differ/)
309
- assert.match(profileRun(runs, 61).dataGaps.testedCheckouts[0].reason, /not reusable gate proof/)
310
- })
311
-
312
- test('profile separates dispatch and queue intervals, includes overhead gaps, sorts steps and retains job identity', () => {
313
- const r = runOf({ id: 51, attempt: 2, url: 'https://github.com/o/r/actions/runs/51',
314
- workflowId: 8, workflowPath: '.github/workflows/ci.yml', conclusion: 'failure', sha: 'abc', tree: 't', jobs: [{
315
- id: 501, name: 'matrix (linux)', attempt: 2, url: 'https://github.com/o/r/actions/runs/51/job/501',
316
- runnerName: 'slot-3', runnerLabels: ['self-hosted', 'linux'], conclusion: 'failure',
317
- created: at(3), started: at(5), completed: at(15), steps: [
318
- { name: 'Set up job', started: at(5), completed: at(6), conclusion: 'success', number: 1 },
319
- { name: 'Unit tests', started: at(7), completed: at(11), conclusion: 'success', number: 2 },
320
- { name: 'Install dependencies', started: at(11), completed: at(13), conclusion: 'success', number: 3 },
321
- { name: 'Smoke', started: at(13), completed: at(14), conclusion: 'failure', number: 4 },
322
- { name: 'Publish', conclusion: 'skipped', number: 5 },
323
- ],
324
- }] })
325
- const p = profileRun([r], '51'), j = p.jobs[0]
326
- assert.deepEqual([j.dispatchDelayS, j.queueS, j.elapsedS, j.setupS, j.workS], [180, 120, 600, 300, 300])
327
- assert.deepEqual(j.steps.map((s) => [s.name, s.durationS]), [['Unit tests', 240], ['Install dependencies', 120], ['Set up job', 60], ['Smoke', 60], ['Publish', 0]])
328
- assert.deepEqual([p.runId, p.attempt, p.workflowId, j.id, j.attempt, j.conclusion, j.runUrl], [51, 2, 8, 501, 2, 'failure', r.url])
329
- assert.deepEqual([j.runnerName, j.runnerLabels], ['slot-3', ['self-hosted', 'linux']])
330
- assert.deepEqual(j.timingGaps, [])
331
- assert.match(renderProfile(p), /not proven runner queue time/)
332
- assert.match(renderProfile(p), /Heuristic: job elapsed/)
333
- })
334
-
335
- test('profile nulls unavailable timing instead of inventing zero work or runner attribution', () => {
336
- const r = runOf({ id: 52, sha: 's', tree: null, jobs: [
337
- { name: 'pending', conclusion: null, created: at(2), steps: [{ name: 'Tests', started: at(3) }] },
338
- { name: 'no steps', started: at(5), completed: at(6), steps: [] },
339
- { name: 'bad ordering', created: at(5), started: at(4), completed: at(3), steps: [{ name: 'Tests', started: at(4), completed: at(3) }] },
340
- { name: 'outside job window', started: at(5), completed: at(6), steps: [
341
- { name: 'Tests', started: at(1), completed: at(2) },
342
- ] },
343
- { name: 'overlapping work', started: at(0), completed: at(1), steps: [
344
- { name: 'Tests A', started: at(0), completed: at(1) }, { name: 'Tests B', started: at(0), completed: at(1) },
345
- ] },
346
- { name: 'overlapping setup and work', started: at(0), completed: at(10), steps: [
347
- { name: 'Install dependencies', started: at(0), completed: at(8) },
348
- { name: 'Tests', started: at(5), completed: at(10) },
349
- ] },
350
- ] })
351
- const p = profileRun([r], 52)
352
- for (const j of p.jobs) {
353
- assert.equal(j.workS, null, j.name)
354
- assert.equal(j.setupS, null, j.name)
355
- assert.equal(j.runnerLabels, null, j.name)
356
- assert.ok(j.timingGaps.length > 0, j.name)
357
- }
358
- assert.equal(p.jobs[0].elapsedS, null)
359
- assert.equal(p.jobs[1].elapsedS, 60)
360
- assert.equal(p.jobs[2].queueS, null)
361
- assert.equal(p.jobs[2].steps[0].durationS, null)
362
- assert.throws(() => profileRun([r], 99), /run 99 not found/)
363
- assert.throws(() => profileRun([r, r], 52), /multiple records for run 52/)
364
- })
365
-
366
- // Only the external GitHub REST boundary is substituted. All collection and analysis run.
367
- function provider(routes) {
368
- const calls = []
369
- return {
370
- calls,
371
- api: async (endpoint) => {
372
- calls.push(endpoint)
373
- assert.ok(routes.has(endpoint), `unexpected GitHub request: ${endpoint}`)
374
- const response = routes.get(endpoint)
375
- if (response instanceof Error) throw response
376
- return structuredClone(response)
377
- },
378
- }
379
- }
380
- const rawRun = (id, extra = {}) => ({
381
- id, run_attempt: 1, name: 'CI', workflow_id: 8, path: '.github/workflows/ci.yml',
382
- html_url: `https://github.com/o/r/actions/runs/${id}`, head_sha: 'a'.repeat(40),
383
- head_branch: 'feat/x', event: 'pull_request', status: 'completed', conclusion: 'success',
384
- created_at: at(id * 10), run_started_at: at(id * 10 + 1), ...extra,
385
- })
386
- const rawJob = (id, extra = {}) => ({
387
- id, run_attempt: 1, name: 'unit', status: 'completed', conclusion: 'success',
388
- html_url: `https://github.com/o/r/actions/runs/1/job/${id}`,
389
- created_at: at(1), started_at: at(2), completed_at: at(4),
390
- runner_id: 3, runner_name: 'slot-3', runner_group_id: 4, runner_group_name: 'linux-pool',
391
- labels: ['self-hosted', 'linux', 'x64'], steps: [
392
- { number: 1, name: 'Set up job', conclusion: 'success', started_at: at(2), completed_at: at(2.5) },
393
- { number: 2, name: 'Unit tests', conclusion: 'success', started_at: at(2.5), completed_at: at(4) },
394
- ], ...extra,
395
- })
396
- const jobsPath = (id, page = 1, attempt = 1) => `repos/o/r/actions/runs/${id}/attempts/${attempt}/jobs?per_page=100&page=${page}`
397
- const historyPath = (page = 1) => `repos/o/r/actions/runs?status=completed&per_page=100&page=${page}`
398
- const commitPath = (sha) => `repos/o/r/commits/${sha}`
399
- const notFound = () => Object.assign(new Error('commit gone'), { status: 404 })
400
-
401
- test('collector resolves and memoizes commit trees, preserves deleted queue unknowns and does not infer equality from SHA', async () => {
402
- const deleted = 'b'.repeat(40), verified = 'c'.repeat(40)
403
- const raw = [rawRun(1), rawRun(2, { head_branch: 'mergify/merge-queue/1' }),
404
- rawRun(3, { head_sha: deleted }), rawRun(4, { head_sha: deleted }),
405
- rawRun(5, { head_sha: verified, head_commit: { tree_id: 'other-tree' } })]
406
- const routes = new Map([
407
- ['repos/o/r', { default_branch: 'main' }], [historyPath(), { workflow_runs: raw }],
408
- [commitPath(raw[0].head_sha), { commit: { tree: { sha: 'same-tree' } } }],
409
- [commitPath(deleted), notFound()],
410
- ...raw.map((r) => [jobsPath(r.id), { total_count: 1, jobs: [rawJob(r.id, {
411
- created_at: at(r.id * 10), started_at: at(r.id * 10 + 1), completed_at: at(r.id * 10 + 3),
412
- })] }]),
413
- ])
414
- const gh = provider(routes)
415
- const runs = await fetchRuns('o/r', 5, gh.api)
416
- const a = analyze(runs)
417
- assert.deepEqual(runs.map((r) => r.tree), ['same-tree', 'same-tree', null, null, 'other-tree'])
418
- assert.equal(gh.calls.filter((p) => p === commitPath(raw[0].head_sha)).length, 1)
419
- assert.equal(gh.calls.filter((p) => p === commitPath(deleted)).length, 1)
420
- assert.equal(gh.calls.includes(commitPath(verified)), false)
421
- assert.equal(a.reproved.reprovedMin, 2)
422
- assert.deepEqual(a.reproved.byLanePair, { 'pr→queue': 2 })
423
- assert.deepEqual(a.dataGaps.unknownTrees.map((r) => r.runId), [3, 4])
424
- assert.match(a.dataGaps.unknownTrees[0].reason, /HTTP 404.*deleted queue/)
425
- assert.equal(runs[0].treeSource, 'commits.commit.tree.sha')
426
- const again = await fetchRuns('o/r', 1, gh.api)
427
- assert.equal(again[0].tree, 'same-tree')
428
- assert.equal(gh.calls.filter((p) => p === commitPath(raw[0].head_sha)).length, 2, 'cache is scoped to one collection')
429
- })
430
-
431
- test('profile collector paginates beyond 100 jobs for the selected attempt and preserves workflow/runner identity', async () => {
432
- const r = rawRun(6, { run_attempt: 2, conclusion: 'failure', head_commit: { tree_id: 't' } })
433
- const first = Array.from({ length: 100 }, (_, i) => rawJob(i + 1, { name: `matrix ${i}`, run_attempt: 2 }))
434
- const last = rawJob(101, { name: 'slow failure', conclusion: 'failure', run_attempt: 2,
435
- created_at: at(63), started_at: at(65), completed_at: at(70), steps: [
436
- { number: 1, name: 'Install dependencies', started_at: at(65), completed_at: at(67), conclusion: 'success' },
437
- { number: 2, name: 'Tests', started_at: at(67), completed_at: at(70), conclusion: 'failure' },
438
- ] })
439
- const gh = provider(new Map([
440
- ['repos/o/r', { default_branch: 'main' }], ['repos/o/r/actions/runs/6', r],
441
- [jobsPath(6, 1, 2), { total_count: 101, jobs: first }],
442
- [jobsPath(6, 2, 2), { total_count: 101, jobs: [last] }],
443
- ]))
444
- const runs = await fetchRun('o/r', 6, gh.api), row = runs[0]
445
- assert.deepEqual(row.jobs.map((j) => j.id), [...first.map((j) => j.id), 101])
446
- assert.equal(analyze(runs).yield.find((j) => j.job === 'slow failure').sole, 1)
447
- assert.deepEqual([row.id, row.attempt, row.workflowId, row.workflowPath, row.url], [6, 2, 8, '.github/workflows/ci.yml', r.html_url])
448
- assert.deepEqual([row.jobs[100].runnerId, row.jobs[100].runnerName, row.jobs[100].runnerGroupId, row.jobs[100].runnerGroupName], [3, 'slot-3', 4, 'linux-pool'])
449
- assert.deepEqual(row.jobs[100].runnerLabels, ['self-hosted', 'linux', 'x64'])
450
- const p = profileRun(runs, 6), lastProfile = p.jobs.find((j) => j.name === 'slow failure')
451
- assert.deepEqual([lastProfile.attempt, lastProfile.dispatchDelayS, lastProfile.queueS, lastProfile.elapsedS], [2, 180, 120, 300])
452
- assert.equal(lastProfile.runUrl, r.html_url)
453
- assert.equal(lastProfile.steps[0].number, 2)
454
- assert.match(p.dataGaps.collection[0].reason, /only attempt 2 collected/)
455
- assert.deepEqual(gh.calls, ['repos/o/r', 'repos/o/r/actions/runs/6', jobsPath(6, 1, 2), jobsPath(6, 2, 2)])
456
- })
457
-
458
- test('history collector paginates runs and the job collector handles absent totals without losing page two', async () => {
459
- const raw = Array.from({ length: 101 }, (_, i) => rawRun(i + 1, { head_commit: { tree_id: 't' } }))
460
- const routes = new Map([
461
- ['repos/o/r', { default_branch: 'main' }],
462
- [historyPath(1), { workflow_runs: raw.slice(0, 100) }], [historyPath(2), { workflow_runs: raw.slice(100) }],
463
- ...raw.map((r) => [jobsPath(r.id), { jobs: [rawJob(r.id)] }]),
464
- ])
465
- routes.set(jobsPath(101), { jobs: Array.from({ length: 100 }, (_, i) => rawJob(1000 + i)) })
466
- routes.set(jobsPath(101, 2), { jobs: [rawJob(2000, { name: 'page two failure', conclusion: 'failure' })] })
467
- const gh = provider(routes), runs = await fetchRuns('o/r', 101, gh.api)
468
- assert.deepEqual(runs.map((r) => r.id), raw.map((r) => r.id))
469
- assert.equal(runs.at(-1).jobs.at(-1).name, 'page two failure')
470
- assert.equal(analyze(runs).yield.find((j) => j.job === 'page two failure').sole, 1)
471
- assert.equal(gh.calls.filter((p) => p === jobsPath(101, 2)).length, 1)
472
- })
473
-
474
- test('collector explicitly marks missing commit/attempt metadata and never substitutes head SHA for a tree', async () => {
475
- const r = rawRun(7, { run_attempt: undefined })
476
- const latest = 'repos/o/r/actions/runs/7/jobs?filter=latest&per_page=100&page=1'
477
- const gh = provider(new Map([
478
- ['repos/o/r', { default_branch: 'main' }], ['repos/o/r/actions/runs/7', r],
479
- [latest, { total_count: 1, jobs: [rawJob(701)] }], [commitPath(r.head_sha), { commit: {} }],
480
- ]))
481
- const [row] = await fetchRun('o/r', 7, gh.api)
482
- assert.equal(row.tree, null)
483
- assert.match(row.treeUnknownReason, /no tree SHA/)
484
- assert.equal(row.attempt, null)
485
- assert.match(row.collectionGaps[0].reason, /attempt unavailable/)
486
- assert.equal(profileRun([row], 7).jobs[0].attempt, 1, 'job API attempt remains authoritative when run attempt is missing')
487
- })
488
-
489
- test('collector rejects provider errors, inconsistent pagination and cross-attempt jobs instead of returning partial evidence', async () => {
490
- const cases = [
491
- ['permission', { jobs: [], total_count: 0 }, Object.assign(new Error('denied'), { status: 403 }), /denied/],
492
- ['short page', { jobs: [rawJob(1)], total_count: 2 }, null, /incomplete jobs response/],
493
- ['duplicate IDs', { jobs: [rawJob(1), rawJob(1)], total_count: 2 }, null, /duplicate job/],
494
- ['cross attempt', { jobs: [rawJob(1, { run_attempt: 2 })], total_count: 1 }, null, /attempt mismatch/],
495
- ['oversize', { jobs: [], total_count: 10001 }, null, /job pagination limit/],
496
- ]
497
- for (const [name, jobs, error, expected] of cases) {
498
- const r = rawRun(1, error ? {} : { head_commit: { tree_id: 't' } })
499
- const gh = provider(new Map([
500
- ['repos/o/r', { default_branch: 'main' }], ['repos/o/r/actions/runs/1', r],
501
- [jobsPath(1), jobs], [commitPath(r.head_sha), error],
502
- ]))
503
- await assert.rejects(fetchRun('o/r', 1, gh.api), expected, name)
504
- }
505
- const gh = provider(new Map())
506
- await assert.rejects(fetchRuns('../bad', 1, gh.api), /--repo must be owner\/name/)
507
- await assert.rejects(fetchRuns('o/r', 2001, gh.api), /--runs must be an integer/)
508
- await assert.rejects(fetchRun('o/r', 0, gh.api), /--profile-run must be an integer/)
509
- assert.deepEqual(gh.calls, [], 'validate before network access')
510
- })
511
-
512
- test('collector refuses changed page totals and an exhausted page budget but accepts an exact full final page', async () => {
513
- const r = rawRun(1, { head_commit: { tree_id: 't' } })
514
- const base = [['repos/o/r', { default_branch: 'main' }], ['repos/o/r/actions/runs/1', r]]
515
- const page = (n) => Array.from({ length: 100 }, (_, i) => rawJob((n - 1) * 100 + i + 1))
516
- const exact = provider(new Map([...base, [jobsPath(1), { total_count: 100, jobs: page(1) }]]))
517
- const [full] = await fetchRun('o/r', 1, exact.api)
518
- assert.deepEqual(full.jobs.map((j) => j.id), page(1).map((j) => j.id))
519
- assert.equal(exact.calls.includes(jobsPath(1, 2)), false, 'no extra page required for a verified total')
520
- const changed = provider(new Map([...base,
521
- [jobsPath(1), { total_count: 101, jobs: page(1) }], [jobsPath(1, 2), { total_count: 102, jobs: [rawJob(101)] }],
522
- ]))
523
- await assert.rejects(fetchRun('o/r', 1, changed.api), /job total changed/)
524
- const bounded = provider(new Map([...base, ...Array.from({ length: 100 }, (_, i) => [jobsPath(1, i + 1), { jobs: page(i + 1) }])]))
525
- await assert.rejects(fetchRun('o/r', 1, bounded.api), /pagination limit reached.*refusing partial/)
526
- assert.equal(bounded.calls.at(-1), jobsPath(1, 100))
527
- assert.equal(bounded.calls.includes(jobsPath(1, 101)), false)
528
- })
529
-
530
- test('history pagination rejects duplicate runs rather than counting the same execution twice', async () => {
531
- const r = rawRun(1)
532
- const gh = provider(new Map([
533
- ['repos/o/r', { default_branch: 'main' }], [historyPath(), { workflow_runs: [r, r] }],
534
- ]))
535
- await assert.rejects(fetchRuns('o/r', 2, gh.api), /duplicate run ID/)
536
- assert.equal(gh.calls.some((p) => p.includes('/jobs?')), false)
537
- })
538
-
539
- const CLI = fileURLToPath(new URL('./ci-run-audit.mjs', import.meta.url))
540
- function cli(argv) {
541
- return spawnSync(process.execPath, [CLI, ...argv], { encoding: 'utf8', timeout: 10000, env: { ...process.env, PATH: '' } })
542
- }
543
- function savedFixture(t, data) {
544
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ci-run-audit-'))
545
- t.after(() => fs.rmSync(dir, { recursive: true, force: true }))
546
- const file = path.join(dir, 'runs.json')
547
- fs.writeFileSync(file, JSON.stringify(data))
548
- return { dir, file }
549
- }
550
-
551
- test('saved-data CLI selects a run, reports all jobs including gates, saves raw rows, and remains re-auditable offline', (t) => {
552
- const target = runOf({ id: 71, attempt: 3, sha: 's1', tree: null, url: 'https://github.com/o/r/actions/runs/71',
553
- jobs: [job('unit', 'failure', 2, 5, 1), job('all-gates', 'failure', 7, 1)] })
554
- const other = runOf({ id: 72, sha: 's2', tree: 't2', jobs: [job('lint', 'success', 0, 1)] })
555
- const { dir, file } = savedFixture(t, [target, other]), save = path.join(dir, 'copied.json')
556
- const result = cli(['--from', file, '--profile-run', '71', '--json', '--save', save])
557
- assert.equal(result.status, 0, result.stderr)
558
- assert.equal(result.stderr, '')
559
- const p = JSON.parse(result.stdout)
560
- assert.deepEqual([p.mode, p.runId, p.attempt, p.runUrl], ['profile', 71, 3, target.url])
561
- assert.deepEqual(p.jobs.map((j) => j.name), ['unit', 'all-gates'])
562
- assert.deepEqual([p.jobs[0].elapsedS, p.jobs[0].workS, p.jobs[0].setupS], [300, 240, 60])
563
- assert.deepEqual(JSON.parse(fs.readFileSync(save, 'utf8')), [target, other])
564
- const audit = cli(['--from', save, '--json', '--min-trees', '1'])
565
- assert.equal(audit.status, 0, audit.stderr)
566
- const a = JSON.parse(audit.stdout)
567
- assert.equal(a.window.runs, 2)
568
- assert.equal(a.yield.find((j) => j.job === 'unit').sole, 1)
569
- assert.equal(a.yield.some((j) => j.job === 'all-gates'), false)
570
- assert.equal(a.dataGaps.unknownTrees[0].runId, 71)
571
- const markdown = cli(['--from', file, '--profile-run', '72'])
572
- assert.equal(markdown.status, 0, markdown.stderr)
573
- assert.match(markdown.stdout, /CI run profile: CI \/ 72/)
574
- assert.doesNotMatch(markdown.stdout, /## unit/)
575
- })
576
-
577
- test('CLI validates options before fetch, rejects unsafe/unknown flags and reports useful errors without a stack trace', () => {
578
- const cases = [
579
- [[], /provide exactly one/],
580
- [['--repo'], /missing value for --repo/],
581
- [['--repo', '--json'], /missing value for --repo/],
582
- [['--repo', 'https://github.com/o/r'], /--repo must be owner\/name/],
583
- [['--repo', 'o/r', '--from', 'x'], /provide exactly one/],
584
- [['--repo', 'o/r', '--runs', '0'], /--runs must be an integer/],
585
- [['--repo', 'o/r', '--runs', '2001'], /--runs must be an integer/],
586
- [['--repo', 'o/r', '--runs', '1.5'], /--runs must be an integer/],
587
- [['--repo', 'o/r', '--runs', 'NaN'], /--runs must be an integer/],
588
- [['--repo', 'o/r', '--min-trees', '0'], /--min-trees must be an integer/],
589
- [['--repo', 'o/r', '--profile-run', '9007199254740992'], /--profile-run must be an integer/],
590
- [['--repo', 'o/r', '--profile-run', '2', '--runs', '3'], /--runs is only supported/],
591
- [['--from', 'x', '--runs', '3'], /--runs is only supported/],
592
- [['--from', 'x', '--profile-run', '2', '--ignore-job', 'gate'], /audit-only/],
593
- [['--from', 'x', '--ignore-job', '['], /Invalid regular expression/],
594
- [['--from', 'x', '--json', '--json'], /duplicate argument/],
595
- [['--unsafe-api', 'evil'], /unknown argument/],
596
- ]
597
- for (const [argv, error] of cases) {
598
- const result = cli(argv)
599
- assert.equal(result.status, 2, argv.join(' '))
600
- assert.equal(result.stdout, '', argv.join(' '))
601
- assert.match(result.stderr, error)
602
- assert.doesNotMatch(result.stderr, /\n\s+at /)
603
- }
604
- const help = cli(['--help'])
605
- assert.equal(help.status, 0, help.stderr)
606
- assert.match(help.stderr, /--profile-run/)
607
- const fetchFailure = cli(['--repo', 'o/r', '--profile-run', '1'])
608
- assert.equal(fetchFailure.status, 2)
609
- assert.match(fetchFailure.stderr, /gh ENOENT/)
610
- assert.equal(fetchFailure.stdout, '')
611
- })
612
-
613
- test('saved-data CLI refuses malformed data, absent/ambiguous run IDs and unwritable output with no report', (t) => {
614
- const { dir, file } = savedFixture(t, [])
615
- const row = runOf({ id: 81, sha: 's', tree: 't', jobs: [job('unit', 'success', 0, 1)] })
616
- const cases = [
617
- ['{bad', [], /JSON/],
618
- [JSON.stringify({ runs: [] }), [], /array of runs/],
619
- [JSON.stringify([{ id: 1, workflow: 'CI', jobs: {} }]), [], /jobs array/],
620
- [JSON.stringify([{ ...row, jobs: [{ name: 'unit', started: 'invalid date' }] }]), [], /invalid job started timestamp/],
621
- [JSON.stringify([row]), ['--profile-run', '99'], /run 99 not found/],
622
- [JSON.stringify([row, row]), ['--profile-run', '81'], /multiple records/],
623
- [JSON.stringify([row]), ['--save', dir], /EISDIR/],
624
- ]
625
- for (const [data, flags, error] of cases) {
626
- fs.writeFileSync(file, data)
627
- const result = cli(['--from', file, ...flags])
628
- assert.equal(result.status, 2, result.stderr)
629
- assert.equal(result.stdout, '')
630
- assert.match(result.stderr, error)
631
- assert.doesNotMatch(result.stderr, /\n\s+at /)
632
- assert.equal(fs.readFileSync(file, 'utf8'), data)
633
- }
634
- const missing = cli(['--from', path.join(dir, 'absent.json')])
635
- assert.equal(missing.status, 2)
636
- assert.match(missing.stderr, /ENOENT/)
637
- })