apify-test-tools 0.10.2-beta.0 → 0.10.2-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/.gitattributes +2 -0
  2. package/.github/actions/checkout-restore-dependencies/action.yaml +64 -27
  3. package/.github/cli/apify-test-tools.mjs +360 -0
  4. package/.github/scripts/check-major-tag-refs.mjs +1 -1
  5. package/.github/scripts/mask-config-secrets.sh +17 -0
  6. package/.github/scripts/run-with-apify-tokens.mjs +37 -12
  7. package/.github/workflows/_check_code.yaml +25 -0
  8. package/.github/workflows/_move_major_tag.yaml +3 -81
  9. package/.github/workflows/_tests.yaml +34 -0
  10. package/.github/workflows/_update_release_metadata.yaml +0 -12
  11. package/.github/workflows/manual_move_major_tag.yaml +3 -3
  12. package/.github/workflows/manual_release_stable.yaml +3 -13
  13. package/.github/workflows/on_master.yaml +9 -5
  14. package/.github/workflows/on_pull_request.yaml +3 -48
  15. package/.github/workflows/public_platform-tests-claude-investigate-and-fix.yaml +13 -2
  16. package/.github/workflows/public_platform-tests.yaml +24 -1
  17. package/.github/workflows/public_pr-build-test.yaml +62 -49
  18. package/.github/workflows/public_push-build-latest.yaml +41 -8
  19. package/.husky/pre-commit +5 -0
  20. package/.prettierignore +3 -0
  21. package/CHANGELOG.md +17 -0
  22. package/CONTRIBUTING.md +105 -21
  23. package/README.md +218 -597
  24. package/bin/affected-actors.ts +198 -0
  25. package/bin/build-from-local.ts +65 -13
  26. package/bin/build.ts +123 -41
  27. package/bin/diff-changes.ts +55 -25
  28. package/bin/diff-json-schema.ts +5 -6
  29. package/bin/git.ts +69 -99
  30. package/bin/main.ts +96 -122
  31. package/bin/slack.ts +43 -10
  32. package/bin/stdin-input.ts +65 -0
  33. package/bin/test-report.ts +6 -9
  34. package/bin/types.ts +16 -0
  35. package/dist/bin/affected-actors.d.ts +46 -0
  36. package/dist/bin/affected-actors.d.ts.map +1 -0
  37. package/dist/bin/affected-actors.js +135 -0
  38. package/dist/bin/affected-actors.js.map +1 -0
  39. package/dist/bin/build-from-local.d.ts +12 -3
  40. package/dist/bin/build-from-local.d.ts.map +1 -1
  41. package/dist/bin/build-from-local.js +34 -12
  42. package/dist/bin/build-from-local.js.map +1 -1
  43. package/dist/bin/build.d.ts +30 -7
  44. package/dist/bin/build.d.ts.map +1 -1
  45. package/dist/bin/build.js +85 -26
  46. package/dist/bin/build.js.map +1 -1
  47. package/dist/bin/diff-changes.d.ts +12 -4
  48. package/dist/bin/diff-changes.d.ts.map +1 -1
  49. package/dist/bin/diff-changes.js +31 -20
  50. package/dist/bin/diff-changes.js.map +1 -1
  51. package/dist/bin/diff-json-schema.d.ts +2 -2
  52. package/dist/bin/diff-json-schema.d.ts.map +1 -1
  53. package/dist/bin/diff-json-schema.js +1 -4
  54. package/dist/bin/diff-json-schema.js.map +1 -1
  55. package/dist/bin/git.d.ts +23 -21
  56. package/dist/bin/git.d.ts.map +1 -1
  57. package/dist/bin/git.js +60 -83
  58. package/dist/bin/git.js.map +1 -1
  59. package/dist/bin/main.js +82 -93
  60. package/dist/bin/main.js.map +1 -1
  61. package/dist/bin/slack.d.ts +14 -4
  62. package/dist/bin/slack.d.ts.map +1 -1
  63. package/dist/bin/slack.js +39 -6
  64. package/dist/bin/slack.js.map +1 -1
  65. package/dist/bin/stdin-input.d.ts +17 -0
  66. package/dist/bin/stdin-input.d.ts.map +1 -0
  67. package/dist/bin/stdin-input.js +54 -0
  68. package/dist/bin/stdin-input.js.map +1 -0
  69. package/dist/bin/test-report.d.ts.map +1 -1
  70. package/dist/bin/test-report.js +6 -8
  71. package/dist/bin/test-report.js.map +1 -1
  72. package/dist/bin/types.d.ts +14 -0
  73. package/dist/bin/types.d.ts.map +1 -1
  74. package/dist/lib/extend-expect.d.ts.map +1 -1
  75. package/dist/lib/extend-expect.js +37 -218
  76. package/dist/lib/extend-expect.js.map +1 -1
  77. package/dist/lib/lib.d.ts +1 -2
  78. package/dist/lib/lib.d.ts.map +1 -1
  79. package/dist/lib/lib.js +15 -22
  80. package/dist/lib/lib.js.map +1 -1
  81. package/dist/lib/run-checks.d.ts +14 -0
  82. package/dist/lib/run-checks.d.ts.map +1 -0
  83. package/dist/lib/run-checks.js +82 -0
  84. package/dist/lib/run-checks.js.map +1 -0
  85. package/dist/lib/run-test-result.d.ts +3 -6
  86. package/dist/lib/run-test-result.d.ts.map +1 -1
  87. package/dist/lib/run-test-result.js +41 -54
  88. package/dist/lib/run-test-result.js.map +1 -1
  89. package/dist/test/bundle/cli-bundle.test.d.ts +2 -0
  90. package/dist/test/bundle/cli-bundle.test.d.ts.map +1 -0
  91. package/dist/test/bundle/cli-bundle.test.js +190 -0
  92. package/dist/test/bundle/cli-bundle.test.js.map +1 -0
  93. package/dist/test/e2e/cli-bundle.e2e.test.d.ts +2 -0
  94. package/dist/test/e2e/cli-bundle.e2e.test.d.ts.map +1 -0
  95. package/dist/test/e2e/cli-bundle.e2e.test.js +138 -0
  96. package/dist/test/e2e/cli-bundle.e2e.test.js.map +1 -0
  97. package/dist/test/e2e/git-build-release.e2e.test.d.ts +2 -0
  98. package/dist/test/e2e/git-build-release.e2e.test.d.ts.map +1 -0
  99. package/dist/test/e2e/git-build-release.e2e.test.js +87 -0
  100. package/dist/test/e2e/git-build-release.e2e.test.js.map +1 -0
  101. package/dist/test/e2e/helpers.d.ts +37 -0
  102. package/dist/test/e2e/helpers.d.ts.map +1 -0
  103. package/dist/test/e2e/helpers.js +57 -0
  104. package/dist/test/e2e/helpers.js.map +1 -0
  105. package/dist/test/unit/bin/affected-actors-cli.test.d.ts +2 -0
  106. package/dist/test/unit/bin/affected-actors-cli.test.d.ts.map +1 -0
  107. package/dist/test/unit/bin/affected-actors-cli.test.js +167 -0
  108. package/dist/test/unit/bin/affected-actors-cli.test.js.map +1 -0
  109. package/dist/test/unit/bin/affected-actors.test.d.ts +2 -0
  110. package/dist/test/unit/bin/affected-actors.test.d.ts.map +1 -0
  111. package/dist/test/unit/bin/affected-actors.test.js +141 -0
  112. package/dist/test/unit/bin/affected-actors.test.js.map +1 -0
  113. package/dist/test/unit/bin/build-cli.test.d.ts +2 -0
  114. package/dist/test/unit/bin/build-cli.test.d.ts.map +1 -0
  115. package/dist/test/unit/bin/build-cli.test.js +44 -0
  116. package/dist/test/unit/bin/build-cli.test.js.map +1 -0
  117. package/dist/test/unit/bin/build-from-local.test.js +65 -1
  118. package/dist/test/unit/bin/build-from-local.test.js.map +1 -1
  119. package/dist/test/unit/bin/build.test.js +223 -3
  120. package/dist/test/unit/bin/build.test.js.map +1 -1
  121. package/dist/test/unit/bin/compiled-cli.d.ts +6 -0
  122. package/dist/test/unit/bin/compiled-cli.d.ts.map +1 -0
  123. package/dist/test/unit/bin/compiled-cli.js +16 -0
  124. package/dist/test/unit/bin/compiled-cli.js.map +1 -0
  125. package/dist/test/unit/bin/diff-changes.test.js +205 -85
  126. package/dist/test/unit/bin/diff-changes.test.js.map +1 -1
  127. package/dist/test/unit/bin/diff.test.js +25 -28
  128. package/dist/test/unit/bin/diff.test.js.map +1 -1
  129. package/dist/test/unit/bin/git.test.js +69 -152
  130. package/dist/test/unit/bin/git.test.js.map +1 -1
  131. package/dist/test/unit/bin/slack.test.d.ts +2 -0
  132. package/dist/test/unit/bin/slack.test.d.ts.map +1 -0
  133. package/dist/test/unit/bin/slack.test.js +71 -0
  134. package/dist/test/unit/bin/slack.test.js.map +1 -0
  135. package/dist/test/unit/bin/stdin-input.test.d.ts +2 -0
  136. package/dist/test/unit/bin/stdin-input.test.d.ts.map +1 -0
  137. package/dist/test/unit/bin/stdin-input.test.js +50 -0
  138. package/dist/test/unit/bin/stdin-input.test.js.map +1 -0
  139. package/dist/test/unit/github-scripts/mask-config-secrets.test.d.ts +2 -0
  140. package/dist/test/unit/github-scripts/mask-config-secrets.test.d.ts.map +1 -0
  141. package/dist/test/unit/github-scripts/mask-config-secrets.test.js +50 -0
  142. package/dist/test/unit/github-scripts/mask-config-secrets.test.js.map +1 -0
  143. package/dist/test/unit/github-scripts/run-with-apify-tokens.test.d.ts +2 -0
  144. package/dist/test/unit/github-scripts/run-with-apify-tokens.test.d.ts.map +1 -0
  145. package/dist/test/unit/github-scripts/run-with-apify-tokens.test.js +102 -0
  146. package/dist/test/unit/github-scripts/run-with-apify-tokens.test.js.map +1 -0
  147. package/dist/test/unit/run-checks.test.d.ts +2 -0
  148. package/dist/test/unit/run-checks.test.d.ts.map +1 -0
  149. package/dist/test/unit/run-checks.test.js +62 -0
  150. package/dist/test/unit/run-checks.test.js.map +1 -0
  151. package/dist/test/unit/run-test-result.test.d.ts +2 -0
  152. package/dist/test/unit/run-test-result.test.d.ts.map +1 -0
  153. package/dist/test/unit/run-test-result.test.js +34 -0
  154. package/dist/test/unit/run-test-result.test.js.map +1 -0
  155. package/dist/test/unit/should-built-and-test.test.js +41 -38
  156. package/dist/test/unit/should-built-and-test.test.js.map +1 -1
  157. package/dist/tsconfig.tsbuildinfo +1 -1
  158. package/docs/cli.md +373 -0
  159. package/docs/lib.md +271 -0
  160. package/docs/workflows.md +367 -0
  161. package/lib/extend-expect.ts +48 -258
  162. package/lib/lib.ts +24 -28
  163. package/lib/run-checks.ts +101 -0
  164. package/lib/run-test-result.ts +45 -55
  165. package/package.json +15 -3
  166. package/test/bundle/cli-bundle.test.ts +235 -0
  167. package/test/e2e/cli-bundle.e2e.test.ts +207 -0
  168. package/test/e2e/git-build-release.e2e.test.ts +113 -0
  169. package/test/e2e/helpers.ts +82 -0
  170. package/test/unit/bin/affected-actors-cli.test.ts +201 -0
  171. package/test/unit/bin/affected-actors.test.ts +182 -0
  172. package/test/unit/bin/build-cli.test.ts +55 -0
  173. package/test/unit/bin/build-from-local.test.ts +71 -0
  174. package/test/unit/bin/build.test.ts +279 -3
  175. package/test/unit/bin/compiled-cli.ts +17 -0
  176. package/test/unit/bin/diff-changes.test.ts +229 -85
  177. package/test/unit/bin/diff.test.ts +25 -28
  178. package/test/unit/bin/git.test.ts +85 -176
  179. package/test/unit/bin/slack.test.ts +84 -0
  180. package/test/unit/bin/stdin-input.test.ts +61 -0
  181. package/test/unit/github-scripts/mask-config-secrets.test.ts +61 -0
  182. package/test/unit/github-scripts/run-with-apify-tokens.test.ts +115 -0
  183. package/test/unit/run-checks.test.ts +76 -0
  184. package/test/unit/run-test-result.test.ts +44 -0
  185. package/test/unit/should-built-and-test.test.ts +46 -39
  186. package/.github/scripts/check-package-version-bump.mjs +0 -74
  187. package/.github/workflows-package-version +0 -1
