browser-commander 0.26.0 → 0.26.2

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 (40) hide show
  1. package/.jscpd-baseline.json +6 -4
  2. package/CHANGELOG.md +30 -0
  3. package/package.json +2 -2
  4. package/scripts/check-duplication.mjs +176 -0
  5. package/scripts/create-manual-changeset.mjs +3 -1
  6. package/scripts/format-release-notes.mjs +8 -3
  7. package/scripts/merge-changesets.mjs +4 -1
  8. package/scripts/validate-changeset.mjs +23 -10
  9. package/scripts/version-and-commit.mjs +8 -10
  10. package/src/browser/webdriver.js +69 -3
  11. package/src/elements/selectors.js +31 -14
  12. package/src/parity/harness.js +4 -1
  13. package/src/tests/index.js +12 -1
  14. package/tests/e2e/engine-matrix.e2e.test.js +2 -1
  15. package/tests/e2e/safari.e2e.test.js +2 -1
  16. package/tests/e2e/webdriver.e2e.test.js +2 -1
  17. package/tests/helpers/fixture-server.js +18 -1
  18. package/tests/unit/browser/webdriver.test.js +100 -0
  19. package/tests/unit/elements/selectors.test.js +75 -0
  20. package/tests/unit/helpers/fixture-server.test.js +60 -0
  21. package/tests/unit/playwright-protocol-coverage.test.js +9 -6
  22. package/tests/unit/puppeteer-bindings-coverage.test.js +28 -7
  23. package/tests/unit/scripts/bot-commit-attribution.test.js +70 -0
  24. package/tests/unit/scripts/check-duplication.test.js +139 -0
  25. package/tests/unit/scripts/check-pipeline-status.test.js +264 -52
  26. package/tests/unit/scripts/check-version-modification.test.js +107 -1
  27. package/tests/unit/scripts/ci-timeout-budgets.test.js +15 -2
  28. package/tests/unit/scripts/ci-workflow-policy.test.js +91 -7
  29. package/tests/unit/scripts/command-stream-errexit.test.js +83 -41
  30. package/tests/unit/scripts/e2e-suite-coverage.test.js +174 -0
  31. package/tests/unit/scripts/github-actions-log.test.js +99 -0
  32. package/tests/unit/scripts/links-workflow.test.js +42 -0
  33. package/tests/unit/scripts/preflight-credentials.test.js +555 -0
  34. package/tests/unit/scripts/run-with-budget-warning.test.js +212 -6
  35. package/tests/unit/scripts/trusted-publishing.test.js +78 -0
  36. package/tests/unit/scripts/use-module-integration.test.js +2 -9
  37. package/tests/unit/scripts/use-module.test.js +20 -0
  38. package/tests/unit/scripts/validate-changeset.test.js +127 -0
  39. package/tests/unit/scripts/zizmor-version.test.js +82 -0
  40. package/tests/unit/tests/index.test.js +43 -0
@@ -6,9 +6,17 @@
6
6
  * `cancelled` as well - run 24045269874 of this repository is one such run on
7
7
  * `main`. scripts/check-pipeline-status.sh is the only thing that looks at
8
8
  * that, so these tests pin the readings it has to get right: a failure is
