apify-test-tools 0.8.6 → 0.9.0-beta.1

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 (79) hide show
  1. package/README.md +83 -13
  2. package/bin/build-from-local.ts +35 -34
  3. package/bin/build.ts +55 -59
  4. package/bin/diff-changes.ts +166 -125
  5. package/bin/dockerignore.ts +50 -0
  6. package/bin/main.ts +7 -7
  7. package/bin/path-utils.ts +13 -0
  8. package/bin/test-report.ts +5 -5
  9. package/bin/types.ts +17 -5
  10. package/bin/utils.ts +122 -95
  11. package/dist/bin/build-from-local.d.ts +2 -2
  12. package/dist/bin/build-from-local.d.ts.map +1 -1
  13. package/dist/bin/build-from-local.js +33 -28
  14. package/dist/bin/build-from-local.js.map +1 -1
  15. package/dist/bin/build.d.ts +5 -8
  16. package/dist/bin/build.d.ts.map +1 -1
  17. package/dist/bin/build.js +49 -56
  18. package/dist/bin/build.js.map +1 -1
  19. package/dist/bin/diff-changes.d.ts +0 -6
  20. package/dist/bin/diff-changes.d.ts.map +1 -1
  21. package/dist/bin/diff-changes.js +129 -89
  22. package/dist/bin/diff-changes.js.map +1 -1
  23. package/dist/bin/dockerignore.d.ts +17 -0
  24. package/dist/bin/dockerignore.d.ts.map +1 -0
  25. package/dist/bin/dockerignore.js +41 -0
  26. package/dist/bin/dockerignore.js.map +1 -0
  27. package/dist/bin/main.js +7 -7
  28. package/dist/bin/main.js.map +1 -1
  29. package/dist/bin/path-utils.d.ts +4 -0
  30. package/dist/bin/path-utils.d.ts.map +1 -0
  31. package/dist/bin/path-utils.js +9 -0
  32. package/dist/bin/path-utils.js.map +1 -0
  33. package/dist/bin/test-report.js +3 -3
  34. package/dist/bin/test-report.js.map +1 -1
  35. package/dist/bin/types.d.ts +15 -4
  36. package/dist/bin/types.d.ts.map +1 -1
  37. package/dist/bin/utils.d.ts +2 -19
  38. package/dist/bin/utils.d.ts.map +1 -1
  39. package/dist/bin/utils.js +82 -81
  40. package/dist/bin/utils.js.map +1 -1
  41. package/dist/lib/lib.d.ts +10 -4
  42. package/dist/lib/lib.d.ts.map +1 -1
  43. package/dist/lib/lib.js +28 -22
  44. package/dist/lib/lib.js.map +1 -1
  45. package/dist/lib/types.d.ts +2 -2
  46. package/dist/lib/types.d.ts.map +1 -1
  47. package/dist/lib/utils.d.ts +1 -1
  48. package/dist/lib/utils.d.ts.map +1 -1
  49. package/dist/lib/utils.js +4 -4
  50. package/dist/lib/utils.js.map +1 -1
  51. package/dist/test/unit/bin/build-from-local.test.js +55 -86
  52. package/dist/test/unit/bin/build-from-local.test.js.map +1 -1
  53. package/dist/test/unit/bin/diff-changes.test.js +358 -36
  54. package/dist/test/unit/bin/diff-changes.test.js.map +1 -1
  55. package/dist/test/unit/bin/dockerignore.test.d.ts +2 -0
  56. package/dist/test/unit/bin/dockerignore.test.d.ts.map +1 -0
  57. package/dist/test/unit/bin/dockerignore.test.js +102 -0
  58. package/dist/test/unit/bin/dockerignore.test.js.map +1 -0
  59. package/dist/test/unit/bin/path-utils.test.d.ts +2 -0
  60. package/dist/test/unit/bin/path-utils.test.d.ts.map +1 -0
  61. package/dist/test/unit/bin/path-utils.test.js +14 -0
  62. package/dist/test/unit/bin/path-utils.test.js.map +1 -0
  63. package/dist/test/unit/bin/utils.test.d.ts +2 -0
  64. package/dist/test/unit/bin/utils.test.d.ts.map +1 -0
  65. package/dist/test/unit/bin/utils.test.js +293 -0
  66. package/dist/test/unit/bin/utils.test.js.map +1 -0
  67. package/dist/test/unit/should-built-and-test.test.js +83 -32
  68. package/dist/test/unit/should-built-and-test.test.js.map +1 -1
  69. package/dist/tsconfig.tsbuildinfo +1 -1
  70. package/lib/lib.ts +28 -22
  71. package/lib/types.ts +2 -2
  72. package/lib/utils.ts +4 -4
  73. package/package.json +62 -61
  74. package/test/unit/bin/build-from-local.test.ts +62 -121
  75. package/test/unit/bin/diff-changes.test.ts +397 -41
  76. package/test/unit/bin/dockerignore.test.ts +123 -0
  77. package/test/unit/bin/path-utils.test.ts +17 -0
  78. package/test/unit/bin/utils.test.ts +343 -0
  79. package/test/unit/should-built-and-test.test.ts +89 -32
