apify-test-tools 0.10.2-beta.13 → 0.10.2-beta.14

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 (113) hide show
  1. package/.github/cli/apify-test-tools.mjs +123 -128
  2. package/.github/workflows/public_platform-tests-claude-investigate-and-fix.yaml +1 -1
  3. package/.github/workflows/public_platform-tests.yaml +36 -22
  4. package/.github/workflows/public_pr-build-test.yaml +7 -6
  5. package/.github/workflows/public_push-build-latest.yaml +6 -4
  6. package/CHANGELOG.md +2 -0
  7. package/CONTRIBUTING.md +8 -6
  8. package/README.md +10 -11
  9. package/bin/actor-ids.ts +38 -0
  10. package/bin/build.ts +1 -1
  11. package/bin/git.ts +20 -92
  12. package/bin/main.ts +87 -73
  13. package/bin/notify.ts +198 -0
  14. package/bin/run-tests.ts +141 -0
  15. package/bin/slack.ts +16 -87
  16. package/bin/stdin-input.ts +98 -7
  17. package/bin/test-report.ts +141 -140
  18. package/bin/types.ts +0 -9
  19. package/dist/bin/actor-ids.d.ts +16 -0
  20. package/dist/bin/actor-ids.d.ts.map +1 -0
  21. package/dist/bin/actor-ids.js +24 -0
  22. package/dist/bin/actor-ids.js.map +1 -0
  23. package/dist/bin/build.js +1 -1
  24. package/dist/bin/build.js.map +1 -1
  25. package/dist/bin/git.d.ts +6 -16
  26. package/dist/bin/git.d.ts.map +1 -1
  27. package/dist/bin/git.js +15 -75
  28. package/dist/bin/git.js.map +1 -1
  29. package/dist/bin/main.js +80 -51
  30. package/dist/bin/main.js.map +1 -1
  31. package/dist/bin/notify.d.ts +35 -0
  32. package/dist/bin/notify.d.ts.map +1 -0
  33. package/dist/bin/notify.js +131 -0
  34. package/dist/bin/notify.js.map +1 -0
  35. package/dist/bin/run-tests.d.ts +21 -0
  36. package/dist/bin/run-tests.d.ts.map +1 -0
  37. package/dist/bin/run-tests.js +116 -0
  38. package/dist/bin/run-tests.js.map +1 -0
  39. package/dist/bin/slack.d.ts +2 -17
  40. package/dist/bin/slack.d.ts.map +1 -1
  41. package/dist/bin/slack.js +17 -58
  42. package/dist/bin/slack.js.map +1 -1
  43. package/dist/bin/stdin-input.d.ts +71 -9
  44. package/dist/bin/stdin-input.d.ts.map +1 -1
  45. package/dist/bin/stdin-input.js +76 -6
  46. package/dist/bin/stdin-input.js.map +1 -1
  47. package/dist/bin/test-report.d.ts +69 -8
  48. package/dist/bin/test-report.d.ts.map +1 -1
  49. package/dist/bin/test-report.js +76 -67
  50. package/dist/bin/test-report.js.map +1 -1
  51. package/dist/bin/types.d.ts +0 -8
  52. package/dist/bin/types.d.ts.map +1 -1
  53. package/dist/lib/lib.d.ts +0 -3
  54. package/dist/lib/lib.d.ts.map +1 -1
  55. package/dist/lib/lib.js +5 -10
  56. package/dist/lib/lib.js.map +1 -1
  57. package/dist/test/bundle/cli-bundle.test.js +40 -48
  58. package/dist/test/bundle/cli-bundle.test.js.map +1 -1
  59. package/dist/test/e2e/cli-bundle.e2e.test.js +15 -16
  60. package/dist/test/e2e/cli-bundle.e2e.test.js.map +1 -1
  61. package/dist/test/e2e/git-build-release.e2e.test.js +2 -2
  62. package/dist/test/e2e/git-build-release.e2e.test.js.map +1 -1
  63. package/dist/test/unit/bin/actor-ids.test.d.ts +2 -0
  64. package/dist/test/unit/bin/actor-ids.test.d.ts.map +1 -0
  65. package/dist/test/unit/bin/actor-ids.test.js +32 -0
  66. package/dist/test/unit/bin/actor-ids.test.js.map +1 -0
  67. package/dist/test/unit/bin/build-cli.test.js +8 -3
  68. package/dist/test/unit/bin/build-cli.test.js.map +1 -1
  69. package/dist/test/unit/bin/diff-changes.test.js +1 -1
  70. package/dist/test/unit/bin/diff-changes.test.js.map +1 -1
  71. package/dist/test/unit/bin/git.test.js +4 -142
  72. package/dist/test/unit/bin/git.test.js.map +1 -1
  73. package/dist/test/unit/bin/notify.test.d.ts +2 -0
  74. package/dist/test/unit/bin/notify.test.d.ts.map +1 -0
  75. package/dist/test/unit/bin/notify.test.js +222 -0
  76. package/dist/test/unit/bin/notify.test.js.map +1 -0
  77. package/dist/test/unit/bin/stdin-input.test.js +51 -9
  78. package/dist/test/unit/bin/stdin-input.test.js.map +1 -1
  79. package/dist/test/unit/bin/test-cli.test.d.ts +2 -0
  80. package/dist/test/unit/bin/test-cli.test.d.ts.map +1 -0
  81. package/dist/test/unit/bin/test-cli.test.js +115 -0
  82. package/dist/test/unit/bin/test-cli.test.js.map +1 -0
  83. package/dist/test/unit/bin/test-report.test.d.ts +2 -0
  84. package/dist/test/unit/bin/test-report.test.d.ts.map +1 -0
  85. package/dist/test/unit/bin/test-report.test.js +100 -0
  86. package/dist/test/unit/bin/test-report.test.js.map +1 -0
  87. package/dist/tsconfig.tsbuildinfo +1 -1
  88. package/docs/cli.md +131 -53
  89. package/docs/lib.md +21 -16
  90. package/docs/workflows.md +24 -23
  91. package/lib/lib.ts +5 -10
  92. package/package.json +1 -1
  93. package/test/bundle/cli-bundle.test.ts +47 -60
  94. package/test/e2e/cli-bundle.e2e.test.ts +18 -25
  95. package/test/e2e/git-build-release.e2e.test.ts +2 -2
  96. package/test/unit/bin/actor-ids.test.ts +43 -0
  97. package/test/unit/bin/build-cli.test.ts +9 -3
  98. package/test/unit/bin/diff-changes.test.ts +1 -1
  99. package/test/unit/bin/git.test.ts +3 -197
  100. package/test/unit/bin/notify.test.ts +299 -0
  101. package/test/unit/bin/stdin-input.test.ts +68 -9
  102. package/test/unit/bin/test-cli.test.ts +137 -0
  103. package/test/unit/bin/test-report.test.ts +118 -0
  104. package/dist/test/unit/bin/logging.test.d.ts +0 -2
  105. package/dist/test/unit/bin/logging.test.d.ts.map +0 -1
  106. package/dist/test/unit/bin/logging.test.js +0 -37
  107. package/dist/test/unit/bin/logging.test.js.map +0 -1
  108. package/dist/test/unit/bin/slack.test.d.ts +0 -2
  109. package/dist/test/unit/bin/slack.test.d.ts.map +0 -1
  110. package/dist/test/unit/bin/slack.test.js +0 -71
  111. package/dist/test/unit/bin/slack.test.js.map +0 -1
  112. package/test/unit/bin/logging.test.ts +0 -48
  113. package/test/unit/bin/slack.test.ts +0 -84
