apify-test-tools 0.10.2-beta.2 → 0.10.2-beta.21

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 (275) hide show
  1. package/.gitattributes +1 -0
  2. package/.github/actionlint.yaml +11 -0
  3. package/.github/actions/checkout-restore-dependencies/action.yaml +58 -6
  4. package/.github/cli/apify-test-tools.mjs +135 -142
  5. package/.github/scripts/mask-config-secrets.sh +17 -0
  6. package/.github/scripts/run-with-apify-tokens.mjs +63 -103
  7. package/.github/workflows/_check_code.yaml +5 -21
  8. package/.github/workflows/_move_major_tag.yaml +2 -2
  9. package/.github/workflows/public_platform-tests-claude-investigate-and-fix.yaml +89 -8
  10. package/.github/workflows/public_platform-tests.yaml +68 -30
  11. package/.github/workflows/public_pr-build-test.yaml +77 -38
  12. package/.github/workflows/public_push-build-latest.yaml +49 -11
  13. package/.github/workflows/public_review.yaml +5 -4
  14. package/.github/workflows/public_stale-issues.yaml +51 -0
  15. package/.husky/pre-commit +4 -4
  16. package/.prettierignore +2 -1
  17. package/CHANGELOG.md +23 -0
  18. package/CONTRIBUTING.md +117 -17
  19. package/README.md +221 -595
  20. package/bin/actor-ids.ts +41 -0
  21. package/bin/affected-actors.ts +211 -0
  22. package/bin/build-from-local.ts +19 -1
  23. package/bin/build.ts +105 -39
  24. package/bin/diff-changes.ts +69 -36
  25. package/bin/diff-json-schema.ts +5 -6
  26. package/bin/git.ts +73 -175
  27. package/bin/main.ts +142 -189
  28. package/bin/notify.ts +198 -0
  29. package/bin/run-tests.ts +182 -0
  30. package/bin/slack.ts +16 -76
  31. package/bin/stdin-input.ts +156 -0
  32. package/bin/test-files-glob.ts +126 -0
  33. package/bin/test-report.ts +141 -140
  34. package/bin/types.ts +4 -7
  35. package/bin/utils/config/load-config.ts +20 -2
  36. package/bin/utils/config/parser.ts +40 -8
  37. package/bin/utils/config/structures/grouped.ts +2 -2
  38. package/bin/utils/config/structures/legacy.ts +1 -1
  39. package/dist/bin/actor-ids.d.ts +15 -0
  40. package/dist/bin/actor-ids.d.ts.map +1 -0
  41. package/dist/bin/actor-ids.js +30 -0
  42. package/dist/bin/actor-ids.js.map +1 -0
  43. package/dist/bin/affected-actors.d.ts +55 -0
  44. package/dist/bin/affected-actors.d.ts.map +1 -0
  45. package/dist/bin/affected-actors.js +142 -0
  46. package/dist/bin/affected-actors.js.map +1 -0
  47. package/dist/bin/build-from-local.d.ts +2 -1
  48. package/dist/bin/build-from-local.d.ts.map +1 -1
  49. package/dist/bin/build-from-local.js +6 -2
  50. package/dist/bin/build-from-local.js.map +1 -1
  51. package/dist/bin/build.d.ts +18 -5
  52. package/dist/bin/build.d.ts.map +1 -1
  53. package/dist/bin/build.js +74 -24
  54. package/dist/bin/build.js.map +1 -1
  55. package/dist/bin/diff-changes.d.ts +21 -5
  56. package/dist/bin/diff-changes.d.ts.map +1 -1
  57. package/dist/bin/diff-changes.js +41 -28
  58. package/dist/bin/diff-changes.js.map +1 -1
  59. package/dist/bin/diff-json-schema.d.ts +2 -2
  60. package/dist/bin/diff-json-schema.d.ts.map +1 -1
  61. package/dist/bin/diff-json-schema.js +1 -4
  62. package/dist/bin/diff-json-schema.js.map +1 -1
  63. package/dist/bin/git.d.ts +26 -34
  64. package/dist/bin/git.d.ts.map +1 -1
  65. package/dist/bin/git.js +60 -143
  66. package/dist/bin/git.js.map +1 -1
  67. package/dist/bin/main.js +126 -140
  68. package/dist/bin/main.js.map +1 -1
  69. package/dist/bin/notify.d.ts +35 -0
  70. package/dist/bin/notify.d.ts.map +1 -0
  71. package/dist/bin/notify.js +131 -0
  72. package/dist/bin/notify.js.map +1 -0
  73. package/dist/bin/run-tests.d.ts +21 -0
  74. package/dist/bin/run-tests.d.ts.map +1 -0
  75. package/dist/bin/run-tests.js +149 -0
  76. package/dist/bin/run-tests.js.map +1 -0
  77. package/dist/bin/slack.d.ts +2 -14
  78. package/dist/bin/slack.d.ts.map +1 -1
  79. package/dist/bin/slack.js +17 -46
  80. package/dist/bin/slack.js.map +1 -1
  81. package/dist/bin/stdin-input.d.ts +79 -0
  82. package/dist/bin/stdin-input.d.ts.map +1 -0
  83. package/dist/bin/stdin-input.js +124 -0
  84. package/dist/bin/stdin-input.js.map +1 -0
  85. package/dist/bin/test-files-glob.d.ts +18 -0
  86. package/dist/bin/test-files-glob.d.ts.map +1 -0
  87. package/dist/bin/test-files-glob.js +108 -0
  88. package/dist/bin/test-files-glob.js.map +1 -0
  89. package/dist/bin/test-report.d.ts +69 -8
  90. package/dist/bin/test-report.d.ts.map +1 -1
  91. package/dist/bin/test-report.js +76 -67
  92. package/dist/bin/test-report.js.map +1 -1
  93. package/dist/bin/types.d.ts +4 -7
  94. package/dist/bin/types.d.ts.map +1 -1
  95. package/dist/bin/utils/config/load-config.d.ts +8 -0
  96. package/dist/bin/utils/config/load-config.d.ts.map +1 -1
  97. package/dist/bin/utils/config/load-config.js +14 -1
  98. package/dist/bin/utils/config/load-config.js.map +1 -1
  99. package/dist/bin/utils/config/parser.d.ts +15 -2
  100. package/dist/bin/utils/config/parser.d.ts.map +1 -1
  101. package/dist/bin/utils/config/parser.js +29 -8
  102. package/dist/bin/utils/config/parser.js.map +1 -1
  103. package/dist/bin/utils/config/structures/grouped.js +2 -2
  104. package/dist/bin/utils/config/structures/grouped.js.map +1 -1
  105. package/dist/bin/utils/config/structures/legacy.js +1 -1
  106. package/dist/bin/utils/config/structures/legacy.js.map +1 -1
  107. package/dist/lib/extend-expect.d.ts.map +1 -1
  108. package/dist/lib/extend-expect.js +37 -218
  109. package/dist/lib/extend-expect.js.map +1 -1
  110. package/dist/lib/lib.d.ts +1 -5
  111. package/dist/lib/lib.d.ts.map +1 -1
  112. package/dist/lib/lib.js +19 -31
  113. package/dist/lib/lib.js.map +1 -1
  114. package/dist/lib/run-checks.d.ts +14 -0
  115. package/dist/lib/run-checks.d.ts.map +1 -0
  116. package/dist/lib/run-checks.js +82 -0
  117. package/dist/lib/run-checks.js.map +1 -0
  118. package/dist/lib/run-test-result.d.ts +3 -6
  119. package/dist/lib/run-test-result.d.ts.map +1 -1
  120. package/dist/lib/run-test-result.js +41 -54
  121. package/dist/lib/run-test-result.js.map +1 -1
  122. package/dist/test/bundle/cli-bundle.test.js +76 -56
  123. package/dist/test/bundle/cli-bundle.test.js.map +1 -1
  124. package/dist/test/bundle/token-wrapper.test.d.ts +2 -0
  125. package/dist/test/bundle/token-wrapper.test.d.ts.map +1 -0
  126. package/dist/test/bundle/token-wrapper.test.js +80 -0
  127. package/dist/test/bundle/token-wrapper.test.js.map +1 -0
  128. package/dist/test/e2e/cli-bundle.e2e.test.js +33 -68
  129. package/dist/test/e2e/cli-bundle.e2e.test.js.map +1 -1
  130. package/dist/test/e2e/git-build-release.e2e.test.d.ts +2 -0
  131. package/dist/test/e2e/git-build-release.e2e.test.d.ts.map +1 -0
  132. package/dist/test/e2e/git-build-release.e2e.test.js +87 -0
  133. package/dist/test/e2e/git-build-release.e2e.test.js.map +1 -0
  134. package/dist/test/e2e/helpers.d.ts +37 -0
  135. package/dist/test/e2e/helpers.d.ts.map +1 -0
  136. package/dist/test/e2e/helpers.js +57 -0
  137. package/dist/test/e2e/helpers.js.map +1 -0
  138. package/dist/test/unit/bin/actor-filtering.test.js +7 -0
  139. package/dist/test/unit/bin/actor-filtering.test.js.map +1 -1
  140. package/dist/test/unit/bin/actor-ids.test.d.ts +2 -0
  141. package/dist/test/unit/bin/actor-ids.test.d.ts.map +1 -0
  142. package/dist/test/unit/bin/actor-ids.test.js +40 -0
  143. package/dist/test/unit/bin/actor-ids.test.js.map +1 -0
  144. package/dist/test/unit/bin/affected-actors-cli.test.d.ts +2 -0
  145. package/dist/test/unit/bin/affected-actors-cli.test.d.ts.map +1 -0
  146. package/dist/test/unit/bin/affected-actors-cli.test.js +224 -0
  147. package/dist/test/unit/bin/affected-actors-cli.test.js.map +1 -0
  148. package/dist/test/unit/bin/affected-actors.test.d.ts +2 -0
  149. package/dist/test/unit/bin/affected-actors.test.d.ts.map +1 -0
  150. package/dist/test/unit/bin/affected-actors.test.js +153 -0
  151. package/dist/test/unit/bin/affected-actors.test.js.map +1 -0
  152. package/dist/test/unit/bin/build-cli.test.d.ts +2 -0
  153. package/dist/test/unit/bin/build-cli.test.d.ts.map +1 -0
  154. package/dist/test/unit/bin/build-cli.test.js +49 -0
  155. package/dist/test/unit/bin/build-cli.test.js.map +1 -0
  156. package/dist/test/unit/bin/build-from-local.test.js +12 -0
  157. package/dist/test/unit/bin/build-from-local.test.js.map +1 -1
  158. package/dist/test/unit/bin/build.test.js +116 -6
  159. package/dist/test/unit/bin/build.test.js.map +1 -1
  160. package/dist/test/unit/bin/compiled-cli.d.ts +6 -0
  161. package/dist/test/unit/bin/compiled-cli.d.ts.map +1 -0
  162. package/dist/test/unit/bin/compiled-cli.js +23 -0
  163. package/dist/test/unit/bin/compiled-cli.js.map +1 -0
  164. package/dist/test/unit/bin/diff-changes.test.js +168 -114
  165. package/dist/test/unit/bin/diff-changes.test.js.map +1 -1
  166. package/dist/test/unit/bin/diff.test.js +25 -28
  167. package/dist/test/unit/bin/diff.test.js.map +1 -1
  168. package/dist/test/unit/bin/git.test.js +67 -288
  169. package/dist/test/unit/bin/git.test.js.map +1 -1
  170. package/dist/test/unit/bin/notify.test.d.ts +2 -0
  171. package/dist/test/unit/bin/notify.test.d.ts.map +1 -0
  172. package/dist/test/unit/bin/notify.test.js +222 -0
  173. package/dist/test/unit/bin/notify.test.js.map +1 -0
  174. package/dist/test/unit/bin/stdin-input.test.d.ts +2 -0
  175. package/dist/test/unit/bin/stdin-input.test.d.ts.map +1 -0
  176. package/dist/test/unit/bin/stdin-input.test.js +92 -0
  177. package/dist/test/unit/bin/stdin-input.test.js.map +1 -0
  178. package/dist/test/unit/bin/test-cli.test.d.ts +2 -0
  179. package/dist/test/unit/bin/test-cli.test.d.ts.map +1 -0
  180. package/dist/test/unit/bin/test-cli.test.js +165 -0
  181. package/dist/test/unit/bin/test-cli.test.js.map +1 -0
  182. package/dist/test/unit/bin/test-files-glob.test.d.ts +2 -0
  183. package/dist/test/unit/bin/test-files-glob.test.d.ts.map +1 -0
  184. package/dist/test/unit/bin/test-files-glob.test.js +102 -0
  185. package/dist/test/unit/bin/test-files-glob.test.js.map +1 -0
  186. package/dist/test/unit/bin/test-report.test.d.ts +2 -0
  187. package/dist/test/unit/bin/test-report.test.d.ts.map +1 -0
  188. package/dist/test/unit/bin/test-report.test.js +100 -0
  189. package/dist/test/unit/bin/test-report.test.js.map +1 -0
  190. package/dist/test/unit/bin/utils/config/load-config.test.js +19 -1
  191. package/dist/test/unit/bin/utils/config/load-config.test.js.map +1 -1
  192. package/dist/test/unit/bin/utils/config/parser.test.js +36 -4
  193. package/dist/test/unit/bin/utils/config/parser.test.js.map +1 -1
  194. package/dist/test/unit/bin/utils/config/structures/grouped.test.js +6 -0
  195. package/dist/test/unit/bin/utils/config/structures/grouped.test.js.map +1 -1
  196. package/dist/test/unit/bin/utils/config/structures/legacy.test.js +1 -0
  197. package/dist/test/unit/bin/utils/config/structures/legacy.test.js.map +1 -1
  198. package/dist/test/unit/github-scripts/mask-config-secrets.test.d.ts +2 -0
  199. package/dist/test/unit/github-scripts/mask-config-secrets.test.d.ts.map +1 -0
  200. package/dist/test/unit/github-scripts/mask-config-secrets.test.js +50 -0
  201. package/dist/test/unit/github-scripts/mask-config-secrets.test.js.map +1 -0
  202. package/dist/test/unit/github-scripts/run-with-apify-tokens.test.d.ts +2 -0
  203. package/dist/test/unit/github-scripts/run-with-apify-tokens.test.d.ts.map +1 -0
  204. package/dist/test/unit/github-scripts/run-with-apify-tokens.test.js +102 -0
  205. package/dist/test/unit/github-scripts/run-with-apify-tokens.test.js.map +1 -0
  206. package/dist/test/unit/run-checks.test.d.ts +2 -0
  207. package/dist/test/unit/run-checks.test.d.ts.map +1 -0
  208. package/dist/test/unit/run-checks.test.js +62 -0
  209. package/dist/test/unit/run-checks.test.js.map +1 -0
  210. package/dist/test/unit/run-test-result.test.d.ts +2 -0
  211. package/dist/test/unit/run-test-result.test.d.ts.map +1 -0
  212. package/dist/test/unit/run-test-result.test.js +34 -0
  213. package/dist/test/unit/run-test-result.test.js.map +1 -0
  214. package/dist/test/unit/should-built-and-test.test.js +41 -38
  215. package/dist/test/unit/should-built-and-test.test.js.map +1 -1
  216. package/dist/tsconfig.tsbuildinfo +1 -1
  217. package/dist/workflow-scripts-src/run-with-apify-tokens.d.ts +3 -0
  218. package/dist/workflow-scripts-src/run-with-apify-tokens.d.ts.map +1 -0
  219. package/dist/workflow-scripts-src/run-with-apify-tokens.js +100 -0
  220. package/dist/workflow-scripts-src/run-with-apify-tokens.js.map +1 -0
  221. package/docs/cli.md +473 -0
  222. package/docs/lib.md +276 -0
  223. package/docs/workflows.md +455 -0
  224. package/eslint.config.mjs +10 -1
  225. package/lib/extend-expect.ts +48 -258
  226. package/lib/lib.ts +25 -34
  227. package/lib/run-checks.ts +101 -0
  228. package/lib/run-test-result.ts +45 -55
  229. package/package.json +4 -2
  230. package/platform-tests-investigate-and-fix/skills/assess-impact/SKILL.md +42 -0
  231. package/platform-tests-investigate-and-fix/skills/measure-flakiness/SKILL.md +4 -2
  232. package/test/bundle/cli-bundle.test.ts +97 -67
  233. package/test/bundle/token-wrapper.test.ts +110 -0
  234. package/test/e2e/cli-bundle.e2e.test.ts +60 -93
  235. package/test/e2e/git-build-release.e2e.test.ts +113 -0
  236. package/test/e2e/helpers.ts +82 -0
  237. package/test/unit/bin/actor-filtering.test.ts +8 -0
  238. package/test/unit/bin/actor-ids.test.ts +47 -0
  239. package/test/unit/bin/affected-actors-cli.test.ts +271 -0
  240. package/test/unit/bin/affected-actors.test.ts +221 -0
  241. package/test/unit/bin/build-cli.test.ts +61 -0
  242. package/test/unit/bin/build-from-local.test.ts +16 -0
  243. package/test/unit/bin/build.test.ts +149 -5
  244. package/test/unit/bin/compiled-cli.ts +23 -0
  245. package/test/unit/bin/diff-changes.test.ts +181 -114
  246. package/test/unit/bin/diff.test.ts +25 -28
  247. package/test/unit/bin/git.test.ts +76 -361
  248. package/test/unit/bin/notify.test.ts +299 -0
  249. package/test/unit/bin/stdin-input.test.ts +120 -0
  250. package/test/unit/bin/test-cli.test.ts +207 -0
  251. package/test/unit/bin/test-files-glob.test.ts +118 -0
  252. package/test/unit/bin/test-report.test.ts +118 -0
  253. package/test/unit/bin/utils/config/load-config.test.ts +26 -1
  254. package/test/unit/bin/utils/config/parser.test.ts +46 -5
  255. package/test/unit/bin/utils/config/structures/grouped.test.ts +12 -0
  256. package/test/unit/bin/utils/config/structures/legacy.test.ts +1 -0
  257. package/test/unit/github-scripts/mask-config-secrets.test.ts +61 -0
  258. package/test/unit/github-scripts/run-with-apify-tokens.test.ts +115 -0
  259. package/test/unit/run-checks.test.ts +76 -0
  260. package/test/unit/run-test-result.test.ts +44 -0
  261. package/test/unit/should-built-and-test.test.ts +46 -39
  262. package/tsconfig.json +1 -1
  263. package/vitest.config.ts +9 -0
  264. package/workflow-scripts-src/run-with-apify-tokens.ts +112 -0
  265. package/.github/scripts/check-major-tag-refs.mjs +0 -72
  266. package/dist/test/unit/bin/logging.test.d.ts +0 -2
  267. package/dist/test/unit/bin/logging.test.d.ts.map +0 -1
  268. package/dist/test/unit/bin/logging.test.js +0 -43
  269. package/dist/test/unit/bin/logging.test.js.map +0 -1
  270. package/dist/test/unit/bin/slack.test.d.ts +0 -2
  271. package/dist/test/unit/bin/slack.test.d.ts.map +0 -1
  272. package/dist/test/unit/bin/slack.test.js +0 -54
  273. package/dist/test/unit/bin/slack.test.js.map +0 -1
  274. package/test/unit/bin/logging.test.ts +0 -52
  275. package/test/unit/bin/slack.test.ts +0 -65