@@ -59,59 +59,70 @@ const amazonActor = actorConfig('junglee/amazon-crawler', 'actors/junglee_Amazon
59
59
  'shared',
60
60
  ]);
61
61
 
62
- const commits = [{ sha: 'Commit1', author: '', date: '', message: '' }];
62
+ const range = { fromRef: 'Commit0', toRef: 'Commit1' };
63
+
64
+ // What a PR (functional changes only) or a release (isLatest, all changes) builds, in config order
65
+ const changedActorsFor = async ({
66
+ isLatest = false,
67
+ ...options
68
+ }: Parameters<typeof getChangedActors>[0] & { isLatest?: boolean }): Promise<ActorConfig[]> => {
69
+ const { functional, cosmetic } = await getChangedActors(options);
70
+ const changed = isLatest ? [...functional, ...cosmetic] : functional;
71
+ return options.actorConfigs.filter((config) => changed.includes(config));
72
+ };
63
73
 
64
74
  describe('getChangedActors', () => {
65
75
  beforeEach(() => {
76
+ vi.restoreAllMocks();
66
77
  vi.spyOn(DiffJsonSchema, 'isCosmeticOnlyJsonSchemaChange').mockResolvedValue(false);
67
78
  });
68
79
 
69
80
  it('returns empty array when no files changed', async () => {
70
- expect(await getChangedActors({ filepathsChanged: [], actorConfigs, commits })).toEqual([]);
81
+ expect(await changedActorsFor({ filepathsChanged: [], actorConfigs, range })).toEqual([]);
71
82
  });
72
83
 
73
84
  it('returns empty array when only ignored top-level files changed', async () => {
74
- const result = await getChangedActors({
85
+ const result = await changedActorsFor({
75
86
  filepathsChanged: ['.gitignore', 'README.md', '.husky/pre-commit', '.vscode/settings.json'],
76
87
  actorConfigs,
77
- commits,
88
+ range,
78
89
  });
79
90
  expect(result).toEqual([]);
80
91
  });
81
92
 
82
93
  it('does not ignore source files whose names contain an ignored file name', async () => {
83
- const result = await getChangedActors({
94
+ const result = await changedActorsFor({
84
95
  filepathsChanged: ['actors/foo_bar/src/.gitignore-helper.ts'],
85
96
  actorConfigs,
86
- commits,
97
+ range,
87
98
  });
88
99
  expect(result).toEqual([miniActor]);
89
100
  });
90
101
 
91
102
  it('returns the actor when a functional file in its folder changes', async () => {
92
- const result = await getChangedActors({
103
+ const result = await changedActorsFor({
93
104
  filepathsChanged: ['actors/foo_bar/src/main.ts'],
94
105
  actorConfigs,
95
- commits,
106
+ range,
96
107
  });
97
108
  expect(result).toEqual([miniActor]);
98
109
  });
99
110
 
100
111
  it('returns actor when isLatest and README in actor folder changed (cosmetic)', async () => {
101
- const result = await getChangedActors({
112
+ const result = await changedActorsFor({
102
113
  filepathsChanged: ['actors/foo_bar/README.md'],
103
114
  actorConfigs,
104
- commits,
115
+ range,
105
116
  isLatest: true,
106
117
  });
107
118
  expect(result).toEqual([miniActor]);
108
119
  });
109
120
 
110
121
  it('does not return actor when not isLatest and only README changed (cosmetic)', async () => {
111
- const result = await getChangedActors({
122
+ const result = await changedActorsFor({
112
123
  filepathsChanged: ['actors/foo_bar/README.md'],
113
124
  actorConfigs,
114
- commits,
125
+ range,
115
126
  isLatest: false,
116
127
  });
117
128
  expect(result).toEqual([]);
@@ -119,10 +130,10 @@ describe('getChangedActors', () => {
119
130
 
120
131
  it('returns actor when isLatest and JSON file has only cosmetic changes', async () => {
121
132
  vi.spyOn(DiffJsonSchema, 'isCosmeticOnlyJsonSchemaChange').mockResolvedValue(true);
122
- const result = await getChangedActors({
133
+ const result = await changedActorsFor({
123
134
  filepathsChanged: ['actors/foo_bar/.actor/actor.json'],
124
135
  actorConfigs,
125
- commits,
136
+ range,
126
137
  isLatest: true,
127
138
  });
128
139
  expect(result).toEqual([miniActor]);
@@ -130,30 +141,30 @@ describe('getChangedActors', () => {
130
141
 
131
142
  it('does not return actor when not isLatest and JSON file has only cosmetic changes', async () => {
132
143
  vi.spyOn(DiffJsonSchema, 'isCosmeticOnlyJsonSchemaChange').mockResolvedValue(true);
133
- const result = await getChangedActors({
144
+ const result = await changedActorsFor({
134
145
  filepathsChanged: ['actors/foo_bar/.actor/actor.json'],
135
146
  actorConfigs,
136
- commits,
147
+ range,
137
148
  isLatest: false,
138
149
  });
139
150
  expect(result).toEqual([]);
140
151
  });
141
152
 
142
153
  it('returns actor when JSON file has functional changes', async () => {
143
- const result = await getChangedActors({
154
+ const result = await changedActorsFor({
144
155
  filepathsChanged: ['actors/foo_bar/.actor/actor.json'],
145
156
  actorConfigs,
146
- commits,
157
+ range,
147
158
  });
148
159
  expect(result).toEqual([miniActor]);
149
160
  });
150
161
 
151
162
  it('JSON file in actor folder but outside .actor/ is functional, not checked for cosmetic', async () => {
152
163
  vi.spyOn(DiffJsonSchema, 'isCosmeticOnlyJsonSchemaChange').mockResolvedValue(true);
153
- const result = await getChangedActors({
164
+ const result = await changedActorsFor({
154
165
  filepathsChanged: ['actors/foo_bar/package.json'],
155
166
  actorConfigs,
156
- commits,
167
+ range,
157
168
  isLatest: false,
158
169
  });
159
170
  expect(result).toEqual([miniActor]);
@@ -162,34 +173,34 @@ describe('getChangedActors', () => {
162
173
 
163
174
  it('JSON file under .actor/ inside actor folder is checked for cosmetic changes', async () => {
164
175
  vi.spyOn(DiffJsonSchema, 'isCosmeticOnlyJsonSchemaChange').mockResolvedValue(true);
165
- const result = await getChangedActors({
176
+ const result = await changedActorsFor({
166
177
  filepathsChanged: ['actors/foo_bar/.actor/input_schema.json'],
167
178
  actorConfigs,
168
- commits,
179
+ range,
169
180
  isLatest: false,
170
181
  });
171
182
  expect(result).toEqual([]);
172
183
  expect(DiffJsonSchema.isCosmeticOnlyJsonSchemaChange).toHaveBeenCalledWith(
173
- commits,
184
+ range,
174
185
  'actors/foo_bar/.actor/input_schema.json',
175
186
  );
176
187
  });
177
188
 
178
189
  it('does not trigger narrow-context actor when shared file changes', async () => {
179
- const result = await getChangedActors({
190
+ const result = await changedActorsFor({
180
191
  filepathsChanged: ['shared/utils.ts'],
181
192
  actorConfigs,
182
- commits,
193
+ range,
183
194
  });
184
195
  expect(result).toContainEqual(miniActor);
185
196
  expect(result).not.toContainEqual(standaloneActor);
186
197
  });
187
198
 
188
199
  it('root-level changelog outside any actor folder is cosmetic for every actor when isLatest', async () => {
189
- const result = await getChangedActors({
200
+ const result = await changedActorsFor({
190
201
  filepathsChanged: ['CHANGELOG.md'],
191
202
  actorConfigs,
192
- commits,
203
+ range,
193
204
  isLatest: true,
194
205
  });
195
206
  expect(result).toEqual(expect.arrayContaining([miniActor, standaloneActor]));
@@ -197,20 +208,20 @@ describe('getChangedActors', () => {
197
208
  });
198
209
 
199
210
  it('root-level changelog is not cosmetic-triggered when not isLatest', async () => {
200
- const result = await getChangedActors({
211
+ const result = await changedActorsFor({
201
212
  filepathsChanged: ['CHANGELOG.md'],
202
213
  actorConfigs,
203
- commits,
214
+ range,
204
215
  isLatest: false,
205
216
  });
206
217
  expect(result).toEqual([]);
207
218
  });
208
219
 
209
220
  it('treats any changelog filename as cosmetic regardless of configured paths', async () => {
210
- const result = await getChangedActors({
221
+ const result = await changedActorsFor({
211
222
  filepathsChanged: ['docs/CHANGELOG.md'],
212
223
  actorConfigs: [miniActor, standaloneActor],
213
- commits,
224
+ range,
214
225
  isLatest: true,
215
226
  });
216
227
  expect(result).toEqual([miniActor, standaloneActor]);
@@ -221,39 +232,39 @@ describe('getChangedActors', () => {
221
232
  ...standaloneActor,
222
233
  actorJson: { ...standaloneActor.actorJson, readme: new RelativeFile('docs/guide.md') },
223
234
  };
224
- const result = await getChangedActors({
235
+ const result = await changedActorsFor({
225
236
  filepathsChanged: ['docs/guide.md'],
226
237
  actorConfigs: [configuredActor],
227
- commits,
238
+ range,
228
239
  isLatest: true,
229
240
  });
230
241
  expect(result).toEqual([]);
231
242
  });
232
243
 
233
244
  it('triggers narrow-context actor when its own folder changes', async () => {
234
- const result = await getChangedActors({
245
+ const result = await changedActorsFor({
235
246
  filepathsChanged: ['standalone-actors/standalone/src/main.ts'],
236
247
  actorConfigs,
237
- commits,
248
+ range,
238
249
  });
239
250
  expect(result).toContainEqual(standaloneActor);
240
251
  });
241
252
 
242
253
  it('deduplicates actors when multiple files in same actor folder change', async () => {
243
- const result = await getChangedActors({
254
+ const result = await changedActorsFor({
244
255
  filepathsChanged: ['actors/foo_bar/src/main.ts', 'actors/foo_bar/package.json'],
245
256
  actorConfigs,
246
- commits,
257
+ range,
247
258
  });
248
259
  expect(result).toHaveLength(1);
249
260
  expect(result).toContainEqual(miniActor);
250
261
  });
251
262
 
252
263
  it('handles mixed changes: returns both broad and narrow-context actors', async () => {
253
- const result = await getChangedActors({
264
+ const result = await changedActorsFor({
254
265
  filepathsChanged: ['actors/foo_bar/src/main.ts', 'standalone-actors/standalone/Dockerfile'],
255
266
  actorConfigs,
256
- commits,
267
+ range,
257
268
  });
258
269
  expect(result).toContainEqual(miniActor);
259
270
  expect(result).toContainEqual(standaloneActor);
@@ -261,29 +272,29 @@ describe('getChangedActors', () => {
261
272
 
262
273
  it('matches folder where folder name differs from actor name', async () => {
263
274
  const ownerlessActor = actorConfig('myteam/shopify-scraper', 'actors/shopify', 'APIFY_TOKEN_MYTEAM', '', ['']);
264
- const result = await getChangedActors({
275
+ const result = await changedActorsFor({
265
276
  filepathsChanged: ['actors/shopify/src/main.ts'],
266
277
  actorConfigs: [ownerlessActor],
267
- commits,
278
+ range,
268
279
  });
269
280
  expect(result).toEqual([ownerlessActor]);
270
281
  });
271
282
 
272
283
  it('in single-actor repo, .actor/ changes trigger builds', async () => {
273
284
  const rootActor = actorConfig('myteam/my-actor', '', 'BUILDER_APIFY_TOKEN', '', ['']);
274
- const result = await getChangedActors({
285
+ const result = await changedActorsFor({
275
286
  filepathsChanged: ['.actor/actor.json'],
276
287
  actorConfigs: [rootActor],
277
- commits,
288
+ range,
278
289
  });
279
290
  expect(result).toEqual([rootActor]);
280
291
  });
281
292
 
282
293
  it('in multi-actor repo, .actor/ changes only trigger broad-context actors', async () => {
283
- const result = await getChangedActors({
294
+ const result = await changedActorsFor({
284
295
  filepathsChanged: ['.actor/actor.json'],
285
296
  actorConfigs,
286
- commits,
297
+ range,
287
298
  });
288
299
  expect(result).toEqual([miniActor]);
289
300
  });
@@ -292,10 +303,10 @@ describe('getChangedActors', () => {
292
303
  const narrowActor = actorConfig('foo/bar', 'actors/foo_bar', 'APIFY_TOKEN_FOO', 'actors/foo_bar', [
293
304
  'actors/foo_bar',
294
305
  ]);
295
- const result = await getChangedActors({
306
+ const result = await changedActorsFor({
296
307
  filepathsChanged: ['Actors/FOO_BAR/Main.ts'],
297
308
  actorConfigs: [narrowActor],
298
- commits,
309
+ range,
299
310
  });
300
311
  expect(result).toEqual([]);
301
312
  });
@@ -308,10 +319,10 @@ describe('getChangedActors', () => {
308
319
  'actors/override',
309
320
  ['actors/override', 'packages'],
310
321
  );
311
- const result = await getChangedActors({
322
+ const result = await changedActorsFor({
312
323
  filepathsChanged: ['packages/shared/utils.ts'],
313
324
  actorConfigs: [overrideActor],
314
- commits,
325
+ range,
315
326
  });
316
327
  expect(result).toEqual([overrideActor]);
317
328
  });
@@ -324,10 +335,10 @@ describe('getChangedActors', () => {
324
335
  'actors/override',
325
336
  ['actors/override', 'packages'],
326
337
  );
327
- const result = await getChangedActors({
338
+ const result = await changedActorsFor({
328
339
  filepathsChanged: ['other-dir/file.ts'],
329
340
  actorConfigs: [overrideActor],
330
- commits,
341
+ range,
331
342
  });
332
343
  expect(result).toEqual([]);
333
344
  });
@@ -335,10 +346,10 @@ describe('getChangedActors', () => {
335
346
  it('broad-context actor skips files in sibling actor folders', async () => {
336
347
  const actorA = actorConfig('team/actor-a', 'actors/a', 'APIFY_TOKEN_TEAM', '', ['']);
337
348
  const actorB = actorConfig('team/actor-b', 'actors/b', 'APIFY_TOKEN_TEAM', '', ['']);
338
- const result = await getChangedActors({
349
+ const result = await changedActorsFor({
339
350
  filepathsChanged: ['actors/b/src/main.ts'],
340
351
  actorConfigs: [actorA, actorB],
341
- commits,
352
+ range,
342
353
  });
343
354
  expect(result).toEqual([actorB]);
344
355
  });
@@ -348,10 +359,10 @@ describe('getChangedActors', () => {
348
359
  const childActor = actorConfig('team/child', 'actors/child', 'APIFY_TOKEN_TEAM', 'actors/child', [
349
360
  'actors/child',
350
361
  ]);
351
- const result = await getChangedActors({
362
+ const result = await changedActorsFor({
352
363
  filepathsChanged: ['actors/child/src/main.ts'],
353
364
  actorConfigs: [rootActor, childActor],
354
- commits,
365
+ range,
355
366
  });
356
367
  expect(result).not.toContainEqual(rootActor);
357
368
  expect(result).toContainEqual(childActor);
@@ -362,10 +373,10 @@ describe('getChangedActors', () => {
362
373
  const childActor = actorConfig('team/child', 'actors/child', 'APIFY_TOKEN_TEAM', 'actors/child', [
363
374
  'actors/child',
364
375
  ]);
365
- const result = await getChangedActors({
376
+ const result = await changedActorsFor({
366
377
  filepathsChanged: ['lib/shared-utils.ts'],
367
378
  actorConfigs: [rootActor, childActor],
368
- commits,
379
+ range,
369
380
  });
370
381
  expect(result).toContainEqual(rootActor);
371
382
  expect(result).not.toContainEqual(childActor);
@@ -375,94 +386,160 @@ describe('getChangedActors', () => {
375
386
  vi.spyOn(Dockerignore, 'loadDockerIgnore').mockReturnValue(
376
387
  (filePath) => filePath === 'actors/foo_bar/node_modules/foo.js',
377
388
  );
378
- const result = await getChangedActors({
389
+ const result = await changedActorsFor({
379
390
  filepathsChanged: ['actors/foo_bar/node_modules/foo.js'],
380
391
  actorConfigs: [miniActor],
381
- commits,
392
+ range,
382
393
  });
383
394
  expect(result).toEqual([]);
384
395
  });
385
396
 
386
397
  it('file not matched by .dockerignore is classified normally', async () => {
387
398
  vi.spyOn(Dockerignore, 'loadDockerIgnore').mockReturnValue((filePath) => filePath.includes('node_modules'));
388
- const result = await getChangedActors({
399
+ const result = await changedActorsFor({
389
400
  filepathsChanged: ['actors/foo_bar/src/main.ts'],
390
401
  actorConfigs: [miniActor],
391
- commits,
402
+ range,
403
+ });
404
+ expect(result).toEqual([miniActor]);
405
+ });
406
+
407
+ it('test file listed in .dockerignore triggers a build when it matches the test files glob', async () => {
408
+ vi.spyOn(Dockerignore, 'loadDockerIgnore').mockReturnValue((filePath) => filePath.startsWith('test/'));
409
+ const result = await changedActorsFor({
410
+ filepathsChanged: ['test/platform/daily/foo.test.ts'],
411
+ actorConfigs: [miniActor],
412
+ range,
413
+ testFilesGlob: 'test/platform/**',
414
+ });
415
+ expect(result).toEqual([miniActor]);
416
+ });
417
+
418
+ it('test file listed in .dockerignore stays ignored without a test files glob', async () => {
419
+ vi.spyOn(Dockerignore, 'loadDockerIgnore').mockReturnValue((filePath) => filePath.startsWith('test/'));
420
+ const result = await changedActorsFor({
421
+ filepathsChanged: ['test/platform/daily/foo.test.ts'],
422
+ actorConfigs: [miniActor],
423
+ range,
424
+ });
425
+ expect(result).toEqual([]);
426
+ });
427
+
428
+ it('test files glob matches case-insensitively and does not bypass .dockerignore for other files', async () => {
429
+ vi.spyOn(Dockerignore, 'loadDockerIgnore').mockReturnValue(() => true);
430
+ const result = await changedActorsFor({
431
+ filepathsChanged: ['Test/Platform/Foo.test.ts'],
432
+ actorConfigs: [miniActor],
433
+ range,
434
+ testFilesGlob: 'test/platform/**',
392
435
  });
393
436
  expect(result).toEqual([miniActor]);
437
+ expect(
438
+ await changedActorsFor({
439
+ filepathsChanged: ['actors/foo_bar/node_modules/foo.js'],
440
+ actorConfigs: [miniActor],
441
+ range,
442
+ testFilesGlob: 'test/platform/**',
443
+ }),
444
+ ).toEqual([]);
445
+ });
446
+
447
+ it('test file outside an actor context still does not trigger it', async () => {
448
+ vi.spyOn(Dockerignore, 'loadDockerIgnore').mockReturnValue(() => true);
449
+ const result = await changedActorsFor({
450
+ filepathsChanged: ['test/platform/daily/foo.test.ts'],
451
+ actorConfigs: [standaloneActor],
452
+ range,
453
+ testFilesGlob: 'test/platform/**',
454
+ });
455
+ expect(result).toEqual([]);
456
+ });
457
+
458
+ it('test file listed in .dockerignore triggers an actor whose overrideActorContext includes it', async () => {
459
+ vi.spyOn(Dockerignore, 'loadDockerIgnore').mockReturnValue(() => true);
460
+ const actorWithTests = {
461
+ ...standaloneActor,
462
+ contextPaths: [...standaloneActor.contextPaths, new RelativeDir('test')],
463
+ };
464
+ const result = await changedActorsFor({
465
+ filepathsChanged: ['test/platform/standalone.test.ts'],
466
+ actorConfigs: [actorWithTests],
467
+ range,
468
+ testFilesGlob: 'test/platform/**',
469
+ });
470
+ expect(result).toEqual([actorWithTests]);
394
471
  });
395
472
 
396
473
  it('JSON file in context but outside actor folder is functional (not checked for cosmetic)', async () => {
397
- const result = await getChangedActors({
474
+ const result = await changedActorsFor({
398
475
  filepathsChanged: ['lib/config.json'],
399
476
  actorConfigs: [miniActor],
400
- commits,
477
+ range,
401
478
  });
402
479
  expect(result).toEqual([miniActor]);
403
480
  });
404
481
 
405
482
  it('README outside actor folder but inside context is ignored (not cosmetic)', async () => {
406
- const result = await getChangedActors({
483
+ const result = await changedActorsFor({
407
484
  filepathsChanged: ['docs/README.md'],
408
485
  actorConfigs: [miniActor],
409
- commits,
486
+ range,
410
487
  isLatest: true,
411
488
  });
412
489
  expect(result).toEqual([]);
413
490
  });
414
491
 
415
492
  it('README inside actor folder is cosmetic', async () => {
416
- const result = await getChangedActors({
493
+ const result = await changedActorsFor({
417
494
  filepathsChanged: ['actors/foo_bar/README.md'],
418
495
  actorConfigs: [miniActor],
419
- commits,
496
+ range,
420
497
  isLatest: true,
421
498
  });
422
499
  expect(result).toEqual([miniActor]);
423
500
  });
424
501
 
425
502
  it('ignores a standalone actor top-level dev file relative to its context', async () => {
426
- const result = await getChangedActors({
503
+ const result = await changedActorsFor({
427
504
  filepathsChanged: ['standalone-actors/standalone/.eslintrc'],
428
505
  actorConfigs,
429
- commits,
506
+ range,
430
507
  });
431
508
  expect(result).toEqual([]);
432
509
  });
433
510
 
434
511
  it('does not special-case code/ and shared/ prefixes anymore — must be declared via overrideActorContext', async () => {
435
- const result = await getChangedActors({
512
+ const result = await changedActorsFor({
436
513
  filepathsChanged: ['code/some/code.ts'],
437
514
  actorConfigs: [miniActor],
438
- commits,
515
+ range,
439
516
  });
440
517
  expect(result).toEqual([miniActor]);
441
518
  });
442
519
 
443
520
  it('ignores code/.eslintrc when "code" is declared via overrideActorContext', async () => {
444
- const result = await getChangedActors({
521
+ const result = await changedActorsFor({
445
522
  filepathsChanged: ['code/.eslintrc'],
446
523
  actorConfigs: [amazonActor],
447
- commits,
524
+ range,
448
525
  });
449
526
  expect(result).toEqual([]);
450
527
  });
451
528
 
452
529
  it('shared/Dockerfile declared via overrideActorContext is functional', async () => {
453
- const result = await getChangedActors({
530
+ const result = await changedActorsFor({
454
531
  filepathsChanged: ['shared/Dockerfile'],
455
532
  actorConfigs: [amazonActor],
456
- commits,
533
+ range,
457
534
  });
458
535
  expect(result).toEqual([amazonActor]);
459
536
  });
460
537
 
461
538
  it('code/README.md declared via overrideActorContext is ignored (outside actor folder)', async () => {
462
- const result = await getChangedActors({
539
+ const result = await changedActorsFor({
463
540
  filepathsChanged: ['code/README.md'],
464
541
  actorConfigs: [amazonActor],
465
- commits,
542
+ range,
466
543
  isLatest: true,
467
544
  });
468
545
  expect(result).toEqual([]);
@@ -473,19 +550,21 @@ describe('getChangedActors logging', () => {
473
550
  beforeEach(() => {
474
551
  vi.spyOn(DiffJsonSchema, 'isCosmeticOnlyJsonSchemaChange').mockResolvedValue(false);
475
552
  vi.spyOn(logger, 'info').mockImplementation(() => undefined);
553
+ vi.spyOn(logger, 'warn').mockImplementation(() => undefined);
476
554
  });
477
555
 
478
556
  it('logs a single "specific" group for a single actor with one functional file', async () => {
479
557
  await getChangedActors({
480
558
  filepathsChanged: ['actors/foo_bar/src/main.ts'],
481
559
  actorConfigs: [miniActor],
482
- commits,
560
+ range,
483
561
  });
484
562
 
485
563
  expect(logger.info).toHaveBeenCalledWith(
486
564
  '[DIFF]: Changes specific to actor foo/bar: actors/foo_bar/src/main.ts',
487
565
  );
488
- expect(logger.info).toHaveBeenCalledWith('[DIFF]: Actors to be built and tested: foo/bar');
566
+ expect(logger.info).toHaveBeenCalledWith('[DIFF]: Actors with functional changes: foo/bar');
567
+ expect(logger.info).toHaveBeenCalledWith('[DIFF]: Actors with only cosmetic changes: none');
489
568
  });
490
569
 
491
570
  it('logs a single "shared" group when two actors are triggered by the exact same file', async () => {
@@ -495,14 +574,14 @@ describe('getChangedActors logging', () => {
495
574
  await getChangedActors({
496
575
  filepathsChanged: ['shared/shared.ts'],
497
576
  actorConfigs: [actorA, actorB],
498
- commits,
577
+ range,
499
578
  });
500
579
 
501
580
  expect(logger.info).toHaveBeenCalledWith(
502
581
  '[DIFF]: Shared changes for actors team/actor-a, team/actor-b: shared/shared.ts',
503
582
  );
504
583
  expect(logger.info).not.toHaveBeenCalledWith(expect.stringContaining('Changes specific to actor'));
505
- expect(logger.info).toHaveBeenCalledWith('[DIFF]: Actors to be built and tested: team/actor-a, team/actor-b');
584
+ expect(logger.info).toHaveBeenCalledWith('[DIFF]: Actors with functional changes: team/actor-a, team/actor-b');
506
585
  });
507
586
 
508
587
  it('logs shared and specific groups in descending-size order for partial overlap across actors', async () => {
@@ -512,7 +591,7 @@ describe('getChangedActors logging', () => {
512
591
  await getChangedActors({
513
592
  filepathsChanged: ['shared.ts', 'actors/a/a-only.ts', 'actors/b/b-only.ts'],
514
593
  actorConfigs: [actorA, actorB],
515
- commits,
594
+ range,
516
595
  });
517
596
 
518
597
  const errorCalls = vi.mocked(logger.info).mock.calls.map((call) => call[0]);
@@ -521,19 +600,84 @@ describe('getChangedActors logging', () => {
521
600
  '[DIFF]: Shared changes for actors team/actor-a, team/actor-b: shared.ts',
522
601
  '[DIFF]: Changes specific to actor team/actor-a: actors/a/a-only.ts',
523
602
  '[DIFF]: Changes specific to actor team/actor-b: actors/b/b-only.ts',
524
- '[DIFF]: Actors to be built and tested: team/actor-a, team/actor-b',
603
+ '[DIFF]: Actors with functional changes: team/actor-a, team/actor-b',
604
+ '[DIFF]: Actors with only cosmetic changes: none',
525
605
  ]);
526
606
  });
527
607
 
608
+ const outsideContextWarning = (testFile: string) =>
609
+ `[DIFF]: Test file ${testFile} is outside every Actor's context, so changing only it builds and tests nothing. ` +
610
+ 'Add its folder to "overrideActorContext" of the Actors it tests.';
611
+
612
+ it('warns about a changed test file outside every Actor context', async () => {
613
+ await getChangedActors({
614
+ filepathsChanged: ['test/platform/standalone.test.ts'],
615
+ actorConfigs: [standaloneActor],
616
+ range,
617
+ testFilesGlob: 'test/platform/**',
618
+ });
619
+
620
+ expect(logger.warn).toHaveBeenCalledWith(outsideContextWarning('test/platform/standalone.test.ts'));
621
+ });
622
+
623
+ it('still warns when the same push also rebuilds an Actor', async () => {
624
+ const result = await changedActorsFor({
625
+ filepathsChanged: ['test/platform/standalone.test.ts', 'standalone-actors/standalone/src/main.ts'],
626
+ actorConfigs: [standaloneActor],
627
+ range,
628
+ testFilesGlob: 'test/platform/**',
629
+ });
630
+
631
+ expect(result).toEqual([standaloneActor]);
632
+ expect(logger.warn).toHaveBeenCalledWith(outsideContextWarning('test/platform/standalone.test.ts'));
633
+ });
634
+
635
+ it('does not warn about test files inside an Actor context, even cosmetic ones', async () => {
636
+ await getChangedActors({
637
+ filepathsChanged: ['test/platform/foo.test.ts', 'test/platform/README.md', 'test/platform/CHANGELOG.md'],
638
+ actorConfigs: [miniActor],
639
+ range,
640
+ testFilesGlob: 'test/platform/**',
641
+ });
642
+
643
+ expect(logger.warn).not.toHaveBeenCalled();
644
+ });
645
+
528
646
  it('logs no group lines when zero actors changed', async () => {
529
647
  await getChangedActors({
530
648
  filepathsChanged: ['.gitignore', '.prettierrc'],
531
649
  actorConfigs,
532
- commits,
650
+ range,
533
651
  });
534
652
 
535
653
  expect(logger.info).not.toHaveBeenCalledWith(expect.stringContaining('Shared changes'));
536
654
  expect(logger.info).not.toHaveBeenCalledWith(expect.stringContaining('Changes specific to'));
537
- expect(logger.info).toHaveBeenCalledWith('[DIFF]: No relevant files changed, skipping builds and tests');
655
+ expect(logger.info).toHaveBeenCalledWith('[DIFF]: Actors with functional changes: none');
656
+ expect(logger.info).toHaveBeenCalledWith('[DIFF]: Actors with only cosmetic changes: none');
657
+ });
658
+ });
659
+
660
+ describe('getChangedActors split by impact', () => {
661
+ beforeEach(() => {
662
+ vi.spyOn(DiffJsonSchema, 'isCosmeticOnlyJsonSchemaChange').mockResolvedValue(false);
663
+ });
664
+
665
+ it('lists an Actor as functional when it has any functional change, and as cosmetic only otherwise', async () => {
666
+ const result = await getChangedActors({
667
+ filepathsChanged: [
668
+ 'actors/foo_bar/README.md',
669
+ 'standalone-actors/standalone/README.md',
670
+ 'actors/foo_bar/src/main.ts',
671
+ ],
672
+ actorConfigs,
673
+ range,
674
+ });
675
+ expect(result).toStrictEqual({ functional: [miniActor], cosmetic: [standaloneActor] });
676
+ });
677
+
678
+ it('returns empty lists when nothing relevant changed', async () => {
679
+ expect(
680
+ await getChangedActors({ filepathsChanged: ['.vscode/settings.json'], actorConfigs, range }),
681
+ ).toStrictEqual({ functional: [], cosmetic: [] });
538
682
  });
539
683
  });