@@ -46,8 +46,6 @@ const runCliWithInput = (input: string, ...args: string[]) => {
46
46
 
47
47
  const runCli = (...args: string[]) => runCliWithInput('', ...args);
48
48
 
49
- const gitRange = ['--source-branch', 'feature', '--target-branch', 'main'];
50
-
51
49
  describe('CLI bundle', () => {
52
50
  // Two Actors on `main`; `feature` changes only tester/foo and the root README.
53
51
  beforeAll(async () => {
@@ -91,14 +89,11 @@ describe('CLI bundle', () => {
91
89
  const { status, stdout } = runCli('--help');
92
90
  expect(status).toBe(0);
93
91
  for (const command of [
94
- 'get-commits',
95
- 'get-latest-commit',
96
- 'get-changed-files',
97
92
  'get-actor-configs',
98
93
  'get-affected-actors',
99
- 'report-tests',
94
+ 'test [filters..]',
95
+ 'notify',
100
96
  'build',
101
- 'notify-release',
102
97
  'build-from-local',
103
98
  'delete-old-builds',
104
99
  ]) {
@@ -106,27 +101,6 @@ describe('CLI bundle', () => {
106
101
  }
107
102
  });
108
103
 
109
- it('get-commits returns the commits on the source branch', () => {
110
- const { status, stdout, stderr } = runCli('get-commits', ...gitRange);
111
- expect(status, stderr).toBe(0);
112
- expect(JSON.parse(stdout).map((commit: { message: string }) => commit.message)).toEqual([
113
- 'fix(foo): change foo',
114
- 'docs: readme',
115
- ]);
116
- });
117
-
118
- it('get-latest-commit returns the head of the source branch', () => {
119
- const { status, stdout, stderr } = runCli('get-latest-commit', ...gitRange);
120
- expect(status, stderr).toBe(0);
121
- expect(JSON.parse(stdout)).toMatchObject({ sha: headSha, message: 'docs: readme' });
122
- });
123
-
124
- it('get-changed-files returns the files changed on the source branch', () => {
125
- const { status, stdout, stderr } = runCli('get-changed-files', ...gitRange);
126
- expect(status, stderr).toBe(0);
127
- expect(JSON.parse(stdout)).toEqual(['README.md', 'actors/foo/main.js']);
128
- });
129
-
130
104
  it('get-actor-configs reads both Actors from the config file', () => {
131
105
  const { status, stdout, stderr } = runCli('get-actor-configs');
132
106
  expect(status, stderr).toBe(0);
@@ -149,6 +123,14 @@ describe('CLI bundle', () => {
149
123
  });
150
124
  });
151
125
 
126
+ it.each(['info', 'silent'])('keeps JSON results intact with --log-level %s', (level) => {
127
+ const { status, stdout, stderr } = runCli('get-affected-actors', '--base', 'main', '--log-level', level);
128
+ expect(status, stderr).toBe(0);
129
+ expect(stderr).toEqual(level === 'silent' ? '' : expect.stringContaining('Changed files'));
130
+ expect(JSON.parse(stdout)).toMatchObject({ head: headSha });
131
+ expect(stdout.endsWith('\n')).toBe(true);
132
+ });
133
+
152
134
  it('build --dry-run with nothing to build needs no token', () => {
153
135
  const { status, stdout, stderr } = runCliWithInput(
154
136
  JSON.stringify({ actors: [] }),
@@ -189,47 +171,52 @@ describe('CLI bundle', () => {
189
171
  });
190
172
  });
191
173
 
192
- it('notify-release announces nothing, and needs no token, when there are no new commits', () => {
174
+ it('notify announces no release, and needs no token, when there are no new commits', () => {
193
175
  const affected = runCli('get-affected-actors', '--since', headSha).stdout;
194
- const { status, stderr } = runCliWithInput(affected, 'notify-release', '--release-slack-channel', '#releases');
176
+ const { status, stderr } = runCliWithInput(affected, 'notify', '--slack-channel', '#releases');
195
177
  expect(status, stderr).toBe(0);
196
178
  expect(stderr).toContain('No new commits, so there is no release to announce');
197
179
  });
198
180
 
199
- it('notify-release skips Slack without a channel', () => {
181
+ it('notify skips a release without a channel', () => {
200
182
  const affected = runCli('get-affected-actors', '--since', baseSha).stdout;
201
- const { status, stderr } = runCliWithInput(affected, 'notify-release', '--dry-run');
183
+ const { status, stderr } = runCliWithInput(affected, 'notify', '--dry-run');
202
184
  expect(status, stderr).toBe(0);
203
- expect(stderr).toContain('Skipping slack notification');
185
+ expect(stderr).toContain('Skipping notification, no --slack-channel passed');
204
186
  });
205
187
 
206
- it('report-tests summarizes a vitest JSON report', async () => {
207
- const assertion = (title: string, status: 'passed' | 'failed', runId: string) => ({
208
- ancestorTitles: [],
209
- fullName: `foo ${title}`,
210
- title,
211
- status,
212
- failureMessages: status === 'failed' ? ['AssertionError: expected 1 to be 2\n at foo.test.ts:1:1'] : [],
213
- meta: { runId, runLink: `https://console.apify.com/view/runs/${runId}`, actorId: 'tester/foo' },
214
- });
215
- await writeRepoFile(
216
- 'test-output.json',
217
- JSON.stringify({
218
- success: false,
219
- testResults: [
220
- {
221
- name: 'test/platform/foo.test.ts',
222
- status: 'failed',
223
- assertionResults: [assertion('works', 'passed', 'run1'), assertion('fails', 'failed', 'run2')],
224
- },
225
- ],
226
- }),
227
- );
188
+ it('test fails without TESTER_APIFY_TOKEN instead of passing with every test skipped', () => {
189
+ // Every Actor in the config, on its default build, as the scheduled tests pipe them in
190
+ const { status, stdout, stderr } = runCliWithInput(runCli('get-actor-configs').stdout, 'test');
191
+ expect(status).toBe(1);
192
+ expect(stderr).toContain('TESTER_APIFY_TOKEN is not set');
193
+ expect(stdout).toBe('');
194
+ });
228
195
 
229
- const { status, stderr } = runCli('report-tests', '--report-file', 'test-output.json', '--dry-run');
230
- expect(status, stderr).toBe(0);
231
- expect(stderr).toContain('PASSED: 1, FAILED: 1');
232
- expect(stderr).toContain('1) foo fails ... https://console.apify.com/view/runs/run2');
233
- expect(stderr).toContain('Skipping slack notification');
196
+ it('notify needs the Slack token to post a failed report, even in a dry run', () => {
197
+ const report = {
198
+ kind: 'test-report',
199
+ version: 1,
200
+ success: false,
201
+ summary: { total: 1, passed: 0, failed: 1, skipped: 0 },
202
+ tests: [
203
+ {
204
+ name: 'tester/foo: fails',
205
+ file: 'test/platform/foo.test.ts',
206
+ status: 'failed',
207
+ durationMs: 1,
208
+ failureMessages: ['AssertionError: expected 1 to be 2'],
209
+ run: { actorId: 'tester/foo', runId: 'run2', runLink: 'https://console.apify.com/view/runs/run2' },
210
+ },
211
+ ],
212
+ errors: [],
213
+ };
214
+ const withChannel = runCliWithInput(JSON.stringify(report), 'notify', '--slack-channel', '#notif', '--dry-run');
215
+ expect(withChannel.status).toBe(1);
216
+ expect(withChannel.stderr).toContain('SLACK_BOT_TOKEN not defined');
217
+
218
+ const withoutChannel = runCliWithInput(JSON.stringify(report), 'notify', '--dry-run');
219
+ expect(withoutChannel.status, withoutChannel.stderr).toBe(0);
220
+ expect(withoutChannel.stderr).toContain('Skipping notification');
234
221
  });
235
222
  });
@@ -1,5 +1,5 @@
1
1
  import { execFileSync } from 'node:child_process';
2
- import { mkdir, mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises';
2
+ import { mkdir, mkdtemp, rm, symlink, writeFile } from 'node:fs/promises';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import process from 'node:process';
@@ -9,6 +9,7 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
9
9
 
10
10
  import type { ActorBuild } from './helpers.js';
11
11
  import {
12
+ bundlePath,
12
13
  cliEnv,
13
14
  gitEnv,
14
15
  projectRoot,
@@ -21,8 +22,8 @@ import {
21
22
 
22
23
  // End-to-end run of the shipped CLI bundle against two dummy Actors in an isolated sandbox account:
23
24
  // dry-run plans read the real Actors, `build-from-local` really builds them, the lib runs them
24
- // through `testActor` on exactly those builds (the ACTOR_BUILDS handoff), `report-tests` reads the
25
- // result, and `delete-old-builds` cleans up. Git builds of a real repository are in
25
+ // through `testActor` on exactly those builds (`build-from-local | test`), `notify` reads the
26
+ // report, and `delete-old-builds` cleans up. Git builds of a real repository are in
26
27
  // git-build-release.e2e.test.ts.
27
28
  //
28
29
  // Runs with `npm run test:e2e`, kept out of `npm test`. Skipped without the token, e.g. on fork PRs.
@@ -30,8 +31,6 @@ const ACTORS = ['test-tools-testing-sandbox/dummy-for-testing-1', 'test-tools-te
30
31
  // Only used as the source URL in dry-run plans; nothing is cloned from it.
31
32
  const REPO_URL = 'https://github.com/apify/apify-test-tools-e2e-fixture';
32
33
 
33
- const vitestPath = path.join(projectRoot, 'node_modules/vitest/vitest.mjs');
34
-
35
34
  let repoDir: string;
36
35
  let baseSha: string;
37
36
 
@@ -166,34 +165,28 @@ describe('sandbox dummies', () => {${actorTests}
166
165
  `,
167
166
  );
168
167
 
169
- const vitest = await runProcess(
170
- process.execPath,
171
- [vitestPath, 'run', 'test/platform', '--reporter=json', '--outputFile=./test-output.json'],
172
- {
173
- cwd: repoDir,
174
- env: { ...cliEnv, TESTER_APIFY_TOKEN: token!, ACTOR_BUILDS: JSON.stringify(builds) },
175
- },
176
- );
177
- expect(vitest.status, `${vitest.stdout}\n${vitest.stderr}`).toBe(0);
168
+ const tested = await runProcess(process.execPath, [bundlePath, 'test', 'test/platform'], {
169
+ cwd: repoDir,
170
+ env: { ...cliEnv, TESTER_APIFY_TOKEN: token! },
171
+ input: JSON.stringify(builds),
172
+ });
173
+ expect(tested.status, tested.stderr).toBe(0);
174
+ expect(tested.stderr).toContain('https://console.apify.com/view/runs/');
178
175
 
179
- // Each run used the build from ACTOR_BUILDS, not the Actor's default build.
180
- const report = JSON.parse(await readFile(path.join(repoDir, 'test-output.json'), 'utf8'));
181
- const assertions = report.testResults.flatMap(
182
- (file: { assertionResults: { meta: { runId: string } }[] }) => file.assertionResults,
183
- );
184
- expect(assertions).toHaveLength(ACTORS.length);
176
+ // Each run used the build piped into `test`, not the Actor's default build.
177
+ const report = JSON.parse(tested.stdout);
178
+ expect(report).toMatchObject({ success: true, summary: { passed: ACTORS.length, failed: 0 } });
185
179
  const client = new ApifyClient({ token });
186
180
  const runBuildIds = await Promise.all(
187
- assertions.map(
188
- async ({ meta }: { meta: { runId: string } }) => (await client.run(meta.runId).get())?.buildId,
181
+ report.tests.map(
182
+ async ({ run }: { run: { runId: string } }) => (await client.run(run.runId).get())?.buildId,
189
183
  ),
190
184
  );
191
185
  expect(runBuildIds.sort()).toEqual(builds.map((build) => build.buildId).sort());
192
186
 
193
- const { status, stderr } = await runCli('report-tests', '--report-file', 'test-output.json', '--dry-run');
187
+ const { status, stderr } = await runCliWithInput(tested.stdout, 'notify', '--dry-run');
194
188
  expect(status, stderr).toBe(0);
195
- expect(stderr).toContain(`PASSED: ${ACTORS.length}, FAILED: 0`);
196
- expect(stderr).toContain('https://console.apify.com/view/runs/');
189
+ expect(stderr).toContain('Skipping notification');
197
190
  });
198
191
 
199
192
  // Last, so builds from earlier runs of this suite don't pile up in the sandbox.
@@ -106,8 +106,8 @@ describe.runIf(token)('CLI bundle building a real repository', { timeout: 2 * 60
106
106
  expect(await versionSource('0.0')).toBe(BUILD_SOURCE);
107
107
 
108
108
  // Without a channel the announcement is skipped, so it needs no Slack token
109
- const notify = await runCliWithInput(affected.stdout, 'notify-release');
109
+ const notify = await runCliWithInput(affected.stdout, 'notify');
110
110
  expect(notify.status, notify.stderr).toBe(0);
111
- expect(notify.stderr).toContain('Skipping slack notification');
111
+ expect(notify.stderr).toContain('Skipping notification');
112
112
  });
113
113
  });
@@ -0,0 +1,43 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ import { withActorIds } from '../../../bin/actor-ids.js';
4
+
5
+ type Client = NonNullable<Parameters<typeof withActorIds>[1]>;
6
+
7
+ /** A client that knows one Actor, by its full name and by its raw ID, as the API does */
8
+ const fakeClient = () => {
9
+ const known = { id: 'rawA', username: 'owner', name: 'a' };
10
+ const actor = vi.fn((actorId: string) => ({
11
+ get: async () => (actorId === 'owner/a' || actorId === 'rawA' ? known : undefined),
12
+ }));
13
+ return { actor } as unknown as Client & { actor: typeof actor };
14
+ };
15
+
16
+ describe('withActorIds', () => {
17
+ it('adds the raw ID to an Actor listed by full name, as get-actor-configs lists them', async () => {
18
+ expect(await withActorIds([{ actorFullName: 'owner/a' }], fakeClient())).toEqual([
19
+ { actorFullName: 'owner/a', actorRawId: 'rawA' },
20
+ ]);
21
+ });
22
+
23
+ it('adds the full name to an Actor listed by raw ID', async () => {
24
+ const build = { actorRawId: 'rawA', buildId: 'b1', buildNumber: '0.99.1' };
25
+ expect(await withActorIds([build], fakeClient())).toEqual([{ ...build, actorFullName: 'owner/a' }]);
26
+ });
27
+
28
+ it('keeps the IDs of a build and does not look them up', async () => {
29
+ const client = fakeClient();
30
+ const build = { actorFullName: 'owner/a', buildId: 'b1', actorRawId: 'raw1', buildNumber: '0.99.1' };
31
+ expect(await withActorIds([build], client)).toEqual([build]);
32
+ expect(client.actor).not.toHaveBeenCalled();
33
+ });
34
+
35
+ it('fails for an Actor the token cannot see, instead of skipping its tests', async () => {
36
+ await expect(withActorIds([{ actorFullName: 'owner/missing' }], fakeClient())).rejects.toThrow(
37
+ 'Actor owner/missing not found',
38
+ );
39
+ await expect(withActorIds([{ actorRawId: 'rawMissing' }], fakeClient())).rejects.toThrow(
40
+ 'Actor rawMissing not found',
41
+ );
42
+ });
43
+ });
@@ -12,7 +12,7 @@ const run = (command: string[], input: string, env: NodeJS.ProcessEnv = process.
12
12
  spawnSync(cliPath, command, { encoding: 'utf8', input, stdio: ['pipe', 'pipe', 'pipe'], env });
13
13
  const runBuild = (input: string, args: string[] = ['--version-number', '0.99']) => run(['build', ...args], input);
14
14
 
15
- describe('build and notify-release CLI', () => {
15
+ describe('build and notify CLI', () => {
16
16
  beforeAll(async () => {
17
17
  ({ cliPath, cleanup } = await compileCli());
18
18
  });
@@ -46,9 +46,15 @@ describe('build and notify-release CLI', () => {
46
46
  expect(result.stderr).toContain('Missing required argument: version-number');
47
47
  });
48
48
 
49
- it('notify-release announces nothing when there are no new commits', () => {
49
+ it('notify takes one channel per call', () => {
50
+ const result = run(['notify', '--slack-channel', '#a', '--slack-channel', '#b'], '{}');
51
+ expect(result.status).toBe(1);
52
+ expect(result.stderr).toContain('--slack-channel takes one channel, call notify once for each channel');
53
+ });
54
+
55
+ it('notify announces no release when there are no new commits', () => {
50
56
  const document = { commits: [], changedFiles: [], changelog: null, functionallyChangedActors: [] };
51
- const result = run(['notify-release'], JSON.stringify(document));
57
+ const result = run(['notify', '--slack-channel', '#notif'], JSON.stringify(document));
52
58
  expect(result.status, result.stderr).toBe(0);
53
59
  expect(result.stderr).toContain('No new commits, so there is no release to announce');
54
60
  });
@@ -508,7 +508,7 @@ describe('getChangedActors', () => {
508
508
  expect(result).toEqual([]);
509
509
  });
510
510
 
511
- it('does not special-case code/ and shared/ prefixes anymore — must be declared via overrideActorContext', async () => {
511
+ it('does not special-case code/ and shared/ prefixes — they must be declared via overrideActorContext', async () => {
512
512
  const result = await changedActorsFor({
513
513
  filepathsChanged: ['code/some/code.ts'],
514
514
  actorConfigs: [miniActor],
@@ -1,206 +1,18 @@
1
- import type { MockInstance } from 'vitest';
2
1
  import { beforeEach, describe, expect, it, vi } from 'vitest';
3
2
 
4
3
  import {
5
4
  findCommitSha,
6
- getChangedFiles,
7
5
  getChangedFilesBetween,
8
- getCommits,
9
6
  getCommitsBetween,
10
7
  getCurrentBranch,
11
8
  getRepoName,
12
9
  normalizeRepoUrl,
13
- parseBaseCommit,
14
10
  resolveCommitSha,
15
11
  resolveReleaseBaseCommit,
16
12
  resolveRepoUrl,
17
13
  } from '../../../bin/git.js';
18
14
  import * as Utils from '../../../bin/utils.js';
19
15
 
20
- describe('getCommits', () => {
21
- const sourceBranch = 'feature-branch';
22
- const targetBranch = 'main';
23
-
24
- const sha1 = '1'.repeat(40);
25
- const sha2 = '2'.repeat(40);
26
- const sha3 = '3'.repeat(40);
27
-
28
- const commit1 = `${sha1}»¦«Author1»¦«Date1»¦«First Change On Feature`;
29
- const commit2 = `${sha2}»¦«Author1»¦«Date2»¦«Second Change On Feature`;
30
- const commit3 = `${sha3}»¦«Author1»¦«Date3»¦«Third Change On Feature`;
31
-
32
- let gitCommandSpy: MockInstance;
33
-
34
- beforeEach(() => {
35
- gitCommandSpy = vi.spyOn(Utils, 'runGitCommand').mockResolvedValue(`${commit3}\n${commit2}\n${commit1}`);
36
- });
37
-
38
- it('should return commits between source and target branches', async () => {
39
- // Act
40
- const commits = await getCommits({ sourceBranch, targetBranch });
41
-
42
- // Assert
43
- expect(commits).toStrictEqual([
44
- { sha: sha1, author: 'Author1', date: 'Date1', message: 'First Change On Feature' },
45
- { sha: sha2, author: 'Author1', date: 'Date2', message: 'Second Change On Feature' },
46
- { sha: sha3, author: 'Author1', date: 'Date3', message: 'Third Change On Feature' },
47
- ]);
48
-
49
- expect(gitCommandSpy).toHaveBeenCalledTimes(1);
50
- expect(gitCommandSpy).toHaveBeenCalledWith(
51
- ['log', '--pretty=format:%H»¦«%aN<%aE>»¦«%aD»¦«%s', 'main..feature-branch'],
52
- { operation: 'list commits reachable from the source ref but absent from the target ref' },
53
- );
54
- });
55
-
56
- it('should return commits after the base commit if provided', async () => {
57
- // Act
58
- const commits = await getCommits({ sourceBranch, targetBranch, baseCommit: sha1 });
59
-
60
- // Assert
61
- expect(commits).toStrictEqual([
62
- { sha: sha2, author: 'Author1', date: 'Date2', message: 'Second Change On Feature' },
63
- { sha: sha3, author: 'Author1', date: 'Date3', message: 'Third Change On Feature' },
64
- ]);
65
-
66
- expect(gitCommandSpy).toHaveBeenCalledTimes(1);
67
- expect(gitCommandSpy).toHaveBeenCalledWith(
68
- ['log', '--pretty=format:%H»¦«%aN<%aE>»¦«%aD»¦«%s', 'main..feature-branch'],
69
- { operation: 'list commits reachable from the source ref but absent from the target ref' },
70
- );
71
- });
72
-
73
- it('should ignore the base commit and return all commits when it is the branch HEAD (rerun or force push)', async () => {
74
- // Act
75
- const commits = await getCommits({ sourceBranch, targetBranch, baseCommit: sha3 });
76
-
77
- // Assert
78
- expect(commits).toStrictEqual([
79
- { sha: sha1, author: 'Author1', date: 'Date1', message: 'First Change On Feature' },
80
- { sha: sha2, author: 'Author1', date: 'Date2', message: 'Second Change On Feature' },
81
- { sha: sha3, author: 'Author1', date: 'Date3', message: 'Third Change On Feature' },
82
- ]);
83
- });
84
-
85
- it('should return all commits if base commit is not found', async () => {
86
- // Act
87
- const commits = await getCommits({ sourceBranch, targetBranch, baseCommit: 'a'.repeat(40) });
88
-
89
- // Assert
90
- expect(commits).toStrictEqual([
91
- { sha: sha1, author: 'Author1', date: 'Date1', message: 'First Change On Feature' },
92
- { sha: sha2, author: 'Author1', date: 'Date2', message: 'Second Change On Feature' },
93
- { sha: sha3, author: 'Author1', date: 'Date3', message: 'Third Change On Feature' },
94
- ]);
95
-
96
- expect(gitCommandSpy).toHaveBeenCalledTimes(1);
97
- expect(gitCommandSpy).toHaveBeenCalledWith(
98
- ['log', '--pretty=format:%H»¦«%aN<%aE>»¦«%aD»¦«%s', 'main..feature-branch'],
99
- { operation: 'list commits reachable from the source ref but absent from the target ref' },
100
- );
101
- });
102
- });
103
-
104
- describe('getChangedFiles', () => {
105
- let gitCommandSpy: MockInstance;
106
-
107
- beforeEach(() => {
108
- gitCommandSpy = vi.spyOn(Utils, 'runGitCommand').mockResolvedValue('file1.txt\nfolder/file2.txt');
109
- });
110
-
111
- it('should return changed files between commits', async () => {
112
- // Arrange
113
- const firstSha = '1'.repeat(40);
114
- const lastSha = '3'.repeat(40);
115
- const commits = [
116
- { sha: firstSha, author: '', date: '', message: '' },
117
- { sha: lastSha, author: '', date: '', message: '' },
118
- ];
119
-
120
- // Act
121
- const changedFiles = await getChangedFiles(commits);
122
-
123
- // Assert
124
- expect(changedFiles).toStrictEqual(['file1.txt', 'folder/file2.txt']);
125
-
126
- expect(gitCommandSpy).toHaveBeenCalledTimes(1);
127
- expect(gitCommandSpy).toHaveBeenCalledWith(
128
- ['diff', '--name-only', '--no-renames', `${firstSha}~..${lastSha}`],
129
- {
130
- operation: 'get the list of changed files between commits',
131
- },
132
- );
133
- });
134
-
135
- it('should throw without running git when the commit list is empty', async () => {
136
- // Act & Assert
137
- await expect(getChangedFiles([])).rejects.toThrow('Cannot get changed files: the commit list is empty');
138
- expect(gitCommandSpy).not.toHaveBeenCalled();
139
- });
140
-
141
- it('should handle only one commit', async () => {
142
- // Arrange
143
- const onlySha = '1'.repeat(40);
144
- const commits = [{ sha: onlySha, author: '', date: '', message: '' }];
145
-
146
- // Act
147
- const changedFiles = await getChangedFiles(commits);
148
-
149
- // Assert
150
- expect(changedFiles).toStrictEqual(['file1.txt', 'folder/file2.txt']);
151
-
152
- expect(gitCommandSpy).toHaveBeenCalledTimes(1);
153
- expect(gitCommandSpy).toHaveBeenCalledWith(['diff', '--name-only', '--no-renames', `${onlySha}~..${onlySha}`], {
154
- operation: 'get the list of changed files between commits',
155
- });
156
- });
157
-
158
- it('should return an empty list when the net diff is empty (e.g. a commit and its revert)', async () => {
159
- // Arrange
160
- gitCommandSpy.mockResolvedValue('');
161
- const commits = [
162
- { sha: '1'.repeat(40), author: '', date: '', message: '' },
163
- { sha: '2'.repeat(40), author: '', date: '', message: '' },
164
- ];
165
-
166
- // Act
167
- const changedFiles = await getChangedFiles(commits);
168
-
169
- // Assert
170
- expect(changedFiles).toStrictEqual([]);
171
- });
172
- });
173
-
174
- const VALID_SHA = 'a'.repeat(40);
175
- const VALID_JSON = JSON.stringify({ sha: VALID_SHA, author: 'test', date: 'now', message: 'msg' });
176
-
177
- describe('parseBaseCommit', () => {
178
- it('should return undefined for undefined input', () => {
179
- expect(parseBaseCommit(undefined)).toBeUndefined();
180
- });
181
-
182
- it('should return undefined for empty string', () => {
183
- expect(parseBaseCommit('')).toBeUndefined();
184
- });
185
-
186
- it('should accept a plain SHA string', () => {
187
- expect(parseBaseCommit(VALID_SHA)).toBe(VALID_SHA);
188
- });
189
-
190
- it('should extract sha from a JSON commit object', () => {
191
- expect(parseBaseCommit(VALID_JSON)).toBe(VALID_SHA);
192
- });
193
-
194
- it('should throw on an invalid SHA string', () => {
195
- expect(() => parseBaseCommit('not-a-sha')).toThrow('Invalid base commit SHA');
196
- });
197
-
198
- it('should throw when JSON contains an invalid sha field', () => {
199
- const badJson = JSON.stringify({ sha: 'bad', author: 'test', date: 'now', message: 'msg' });
200
- expect(() => parseBaseCommit(badJson)).toThrow('Invalid base commit SHA');
201
- });
202
- });
203
-
204
16
  describe('getCurrentBranch', () => {
205
17
  it('should return the checked-out branch', async () => {
206
18
  const gitCommandSpy = vi.spyOn(Utils, 'runGitCommand').mockResolvedValue('master');
@@ -279,7 +91,7 @@ describe('resolveReleaseBaseCommit', () => {
279
91
  });
280
92
  await expect(resolveReleaseBaseCommit(baseSha)).resolves.toBe(baseSha);
281
93
  expect(gitCommandSpy).toHaveBeenCalledWith(['rev-parse', '--verify', '--quiet', `${baseSha}^{commit}`], {
282
- operation: 'verify the release base commit in local Git history',
94
+ operation: `look up ${baseSha} in local Git history`,
283
95
  });
284
96
  expect(gitCommandSpy).toHaveBeenCalledWith(['merge-base', baseSha, 'HEAD'], {
285
97
  operation: 'check whether the release base commit is an ancestor of HEAD',
@@ -348,11 +160,11 @@ describe('resolveReleaseBaseCommit', () => {
348
160
  new Utils.GitCommandError(
349
161
  ['rev-parse', '--verify', '--quiet', `${baseSha}^{commit}`],
350
162
  Object.assign(new Error('Git failed'), { code: 128 }),
351
- 'verify the release base commit in local Git history',
163
+ `look up ${baseSha} in local Git history`,
352
164
  ),
353
165
  );
354
166
  await expect(resolveReleaseBaseCommit(baseSha)).rejects.toThrow(
355
- 'Failed to verify the release base commit in local Git history.\n' +
167
+ `Failed to look up ${baseSha} in local Git history.\n` +
356
168
  `Command: git "rev-parse" "--verify" "--quiet" "${baseSha}^{commit}"\nGit error: Git failed`,
357
169
  );
358
170
  });
@@ -399,12 +211,6 @@ describe('empty commit ranges', () => {
399
211
  it('getCommitsBetween returns no commits', async () => {
400
212
  await expect(getCommitsBetween('HEAD', 'HEAD')).resolves.toStrictEqual([]);
401
213
  });
402
-
403
- it('getCommits fails clearly, since the commands built on it need at least one commit', async () => {
404
- await expect(getCommits({ sourceBranch: 'HEAD', targetBranch: 'HEAD' })).rejects.toThrow(
405
- 'HEAD has no commits that are not on HEAD.',
406
- );
407
- });
408
214
  });
409
215
 
410
216
  describe('getChangedFilesBetween', () => {