@@ -1,158 +1,159 @@
1
- import fs from 'node:fs/promises';
1
+ import path from 'node:path';
2
2
 
3
3
  import { logger } from './logger.js';
4
- import { getSlackClient, sendingLabel, sendSlackMessage } from './slack.js';
5
-
6
- interface ReportTestResultsOptions {
7
- reportFile: string;
8
- dryRun: boolean;
9
- reportSlackChannel?: string;
10
- jobUrl?: string;
11
- workflowName?: string;
12
- }
13
4
 
14
- export const reportTestResults = async ({
15
- dryRun,
16
- reportSlackChannel,
17
- reportFile: jsonResultsPath,
18
- jobUrl,
19
- workflowName,
20
- }: ReportTestResultsOptions) => {
21
- const results: JsonTestResults = JSON.parse((await fs.readFile(jsonResultsPath)).toString());
22
- const passed: JsonAssertionResult[] = [];
23
- const failed: JsonAssertionResult[] = [];
24
-
25
- for (const result of results.testResults) {
26
- if (result.status !== 'failed') {
27
- passed.push(...result.assertionResults);
28
- continue;
29
- }
30
- for (const aResult of result.assertionResults) {
31
- if (aResult.status !== 'failed') {
32
- passed.push(aResult);
33
- } else {
34
- failed.push(aResult);
35
- }
36
- }
37
- }
38
-
39
- const failedAssertions: { message: string; runLink: string; actorId: string }[] = [];
40
-
41
- logger.info();
42
- logger.info(`PASSED: ${passed.length}, FAILED: ${failed.length}`);
43
- logger.info();
44
- logger.info('**************************************************');
45
- logger.info('* Successes *');
46
- logger.info('**************************************************');
47
- logger.info();
48
- for (const [i, aResult] of passed.entries()) {
49
- const { fullName } = aResult;
50
- logger.info(`${i + 1}) ${fullName} ... ${aResult.meta.runLink}`);
51
- logger.info();
52
- }
5
+ export type TestStatus = 'passed' | 'failed' | 'skipped';
53
6
 
54
- logger.info('**************************************************');
55
- logger.info('* Failures *');
56
- logger.info('**************************************************');
57
- logger.info();
58
- for (const [i, aResult] of failed.entries()) {
59
- const { failureMessages, fullName, meta } = aResult;
60
- if (failureMessages) {
61
- failedAssertions.push(
62
- ...failureMessages.map((message) => ({
63
- message: message.split('\n')?.[0],
64
- runLink: meta.runLink,
65
- actorId: meta.actorId,
66
- })),
67
- );
68
- }
69
- logger.info(`${i + 1}) ${fullName} ... ${meta.runLink}`);
70
- logger.info();
71
- }
72
- logger.info();
73
- logger.info(`PASSED: ${passed.length}, FAILED: ${failed.length}`);
74
- logger.info();
75
-
76
- if (!reportSlackChannel) {
77
- logger.info(
78
- `Skipping slack notification. If you want to enable it, add --report-slack-channel flag and make sure SLACK_TOKEN_TESTS_BOT env variable is set.`,
79
- );
80
- return;
81
- }
82
- // Checked before knowing if there's anything to send, so a broken token shows up on a green run
83
- // (and in dry runs) rather than on the day tests fail
84
- const slack = await getSlackClient('SLACK_TOKEN_TESTS_BOT', dryRun);
85
-
86
- if (failedAssertions.length === 0) {
87
- return;
88
- }
7
+ /** The Actor run a test started, as the lib records it on the test's `meta`. */
8
+ export interface TestRun {
9
+ actorId: string;
10
+ runId: string;
11
+ runLink: string;
12
+ }
89
13
 
90
- // TODO: add slack profiles
91
- const total = failed.length + passed.length;
92
- const jobLink = jobUrl ? ` Check <${jobUrl}|the job>.` : '';
93
- let slackMessage = `\`${workflowName ?? '-'}\``;
94
- slackMessage += `: has ${failedAssertions.length} failed assertions. Failing test suites: ${failed.length}/${total}.${jobLink}`;
95
- slackMessage += `\n\n${failedAssertions[0].message} --- <${failedAssertions[0].runLink}|${failedAssertions[0].actorId}>`;
96
- const blocks = failedAssertions
97
- .slice(1)
98
- .map(({ message, runLink, actorId }) => `• ${message} --- <${runLink}|${actorId}>`);
99
-
100
- logger.info(`${sendingLabel(dryRun)} slack message to channel ${reportSlackChannel}:`, slackMessage);
101
- logger.info('\tblocks:', blocks.join('\n\t\t'));
102
-
103
- if (!dryRun) {
104
- await sendSlackMessage(slack, reportSlackChannel, slackMessage, blocks);
105
- }
106
- };
14
+ export interface TestCaseReport {
15
+ /** The full test name, including its `describe` blocks, e.g. `owner/name: finishes` */
16
+ name: string;
17
+ /** Relative to the directory the tests ran in */
18
+ file: string;
19
+ status: TestStatus;
20
+ durationMs: number | null;
21
+ /** The first line of each failure, the rest (diffs, stack traces) is in the vitest output */
22
+ failureMessages: string[];
23
+ /** Null for tests that didn't start a run: skipped ones, standby tests, or ones that failed before the run */
24
+ run: TestRun | null;
25
+ }
107
26
 