9
- * always an error, a cancellation off the default branch is a warning, a
10
- * cancellation on the default branch is an error unless a newer run has already
11
- * taken over the branch head.
9
+ * always an error, and a cancellation is an error unless a newer commit has
10
+ * overtaken the run *and* the cancelled job is one a newer run can cancel
11
+ * (`cancel-in-progress: true`). Anything else - a job that queues instead, a
12
+ * run still at the branch head, a value the gate cannot read - is a timeout or
13
+ * a manual stop that nothing else will report.
14
+ *
15
+ * The gate used to warn about every cancellation off the default branch, so a
16
+ * pull request whose test job hit `timeout-minutes` passed its gate (issue
17
+ * #128), and it excused a cancelled release writer on a superseded `main` run
18
+ * although `cancel-in-progress: false` means no newer run can cancel it.
19
+ *
12
20
  * Hosted-runner failures can report an undocumented abandoned result; it and
13
21
  * other unexpected/missing results must fail on every branch.
14
22
  *
@@ -16,27 +24,65 @@
16
24
  */
17
25
 
18
26
  import assert from 'node:assert/strict';
19
- import { mkdtempSync, rmSync } from 'node:fs';
27
+ import { mkdtempSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
20
28
  import { tmpdir } from 'node:os';
21
29
  import path from 'node:path';
22
30
  import { spawnSync } from 'node:child_process';
23
- import { describe, it } from 'node:test';
31
+ import { after, before, describe, it } from 'node:test';
24
32
 
33
+ import { readCancelInProgress } from '../../../../scripts/read-job-cancel-in-progress.mjs';
25
34
  import {
26
35
  BASH_AVAILABLE,
27
36
  REPO_ROOT,
37
+ readRepoText,
28
38
  repoPath,
29
39
  runBashScript,
30
40
  } from '../../helpers/repo.js';
31
41
 
32
42
  const SCRIPT = repoPath('scripts/check-pipeline-status.sh');
33
43
 
34
- function runGate(needs, env = {}, options = {}) {
35
- return runBashScript(SCRIPT, [], {
36
- cwd: options.cwd,
37
- env: { NEEDS_JSON: JSON.stringify(needs), ...env },
38
- });
39
- }
44
+ const FIXTURE_WORKFLOW = `name: Fixture
45
+
46
+ concurrency:
47
+ group: fixture-\${{ github.ref }}
48
+ cancel-in-progress: false
49
+
50
+ jobs:
51
+ test:
52
+ runs-on: ubuntu-24.04
53
+ concurrency:
54
+ group: \${{ github.workflow }}-\${{ github.ref }}-test
55
+ cancel-in-progress: true # superseded runs stop here
56
+ steps:
57
+ - run: echo test
58
+
59
+ release:
60
+ runs-on: ubuntu-24.04
61
+ concurrency:
62
+ group: main-writer
63
+ cancel-in-progress: false
64
+ steps:
65
+ - run: echo release
66
+
67
+ dynamic:
68
+ runs-on: ubuntu-24.04
69
+ concurrency:
70
+ group: dynamic
71
+ cancel-in-progress: \${{ github.event_name == 'pull_request' }}
72
+ steps:
73
+ - run: echo dynamic
74
+
75
+ inherits:
76
+ runs-on: ubuntu-24.04
77
+ steps:
78
+ - run: echo inherits
79
+
80
+ group-only:
81
+ runs-on: ubuntu-24.04
82
+ concurrency: just-a-group
83
+ steps:
84
+ - run: echo group
85
+ `;
40
86
 
41
87
  function results(map) {
42
88
  return Object.fromEntries(
@@ -44,11 +90,76 @@ function results(map) {
44
90
  );
45
91
  }
46
92
 
93
+ describe('readCancelInProgress', () => {
94
+ const policy = readCancelInProgress(FIXTURE_WORKFLOW, [
95
+ 'test',
96
+ 'release',
97
+ 'dynamic',
98
+ 'inherits',
99
+ 'group-only',
100
+ 'absent',
101
+ ]);
102
+
103
+ it('reads a literal job-level value, comments and all', () => {
104
+ assert.equal(policy.get('test'), 'true');
105
+ assert.equal(policy.get('release'), 'false');
106
+ });
107
+
108
+ it('reports an expression as unknown instead of guessing', () => {
109
+ assert.equal(policy.get('dynamic'), 'unknown');
110
+ });
111
+
112
+ it('falls back to the workflow-level group, and to false for a bare group', () => {
113
+ assert.equal(policy.get('inherits'), 'false');
114
+ assert.equal(policy.get('group-only'), 'false');
115
+ });
116
+
117
+ it('says when the workflow has no such job', () => {
118
+ assert.equal(policy.get('absent'), 'missing');
119
+ });
120
+
121
+ it('says when neither the job nor the workflow has a group', () => {
122
+ const bare = 'name: Bare\non: push\njobs:\n only:\n runs-on: x\n';
123
+ assert.equal(readCancelInProgress(bare, ['only']).get('only'), 'none');
124
+ });
125
+ });
126
+
47
127
  describe('check-pipeline-status.sh', { skip: !BASH_AVAILABLE }, () => {
128
+ let directory;
129
+ let workflowFile;
130
+
131
+ before(() => {
132
+ directory = mkdtempSync(path.join(tmpdir(), 'pipeline-status-'));
133
+ workflowFile = path.join(directory, 'fixture.yml');
134
+ writeFileSync(workflowFile, FIXTURE_WORKFLOW);
135
+ });
136
+
137
+ after(() => {
138
+ rmSync(directory, { force: true, recursive: true });
139
+ });
140
+
141
+ // GITHUB_WORKFLOW_REF is set when this suite itself runs in Actions; blank
142
+ // it so only what a test passes decides which workflow the gate reads.
143
+ function runGate(needs, env = {}, options = {}) {
144
+ return runBashScript(SCRIPT, [], {
145
+ cwd: options.cwd,
146
+ env: {
147
+ GITHUB_WORKFLOW_REF: '',
148
+ WORKFLOW_FILE: workflowFile,
149
+ BRANCH_NAME: 'main',
150
+ RUN_SHA: 'abc',
151
+ BRANCH_HEAD_SHA: 'abc',
152
+ NEEDS_JSON: JSON.stringify(needs),
153
+ ...env,
154
+ },
155
+ });
156
+ }
157
+
158
+ const superseded = { BRANCH_HEAD_SHA: 'def' };
159
+
48
160
  it('passes when every job succeeded or was legitimately skipped', () => {
49
161
  const { status, output } = runGate(
50
- results({ lint: 'success', release: 'skipped' }),
51
- { IS_MAIN: 'true', RUN_SHA: 'abc', BRANCH_HEAD_SHA: 'abc' }
162
+ results({ lint: 'success', release: 'skipped' })
52
163
  );
53
164
 
54
165
  assert.equal(status, 0);
@@ -60,19 +171,15 @@ describe('check-pipeline-status.sh', { skip: !BASH_AVAILABLE }, () => {
60
171
 
61
172
  it('fails on a failed job and names it', () => {
62
173
  const { status, output } = runGate(
63
- results({ lint: 'failure', test: 'success' }),
64
- { IS_MAIN: 'true', RUN_SHA: 'abc', BRANCH_HEAD_SHA: 'abc' }
174
+ results({ lint: 'failure', test: 'success' })
65
175
  );
66
176
 
67
177
  assert.equal(status, 1);
68
178
  assert.match(output, /::error::Pipeline failed\. Failing jobs: lint/);
69
179
  });
70
180
 
71
- for (const env of [
72
- { IS_MAIN: 'false' },
73
- { IS_MAIN: 'true', RUN_SHA: 'abc', BRANCH_HEAD_SHA: 'def' },
74
- ]) {
75
- it(`fails on an abandoned runner job with IS_MAIN=${env.IS_MAIN}`, () => {
181
+ for (const env of [{}, superseded]) {
182
+ it(`fails on an abandoned runner job (branch head ${env.BRANCH_HEAD_SHA ?? 'abc'})`, () => {
76
183
  // Hosted-runner acquisition failure in run 37362314527 left the macOS
77
184
  // check cancelled, but GitHub sent "abandoned" to the needs context.
78
185
  const { status, output } = runGate(
@@ -98,58 +205,124 @@ describe('check-pipeline-status.sh', { skip: !BASH_AVAILABLE }, () => {
98
205
  });
99
206
  }
100
207
 
101
- it('only warns about a cancellation off the default branch', () => {
208
+ it('fails on a cancellation in a pull request run that nothing overtook', () => {
209
+ // The old gate only warned off the default branch, so a pull request whose
210
+ // test job hit timeout-minutes passed its gate.
102
211
  const { status, output } = runGate(results({ test: 'cancelled' }), {
103
- IS_MAIN: 'false',
104
- RUN_SHA: 'abc',
105
- BRANCH_HEAD_SHA: 'abc',
212
+ BRANCH_NAME: 'issue-128-feature',
106
213
  });
107
214
 
215
+ assert.equal(status, 1);
216
+ assert.match(output, /This run tests abc; issue-128-feature is at abc/);
217
+ assert.match(
218
+ output,
219
+ /test: the run is still the head of issue-128-feature/
220
+ );
221
+ assert.match(output, /::error::Pipeline has cancelled jobs: test\./);
222
+ });
223
+
224
+ it('fails on a cancellation at the default branch head', () => {
225
+ const { status, output } = runGate(results({ test: 'cancelled' }));
226
+
227
+ assert.equal(status, 1);
228
+ assert.match(output, /::error::Pipeline has cancelled jobs: test\./);
229
+ });
230
+
231
+ it('only warns when a newer commit overtook a job that cancels in progress', () => {
232
+ const { status, output } = runGate(
233
+ results({ test: 'cancelled' }),
234
+ superseded
235
+ );
236
+
108
237
  assert.equal(status, 0);
109
- assert.match(output, /::warning::Cancelled jobs: test/);
238
+ assert.match(output, /This run tests abc; main is at def/);
239
+ assert.match(output, /test: it sets cancel-in-progress: true/);
240
+ assert.match(
241
+ output,
242
+ /::warning::Cancelled jobs in a superseded run: test\./
243
+ );
244
+ assert.doesNotMatch(output, /::error::/);
110
245
  });
111
246
 
112
- it('fails on a cancellation on the default branch head', () => {
247
+ for (const [job, reason] of [
248
+ ['release', /it sets cancel-in-progress: false, so a newer run queues/],
249
+ ['dynamic', /cancel-in-progress is an expression or otherwise unreadable/],
250
+ ['inherits', /it sets cancel-in-progress: false, so a newer run queues/],
251
+ ['absent', /declares no job by that name/],
252
+ ]) {
253
+ it(`fails on a superseded run's cancelled ${job} job, which no supersede explains`, () => {
254
+ const { status, output } = runGate(
255
+ results({ test: 'cancelled', [job]: 'cancelled' }),
256
+ superseded
257
+ );
258
+
259
+ assert.equal(status, 1);
260
+ assert.match(output, reason);
261
+ assert.match(
262
+ output,
263
+ /::warning::Cancelled jobs in a superseded run: test\./
264
+ );
265
+ assert.match(
266
+ output,
267
+ new RegExp(`::error::Pipeline has cancelled jobs: ${job}\\.`)
268
+ );
269
+ });
270
+ }
271
+
272
+ it('fails closed when it cannot tell which workflow it is gating', () => {
113
273
  const { status, output } = runGate(results({ test: 'cancelled' }), {
114
- IS_MAIN: 'true',
115
- RUN_SHA: 'abc',
116
- BRANCH_HEAD_SHA: 'abc',
274
+ ...superseded,
275
+ WORKFLOW_FILE: '',
117
276
  });
118
277
 
119
278
  assert.equal(status, 1);
120
- assert.match(output, /::error::Pipeline has cancelled jobs on main: test/);
279
+ assert.match(output, /could not identify its workflow/);
121
280
  });
122
281
 
123
- it('only warns when a newer commit already superseded the run', () => {
124
- const { status, output } = runGate(results({ test: 'cancelled' }), {
125
- IS_MAIN: 'true',
126
- RUN_SHA: 'abc',
127
- BRANCH_HEAD_SHA: 'def',
128
- });
282
+ it('finds its workflow through GITHUB_WORKFLOW_REF, from the repository root', () => {
283
+ // ci-policy.yml's `check` job cancels in progress; `pipeline-status` reads
284
+ // it from the checkout whichever directory the step runs in.
285
+ const { status, output } = runGate(
286
+ results({ check: 'cancelled' }),
287
+ {
288
+ ...superseded,
289
+ WORKFLOW_FILE: '',
290
+ GITHUB_WORKFLOW_REF:
291
+ 'link-foundation/browser-commander/.github/workflows/ci-policy.yml@refs/pull/129/merge',
292
+ },
293
+ { cwd: directory }
294
+ );
129
295
 
130
- assert.equal(status, 0);
131
- assert.match(output, /This run tests abc; main is at def/);
132
- assert.match(output, /::warning::Cancelled jobs: test/);
296
+ assert.equal(status, 0, output);
297
+ assert.match(output, /check: it sets cancel-in-progress: true/);
133
298
  });
134
299
 
135
300
  it('fails rather than guesses when the branch head cannot be resolved', () => {
136
301
  // A missed supersede costs one noisy warning; a missed overrun on main
137
302
  // costs a silent failure, so the unresolvable case has to be the loud one.
138
- const directory = mkdtempSync(path.join(tmpdir(), 'pipeline-status-'));
303
+ const { status, output } = runGate(
304
+ results({ test: 'cancelled' }),
305
+ { BRANCH_HEAD_SHA: '', GIT_REMOTE: 'no-such-remote' },
306
+ { cwd: directory }
307
+ );
139
308
 
140
- try {
141
- const { status, output } = runGate(
142
- results({ test: 'cancelled' }),
143
- { IS_MAIN: 'true', RUN_SHA: 'abc', BRANCH_HEAD_SHA: '' },
144
- { cwd: directory }
145
- );
309
+ assert.equal(status, 1);
310
+ assert.match(output, /assuming it is current/);
311
+ assert.match(output, /::error::Pipeline has cancelled jobs: test\./);
312
+ });
146
313
 
147
- assert.equal(status, 1);
148
- assert.match(output, /assuming it is current/);
149
- assert.match(output, /::error::Pipeline has cancelled jobs on main/);
150
- } finally {
151
- rmSync(directory, { force: true, recursive: true });
152
- }
314
+ it('prints the classification trace only when asked', () => {
315
+ const quiet = runGate(results({ test: 'cancelled' }), superseded);
316
+ const verbose = runGate(results({ test: 'cancelled' }), {
317
+ ...superseded,
318
+ PIPELINE_STATUS_VERBOSE: '1',
319
+ });
320
+
321
+ assert.doesNotMatch(quiet.output, /\[pipeline-status\]/);
322
+ assert.match(
323
+ verbose.output,
324
+ /\[pipeline-status\] cancelled test: cancel-in-progress=true, superseded=yes/
325
+ );
153
326
  });
154
327
 
155
328
  it('refuses to run without the job results', () => {
@@ -163,3 +336,42 @@ describe('check-pipeline-status.sh', { skip: !BASH_AVAILABLE }, () => {
163
336
  assert.match(result.stderr, /NEEDS_JSON is required/);
164
337
  });
165
338
  });
339
+
340
+ describe('workflows that run the gate', () => {
341
+ const workflows = readdirSync(repoPath('.github/workflows'))
342
+ .filter((name) => name.endsWith('.yml'))
343
+ .map((name) => ({ name, source: readRepoText('.github/workflows', name) }))
344
+ .filter(({ source }) => source.includes('check-pipeline-status.sh'));
345
+
346
+ it('exist', () => {
347
+ assert.ok(workflows.length > 0);
348
+ });
349
+
350
+ for (const { name, source } of workflows) {
351
+ it(`${name} tells the gate which commit and branch the run tests`, () => {
352
+ // A pull request run's github.sha is a merge commit no branch points at,
353
+ // and github.ref_name is "129/merge"; the head commit and head branch are
354
+ // what a newer push moves.
355
+ assert.match(
356
+ source,
357
+ /RUN_SHA: \$\{\{ github\.event\.pull_request\.head\.sha \|\| github\.sha \}\}/
358
+ );
359
+ assert.match(
360
+ source,
361
+ /BRANCH_NAME: \$\{\{ github\.head_ref \|\| github\.ref_name \}\}/
362
+ );
363
+ assert.doesNotMatch(source, /IS_MAIN:/);
364
+ });
365
+
366
+ it(`${name} gives every job a cancel-in-progress the gate can read`, () => {
367
+ const jobsBlock = source.slice(source.search(/^jobs:\s*$/m));
368
+ const jobs = [
369
+ ...jobsBlock.matchAll(/^ {2}([A-Za-z_][\w.-]*):\s*$/gm),
370
+ ].map((match) => match[1]);
371
+ const unreadable = [...readCancelInProgress(source, jobs)]
372
+ .filter(([, value]) => !['true', 'false'].includes(value))
373
+ .map(([job, value]) => `${job}=${value}`);
374
+ assert.deepEqual(unreadable, []);
375
+ });
376
+ }
377
+ });
@@ -1,7 +1,17 @@
1
1
  import assert from 'node:assert/strict';
2
+ import { spawnSync } from 'node:child_process';
3
+ import { mkdtempSync, rmSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import path from 'node:path';
2
6
  import { describe, it } from 'node:test';
3
7
 
4
- import { findManualVersionChanges } from '../../../../scripts/check-version-modification.mjs';
8
+ import {
9
+ checkVersionModification,
10
+ findManualVersionChanges,
11
+ } from '../../../../scripts/check-version-modification.mjs';
12
+ import { repoPath } from '../../helpers/repo.js';
13
+
14
+ const SCRIPT = repoPath('scripts/check-version-modification.mjs');
5
15
 
6
16
  // The real implementation shells out to git; injecting the diff keeps these
7
17
  // cases independent of the branch the suite happens to run on.
@@ -68,3 +78,99 @@ describe('check-version-modification', () => {
68
78
  assert.deepEqual(findManualVersionChanges('main', diff), []);
69
79
  });
70
80
  });
81
+
82
+ describe('check-version-modification when git cannot diff', () => {
83
+ // Issue #128 (link-foundation/rust-ai-driven-development-pipeline-template
84
+ // #174): a failed `git diff` used to read as "nothing changed", so a missing
85
+ // base ref turned the check into a pass.
86
+ function failingDiff() {
87
+ throw new Error("fatal: ambiguous argument 'origin/main...HEAD'");
88
+ }
89
+
90
+ it('fails rather than reporting no change', () => {
91
+ assert.throws(
92
+ () => findManualVersionChanges('main', failingDiff),
93
+ /Could not diff js\/package\.json against origin\/main/
94
+ );
95
+ });
96
+
97
+ it('fetches the base once and retries before giving up', () => {
98
+ let fetched = 0;
99
+ const diff = (_baseRef, path) => {
100
+ if (fetched === 0) {
101
+ failingDiff();
102
+ }
103
+ return path === 'rust/Cargo.toml' ? '+version = "0.9.1"' : '';
104
+ };
105
+
106
+ assert.deepEqual(
107
+ checkVersionModification('main', {
108
+ diff,
109
+ fetchBase: () => {
110
+ fetched += 1;
111
+ },
112
+ }),
113
+ ['rust/Cargo.toml']
114
+ );
115
+ assert.equal(fetched, 1);
116
+ });
117
+
118
+ it('reports the failure when the retry fails too', () => {
119
+ let fetched = 0;
120
+
121
+ assert.throws(
122
+ () =>
123
+ checkVersionModification('main', {
124
+ diff: failingDiff,
125
+ fetchBase: () => {
126
+ fetched += 1;
127
+ },
128
+ }),
129
+ /Could not diff/
130
+ );
131
+ assert.equal(fetched, 1);
132
+ });
133
+
134
+ it('exits 1 with an error annotation from the command line', () => {
135
+ const directory = mkdtempSync(path.join(tmpdir(), 'version-check-'));
136
+
137
+ try {
138
+ // A repository with no origin: the base ref cannot exist, and neither
139
+ // can the fetch that would bring it in.
140
+ spawnSync('git', ['init', '-q', directory]);
141
+ spawnSync(
142
+ 'git',
143
+ [
144
+ '-C',
145
+ directory,
146
+ '-c',
147
+ 'user.name=t',
148
+ '-c',
149
+ 'user.email=t@example.com',
150
+ 'commit',
151
+ '-q',
152
+ '--allow-empty',
153
+ '-m',
154
+ 'init',
155
+ ],
156
+ { encoding: 'utf8' }
157
+ );
158
+
159
+ const result = spawnSync(process.execPath, [SCRIPT], {
160
+ cwd: directory,
161
+ encoding: 'utf8',
162
+ env: {
163
+ ...process.env,
164
+ GITHUB_BASE_REF: 'main',
165
+ GITHUB_HEAD_REF: 'feature',
166
+ },
167
+ });
168
+
169
+ assert.equal(result.status, 1, result.stdout + result.stderr);
170
+ assert.match(result.stderr, /::error::Could not diff/);
171
+ assert.doesNotMatch(result.stdout, /No manual version changes/);
172
+ } finally {
173
+ rmSync(directory, { force: true, recursive: true });
174
+ }
175
+ });
176
+ });
@@ -199,18 +199,31 @@ describe('CI execution budgets', () => {
199
199
  'parity.yml cli CLI and API coverage suites',
200
200
  'parity.yml cli Cross-language CLI contract',
201
201
  'parity.yml cli Rust CLI build',
202
+ 'parity.yml engine-suites Engine lifecycle suites',
203
+ 'parity.yml fixture-suites React fixture suites',
202
204
  'parity.yml parity Fingerprint parity suite',
203
205
  'parity.yml parity WebDriver suite',
206
+ 'parity.yml snapshots Python headful real-browser launches',
204
207
  'parity.yml snapshots Python native snapshot launches',
208
+ 'parity.yml snapshots Rust launch and connect smokes',
205
209
  'parity.yml snapshots Rust native snapshots and parity',
206
210
  'parity.yml storage-state Rust storage state transfer',
207
211
  'python.yml test pytest suite',
208
212
  'rust.yml coverage Rust code coverage',
209
- 'rust.yml test Rust doc tests',
210
213
  'rust.yml test Rust test suite',
211
214
  ]);
212
215
  });
213
216
 
217
+ it('runs the Rust doc tests once, inside the main test suite', () => {
218
+ // `cargo test --all-features` already runs them. A second `cargo test
219
+ // --doc` with the default features rebuilt the crate and, on Windows,
220
+ // stalled silently until its budget killed it (issue #128).
221
+ const block = getJobBlock(readWorkflow('rust.yml'), 'test');
222
+
223
+ assert.match(block, /cargo test --all-features/);
224
+ assert.doesNotMatch(block, /cargo test --doc/);
225
+ });
226
+
214
227
  it('warns at the same share of a budget that this invariant allows', () => {
215
228
  const wrapper = readFileSync(
216
229
  repoPath('scripts/run-with-budget-warning.sh'),
@@ -313,7 +326,7 @@ describe('pipeline status gate', () => {
313
326
 
314
327
  assert.match(
315
328
  block,
316
- /RUN_SHA: \$\{\{ github\.sha \}\}/,
329
+ /RUN_SHA: \$\{\{ github\.event\.pull_request\.head\.sha \|\| github\.sha \}\}/,
317
330
  `${fileName}: ${GATE_JOB} cannot tell a supersede from an overrun without RUN_SHA`
318
331
  );
319
332
  }