package/lib/lib.ts CHANGED
@@ -24,8 +24,8 @@ try {
24
24
  }
25
25
 
26
26
  const config = actorBuilds.reduce<Map<string, ActorBuild>>((map, cfg) => {
27
- map.set(cfg.actorName, cfg);
28
- map.set(cfg.actorId, cfg);
27
+ map.set(cfg.actorFullName, cfg);
28
+ map.set(cfg.actorRawId, cfg);
29
29
  return map;
30
30
  }, new Map<string, ActorBuild>());
31
31
 
@@ -51,8 +51,11 @@ const DEFAULT_TEST_ACTOR_OPTIONS: ActorTestOptions = {
51
51
  timeout: DEFAULT_TEST_RUN_DURATION_MS,
52
52
  };
53
53
 
54
+ /**
55
+ * @param actorId - The actor's raw platform ID or its full name (`owner/name`, e.g. `"apify/web-scraper"`).
56
+ */
54
57
  export const testActor = <T>(
55
- actorName: string,
58
+ actorId: string,
56
59
  testName: string,
57
60
  fn: TestFunction<{ run: ReturnType<typeof createStartRunFn<T>> }>,
58
61
  testOptions?: ActorTestOptions,
@@ -61,13 +64,13 @@ export const testActor = <T>(
61
64
  ...DEFAULT_TEST_ACTOR_OPTIONS,
62
65
  ...testOptions,
63
66
  };
64
- const name = `${actorName}: ${testName}`;
65
- const shouldRun = !!RUN_ALL_PLATFORM_TESTS || config.has(actorName);
67
+ const name = `${actorId}: ${testName}`;
68
+ const shouldRun = !!RUN_ALL_PLATFORM_TESTS || config.has(actorId);
66
69
  vitestTest.runIf(shouldRun)(name, options, async <TYPE extends TestContext>(context: TYPE) => {
67
70
  const { expect, ...rest } = context;
68
71
  await fn({
69
72
  expect: extendExpect(expect),
70
- run: createStartRunFn(actorName, context),
73
+ run: createStartRunFn(actorId, context),
71
74
  ...rest,
72
75
  });
73
76
  });
@@ -79,9 +82,12 @@ export const testActor = <T>(
79
82
  *
80
83
  * Using task is just current shortcoming of standby feature but ideally we would use Actor directly
81
84
  */
85
+ /**
86
+ * @param actorId - The actor's raw platform ID or its full name (`owner/name`, e.g. `"apify/web-scraper"`).
87
+ */
82
88
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
83
89
  export const testStandbyActor = <I = any, O = any>(
84
- actorName: string,
90
+ actorId: string,
85
91
  testName: string,
86
92
  fn: TestFunction<{ callStandby: ReturnType<typeof createStartStandbyFn<I, O>> }>,
87
93
  testOptions?: ActorTestOptions,
@@ -90,11 +96,11 @@ export const testStandbyActor = <I = any, O = any>(
90
96
  ...DEFAULT_TEST_ACTOR_OPTIONS,
91
97
  ...testOptions,
92
98
  };
93
- const name = `${actorName}: ${testName}`;
94
- const shouldRun = !!RUN_ALL_PLATFORM_TESTS || config.has(actorName);
99
+ const name = `${actorId}: ${testName}`;
100
+ const shouldRun = !!RUN_ALL_PLATFORM_TESTS || config.has(actorId);
95
101
 
96
102
  vitestTest.runIf(shouldRun)(name, options, async <T extends TestContext>(context: T) => {
97
- const standbyTask = await createStandbyTask(actorName, config.get(actorName)?.buildNumber);
103
+ const standbyTask = await createStandbyTask(actorId, config.get(actorId)?.buildNumber);
98
104
  const { expect, ...rest } = context;
99
105
 
100
106
  // NOTE: we wrap `fn` in try/finally so cleanup (deleting the task) always runs afterwards
@@ -193,20 +199,20 @@ interface StandbyTask {
193
199
  *
194
200
  * @throws if actor doesn't exist or it doesn't support standby mode.
195
201
  */
196
- const createStandbyTask = async (actorNameOrId: string, buildNumber?: string): Promise<StandbyTask> => {
197
- const actor = apifyClient.actor(actorNameOrId);
202
+ const createStandbyTask = async (actorId: string, buildNumber?: string): Promise<StandbyTask> => {
203
+ const actor = apifyClient.actor(actorId);
198
204
 
199
205
  const actorInfo = (await actor.get()) as Actor & { standbyUrl?: string };
200
206
  if (!actorInfo) {
201
- throw new Error(`Actor "${actorNameOrId}" not found`);
207
+ throw new Error(`Actor "${actorId}" not found`);
202
208
  }
203
209
 
204
210
  if (!actorInfo.standbyUrl) {
205
- throw new Error(`Actor "${actorNameOrId}" doesn't support standby mode`);
211
+ throw new Error(`Actor "${actorId}" doesn't support standby mode`);
206
212
  }
207
213
 
208
214
  if (!actorInfo.actorStandby) {
209
- throw new Error(`Actor "${actorNameOrId} doesn't contain actorStandby options`);
215
+ throw new Error(`Actor "${actorId} doesn't contain actorStandby options`);
210
216
  }
211
217
  const { isEnabled, ...defaultActorStandby } = actorInfo.actorStandby;
212
218
  delete defaultActorStandby.disableStandbyFieldsOverride;
@@ -225,9 +231,9 @@ const createStandbyTask = async (actorNameOrId: string, buildNumber?: string): P
225
231
  const randomValue = Math.random().toString(10).slice(2).padEnd(randomValueLength, '0');
226
232
  const name = `test-${randomValue.slice(0, randomValueLength)}`;
227
233
  const newTask = (await apifyClient.tasks().create({
228
- actId: actorNameOrId,
234
+ actId: actorId,
229
235
  actorStandby: actorStandbyOptions,
230
- description: `Task for testing standby version ${build} of actor "${actorNameOrId}"`,
236
+ description: `Task for testing standby version ${build} of actor "${actorId}"`,
231
237
  title,
232
238
  name,
233
239
  })) as Task & { standbyUrl?: string };
@@ -247,9 +253,9 @@ const createStandbyTask = async (actorNameOrId: string, buildNumber?: string): P
247
253
  }
248
254
  };
249
255
 
250
- const createStartRunFn = <T>(actorNameOrId: string, testContext: TestContext) => {
256
+ const createStartRunFn = <T>(actorId: string, testContext: TestContext) => {
251
257
  const { annotate, task } = testContext;
252
- const actorConfig = config.get(actorNameOrId);
258
+ const actorConfig = config.get(actorId);
253
259
  const build = actorConfig?.buildNumber;
254
260
  const buildId = actorConfig?.buildId;
255
261
  return async (runOptions: RunOptions<T>) => {
@@ -263,10 +269,10 @@ const createStartRunFn = <T>(actorNameOrId: string, testContext: TestContext) =>
263
269
  return new RunTestResult(apifyClient, run);
264
270
  }
265
271
 
266
- const actor = apifyClient.actor(actorNameOrId);
272
+ const actor = apifyClient.actor(actorId);
267
273
 
268
274
  const actorInput = {
269
- ...(prefilledInput && (await getActorPrefilledInput(apifyClient, actorNameOrId, buildId))),
275
+ ...(prefilledInput && (await getActorPrefilledInput(apifyClient, actorId, buildId))),
270
276
  ...input,
271
277
  };
272
278
  const run = await actor.call(actorInput, { build, log: null, ...options });
@@ -277,7 +283,7 @@ const createStartRunFn = <T>(actorNameOrId: string, testContext: TestContext) =>
277
283
  task.meta = {
278
284
  runId: run.id,
279
285
  runLink,
280
- actorName: actorNameOrId,
286
+ actorId: actorConfig?.actorFullName ?? actorId,
281
287
  };
282
288
 
283
289
  // waiting for datasetItemCount and chargedEventCounts to sync
package/lib/types.ts CHANGED
@@ -3,9 +3,9 @@ import type { Assertion, TestOptions } from 'vitest';
3
3
 
4
4
  export type ActorBuild = {
5
5
  buildId: string;
6
- actorId: string;
6
+ actorRawId: string;
7
7
  buildNumber: string;
8
- actorName: string;
8
+ actorFullName: string;
9
9
  };
10
10
 
11
11
  export type RunOptions<T> = {
package/lib/utils.ts CHANGED
@@ -6,11 +6,11 @@ import type { ApifyClient } from 'apify-client';
6
6
  */
7
7
  export const getActorPrefilledInput = async (
8
8
  apifyClient: ApifyClient,
9
- actorNameOrId: string,
9
+ actorId: string,
10
10
  buildId: string | undefined,
11
11
  ) => {
12
12
  if (!buildId) {
13
- const actorInfo = await apifyClient.actor(actorNameOrId).get();
13
+ const actorInfo = await apifyClient.actor(actorId).get();
14
14
 
15
15
  const defaultBuildTag = actorInfo?.defaultRunOptions.build;
16
16
 
@@ -19,7 +19,7 @@ export const getActorPrefilledInput = async (
19
19
  buildId = taggedBuild?.buildId;
20
20
 
21
21
  if (!buildId) {
22
- console.error(`Coudn't find default build for actor ${actorNameOrId}. Prefilled values will not be used.`);
22
+ console.error(`Coudn't find default build for actor ${actorId}. Prefilled values will not be used.`);
23
23
  return {};
24
24
  }
25
25
  }
@@ -34,7 +34,7 @@ export const getActorPrefilledInput = async (
34
34
 
35
35
  if (!inputSchema) {
36
36
  console.error(
37
- `Coudn't find input schema definition for actor ${actorNameOrId}, build ${buildId}.`,
37
+ `Coudn't find input schema definition for actor ${actorId}, build ${buildId}.`,
38
38
  'Prefilled values will not be used',
39
39
  );
40
40
  return {};
package/package.json CHANGED
@@ -1,63 +1,64 @@
1
1
  {
2
- "name": "apify-test-tools",
3
- "version": "0.8.6",
4
- "type": "module",
5
- "description": "TBD",
6
- "repository": {
7
- "type": "git",
8
- "url": "https://github.com/apify/apify-test-tools.git"
9
- },
10
- "engines": {
11
- "node": ">=18.0.0"
12
- },
13
- "bin": {
14
- "apify-test-tools": "dist/bin/main.js"
15
- },
16
- "types": "./dist/index.d.ts",
17
- "main": "./dist/lib/lib.js",
18
- "dependencies": {
19
- "@apify/consts": "^2.43.0",
20
- "@slack/web-api": "^7.9.2",
21
- "apify-client": "^2.22.2",
22
- "yargs": "^18.0.0"
23
- },
24
- "devDependencies": {
25
- "@apify/eslint-config": "^1.0.0",
26
- "@apify/tsconfig": "^0.1.1",
27
- "@types/node": "^24.0.0",
28
- "@types/yargs": "^17.0.33",
29
- "eslint": "^9.29.0",
30
- "eslint-config-prettier": "^10.1.5",
31
- "globals": "^17.0.0",
32
- "prettier": "^3.5.3",
33
- "tsx": "^4.20.3",
34
- "typescript": "^5.9.3",
35
- "typescript-eslint": "^8.34.1",
36
- "vitest": "^3.2.4",
37
- "knip": "^5.65.0",
38
- "husky": "^9.0.11",
39
- "lint-staged": "^15.2.2"
40
- },
41
- "peerDependencies": {
42
- "vitest": ">=3.2.4"
43
- },
44
- "scripts": {
45
- "start": "npm run dist/bin/main.js",
46
- "start:dev": "GITHUB_WORKSPACE=local-clone tsx bin/main.ts",
47
- "build": "tsc && chmod +x ./dist/bin/main.js",
48
- "lint": "eslint",
49
- "lint:fix": "eslint --fix",
50
- "format": "prettier --write .",
51
- "format:check": "prettier --check .",
52
- "type-check": "tsc --noEmit",
53
- "test": "vitest run",
54
- "check-unused": "npx knip",
55
- "prepare": "husky .husky || true"
56
- },
57
- "lint-staged": {
58
- "*.js": "eslint",
59
- "*.ts": "eslint"
60
- },
61
- "author": "oklinov",
62
- "license": "ISC"
2
+ "name": "apify-test-tools",
3
+ "version": "0.9.0-beta.1",
4
+ "type": "module",
5
+ "description": "TBD",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/apify/apify-test-tools.git"
9
+ },
10
+ "engines": {
11
+ "node": ">=18.0.0"
12
+ },
13
+ "bin": {
14
+ "apify-test-tools": "dist/bin/main.js"
15
+ },
16
+ "types": "./dist/index.d.ts",
17
+ "main": "./dist/lib/lib.js",
18
+ "dependencies": {
19
+ "@apify/consts": "^2.43.0",
20
+ "@slack/web-api": "^7.9.2",
21
+ "apify-client": "^2.22.2",
22
+ "ignore": "^7.0.5",
23
+ "yargs": "^18.0.0"
24
+ },
25
+ "devDependencies": {
26
+ "@apify/eslint-config": "^1.0.0",
27
+ "@apify/tsconfig": "^0.1.1",
28
+ "@types/node": "^24.0.0",
29
+ "@types/yargs": "^17.0.33",
30
+ "eslint": "^9.29.0",
31
+ "eslint-config-prettier": "^10.1.5",
32
+ "globals": "^17.0.0",
33
+ "husky": "^9.0.11",
34
+ "knip": "^5.65.0",
35
+ "lint-staged": "^15.2.2",
36
+ "prettier": "^3.5.3",
37
+ "tsx": "^4.20.3",
38
+ "typescript": "^5.9.3",
39
+ "typescript-eslint": "^8.34.1",
40
+ "vitest": "^3.2.4"
41
+ },
42
+ "peerDependencies": {
43
+ "vitest": ">=3.2.4"
44
+ },
45
+ "scripts": {
46
+ "start": "npm run dist/bin/main.js",
47
+ "start:dev": "GITHUB_WORKSPACE=local-clone tsx bin/main.ts",
48
+ "build": "tsc && chmod +x ./dist/bin/main.js",
49
+ "lint": "eslint",
50
+ "lint:fix": "eslint --fix",
51
+ "format": "prettier --write .",
52
+ "format:check": "prettier --check .",
53
+ "type-check": "tsc --noEmit",
54
+ "test": "vitest run",
55
+ "check-unused": "npx knip",
56
+ "prepare": "husky .husky || true"
57
+ },
58
+ "lint-staged": {
59
+ "*.js": "eslint",
60
+ "*.ts": "eslint"
61
+ },
62
+ "author": "oklinov",
63
+ "license": "ISC"
63
64
  }
@@ -8,6 +8,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
8
8
 
9
9
  import {
10
10
  collectNonIgnoredFiles,
11
+ collectSourceFiles,
11
12
  flattenMonorepoContext,
12
13
  rewriteActorJsonPaths,
13
14
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -62,86 +63,76 @@ describe('build-from-local helpers', () => {
62
63
  });
63
64
 
64
65
  describe('flattenMonorepoContext', () => {
65
- it('filters the .actor/ overlay through the same secret-pattern check as the rest of the context', async () => {
66
+ it("overlays the actor's own .actor/ files at the flattened root", async () => {
67
+ // Simulates flattening from a monorepo with an actors/<name>/.actor structure.
66
68
  const repoRoot = await mkTempDir('apify-test-tools-repo-');
67
69
  tempDirs.push(repoRoot);
68
- initGitRepo(repoRoot);
69
70
 
70
71
  const absActorDir = path.join(repoRoot, 'actors', 'owner_actor');
71
- await fs.mkdir(path.join(absActorDir, '.actor'), { recursive: true });
72
- await fs.writeFile(
73
- path.join(absActorDir, '.actor', 'actor.json'),
74
- JSON.stringify({ actorSpecification: 1, name: 'actor' }),
75
- );
76
- // A stray secret file inside .actor/ must never survive into the build.
77
- await fs.writeFile(path.join(absActorDir, '.actor', '.env'), 'SECRET=leaked');
78
- await fs.writeFile(path.join(absActorDir, '.actor', 'INPUT_SCHEMA.json'), '{}');
72
+ const actorJsonPath = path.join(absActorDir, '.actor', 'actor.json');
73
+ const inputSchemaPath = path.join(absActorDir, '.actor', 'INPUT_SCHEMA.json');
74
+ const mainSrcPath = path.join(absActorDir, 'src', 'main.ts');
75
+ const packageJsonPath = path.join(repoRoot, 'package.json');
79
76
 
80
- const keptContextFile = path.join(repoRoot, 'package.json');
81
- await fs.writeFile(keptContextFile, '{}');
77
+ await fs.mkdir(path.join(absActorDir, '.actor'), { recursive: true });
78
+ await fs.mkdir(path.join(absActorDir, 'src'), { recursive: true });
79
+ await fs.writeFile(actorJsonPath, JSON.stringify({ actorSpecification: 1, name: 'actor' }));
80
+ await fs.writeFile(inputSchemaPath, '{}');
81
+ await fs.writeFile(mainSrcPath, 'console.log(1)');
82
+ await fs.writeFile(packageJsonPath, '{}');
82
83
 
83
- // Nothing is gitignored here — isolates the assertion to the secret-pattern filter.
84
- vi.spyOn(Utils, 'getGitignoredPaths').mockReturnValue(new Set());
84
+ const keptContextFiles = [packageJsonPath, mainSrcPath, actorJsonPath, inputSchemaPath];
85
85
 
86
- const actorJson = JSON.parse(
87
- await fs.readFile(path.join(absActorDir, '.actor', 'actor.json'), 'utf8'),
88
- ) as Record<string, unknown>;
86
+ const actorJson = JSON.parse(await fs.readFile(actorJsonPath, 'utf8')) as Record<string, unknown>;
89
87
 
90
88
  const { tempDir: flattenedDir, filePaths } = await flattenMonorepoContext(
91
89
  'test/actor',
92
90
  absActorDir,
93
91
  repoRoot,
94
92
  actorJson,
95
- [keptContextFile],
96
- repoRoot,
93
+ keptContextFiles,
97
94
  );
98
95
  tempDirs.push(flattenedDir);
99
96
 
100
- await expect(fs.access(path.join(flattenedDir, '.actor', '.env'))).rejects.toThrow();
101
- await expect(fs.access(path.join(flattenedDir, '.actor', 'INPUT_SCHEMA.json'))).resolves.toBeUndefined();
97
+ const flattenedMainSrcPath = path.join(flattenedDir, 'actors', 'owner_actor', 'src', 'main.ts');
102
98
  await expect(fs.access(path.join(flattenedDir, '.actor', 'actor.json'))).resolves.toBeUndefined();
99
+ await expect(fs.access(path.join(flattenedDir, '.actor', 'INPUT_SCHEMA.json'))).resolves.toBeUndefined();
103
100
  await expect(fs.access(path.join(flattenedDir, 'package.json'))).resolves.toBeUndefined();
104
-
105
- // The returned filePaths must match what actually landed on disk — no .env, everything else present.
106
- expect(new Set(filePaths)).toStrictEqual(
107
- new Set([
108
- path.join(flattenedDir, 'package.json'),
109
- path.join(flattenedDir, '.actor', 'INPUT_SCHEMA.json'),
110
- path.join(flattenedDir, '.actor', 'actor.json'),
111
- ]),
112
- );
101
+ await expect(fs.access(flattenedMainSrcPath)).resolves.toBeUndefined();
102
+ expect(filePaths).toContain(path.join(flattenedDir, '.actor', 'actor.json'));
103
+ expect(filePaths).toContain(path.join(flattenedDir, '.actor', 'INPUT_SCHEMA.json'));
104
+ expect(filePaths).toContain(flattenedMainSrcPath);
113
105
  });
114
106
 
115
107
  it("keeps another actor's .actor/ directory intact at its original nested path", async () => {
108
+ // Simulates flattening from a monorepo with an actors/<name>/.actor structure — two
109
+ // sibling actors share the same context, but only one is being flattened here.
116
110
  const repoRoot = await mkTempDir('apify-test-tools-repo-');
117
111
  tempDirs.push(repoRoot);
118
- initGitRepo(repoRoot);
119
112
 
120
113
  const absActorDir = path.join(repoRoot, 'actors', 'owner_actor');
121
- await fs.mkdir(path.join(absActorDir, '.actor'), { recursive: true });
122
- await fs.writeFile(
123
- path.join(absActorDir, '.actor', 'actor.json'),
124
- JSON.stringify({ actorSpecification: 1, name: 'actor' }),
125
- );
126
-
114
+ const actorJsonPath = path.join(absActorDir, '.actor', 'actor.json');
127
115
  const otherActorDir = path.join(repoRoot, 'actors', 'owner_other-actor');
128
- await fs.mkdir(path.join(otherActorDir, '.actor'), { recursive: true });
129
116
  const otherActorSchemaFile = path.join(otherActorDir, '.actor', 'input_schema.json');
117
+
118
+ await fs.mkdir(path.join(absActorDir, '.actor'), { recursive: true });
119
+ await fs.writeFile(actorJsonPath, JSON.stringify({ actorSpecification: 1, name: 'actor' }));
120
+ await fs.mkdir(path.join(otherActorDir, '.actor'), { recursive: true });
130
121
  await fs.writeFile(otherActorSchemaFile, JSON.stringify({ schema: 'other' }));
131
122
 
132
- vi.spyOn(Utils, 'getGitignoredPaths').mockReturnValue(new Set());
123
+ // Stands in for collectNonIgnoredFiles's already-filtered output: the current actor's own
124
+ // actor.json (always kept by collectNonIgnoredFiles's .actor/ bypass in the real flow —
125
+ // see the collectSourceFiles test below) plus the other actor's file.
126
+ const keptContextFiles = [actorJsonPath, otherActorSchemaFile];
133
127
 
134
- const actorJson = JSON.parse(
135
- await fs.readFile(path.join(absActorDir, '.actor', 'actor.json'), 'utf8'),
136
- ) as Record<string, unknown>;
128
+ const actorJson = JSON.parse(await fs.readFile(actorJsonPath, 'utf8')) as Record<string, unknown>;
137
129
 
138
130
  const { tempDir: flattenedDir, filePaths } = await flattenMonorepoContext(
139
131
  'test/actor',
140
132
  absActorDir,
141
133
  repoRoot,
142
134
  actorJson,
143
- [otherActorSchemaFile],
144
- repoRoot,
135
+ keptContextFiles,
145
136
  );
146
137
  tempDirs.push(flattenedDir);
147
138
 
@@ -151,6 +142,33 @@ describe('build-from-local helpers', () => {
151
142
  });
152
143
  });
153
144
 
145
+ describe('collectSourceFiles', () => {
146
+ it("always collects the actor's own .actor/actor.json for a monorepo actor, since flattenMonorepoContext depends on it", async () => {
147
+ const repoRoot = await mkTempDir('apify-test-tools-collect-source-');
148
+ tempDirs.push(repoRoot);
149
+ initGitRepo(repoRoot);
150
+
151
+ const originalCwd = process.cwd();
152
+ // We simulate the working directory being the repo root, since collectSourceFiles uses relative paths to the repo root.
153
+ Utils.setCwd({ workspace: repoRoot });
154
+ try {
155
+ const cwd = process.cwd();
156
+ const actorDir = path.join(cwd, 'actors', 'owner_actor');
157
+ await fs.mkdir(path.join(actorDir, '.actor'), { recursive: true });
158
+ await fs.writeFile(
159
+ path.join(actorDir, '.actor', 'actor.json'),
160
+ JSON.stringify({ actorSpecification: 1, name: 'actor', dockerContextDir: '../../..' }),
161
+ );
162
+ await fs.writeFile(path.join(cwd, 'package.json'), '{}');
163
+
164
+ const sourceFiles = await collectSourceFiles('owner/actor', actorDir);
165
+ expect(sourceFiles.map((file) => file.name)).toContain('.actor/actor.json');
166
+ } finally {
167
+ process.chdir(originalCwd);
168
+ }
169
+ });
170
+ });
171
+
154
172
  describe('rewriteActorJsonPaths', () => {
155
173
  it('rewrites path fields that escape .actor/ relative to the new flattened location', async () => {
156
174
  const repoRoot = await mkTempDir('apify-test-tools-rewrite-');
@@ -182,83 +200,6 @@ describe('build-from-local helpers', () => {
182
200
  });
183
201
  });
184
202
 
185
- describe('getDockerignoredPaths', () => {
186
- const tempDirs: string[] = [];
187
-
188
- afterEach(async () => {
189
- vi.mocked(spawnSync).mockClear();
190
- await Promise.all(tempDirs.splice(0).map(async (dir) => fs.rm(dir, { recursive: true, force: true })));
191
- });
192
-
193
- it('returns an empty set without calling git when given no paths', () => {
194
- const result = Utils.getDockerignoredPaths('/does/not/matter', []);
195
-
196
- expect(result).toStrictEqual(new Set());
197
- expect(spawnSync).not.toHaveBeenCalled();
198
- });
199
-
200
- it('returns an empty set when rootDir has no .dockerignore', async () => {
201
- const rootDir = await mkTempDir('apify-test-tools-dockerignore-');
202
- tempDirs.push(rootDir);
203
- initGitRepo(rootDir);
204
-
205
- expect(Utils.getDockerignoredPaths(rootDir, ['main.js'])).toStrictEqual(new Set());
206
- });
207
-
208
- it('returns the paths that match .dockerignore patterns rooted at rootDir', async () => {
209
- const rootDir = await mkTempDir('apify-test-tools-dockerignore-');
210
- tempDirs.push(rootDir);
211
- initGitRepo(rootDir);
212
-
213
- await fs.writeFile(path.join(rootDir, '.dockerignore'), 'test/\n*.log\n');
214
-
215
- const result = Utils.getDockerignoredPaths(rootDir, ['main.js', 'test/fixture.json', 'debug.log']);
216
-
217
- expect(result).toStrictEqual(new Set(['test/fixture.json', 'debug.log']));
218
- });
219
-
220
- it('matches "./"-prefixed patterns, which Docker treats as a no-op but git treats as literal text', async () => {
221
- const rootDir = await mkTempDir('apify-test-tools-dockerignore-');
222
- tempDirs.push(rootDir);
223
- initGitRepo(rootDir);
224
-
225
- // The common real-world .dockerignore style: every pattern prefixed with "./",
226
- // including a negation re-including one of the otherwise-matched files.
227
- await fs.writeFile(path.join(rootDir, '.dockerignore'), './node_modules\n./*.log\n!./keep.log\n');
228
-
229
- const result = Utils.getDockerignoredPaths(rootDir, [
230
- 'node_modules/foo.js',
231
- 'debug.log',
232
- 'keep.log',
233
- 'main.js',
234
- ]);
235
-
236
- expect(result).toStrictEqual(new Set(['node_modules/foo.js', 'debug.log']));
237
- });
238
-
239
- it('matches already-tracked files too, since Docker excludes them regardless of git tracking', async () => {
240
- const rootDir = await mkTempDir('apify-test-tools-dockerignore-');
241
- tempDirs.push(rootDir);
242
- initGitRepo(rootDir);
243
- spawnSync('git', ['config', 'user.email', 'test@example.com'], { cwd: rootDir });
244
- spawnSync('git', ['config', 'user.name', 'Test'], { cwd: rootDir });
245
-
246
- // Husky hooks are committed to the repo, so this path is tracked by git — by default
247
- // `git check-ignore` never reports a tracked file as ignored, which would otherwise mask
248
- // this exact pattern from matching, unlike a real Docker build which excludes it anyway.
249
- await fs.mkdir(path.join(rootDir, '.husky'));
250
- await fs.writeFile(path.join(rootDir, '.husky', 'pre-commit'), '#!/bin/sh\n');
251
- spawnSync('git', ['add', '.husky/pre-commit'], { cwd: rootDir });
252
- spawnSync('git', ['commit', '-q', '-m', 'add husky hook'], { cwd: rootDir });
253
-
254
- await fs.writeFile(path.join(rootDir, '.dockerignore'), './.husky\n');
255
-
256
- const result = Utils.getDockerignoredPaths(rootDir, ['.husky/pre-commit', 'main.js']);
257
-
258
- expect(result).toStrictEqual(new Set(['.husky/pre-commit']));
259
- });
260
- });
261
-
262
203
  describe('getGitignoredPaths', () => {
263
204
  beforeEach(() => {
264
205
  vi.mocked(spawnSync).mockReset();