108
- type Status = 'passed' | 'failed' | 'skipped' | 'pending' | 'todo' | 'disabled';
109
- type Milliseconds = number;
110
- interface Callsite {
111
- line: number;
112
- column: number;
27
+ /**
28
+ * What `test` writes to stdout and `notify` reads. A contract between the CLI and whoever reads it, so add fields
29
+ * freely, but renaming or removing one needs a new `version`.
30
+ */
31
+ export interface TestReport {
32
+ /** What the document is, so `notify` and others can tell it from other outputs */
33
+ kind: 'test-report';
34
+ version: 1;
35
+ /** False if any test failed, or the run failed in a way no test accounts for (see `errors`) */
36
+ success: boolean;
37
+ summary: { total: number; passed: number; failed: number; skipped: number };
38
+ tests: TestCaseReport[];
39
+ /** Failures outside any test, e.g. a test file that failed to import, or vitest crashing without a report */
40
+ errors: string[];
113
41
  }
114
42
 
115
- interface JsonAssertionResult {
116
- ancestorTitles: string[];
43
+ // Vitest's JSON reporter output, only the parts we read
44
+ type VitestStatus = 'passed' | 'failed' | 'skipped' | 'pending' | 'todo' | 'disabled';
45
+
46
+ interface VitestAssertionResult {
117
47
  fullName: string;
118
- status: Status;
119
- title: string;
120
- meta: {
121
- runId: string;
122
- runLink: string;
123
- actorId: string;
124
- };
125
- duration?: Milliseconds | null;
48
+ status: VitestStatus;
49
+ duration?: number | null;
126
50
  failureMessages: string[] | null;
127
- location?: Callsite | null;
51
+ meta?: Partial<TestRun>;
128
52
  }
129
53
 
130
- interface JsonTestResult {
131
- message: string;
54
+ interface VitestFileResult {
132
55
  name: string;
133
56
  status: 'failed' | 'passed';
134
- startTime: number;
135
- endTime: number;
136
- assertionResults: JsonAssertionResult[];
137
- // summary: string
138
- // coverage: unknown
57
+ message: string;
58
+ assertionResults: VitestAssertionResult[];
139
59
  }
140
60
 
141
- interface JsonTestResults {
142
- numFailedTests: number;
143
- numFailedTestSuites: number;
144
- numPassedTests: number;
145
- numPassedTestSuites: number;
146
- numPendingTests: number;
147
- numPendingTestSuites: number;
148
- numTodoTests: number;
149
- numTotalTests: number;
150
- numTotalTestSuites: number;
151
- startTime: number;
61
+ export interface VitestJsonReport {
152
62
  success: boolean;
153
- testResults: JsonTestResult[];
154
- // snapshot: SnapshotSummary
155
- // coverageMap?: CoverageMap | null | undefined
156
- // numRuntimeErrorTestSuites: number
157
- // wasInterrupted: boolean
63
+ testResults: VitestFileResult[];
158
64
  }
65
+
66
+ const firstLine = (text: string) => text.split('\n')[0];
67
+
68
+ const toRun = (meta: Partial<TestRun> | undefined): TestRun | null => {
69
+ const { actorId, runId, runLink } = meta ?? {};
70
+ return actorId && runId && runLink ? { actorId, runId, runLink } : null;
71
+ };
72
+
73
+ /**
74
+ * Turns vitest's JSON report into a `TestReport`. `exitCode` is vitest's: a non-zero exit with every test passing
75
+ * still fails the report (e.g. an unhandled error between tests), so the report never reads greener than the run.
76
+ */
77
+ export const toTestReport = (
78
+ vitest: VitestJsonReport,
79
+ { cwd, exitCode }: { cwd: string; exitCode: number },
80
+ ): TestReport => {
81
+ const tests: TestCaseReport[] = [];
82
+ const errors: string[] = [];
83
+
84
+ for (const file of vitest.testResults) {
85
+ const relativeFile = path.relative(cwd, file.name);
86
+ // A file that fails to load (an import error, a syntax error) has no tests to blame, only its message
87
+ if (file.status === 'failed' && file.message) {
88
+ errors.push(`${relativeFile}: ${firstLine(file.message)}`);
89
+ }
90
+ for (const assertion of file.assertionResults) {
91
+ const status: TestStatus =
92
+ assertion.status === 'passed' || assertion.status === 'failed' ? assertion.status : 'skipped';
93
+ tests.push({
94
+ name: assertion.fullName,
95
+ file: relativeFile,
96
+ status,
97
+ durationMs: assertion.duration != null ? Math.round(assertion.duration) : null,
98
+ failureMessages: (assertion.failureMessages ?? []).map(firstLine),
99
+ run: toRun(assertion.meta),
100
+ });
101
+ }
102
+ }
103
+
104
+ const count = (status: TestStatus) => tests.filter((test) => test.status === status).length;
105
+ const summary = {
106
+ total: tests.length,
107
+ passed: count('passed'),
108
+ failed: count('failed'),
109
+ skipped: count('skipped'),
110
+ };
111
+ if (exitCode !== 0 && summary.failed === 0 && errors.length === 0) {
112
+ errors.push(`Vitest exited with code ${exitCode}, see its output above`);
113
+ }
114
+
115
+ return {
116
+ kind: 'test-report',
117
+ version: 1,
118
+ success: vitest.success && exitCode === 0 && summary.failed === 0 && errors.length === 0,
119
+ summary,
120
+ tests,
121
+ errors,
122
+ };
123
+ };
124
+
125
+ /** The report of a run that left no vitest report behind, e.g. because vitest crashed or was killed. */
126
+ export const failedRunReport = (error: string): TestReport => ({
127
+ kind: 'test-report',
128
+ version: 1,
129
+ success: false,
130
+ summary: { total: 0, passed: 0, failed: 0, skipped: 0 },
131
+ tests: [],
132
+ errors: [error],
133
+ });
134
+
135
+ /** Logs the tests with their run links, which vitest's own output doesn't show. */
136
+ export const logTestReport = ({ summary, tests, errors }: TestReport) => {
137
+ const runLink = ({ run }: TestCaseReport) => (run ? ` ... ${run.runLink}` : '');
138
+ const totals = `PASSED: ${summary.passed}, FAILED: ${summary.failed}, SKIPPED: ${summary.skipped}`;
139
+
140
+ logger.info();
141
+ logger.info(totals);
142
+ for (const status of ['passed', 'failed'] as const) {
143
+ const matching = tests.filter((test) => test.status === status);
144
+ if (matching.length === 0) continue;
145
+ logger.info();
146
+ logger.info(status === 'passed' ? 'Passed:' : 'Failed:');
147
+ for (const [i, test] of matching.entries()) {
148
+ logger.info(`${i + 1}) ${test.name}${runLink(test)}`);
149
+ for (const message of test.failureMessages) logger.info(` ${message}`);
150
+ }
151
+ }
152
+ if (errors.length > 0) {
153
+ logger.info();
154
+ logger.info('Errors outside of tests:');
155
+ for (const error of errors) logger.info(`- ${error}`);
156
+ }
157
+ logger.info();
158
+ logger.info(totals);
159
+ };
package/bin/types.ts CHANGED
@@ -1,13 +1,10 @@
1
1
  import type { ActorJsonPaths } from './utils/config/actor-json.js';
2
2
  import type { ExistingDir, RelativeDir } from './utils/path/repo-relative.js';
3
3
 
4
- export interface Config {
5
- targetBranch: string;
6
- sourceBranch: string;
7
- baseCommit?: string;
8
- actors: string[];
9
- ignore: string[];
10
- testFilesGlob?: string;
4
+ /** The changes from `fromRef` (excluded) to `toRef`, compared as trees like `git diff fromRef toRef`. */
5
+ export interface GitRange {
6
+ fromRef: string;
7
+ toRef: string;
11
8
  }
12
9
 
13
10
  export type Commit = {
@@ -3,8 +3,8 @@ import type { ActorConfig } from '../../types.js';
3
3
  import { safeReadJsonObjectFile } from '../json-file.js';
4
4
  import { ExistingFile, type RelativeDir } from '../path/repo-relative.js';
5
5
  import { readActorJson } from './actor-json.js';
6
- import { parseConfigFile } from './parser.js';
7
- import type { ValidatedActorConfig } from './structures/base.js';
6
+ import { parseConfigFile, parseRepoSettings, parseResolvedConfigFile, type RepoSettings } from './parser.js';
7
+ import type { ResolvedActorConfig, ValidatedActorConfig } from './structures/base.js';
8
8
 
9
9
  export const CONFIG_FILE_NAME = 'apify-test-tools.config.json';
10
10
 
@@ -111,3 +111,21 @@ export const readConfigFile = async (selection: { actors: string[]; ignore: stri
111
111
 
112
112
  return selectActors(selection, actorConfigs);
113
113
  };
114
+
115
+ type PathlessConfig = Omit<ResolvedActorConfig, 'folder' | 'overrideActorContext'>;
116
+ /** Read settings shared by all commands without initializing or validating local paths. */
117
+ export const readBaseConfigFile = async () => {
118
+ const rawFileContents = await readConfigFileContents();
119
+ return parseResolvedConfigFile(rawFileContents).map(
120
+ ({ actorFullName, tokenEnvVar }) =>
121
+ // using `satisfies` here instead of return type annotation
122
+ // so its not shown as `PathlessConfig` type on output or IDE, but the expanded form
123
+ ({
124
+ actorFullName,
125
+ tokenEnvVar,
126
+ }) satisfies PathlessConfig,
127
+ );
128
+ };
129
+
130
+ /** The settings for the whole repository, such as `platformTestsGlob`, with their defaults filled in. */
131
+ export const readRepoSettings = async (): Promise<RepoSettings> => parseRepoSettings(await readConfigFileContents());
@@ -47,8 +47,8 @@ const ValidatedConfigSchema = z.array(
47
47
  }),
48
48
  );
49
49
 
50
- /** Normalize and validate paths, then check collisions using their final spelling. */
51
- function verifyConfiguration(body: ResolvedActorConfig[]): ValidatedActorConfig[] {
50
+ /** Normalize and initialize paths, including checking that actor folders exist. */
51
+ function initializePaths(body: ResolvedActorConfig[]): ValidatedActorConfig[] {
52
52
  const normalized = body.map((entry) => ({
53
53
  ...entry,
54
54
  folder: normalizeFolder(entry.folder),
@@ -56,14 +56,18 @@ function verifyConfiguration(body: ResolvedActorConfig[]): ValidatedActorConfig[
56
56
  }));
57
57
  const parsed = ValidatedConfigSchema.safeParse(normalized);
58
58
  if (!parsed.success) throw new Error(z.prettifyError(parsed.error));
59
+ return parsed.data;
60
+ }
59
61
 
62
+ /** Check rules that involve more than one Actor, using initialized paths for comparisons. */
63
+ function verifyConfiguration(body: ValidatedActorConfig[]): void {
60
64
  const seenFolders = new Set<string>();
61
65
  const seenActorFullNames = new Set<string>();
62
66
  const errors: string[] = [];
63
- for (const [index, entry] of parsed.data.entries()) {
67
+ for (const entry of body) {
64
68
  // TODO: drop folder uniqueness (this requires several changes on other places)
65
69
  if (seenFolders.has(entry.folder.path)) {
66
- errors.push(`Duplicate folder "${body[index].folder}". Each actor must have a unique folder.`);
70
+ errors.push(`Duplicate folder "${entry.folder.path}". Each actor must have a unique folder.`);
67
71
  } else {
68
72
  seenFolders.add(entry.folder.path);
69
73
  }
@@ -76,19 +80,47 @@ function verifyConfiguration(body: ResolvedActorConfig[]): ValidatedActorConfig[
76
80
  }
77
81
 
78
82
  if (errors.length > 0) throw new Error(errors.join('\n'));
83
+ }
84
+
85
+ /** Which changed files are platform tests, unless the config sets `platformTestsGlob` */
86
+ const DEFAULT_PLATFORM_TESTS_GLOB = 'test/platform/**';
87
+
88
+ /** Settings for the whole repository, next to the Actor entries at the top level of the config. */
89
+ export interface RepoSettings {
90
+ /**
91
+ * Glob from the repo root of the platform test files and their helpers. A change to a matching file is a test
92
+ * change: the tests run, but no release is built for it.
93
+ */
94
+ platformTestsGlob: string;
95
+ }
96
+
97
+ const RepoSettingsSchema = z.object({
98
+ platformTestsGlob: z.string().min(1).default(DEFAULT_PLATFORM_TESTS_GLOB),
99
+ });
100
+
101
+ export function parseRepoSettings(body: Record<string, unknown>): RepoSettings {
102
+ const parsed = RepoSettingsSchema.safeParse(body);
103
+ if (!parsed.success) throw new Error(z.prettifyError(parsed.error));
79
104
  return parsed.data;
80
105
  }
81
106
 
82
107
  // eslint-disable-next-line no-underscore-dangle
83
108
  export const _privates = {
84
109
  selectStrategy,
110
+ initializePaths,
85
111
  verifyConfiguration,
86
112
  };
87
113
 
88
- export function parseConfigFile(body: Record<string, unknown>): ValidatedActorConfig[] {
89
- const { mode, ...rest } = body;
114
+ export function parseResolvedConfigFile(body: Record<string, unknown>): ResolvedActorConfig[] {
115
+ // The repo settings are read by parseRepoSettings, the strategies only see the Actor entries
116
+ const { mode, platformTestsGlob: _platformTestsGlob, ...rest } = body;
90
117
  const strategy = selectStrategy({ mode });
118
+ return strategy.parse(rest);
119
+ }
91
120
 
92
- const resolved = strategy.parse(rest);
93
- return verifyConfiguration(resolved);
121
+ export function parseConfigFile(body: Record<string, unknown>): ValidatedActorConfig[] {
122
+ const resolved = parseResolvedConfigFile(body);
123
+ const initialized = initializePaths(resolved);
124
+ verifyConfiguration(initialized);
125
+ return initialized;
94
126
  }
@@ -13,12 +13,12 @@ const schema = z.object({
13
13
  folder: z.string(),
14
14
  actorFullName: z.string().regex(ACTOR_FULL_NAME_REGEX),
15
15
  // these allow overrides from stuff set at the group level
16
- tokenEnvVar: z.string().optional(),
16
+ tokenEnvVar: z.string().min(1).optional(),
17
17
  overrideActorContext: z.array(z.string()).optional(),
18
18
  }),
19
19
  )
20
20
  .min(1),
21
- tokenEnvVar: z.string(),
21
+ tokenEnvVar: z.string().min(1),
22
22
  overrideActorContext: z.array(z.string()).optional(),
23
23
  }),
24
24
  )
@@ -8,7 +8,7 @@ const schema = z.object({
8
8
  z.object({
9
9
  folder: z.string(),
10
10
  actorFullName: z.string().regex(ACTOR_FULL_NAME_REGEX),
11
- tokenEnvVar: z.string(),
11
+ tokenEnvVar: z.string().min(1),
12
12
  overrideActorContext: z.array(z.string()).optional(),
13
13
  }),
14
14
  )
@@ -0,0 +1,15 @@
1
+ import { ApifyClient } from 'apify-client';
2
+ import type { TestTarget } from './stdin-input.js';
3
+ type ActorGetter = Pick<ApifyClient, 'actor'>;
4
+ /**
5
+ * Adds the ID each target is missing: the raw ID to those listed by full name, e.g. by `get-actor-configs`, and the
6
+ * full name to those listed by raw ID. The lib matches a test to its Actor by the ID the test names, so a target
7
+ * without either ID would skip the tests that name its Actor by the other one.
8
+ *
9
+ * An Actor the token can't see keeps only the ID it was listed with, with a warning, rather than failing the run:
10
+ * `get-actor-configs` lists every Actor in the repository, also ones no platform test uses, such as Actors the tester
11
+ * account was never given access to. A test that names it by the listed ID still runs and fails on its own.
12
+ */
13
+ export declare const withActorIds: (targets: TestTarget[], client?: ActorGetter) => Promise<TestTarget[]>;
14
+ export {};
15
+ //# sourceMappingURL=actor-ids.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"actor-ids.d.ts","sourceRoot":"","sources":["../../bin/actor-ids.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAG3C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,KAAK,WAAW,GAAG,IAAI,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;AAE9C;;;;;;;;GAQG;AACH,eAAO,MAAM,YAAY,GACrB,SAAS,UAAU,EAAE,EACrB,SAAQ,WAAwE,KACjF,OAAO,CAAC,UAAU,EAAE,CAqBlB,CAAC"}
@@ -0,0 +1,30 @@
1
+ import { ApifyClient } from 'apify-client';
2
+ import { logger } from './logger.js';
3
+ /**
4
+ * Adds the ID each target is missing: the raw ID to those listed by full name, e.g. by `get-actor-configs`, and the
5
+ * full name to those listed by raw ID. The lib matches a test to its Actor by the ID the test names, so a target
6
+ * without either ID would skip the tests that name its Actor by the other one.
7
+ *
8
+ * An Actor the token can't see keeps only the ID it was listed with, with a warning, rather than failing the run:
9
+ * `get-actor-configs` lists every Actor in the repository, also ones no platform test uses, such as Actors the tester
10
+ * account was never given access to. A test that names it by the listed ID still runs and fails on its own.
11
+ */
12
+ export const withActorIds = async (targets, client = new ApifyClient({ token: process.env.TESTER_APIFY_TOKEN })) => Promise.all(targets.map(async (target) => {
13
+ const { actorFullName, actorRawId } = target;
14
+ if (actorFullName && actorRawId)
15
+ return target;
16
+ // The parser lets no target through without either ID
17
+ const actorId = (actorFullName ?? actorRawId);
18
+ const actor = await client.actor(actorId).get();
19
+ if (!actor) {
20
+ logger.warn(`Actor ${actorId} not found, or TESTER_APIFY_TOKEN can't access it. Tests that name it by ` +
21
+ `${actorFullName ? 'its raw ID' : 'its full name'} will be skipped.`);
22
+ return target;
23
+ }
24
+ return {
25
+ ...target,
26
+ actorFullName: actorFullName ?? `${actor.username}/${actor.name}`,
27
+ actorRawId: actorRawId ?? actor.id,
28
+ };
29
+ }));
30
+ //# sourceMappingURL=actor-ids.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"actor-ids.js","sourceRoot":"","sources":["../../bin/actor-ids.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAE3C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAKrC;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,KAAK,EAC7B,OAAqB,EACrB,SAAsB,IAAI,WAAW,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,GAAG,CAAC,kBAAkB,EAAE,CAAC,EAC3D,EAAE,CACvB,OAAO,CAAC,GAAG,CACP,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE;IACzB,MAAM,EAAE,aAAa,EAAE,UAAU,EAAE,GAAG,MAAM,CAAC;IAC7C,IAAI,aAAa,IAAI,UAAU;QAAE,OAAO,MAAM,CAAC;IAC/C,sDAAsD;IACtD,MAAM,OAAO,GAAG,CAAC,aAAa,IAAI,UAAU,CAAE,CAAC;IAC/C,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,CAAC;IAChD,IAAI,CAAC,KAAK,EAAE,CAAC;QACT,MAAM,CAAC,IAAI,CACP,SAAS,OAAO,2EAA2E;YACvF,GAAG,aAAa,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,eAAe,mBAAmB,CAC3E,CAAC;QACF,OAAO,MAAM,CAAC;IAClB,CAAC;IACD,OAAO;QACH,GAAG,MAAM;QACT,aAAa,EAAE,aAAa,IAAI,GAAG,KAAK,CAAC,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE;QACjE,UAAU,EAAE,UAAU,IAAI,KAAK,CAAC,EAAE;KACrC,CAAC;AACN,CAAC,CAAC,CACL,CAAC"}
@@ -0,0 +1,55 @@
1
+ import type { Commit } from './types.js';
2
+ interface AffectedActorsOptions {
3
+ /**
4
+ * Base usually points to the master branch
5
+ * Only what HEAD changes compared to base counts, like `git diff base...HEAD` or GitHub's "Files changed" tab:
6
+ * the branch's own changes, not what it merged in from base.
7
+ */
8
+ base?: string;
9
+ /**
10
+ * With `base` it's the last validated commit of the branch: Actors that didn't change after it are left out.
11
+ * It's ignored when it is HEAD (a rerun) or isn't in the local history. Without `base` everything after it
12
+ * counts, and it must be an ancestor of HEAD (e.g. the last release).
13
+ */
14
+ since?: string;
15
+ actors: string[];
16
+ ignore: string[];
17
+ }
18
+ /** The JSON document `get-affected-actors` prints. Versioned, since other commands and workflows consume it. */
19
+ interface AffectedActors {
20
+ version: 1;
21
+ head: string;
22
+ /** The commits, changed files and changelog describe the whole range, `--since` with `--base` only narrows the Actors. */
23
+ commits: Commit[];
24
+ changedFiles: string[];
25
+ changelog: string | null;
26
+ /**
27
+ * Actors with at least one functional change. They need a new build and tests. Only the names: the config file
28
+ * stays the source of truth for the rest.
29
+ */
30
+ functionallyChangedActors: {
31
+ actorFullName: string;
32
+ }[];
33
+ /**
34
+ * Actors with cosmetic changes (README, CHANGELOG, cosmetic .actor JSON) and no functional one. They only need a
35
+ * release build.
36
+ */
37
+ cosmeticallyChangedActors: {
38
+ actorFullName: string;
39
+ }[];
40
+ /**
41
+ * Actors whose platform tests (`platformTestsGlob` in the config) changed in their context, whatever else
42
+ * changed. Their tests need to run, but a test change alone needs no release. An Actor can also be in one of the
43
+ * lists above.
44
+ */
45
+ testChangedActors: {
46
+ actorFullName: string;
47
+ }[];
48
+ }
49
+ /**
50
+ * Finds the Actors whose context changed in the given range. `base` selects the branch's own changes
51
+ * (a PR), `since` the changes after a known commit (a release, or the last validated commit of a PR).
52
+ */
53
+ export declare const getAffectedActors: ({ base, since: sinceOption, actors, ignore, }: AffectedActorsOptions) => Promise<AffectedActors>;
54
+ export {};
55
+ //# sourceMappingURL=affected-actors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"affected-actors.d.ts","sourceRoot":"","sources":["../../bin/affected-actors.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAe,MAAM,EAAY,MAAM,YAAY,CAAC;AAMhE,UAAU,qBAAqB;IAC3B;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,MAAM,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,gHAAgH;AAChH,UAAU,cAAc;IACpB,OAAO,EAAE,CAAC,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,0HAA0H;IAC1H,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB;;;OAGG;IACH,yBAAyB,EAAE;QAAE,aAAa,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACvD;;;OAGG;IACH,yBAAyB,EAAE;QAAE,aAAa,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACvD;;;;OAIG;IACH,iBAAiB,EAAE;QAAE,aAAa,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAClD;AA8FD;;;GAGG;AACH,eAAO,MAAM,iBAAiB,GAAU,+CAKrC,qBAAqB,KAAG,OAAO,CAAC,cAAc,CAiDhD,CAAC"}