@zzzen/pyright-internal 1.2.0-dev.20260906 → 1.2.0-dev.20260920

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 (135) hide show
  1. package/dist/analyzer/checker.d.ts +2 -1
  2. package/dist/analyzer/checker.js +80 -5
  3. package/dist/analyzer/checker.js.map +1 -1
  4. package/dist/analyzer/constraintSolver.js +10 -2
  5. package/dist/analyzer/constraintSolver.js.map +1 -1
  6. package/dist/analyzer/constructors.js +7 -18
  7. package/dist/analyzer/constructors.js.map +1 -1
  8. package/dist/analyzer/decorators.js +45 -2
  9. package/dist/analyzer/decorators.js.map +1 -1
  10. package/dist/analyzer/program.js +1 -0
  11. package/dist/analyzer/program.js.map +1 -1
  12. package/dist/analyzer/sourceFileInfo.d.ts +0 -1
  13. package/dist/analyzer/sourceFileInfo.js +0 -3
  14. package/dist/analyzer/sourceFileInfo.js.map +1 -1
  15. package/dist/analyzer/typeCacheUtils.d.ts +1 -0
  16. package/dist/analyzer/typeCacheUtils.js +28 -1
  17. package/dist/analyzer/typeCacheUtils.js.map +1 -1
  18. package/dist/analyzer/typeEvaluator.d.ts +1 -1
  19. package/dist/analyzer/typeEvaluator.js +63 -389
  20. package/dist/analyzer/typeEvaluator.js.map +1 -1
  21. package/dist/analyzer/typeEvaluatorTypes.d.ts +1 -0
  22. package/dist/analyzer/typedDicts.js +3 -7
  23. package/dist/analyzer/typedDicts.js.map +1 -1
  24. package/dist/analyzer/types.js +7 -0
  25. package/dist/analyzer/types.js.map +1 -1
  26. package/dist/common/configOptions.d.ts +1 -0
  27. package/dist/common/configOptions.js +16 -0
  28. package/dist/common/configOptions.js.map +1 -1
  29. package/dist/common/envVarUtils.js +3 -3
  30. package/dist/common/envVarUtils.js.map +1 -1
  31. package/dist/common/extensibility.d.ts +0 -1
  32. package/dist/common/extensibility.js.map +1 -1
  33. package/dist/common/fileSystem.d.ts +1 -0
  34. package/dist/common/fileSystem.js.map +1 -1
  35. package/dist/common/realFileSystem.d.ts +1 -0
  36. package/dist/common/realFileSystem.js +12 -0
  37. package/dist/common/realFileSystem.js.map +1 -1
  38. package/dist/fileSystemMapping.d.ts +1 -1
  39. package/dist/fileSystemMapping.js +3 -0
  40. package/dist/fileSystemMapping.js.map +1 -1
  41. package/dist/languageService/completionProvider.d.ts +2 -1
  42. package/dist/languageService/completionProvider.js +4 -1
  43. package/dist/languageService/completionProvider.js.map +1 -1
  44. package/dist/localization/package.nls.cs.json +1 -0
  45. package/dist/localization/package.nls.de.json +1 -0
  46. package/dist/localization/package.nls.es.json +1 -0
  47. package/dist/localization/package.nls.fr.json +1 -0
  48. package/dist/localization/package.nls.it.json +1 -0
  49. package/dist/localization/package.nls.ja.json +1 -0
  50. package/dist/localization/package.nls.ko.json +1 -0
  51. package/dist/localization/package.nls.pl.json +1 -0
  52. package/dist/localization/package.nls.pt-br.json +1 -0
  53. package/dist/localization/package.nls.qps-ploc.json +1 -0
  54. package/dist/localization/package.nls.ru.json +1 -0
  55. package/dist/localization/package.nls.tr.json +1 -0
  56. package/dist/localization/package.nls.zh-cn.json +1 -0
  57. package/dist/localization/package.nls.zh-tw.json +1 -0
  58. package/dist/readonlyAugmentedFileSystem.d.ts +1 -0
  59. package/dist/readonlyAugmentedFileSystem.js +3 -0
  60. package/dist/readonlyAugmentedFileSystem.js.map +1 -1
  61. package/dist/tests/checker.test.js +8 -0
  62. package/dist/tests/checker.test.js.map +1 -1
  63. package/dist/tests/envVarUtils.test.js +16 -0
  64. package/dist/tests/envVarUtils.test.js.map +1 -1
  65. package/dist/tests/evaluationTrace.integration.test.d.ts +1 -0
  66. package/dist/tests/evaluationTrace.integration.test.js +266 -0
  67. package/dist/tests/evaluationTrace.integration.test.js.map +1 -0
  68. package/dist/tests/evaluationTrace.test.d.ts +1 -0
  69. package/dist/tests/evaluationTrace.test.js +422 -0
  70. package/dist/tests/evaluationTrace.test.js.map +1 -0
  71. package/dist/tests/evaluationTrace.viewer.test.d.ts +1 -0
  72. package/dist/tests/evaluationTrace.viewer.test.js +529 -0
  73. package/dist/tests/evaluationTrace.viewer.test.js.map +1 -0
  74. package/dist/tests/fileSystemMapping.test.js +3 -0
  75. package/dist/tests/fileSystemMapping.test.js.map +1 -1
  76. package/dist/tests/harness/vfs/filesystem.d.ts +1 -0
  77. package/dist/tests/harness/vfs/filesystem.js +3 -0
  78. package/dist/tests/harness/vfs/filesystem.js.map +1 -1
  79. package/dist/tests/importResolver.test.js +5 -0
  80. package/dist/tests/importResolver.test.js.map +1 -1
  81. package/dist/tests/mypyPrimerAnalysis.test.d.ts +1 -0
  82. package/dist/tests/mypyPrimerAnalysis.test.js +1779 -0
  83. package/dist/tests/mypyPrimerAnalysis.test.js.map +1 -0
  84. package/dist/tests/mypyPrimerMcp.test.d.ts +1 -0
  85. package/dist/tests/mypyPrimerMcp.test.js +186 -0
  86. package/dist/tests/mypyPrimerMcp.test.js.map +1 -0
  87. package/dist/tests/typeCacheUtils.test.js +15 -0
  88. package/dist/tests/typeCacheUtils.test.js.map +1 -1
  89. package/dist/tests/typeEvaluator1.test.js +2 -2
  90. package/dist/tests/typeEvaluator6.test.js +155 -3
  91. package/dist/tests/typeEvaluator6.test.js.map +1 -1
  92. package/dist/tests/typeEvaluator7.test.js +4 -0
  93. package/dist/tests/typeEvaluator7.test.js.map +1 -1
  94. package/dist/tests/typeUtils.test.js +15 -0
  95. package/dist/tests/typeUtils.test.js.map +1 -1
  96. package/dist/trace/capture.d.ts +31 -0
  97. package/dist/trace/capture.js +272 -0
  98. package/dist/trace/capture.js.map +1 -0
  99. package/dist/trace/cli.d.ts +1 -0
  100. package/dist/trace/cli.js +196 -0
  101. package/dist/trace/cli.js.map +1 -0
  102. package/dist/trace/nativeAdapter.d.ts +16 -0
  103. package/dist/trace/nativeAdapter.js +422 -0
  104. package/dist/trace/nativeAdapter.js.map +1 -0
  105. package/dist/trace/nativeBridge.d.ts +46 -0
  106. package/dist/trace/nativeBridge.js +416 -0
  107. package/dist/trace/nativeBridge.js.map +1 -0
  108. package/dist/trace/reader.d.ts +83 -0
  109. package/dist/trace/reader.js +296 -0
  110. package/dist/trace/reader.js.map +1 -0
  111. package/dist/trace/recorder.d.ts +59 -0
  112. package/dist/trace/recorder.js +303 -0
  113. package/dist/trace/recorder.js.map +1 -0
  114. package/dist/trace/runtimeLoader.d.ts +23 -0
  115. package/dist/trace/runtimeLoader.js +172 -0
  116. package/dist/trace/runtimeLoader.js.map +1 -0
  117. package/dist/trace/viewerAssets.d.ts +2 -0
  118. package/dist/trace/viewerAssets.js +76 -0
  119. package/dist/trace/viewerAssets.js.map +1 -0
  120. package/dist/trace/viewerClient.d.ts +1 -0
  121. package/dist/trace/viewerClient.js +485 -0
  122. package/dist/trace/viewerClient.js.map +1 -0
  123. package/dist/trace/viewerModel.d.ts +308 -0
  124. package/dist/trace/viewerModel.js +524 -0
  125. package/dist/trace/viewerModel.js.map +1 -0
  126. package/dist/trace/viewerServer.d.ts +7 -0
  127. package/dist/trace/viewerServer.js +185 -0
  128. package/dist/trace/viewerServer.js.map +1 -0
  129. package/dist/typeServer/typeServerFileSystem.d.ts +1 -0
  130. package/dist/typeServer/typeServerFileSystem.js +3 -0
  131. package/dist/typeServer/typeServerFileSystem.js.map +1 -1
  132. package/dist/typeServer/virtualFileOverlayFileSystem.d.ts +1 -0
  133. package/dist/typeServer/virtualFileOverlayFileSystem.js +3 -0
  134. package/dist/typeServer/virtualFileOverlayFileSystem.js.map +1 -1
  135. package/package.json +4 -2
@@ -0,0 +1,1779 @@
1
+ "use strict";
2
+ /*
3
+ * mypyPrimerAnalysis.test.ts
4
+ * Copyright (c) Microsoft Corporation.
5
+ * Licensed under the MIT license.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ const fs_1 = require("fs");
9
+ const os_1 = require("os");
10
+ const path_1 = require("path");
11
+ const vm_1 = require("vm");
12
+ const yaml_1 = require("yaml");
13
+ const mypyPrimerAnalysis_1 = require("../../../../build/ci/mypyPrimerAnalysis");
14
+ const repository = 'microsoft/pyright';
15
+ const headSha = 'a'.repeat(40);
16
+ const sample = [
17
+ 'example (https://github.com/example/project)',
18
+ '- .../projects/example/test.py',
19
+ '- .../projects/example/test.py:1:1 - error: Old message',
20
+ '- Details (reportArgumentType)',
21
+ '+ .../projects/example/test.py:1:1 - error: New message',
22
+ '+ Details (reportArgumentType)',
23
+ ].join('\n');
24
+ const expectedProject = {
25
+ name: 'example',
26
+ url: 'https://github.com/example/project',
27
+ shard: 0,
28
+ added: [
29
+ {
30
+ severity: 'error',
31
+ rule: 'unspecified',
32
+ text: '.../projects/example/test.py:1:1 - error: New message',
33
+ },
34
+ ],
35
+ removed: [
36
+ {
37
+ severity: 'error',
38
+ rule: 'unspecified',
39
+ text: '.../projects/example/test.py:1:1 - error: Old message',
40
+ },
41
+ ],
42
+ detailsAdded: [{ text: ' Details (reportArgumentType)', rule: 'reportArgumentType' }],
43
+ detailsRemoved: [{ text: ' Details (reportArgumentType)', rule: 'reportArgumentType' }],
44
+ };
45
+ // Reduced forms of the headerless pip and xarray changes in PR #11601's primer artifacts.
46
+ const detailOnlySample = [
47
+ 'example (https://github.com/example/project)',
48
+ '- \u00a0\u00a0Return type mismatch: override returns "Match[str] | None"',
49
+ '+ \u00a0\u00a0Return type mismatch: override returns "Unknown"',
50
+ '- Operator "+" not supported when expected type is "NDArray[Any]" (reportOperatorIssue)',
51
+ '+ Operator "+" not supported when expected type is "ndarray[_AnyShape, dtype[Any]]" (reportOperatorIssue)',
52
+ ].join('\n');
53
+ const expectedDetailOnlyProject = {
54
+ ...expectedProject,
55
+ added: [],
56
+ removed: [],
57
+ detailsAdded: [
58
+ { text: ' \u00a0\u00a0Return type mismatch: override returns "Unknown"', rule: 'unspecified' },
59
+ {
60
+ text: ' Operator "+" not supported when expected type is "ndarray[_AnyShape, dtype[Any]]" (reportOperatorIssue)',
61
+ rule: 'reportOperatorIssue',
62
+ },
63
+ ],
64
+ detailsRemoved: [
65
+ { text: ' \u00a0\u00a0Return type mismatch: override returns "Match[str] | None"', rule: 'unspecified' },
66
+ {
67
+ text: ' Operator "+" not supported when expected type is "NDArray[Any]" (reportOperatorIssue)',
68
+ rule: 'reportOperatorIssue',
69
+ },
70
+ ],
71
+ };
72
+ const expectedDetailOnlySummary = {
73
+ regressionSignals: {
74
+ example: [
75
+ {
76
+ kind: 'gradual-detail',
77
+ count: 1,
78
+ examplesAdded: [expectedDetailOnlyProject.detailsAdded[0].text],
79
+ examplesRemoved: expectedDetailOnlyProject.detailsRemoved.map((entry) => entry.text),
80
+ },
81
+ ],
82
+ },
83
+ projects: [
84
+ {
85
+ name: 'example',
86
+ url: 'https://github.com/example/project',
87
+ shard: 0,
88
+ added: 0,
89
+ removed: 0,
90
+ detailLinesAdded: 2,
91
+ detailLinesRemoved: 2,
92
+ },
93
+ ],
94
+ groups: { example: {} },
95
+ detailGroups: {
96
+ example: {
97
+ unspecified: {
98
+ added: 1,
99
+ removed: 1,
100
+ examplesAdded: [' \u00a0\u00a0Return type mismatch: override returns "Unknown"'],
101
+ examplesRemoved: [' \u00a0\u00a0Return type mismatch: override returns "Match[str] | None"'],
102
+ },
103
+ reportOperatorIssue: {
104
+ added: 1,
105
+ removed: 1,
106
+ examplesAdded: [
107
+ ' Operator "+" not supported when expected type is "ndarray[_AnyShape, dtype[Any]]" (reportOperatorIssue)',
108
+ ],
109
+ examplesRemoved: [
110
+ ' Operator "+" not supported when expected type is "NDArray[Any]" (reportOperatorIssue)',
111
+ ],
112
+ },
113
+ },
114
+ },
115
+ };
116
+ const expectedReportProvenance = [
117
+ `<!-- pyright-primer-analysis:${headSha}:42:1 -->`,
118
+ '## mypy_primer analysis',
119
+ '',
120
+ 'Advisory AI analysis of [primer run 42, attempt 1](https://github.com/microsoft/pyright/actions/runs/42)',
121
+ `for commit \`${headSha}\`. This is not an approval or proof of correctness.`,
122
+ '',
123
+ ];
124
+ const expectedReportHeading = [
125
+ ...expectedReportProvenance,
126
+ 'Added/removed counts are diagnostic headers; message rewrites can appear on both sides.',
127
+ 'Detail lines are counted separately, without assuming a location or association with nearby headers.',
128
+ '',
129
+ '| Project | Added | Removed | Detail lines + / - | Assessment | Confidence | Explanation |',
130
+ '| --- | ---: | ---: | ---: | --- | --- | --- |',
131
+ ];
132
+ const expectedReportRow = '| example | 1 | 1 | 1 / 1 | Needs human review | low | The argument error changed its wording. |';
133
+ const expectedReportDetails = [
134
+ '### example: The argument error changed its wording.',
135
+ '',
136
+ '**AI assessment:** Needs human review (low confidence). **PR attribution:** Not established.',
137
+ '',
138
+ '**Before / baseline evidence:** An argument error was reported.',
139
+ '',
140
+ '**After / PR evidence:** An argument error remains.',
141
+ '',
142
+ '**Why this matters:** No changed checking has been established.',
143
+ '',
144
+ '**Causal analysis:**',
145
+ '',
146
+ 'The error remains at the same location. Its cause has not been established.',
147
+ '',
148
+ '**Uncertainty / next check:** Exact dependency revision unavailable.',
149
+ '',
150
+ '',
151
+ ];
152
+ const expectedReportFooter = [
153
+ '**Full evidence and limitations:** download `mypy-primer-analysis-report` from ' +
154
+ '[this analysis run](https://github.com/microsoft/pyright/actions/runs/100).',
155
+ 'The original raw primer comment is unchanged. Treat uncertainty and possible regressions as requests for human review.',
156
+ ];
157
+ const incompleteOverview = '**Regression analysis incomplete.** No concrete regression has been established; unresolved investigations follow.';
158
+ const riskOverview = '**Regression review required.** Potential regressions or recorded warning signals are listed first; causation must be established separately.';
159
+ const expectedReport = {
160
+ body: [
161
+ ...expectedReportProvenance,
162
+ incompleteOverview,
163
+ '',
164
+ '## Unresolved investigations',
165
+ '',
166
+ ...expectedReportDetails,
167
+ ...expectedReportFooter,
168
+ ].join('\n'),
169
+ fullReport: [...expectedReportHeading, expectedReportRow, '', ...expectedReportDetails].join('\n'),
170
+ };
171
+ function expectedLargeReport(projectCount, explanation) {
172
+ const escapedNames = Array.from({ length: projectCount }, (_, index) => `project\\_${index}`);
173
+ const rows = escapedNames.map((name) => expectedReportRow.replace('example', name));
174
+ const details = escapedNames.flatMap((name) => expectedReportDetails.map((line) => line
175
+ .replace('### example:', `### ${name}:`)
176
+ .replace('The error remains at the same location. Its cause has not been established.', explanation)));
177
+ return {
178
+ body: [
179
+ ...expectedReportProvenance,
180
+ incompleteOverview,
181
+ '',
182
+ 'Full causal analyses are available in the report artifact.',
183
+ '',
184
+ ...escapedNames.map((name) => `- **${name}: Needs human review.** The argument error changed its wording. PR attribution: Not established.`),
185
+ '',
186
+ ...expectedReportFooter,
187
+ ].join('\n'),
188
+ fullReport: [...expectedReportHeading, ...rows, '', ...details].join('\n'),
189
+ };
190
+ }
191
+ function fixture(contents = sample, headRepository = repository) {
192
+ const source = {
193
+ runId: 42,
194
+ runAttempt: 1,
195
+ headSha,
196
+ headRepository,
197
+ headBranch: 'feature',
198
+ pullRequests: headRepository === repository ? [7] : [],
199
+ };
200
+ const manifest = { repository, source, prNumber: 7, ...(0, mypyPrimerAnalysis_1.summarizeDiffs)((0, mypyPrimerAnalysis_1.parseDiff)(contents, 0)) };
201
+ const pr = {
202
+ number: 7,
203
+ state: 'open',
204
+ base: { repo: { full_name: repository } },
205
+ head: { sha: headSha, ref: 'feature', repo: { full_name: headRepository } },
206
+ };
207
+ const run = {
208
+ id: 42,
209
+ run_attempt: 1,
210
+ event: 'pull_request',
211
+ path: '.github/workflows/mypy_primer_pr.yaml',
212
+ conclusion: 'success',
213
+ repository: { full_name: repository },
214
+ head_sha: headSha,
215
+ head_repository: { full_name: headRepository },
216
+ head_branch: 'feature',
217
+ pull_requests: source.pullRequests.map((number) => ({ number })),
218
+ run_started_at: '2026-09-10T00:00:00Z',
219
+ };
220
+ const artifacts = [
221
+ 'mypy_primer_diffs_pr_number',
222
+ ...Array.from({ length: 8 }, (_, shard) => `mypy_primer_diffs_${shard}`),
223
+ ].map((name) => ({ name, expired: false, size_in_bytes: 100, created_at: '2026-09-10T00:01:00Z' }));
224
+ const comments = [];
225
+ const associated = [pr];
226
+ const request = jest.fn(async (route, parameters) => {
227
+ if (route === 'GET /repos/{owner}/{repo}/actions/runs/{run_id}') {
228
+ return { data: run };
229
+ }
230
+ if (route.endsWith('/artifacts')) {
231
+ return { data: { artifacts } };
232
+ }
233
+ if (route === 'GET /repos/{owner}/{repo}/pulls/{pull_number}') {
234
+ return { data: pr };
235
+ }
236
+ if (route === 'GET /repos/{owner}/{repo}/commits/{commit_sha}/pulls') {
237
+ return { data: [] };
238
+ }
239
+ if (route === 'GET /repos/{owner}/{repo}/pulls') {
240
+ return { data: associated };
241
+ }
242
+ if (route === 'GET /repos/{owner}/{repo}/issues/{issue_number}/comments') {
243
+ return { data: comments };
244
+ }
245
+ if (route.startsWith('POST ') || route.startsWith('PATCH ')) {
246
+ return { data: { id: 123 } };
247
+ }
248
+ throw new Error(`Unexpected route: ${route}`);
249
+ });
250
+ const report = {
251
+ projects: [
252
+ {
253
+ name: 'example',
254
+ assessment: 'needs-review',
255
+ confidence: 'low',
256
+ attribution: 'unclear',
257
+ summary: 'The argument error changed its wording.',
258
+ before: 'An argument error was reported.',
259
+ after: 'An argument error remains.',
260
+ impact: 'No changed checking has been established.',
261
+ explanation: 'The error remains at the same location. Its cause has not been established.',
262
+ unresolved: 'Exact dependency revision unavailable.',
263
+ evidence: [],
264
+ },
265
+ ],
266
+ };
267
+ const output = (perProject = false) => ({
268
+ items: (perProject ? report.projects.map((project) => ({ projects: [project] })) : [report]).map((report) => ({
269
+ type: 'publish_primer_analysis',
270
+ report: JSON.stringify(report),
271
+ })),
272
+ });
273
+ return { source, manifest, pr, run, artifacts, comments, associated, request, report, output };
274
+ }
275
+ describe('mypy_primer analysis', () => {
276
+ let directory;
277
+ beforeEach(() => {
278
+ directory = (0, fs_1.mkdtempSync)((0, path_1.join)((0, os_1.tmpdir)(), 'pyright-primer-analysis-'));
279
+ });
280
+ afterEach(() => {
281
+ (0, fs_1.rmSync)(directory, { recursive: true, force: true });
282
+ });
283
+ function writeShards(contents = sample) {
284
+ for (let shard = 0; shard < 8; shard++) {
285
+ const folder = (0, path_1.join)(directory, `mypy_primer_diffs_${shard}`);
286
+ (0, fs_1.mkdirSync)(folder);
287
+ (0, fs_1.writeFileSync)((0, path_1.join)(folder, `diff_${shard}.txt`), shard === 0 ? contents : '');
288
+ }
289
+ (0, fs_1.mkdirSync)((0, path_1.join)(directory, 'mypy_primer_diffs_pr_number'));
290
+ (0, fs_1.writeFileSync)((0, path_1.join)(directory, 'mypy_primer_diffs_pr_number', 'pr_number.txt'), '7\n');
291
+ }
292
+ test('counts diagnostic records, not file headings or continuation lines', () => {
293
+ expect((0, mypyPrimerAnalysis_1.parseDiff)(sample.replace(/\n/g, '\r\n'), 3)).toStrictEqual([{ ...expectedProject, shard: 3 }]);
294
+ });
295
+ test('preserves headerless details and groups only explicitly present rules', () => {
296
+ const projects = (0, mypyPrimerAnalysis_1.parseDiff)(detailOnlySample, 0);
297
+ expect(projects).toStrictEqual([expectedDetailOnlyProject]);
298
+ expect((0, mypyPrimerAnalysis_1.summarizeDiffs)(projects)).toStrictEqual(expectedDetailOnlySummary);
299
+ });
300
+ test.each(['+', '-'])('accepts one-sided %s detail changes without inventing diagnostics', (sign) => {
301
+ const contents = [
302
+ 'example (https://github.com/example/project)',
303
+ `${sign} Changed detail (reportArgumentType)`,
304
+ ].join('\n');
305
+ const detail = { text: ' Changed detail (reportArgumentType)', rule: 'reportArgumentType' };
306
+ expect((0, mypyPrimerAnalysis_1.parseDiff)(contents, 0)).toStrictEqual([
307
+ {
308
+ ...expectedProject,
309
+ added: [],
310
+ removed: [],
311
+ detailsAdded: sign === '+' ? [detail] : [],
312
+ detailsRemoved: sign === '-' ? [detail] : [],
313
+ },
314
+ ]);
315
+ });
316
+ test('does not attach detail changes to nearby headers or leak them across projects', () => {
317
+ const contents = [
318
+ sample.replace('- .../projects/example/test.py', '- Earlier detail (reportGeneralTypeIssues)\n- .../projects/example/test.py'),
319
+ '+ .../projects/example/other.py',
320
+ '+ other.py:3:4 - error: Nested description (reportOperatorIssue)',
321
+ 'second (https://github.com/example/second)',
322
+ '- Unlocated detail',
323
+ ].join('\n');
324
+ expect((0, mypyPrimerAnalysis_1.parseDiff)(contents, 0)).toStrictEqual([
325
+ {
326
+ ...expectedProject,
327
+ detailsAdded: [
328
+ ...expectedProject.detailsAdded,
329
+ {
330
+ text: ' other.py:3:4 - error: Nested description (reportOperatorIssue)',
331
+ rule: 'reportOperatorIssue',
332
+ },
333
+ ],
334
+ detailsRemoved: [
335
+ { text: ' Earlier detail (reportGeneralTypeIssues)', rule: 'reportGeneralTypeIssues' },
336
+ ...expectedProject.detailsRemoved,
337
+ ],
338
+ },
339
+ {
340
+ name: 'second',
341
+ url: 'https://github.com/example/second',
342
+ shard: 0,
343
+ added: [],
344
+ removed: [],
345
+ detailsAdded: [],
346
+ detailsRemoved: [{ text: ' Unlocated detail', rule: 'unspecified' }],
347
+ },
348
+ ]);
349
+ });
350
+ test('keeps the complete project inventory before bounded diagnostic examples', () => {
351
+ const diagnostics = (0, mypyPrimerAnalysis_1.parseDiff)(sample, 0);
352
+ diagnostics[0].added.push(...Array.from({ length: 20 }, () => diagnostics[0].added[0]));
353
+ diagnostics[0].detailsAdded.push(...Array.from({ length: 20 }, () => diagnostics[0].detailsAdded[0]));
354
+ const summary = (0, mypyPrimerAnalysis_1.summarizeDiffs)(diagnostics);
355
+ expect(summary).toStrictEqual({
356
+ projects: [
357
+ {
358
+ name: 'example',
359
+ url: 'https://github.com/example/project',
360
+ shard: 0,
361
+ added: 21,
362
+ removed: 1,
363
+ detailLinesAdded: 21,
364
+ detailLinesRemoved: 1,
365
+ },
366
+ ],
367
+ groups: {
368
+ example: {
369
+ unspecified: {
370
+ added: 21,
371
+ removed: 1,
372
+ examplesAdded: Array(3).fill('.../projects/example/test.py:1:1 - error: New message'),
373
+ examplesRemoved: ['.../projects/example/test.py:1:1 - error: Old message'],
374
+ },
375
+ },
376
+ },
377
+ detailGroups: {
378
+ example: {
379
+ reportArgumentType: {
380
+ added: 21,
381
+ removed: 1,
382
+ examplesAdded: Array(3).fill(' Details (reportArgumentType)'),
383
+ examplesRemoved: [' Details (reportArgumentType)'],
384
+ },
385
+ },
386
+ },
387
+ });
388
+ });
389
+ test('accepts empty shards and reconciles changed totals', () => {
390
+ expect((0, mypyPrimerAnalysis_1.parseDiff)('', 0)).toEqual([]);
391
+ const change = sample.replace(/^-.*\n/gm, '') +
392
+ '\n- 0 errors, 0 warnings, 0 informations\n+ 1 error, 0 warnings, 0 informations';
393
+ expect((0, mypyPrimerAnalysis_1.parseDiff)(change, 0)[0].added).toHaveLength(1);
394
+ expect(() => (0, mypyPrimerAnalysis_1.parseDiff)(change.replace('+ 1 error', '+ 2 errors'), 0)).toThrow('counts');
395
+ expect(() => (0, mypyPrimerAnalysis_1.parseDiff)(change.replace('- 0 errors, 0 warnings, 0 informations\n', ''), 0)).toThrow('Incomplete');
396
+ });
397
+ test('rejects malformed output instead of silently omitting projects', () => {
398
+ expect(() => (0, mypyPrimerAnalysis_1.parseDiff)('unexpected output', 0)).toThrow('Unrecognized');
399
+ expect(() => (0, mypyPrimerAnalysis_1.parseDiff)(sample.replace('Old message', 'Old message\nnot a diff line'), 0)).toThrow();
400
+ });
401
+ test.each(['+ unrecognized output', '- not a diagnostic header', '+ '])('rejects malformed signed output: %s', (line) => {
402
+ expect(() => (0, mypyPrimerAnalysis_1.parseDiff)(`${sample}\n${line}`, 0)).toThrow(new Error('Unrecognized diagnostic for example'));
403
+ });
404
+ test('detail changes cannot account for missing diagnostic headers in the totals', () => {
405
+ expect(() => (0, mypyPrimerAnalysis_1.parseDiff)(`${detailOnlySample}\n- 0 errors, 0 warnings, 0 informations\n+ 1 error, 0 warnings, 0 informations`, 0)).toThrow(new Error('Diagnostic counts do not match the totals for example'));
406
+ });
407
+ test('requires all artifacts from the exact successful PR run and attempt', async () => {
408
+ const f = fixture();
409
+ await expect((0, mypyPrimerAnalysis_1.loadSource)(f.request, repository, f.source)).resolves.toEqual(f.source);
410
+ f.artifacts.pop();
411
+ await expect((0, mypyPrimerAnalysis_1.loadSource)(f.request, repository, f.source)).rejects.toThrow('Missing');
412
+ f.run.run_attempt = 2;
413
+ await expect((0, mypyPrimerAnalysis_1.loadSource)(f.request, repository, f.source)).rejects.toThrow('matching');
414
+ });
415
+ test('rejects expired, duplicate, old-attempt, and oversized artifacts', async () => {
416
+ for (const mutation of [
417
+ (f) => (f.artifacts[0].expired = true),
418
+ (f) => f.artifacts.push(f.artifacts[0]),
419
+ (f) => (f.artifacts[0].created_at = '2026-09-09T00:00:00Z'),
420
+ (f) => (f.artifacts[0].size_in_bytes = 9 * 1024 * 1024),
421
+ ]) {
422
+ const f = fixture();
423
+ mutation(f);
424
+ await expect((0, mypyPrimerAnalysis_1.loadSource)(f.request, repository, f.source)).rejects.toThrow('artifact');
425
+ }
426
+ });
427
+ test('rejects another workflow or repository', async () => {
428
+ const f = fixture();
429
+ f.run.path = '.github/workflows/other.yml';
430
+ await expect((0, mypyPrimerAnalysis_1.loadSource)(f.request, repository, f.source)).rejects.toThrow('matching');
431
+ f.run.path = '.github/workflows/mypy_primer_pr.yaml';
432
+ f.run.repository.full_name = 'someone/else';
433
+ await expect((0, mypyPrimerAnalysis_1.loadSource)(f.request, repository, f.source)).rejects.toThrow('matching');
434
+ });
435
+ test('prepares all shards and skips a superseded head', async () => {
436
+ writeShards();
437
+ const f = fixture();
438
+ await expect((0, mypyPrimerAnalysis_1.prepareAnalysis)(f.request, repository, f.source, directory)).resolves.toEqual(f.manifest);
439
+ f.pr.head.sha = 'b'.repeat(40);
440
+ await expect((0, mypyPrimerAnalysis_1.prepareAnalysis)(f.request, repository, f.source, directory)).resolves.toBeUndefined();
441
+ });
442
+ test.each([repository, 'contributor/pyright'])('previews a verified closed PR from %s', async (headRepository) => {
443
+ writeShards();
444
+ const f = fixture(sample, headRepository);
445
+ f.pr.state = 'closed';
446
+ f.source.pullRequests = [];
447
+ await expect((0, mypyPrimerAnalysis_1.prepareAnalysis)(f.request, repository, f.source, directory)).resolves.toBeUndefined();
448
+ await expect((0, mypyPrimerAnalysis_1.prepareAnalysis)(f.request, repository, f.source, directory, true)).resolves.toStrictEqual({
449
+ ...f.manifest,
450
+ preview: true,
451
+ });
452
+ expect(f.request.mock.calls.filter(([route]) => route === 'GET /repos/{owner}/{repo}/pulls')).toStrictEqual([
453
+ [
454
+ 'GET /repos/{owner}/{repo}/pulls',
455
+ {
456
+ owner: 'microsoft',
457
+ repo: 'pyright',
458
+ state: 'all',
459
+ head: `${headRepository.split('/')[0]}:feature`,
460
+ per_page: 100,
461
+ page: 1,
462
+ },
463
+ ],
464
+ ]);
465
+ });
466
+ test('previews reject ambiguous closed-PR associations and superseded heads', async () => {
467
+ writeShards();
468
+ const f = fixture(sample, 'contributor/pyright');
469
+ f.pr.state = 'closed';
470
+ f.associated.push({ ...f.pr, number: 8 });
471
+ await expect((0, mypyPrimerAnalysis_1.prepareAnalysis)(f.request, repository, f.source, directory, true)).rejects.toThrow('found 2');
472
+ f.pr.head.sha = 'b'.repeat(40);
473
+ await expect((0, mypyPrimerAnalysis_1.prepareAnalysis)(f.request, repository, f.source, directory, true)).resolves.toBeUndefined();
474
+ });
475
+ test.each(['repository', 'branch', 'number', 'shard'])('previews retain %s validation', async (mismatch) => {
476
+ writeShards();
477
+ const f = fixture();
478
+ f.pr.state = 'closed';
479
+ if (mismatch === 'repository') {
480
+ f.pr.head.repo.full_name = 'someone/else';
481
+ }
482
+ else if (mismatch === 'branch') {
483
+ f.pr.head.ref = 'different';
484
+ }
485
+ else if (mismatch === 'number') {
486
+ f.source.pullRequests = [99];
487
+ }
488
+ else {
489
+ (0, fs_1.rmSync)((0, path_1.join)(directory, 'mypy_primer_diffs_7'), { recursive: true });
490
+ }
491
+ await expect((0, mypyPrimerAnalysis_1.prepareAnalysis)(f.request, repository, f.source, directory, true)).rejects.toThrow();
492
+ });
493
+ test('prepares and renders a detail-only project without treating zero headers as no changes', async () => {
494
+ writeShards(detailOnlySample);
495
+ const f = fixture(detailOnlySample);
496
+ const manifest = await (0, mypyPrimerAnalysis_1.prepareAnalysis)(f.request, repository, f.source, directory);
497
+ expect(manifest).toStrictEqual({
498
+ repository,
499
+ source: f.source,
500
+ prNumber: 7,
501
+ ...expectedDetailOnlySummary,
502
+ });
503
+ if (!manifest) {
504
+ throw new Error('Preparation unexpectedly skipped the detail-only project');
505
+ }
506
+ f.report.projects[0] = {
507
+ ...f.report.projects[0],
508
+ name: 'example',
509
+ assessment: 'needs-review',
510
+ confidence: 'low',
511
+ summary: 'Only diagnostic details changed.',
512
+ explanation: 'The return type changed from Match[str] | None to Unknown.',
513
+ unresolved: 'Diagnostic headers and locations are absent from the concise diff.',
514
+ evidence: [],
515
+ };
516
+ const expectedDetails = [
517
+ '### example: Only diagnostic details changed.',
518
+ '',
519
+ '**Regression warning signals require review, regardless of the AI assessment:**',
520
+ '- Changed diagnostic details introduce Any/Unknown.',
521
+ '',
522
+ '**AI assessment:** Needs human review (low confidence). **PR attribution:** Not established.',
523
+ '',
524
+ '**Before / baseline evidence:** An argument error was reported.',
525
+ '',
526
+ '**After / PR evidence:** An argument error remains.',
527
+ '',
528
+ '**Why this matters:** No changed checking has been established.',
529
+ '',
530
+ '**Causal analysis:**',
531
+ '',
532
+ 'The return type changed from Match\\[str\\] \\| None to Unknown.',
533
+ '',
534
+ '**Uncertainty / next check:** Diagnostic headers and locations are absent from the concise diff.',
535
+ '',
536
+ '',
537
+ ];
538
+ const expectedRow = '| example | 0 | 0 | 2 / 2 | Needs human review | low | Only diagnostic details changed. |';
539
+ const expected = {
540
+ body: [
541
+ ...expectedReportProvenance,
542
+ riskOverview,
543
+ '',
544
+ '## Potential regressions',
545
+ '',
546
+ ...expectedDetails,
547
+ ...expectedReportFooter,
548
+ ].join('\n'),
549
+ fullReport: [...expectedReportHeading, expectedRow, '', ...expectedDetails].join('\n'),
550
+ };
551
+ expect((0, mypyPrimerAnalysis_1.renderReport)(manifest, f.report, 100)).toStrictEqual(expected);
552
+ expect(() => (0, mypyPrimerAnalysis_1.renderReport)(manifest, { projects: [] }, 100)).toThrow(new Error('The analysis must cover every changed project exactly once'));
553
+ const path = (0, path_1.join)(directory, 'report.md');
554
+ await expect((0, mypyPrimerAnalysis_1.publishAnalysis)(f.request, manifest, f.output(), 100, path, true)).resolves.toBe('Staged: report saved without posting a comment');
555
+ expect((0, fs_1.readFileSync)(path, 'utf8')).toBe(expected.fullReport);
556
+ expect(f.request.mock.calls.filter(([route]) => /^(POST|PATCH) /.test(route))).toStrictEqual([]);
557
+ });
558
+ test('does not trust the PR number artifact', async () => {
559
+ writeShards();
560
+ const f = fixture();
561
+ f.source.pullRequests = [99];
562
+ await expect((0, mypyPrimerAnalysis_1.prepareAnalysis)(f.request, repository, f.source, directory)).rejects.toThrow('not associated');
563
+ (0, fs_1.writeFileSync)((0, path_1.join)(directory, 'mypy_primer_diffs_pr_number', 'pr_number.txt'), '7; echo unsafe');
564
+ await expect((0, mypyPrimerAnalysis_1.prepareAnalysis)(f.request, repository, f.source, directory)).rejects.toThrow('Invalid PR number');
565
+ });
566
+ test('associates a fork by its head branch when GitHub omits commit and run PR associations', async () => {
567
+ writeShards();
568
+ const f = fixture(sample, 'contributor/pyright');
569
+ f.source.headBranch = f.run.head_branch = f.pr.head.ref = 'fix/feature';
570
+ const source = await (0, mypyPrimerAnalysis_1.loadSource)(f.request, repository, f.source);
571
+ expect(source).toStrictEqual(f.source);
572
+ await expect((0, mypyPrimerAnalysis_1.prepareAnalysis)(f.request, repository, source, directory)).resolves.toStrictEqual(f.manifest);
573
+ expect(f.request).toHaveBeenCalledWith('GET /repos/{owner}/{repo}/pulls', {
574
+ owner: 'microsoft',
575
+ repo: 'pyright',
576
+ state: 'open',
577
+ head: 'contributor:fix/feature',
578
+ per_page: 100,
579
+ page: 1,
580
+ });
581
+ expect(f.request.mock.calls.some(([route]) => route.includes('/commits/'))).toBe(false);
582
+ });
583
+ test('rejects missing or ambiguous fork associations', async () => {
584
+ writeShards();
585
+ const f = fixture(sample, 'contributor/pyright');
586
+ f.associated.length = 0;
587
+ await expect((0, mypyPrimerAnalysis_1.prepareAnalysis)(f.request, repository, f.source, directory)).rejects.toThrow('found 0');
588
+ f.associated.push(f.pr);
589
+ f.associated.push({ ...f.pr, number: 8 });
590
+ await expect((0, mypyPrimerAnalysis_1.prepareAnalysis)(f.request, repository, f.source, directory)).rejects.toThrow('found 2');
591
+ });
592
+ test('filters fork candidates by state, head SHA, branch, head repository, and base repository', async () => {
593
+ writeShards();
594
+ const f = fixture(sample, 'contributor/pyright');
595
+ const other = { ...f.pr, number: 8 };
596
+ f.associated.push({ ...other, state: 'closed' }, { ...other, head: { ...other.head, sha: 'b'.repeat(40) } }, { ...other, head: { ...other.head, ref: 'different' } }, { ...other, head: { ...other.head, repo: { full_name: 'someone/pyright' } } }, { ...other, head: { ...other.head, repo: { full_name: 'contributor/other' } } }, { ...other, base: { repo: { full_name: 'someone/else' } } });
597
+ await expect((0, mypyPrimerAnalysis_1.prepareAnalysis)(f.request, repository, f.source, directory)).resolves.toStrictEqual(f.manifest);
598
+ });
599
+ test('does not trust a fork PR number artifact without a matching branch association', async () => {
600
+ writeShards();
601
+ const f = fixture(sample, 'contributor/pyright');
602
+ f.associated.splice(0, 1, { ...f.pr, number: 8 });
603
+ await expect((0, mypyPrimerAnalysis_1.prepareAnalysis)(f.request, repository, f.source, directory)).rejects.toThrow('not associated');
604
+ });
605
+ test('paginates fork branch associations before requiring a unique match', async () => {
606
+ writeShards();
607
+ const f = fixture(sample, 'contributor/pyright');
608
+ const original = f.request.getMockImplementation();
609
+ f.request.mockImplementation(async (route, parameters) => {
610
+ if (route === 'GET /repos/{owner}/{repo}/pulls' && parameters.page === 1) {
611
+ return { data: Array.from({ length: 100 }, () => ({ ...f.pr, state: 'closed' })) };
612
+ }
613
+ return original(route, parameters);
614
+ });
615
+ await expect((0, mypyPrimerAnalysis_1.prepareAnalysis)(f.request, repository, f.source, directory)).resolves.toStrictEqual(f.manifest);
616
+ expect(f.request.mock.calls
617
+ .filter(([route]) => route === 'GET /repos/{owner}/{repo}/pulls')
618
+ .map(([, parameters]) => parameters.page)).toStrictEqual([1, 2]);
619
+ });
620
+ test.each(['closed', 'superseded'])('skips a %s fork PR without looking up associations', async (state) => {
621
+ writeShards();
622
+ const f = fixture(sample, 'contributor/pyright');
623
+ if (state === 'closed') {
624
+ f.pr.state = 'closed';
625
+ }
626
+ else {
627
+ f.pr.head.sha = 'b'.repeat(40);
628
+ }
629
+ await expect((0, mypyPrimerAnalysis_1.prepareAnalysis)(f.request, repository, f.source, directory)).resolves.toBeUndefined();
630
+ expect(f.request.mock.calls.map(([route]) => route)).toStrictEqual([
631
+ 'GET /repos/{owner}/{repo}/pulls/{pull_number}',
632
+ ]);
633
+ });
634
+ test('revalidates fork branch associations before publishing', async () => {
635
+ const f = fixture(sample, 'contributor/pyright');
636
+ await expect((0, mypyPrimerAnalysis_1.publishAnalysis)(f.request, f.manifest, f.output(), 100, (0, path_1.join)(directory, 'report.md'), false)).resolves.toBe('Published: advisory analysis of every changed project');
637
+ expect(f.request.mock.calls.filter(([route]) => route === 'GET /repos/{owner}/{repo}/pulls')).toHaveLength(2);
638
+ expect(f.request.mock.calls.filter(([route]) => /^(POST|PATCH) /.test(route))).toStrictEqual([
639
+ [
640
+ 'POST /repos/{owner}/{repo}/issues/{issue_number}/comments',
641
+ { owner: 'microsoft', repo: 'pyright', issue_number: 7, body: expectedReport.body },
642
+ ],
643
+ ]);
644
+ });
645
+ test('refuses to publish when a fork branch association becomes ambiguous', async () => {
646
+ const f = fixture(sample, 'contributor/pyright');
647
+ const original = f.request.getMockImplementation();
648
+ f.request.mockImplementation(async (route, parameters) => {
649
+ if (route === 'GET /repos/{owner}/{repo}/issues/{issue_number}/comments') {
650
+ f.associated.push({ ...f.pr, number: 8 });
651
+ }
652
+ return original(route, parameters);
653
+ });
654
+ await expect((0, mypyPrimerAnalysis_1.publishAnalysis)(f.request, f.manifest, f.output(), 100, (0, path_1.join)(directory, 'report.md'), false)).rejects.toThrow('found 2');
655
+ expect(f.request.mock.calls.some(([route]) => /^(POST|PATCH) /.test(route))).toBe(false);
656
+ });
657
+ test('rejects duplicate project sections and unexpected shard files', async () => {
658
+ writeShards(sample + '\n' + sample);
659
+ const f = fixture();
660
+ await expect((0, mypyPrimerAnalysis_1.prepareAnalysis)(f.request, repository, f.source, directory)).rejects.toThrow('duplicate project');
661
+ (0, fs_1.writeFileSync)((0, path_1.join)(directory, 'mypy_primer_diffs_0', 'unexpected.js'), '');
662
+ await expect((0, mypyPrimerAnalysis_1.prepareAnalysis)(f.request, repository, f.source, directory)).rejects.toThrow('Unexpected contents');
663
+ });
664
+ test('renders authoritative counts and preserves limitations in the full report', () => {
665
+ const f = fixture();
666
+ const result = (0, mypyPrimerAnalysis_1.renderReport)(f.manifest, f.report, 100);
667
+ expect(result).toStrictEqual(expectedReport);
668
+ });
669
+ test('extracts type-erasure and other assertion signals without inferring baseline reveals', () => {
670
+ const erased = [
671
+ '.../projects/pandas-stubs/test.py:1:1 - error: "assert_type" mismatch: expected "Index[Any]" but received "Any" (reportAssertTypeFailure)',
672
+ '.../projects/pandas-stubs/test.py:2:1 - error: "assert_type" mismatch: expected "Rotation[tuple[Any, ...]]" but received "Unknown" (reportAssertTypeFailure)',
673
+ ];
674
+ const assertion = '.../projects/pandas-stubs/test.py:3:1 - error: "assert_type" mismatch: expected "int" but received "Literal[1]" (reportAssertTypeFailure)';
675
+ const diff = [
676
+ 'pandas-stubs (https://github.com/pandas-dev/pandas-stubs)',
677
+ ...[...erased, assertion].map((line) => `+ ${line}`),
678
+ '- 0 errors, 0 warnings, 0 informations',
679
+ '+ 3 errors, 0 warnings, 0 informations',
680
+ ].join('\n');
681
+ expect((0, mypyPrimerAnalysis_1.summarizeDiffs)((0, mypyPrimerAnalysis_1.parseDiff)(diff, 0)).regressionSignals).toStrictEqual({
682
+ 'pandas-stubs': [
683
+ { kind: 'type-erasure', count: 2, examplesAdded: erased, examplesRemoved: [] },
684
+ { kind: 'assertion-failure', count: 1, examplesAdded: [assertion], examplesRemoved: [] },
685
+ ],
686
+ });
687
+ });
688
+ test('flags removed checks but not replacements or unrelated lint removals', () => {
689
+ const removed = '.../projects/example/test.py:2:1 - error: Missing member (reportAttributeAccessIssue)';
690
+ const diff = [
691
+ 'example (https://github.com/example/project)',
692
+ '- .../projects/example/test.py:1:1 - error: Old argument error (reportArgumentType)',
693
+ '+ .../projects/example/test.py:1:1 - error: Reworded argument error (reportArgumentType)',
694
+ `- ${removed}`,
695
+ '+ .../projects/example/test.py:2:1 - error: Different argument error (reportArgumentType)',
696
+ '- .../projects/example/test.py:3:1 - warning: Unused import (reportUnusedImport)',
697
+ '- 2 errors, 1 warning, 0 informations',
698
+ '+ 2 errors, 0 warnings, 0 informations',
699
+ ].join('\n');
700
+ expect((0, mypyPrimerAnalysis_1.summarizeDiffs)((0, mypyPrimerAnalysis_1.parseDiff)(diff, 0)).regressionSignals).toStrictEqual({
701
+ example: [{ kind: 'removed-check', count: 1, examplesAdded: [], examplesRemoved: [removed] }],
702
+ });
703
+ expect((0, mypyPrimerAnalysis_1.summarizeDiffs)((0, mypyPrimerAnalysis_1.parseDiff)(sample, 0)).regressionSignals).toBeUndefined();
704
+ });
705
+ test('detects new Unknown returns despite pre-existing Unknown parameters', () => {
706
+ const before = ' Return type mismatch: override returns type "(value: Unknown) -> int"';
707
+ const after = ' Return type mismatch: override returns type "(value: Unknown) -> Unknown"';
708
+ const diff = ['pip (https://github.com/pypa/pip)', `-${before}`, `+${after}`].join('\n');
709
+ expect((0, mypyPrimerAnalysis_1.summarizeDiffs)((0, mypyPrimerAnalysis_1.parseDiff)(diff, 0)).regressionSignals).toStrictEqual({
710
+ pip: [{ kind: 'gradual-detail', count: 1, examplesAdded: [after], examplesRemoved: [before] }],
711
+ });
712
+ });
713
+ test('does not flag a gradual type merely moving from a header into a detail', () => {
714
+ const diff = [
715
+ 'example (https://github.com/example/project)',
716
+ '- .../projects/example/test.py:1:1 - error: Argument of type Unknown is incompatible (reportArgumentType)',
717
+ '+ .../projects/example/test.py:1:1 - error: Argument is incompatible (reportArgumentType)',
718
+ '+ Type Unknown is incompatible (reportArgumentType)',
719
+ ].join('\n');
720
+ expect((0, mypyPrimerAnalysis_1.summarizeDiffs)((0, mypyPrimerAnalysis_1.parseDiff)(diff, 0)).regressionSignals).toBeUndefined();
721
+ });
722
+ test('bounds warning examples without undercounting the observed signal', () => {
723
+ const lines = Array.from({ length: 5 }, (_, index) => `.../projects/example/test.py:${index + 1}:1 - error: "assert_type" mismatch: expected "${'X'.repeat(800)}" but received "Any" (reportAssertTypeFailure)`);
724
+ const diff = [
725
+ 'example (https://github.com/example/project)',
726
+ ...lines.map((line) => `+ ${line}`),
727
+ '- 0 errors, 0 warnings, 0 informations',
728
+ '+ 5 errors, 0 warnings, 0 informations',
729
+ ].join('\n');
730
+ expect((0, mypyPrimerAnalysis_1.summarizeDiffs)((0, mypyPrimerAnalysis_1.parseDiff)(diff, 0)).regressionSignals).toStrictEqual({
731
+ example: [
732
+ {
733
+ kind: 'type-erasure',
734
+ count: 5,
735
+ examplesAdded: lines.slice(0, 3).map((line) => line.slice(0, 600)),
736
+ examplesRemoved: [],
737
+ },
738
+ ],
739
+ });
740
+ });
741
+ test.each([
742
+ ['__proto__', '\\_\\_proto\\_\\_'],
743
+ ['constructor', 'constructor'],
744
+ ['toString', 'toString'],
745
+ ])('treats project name %s as data rather than an inherited key', (name, escapedName) => {
746
+ const f = fixture(sample.replace(/example/g, name));
747
+ f.report.projects[0].name = name;
748
+ expect((0, mypyPrimerAnalysis_1.renderReport)({ ...f.manifest, regressionSignals: {} }, f.report, 100)).toStrictEqual({
749
+ body: expectedReport.body.replace(/example/g, escapedName),
750
+ fullReport: expectedReport.fullReport.replace(/example/g, escapedName),
751
+ });
752
+ const line = `.../projects/${name}/test.py:1:1 - error: "assert_type" mismatch: expected "int" but received "Any" (reportAssertTypeFailure)`;
753
+ const diff = [
754
+ `${name} (https://github.com/example/project)`,
755
+ `+ ${line}`,
756
+ '- 0 errors, 0 warnings, 0 informations',
757
+ '+ 1 error, 0 warnings, 0 informations',
758
+ ].join('\n');
759
+ // Compare entries so Jest does not treat a constructor data key as a class identity.
760
+ expect(Object.entries(JSON.parse(JSON.stringify((0, mypyPrimerAnalysis_1.summarizeDiffs)((0, mypyPrimerAnalysis_1.parseDiff)(diff, 0)).regressionSignals)))).toStrictEqual([[name, [{ kind: 'type-erasure', count: 1, examplesAdded: [line], examplesRemoved: [] }]]]);
761
+ });
762
+ test('keeps type-erasure risks first even when the model blames stubs', () => {
763
+ const diff = [
764
+ sample,
765
+ 'pandas-stubs (https://github.com/pandas-dev/pandas-stubs)',
766
+ '+ .../projects/pandas-stubs/test.py:1:1 - error: "assert_type" mismatch: expected "Index[Any]" but received "Any" (reportAssertTypeFailure)',
767
+ '- 0 errors, 0 warnings, 0 informations',
768
+ '+ 1 error, 0 warnings, 0 informations',
769
+ ].join('\n');
770
+ const f = fixture(diff);
771
+ const pandas = {
772
+ ...f.report.projects[0],
773
+ name: 'pandas-stubs',
774
+ assessment: 'exposed-typing-issue',
775
+ confidence: 'high',
776
+ summary: 'The model claims that the stub expectation is wrong.',
777
+ before: 'The assertion expects Index[Any]; the baseline reveal is not recorded.',
778
+ after: 'The new assertion receives Any.',
779
+ impact: 'The expected outer container type is absent from the reported result.',
780
+ explanation: 'Conformance intent alone does not establish that the lost type information is harmless.',
781
+ evidence: [
782
+ {
783
+ url: 'https://typing.python.org/en/latest/spec/overload.html',
784
+ detail: 'Specification context, not proof of a bad stub.',
785
+ },
786
+ ],
787
+ };
788
+ const pandasDetails = [
789
+ '### pandas-stubs: The model claims that the stub expectation is wrong.',
790
+ '',
791
+ '**Regression warning signals require review, regardless of the AI assessment:**',
792
+ '- New assertions receive bare Any/Unknown instead of their expected type.',
793
+ '',
794
+ '**AI assessment:** Exposed typing issue (high confidence). **PR attribution:** Not established.',
795
+ '',
796
+ '**Before / baseline evidence:** The assertion expects Index\\[Any\\]; the baseline reveal is not recorded.',
797
+ '',
798
+ '**After / PR evidence:** The new assertion receives Any.',
799
+ '',
800
+ '**Why this matters:** The expected outer container type is absent from the reported result.',
801
+ '',
802
+ '**Causal analysis:**',
803
+ '',
804
+ 'Conformance intent alone does not establish that the lost type information is harmless.',
805
+ '',
806
+ '**Uncertainty / next check:** Exact dependency revision unavailable.',
807
+ '',
808
+ '- Specification context, not proof of a bad stub.: <https://typing.python.org/en/latest/spec/overload.html>',
809
+ '',
810
+ ];
811
+ expect((0, mypyPrimerAnalysis_1.renderReport)(f.manifest, { projects: [...f.report.projects, pandas] }, 100)).toStrictEqual({
812
+ body: [
813
+ ...expectedReportProvenance,
814
+ riskOverview,
815
+ '',
816
+ '## Potential regressions',
817
+ '',
818
+ ...pandasDetails,
819
+ '## Unresolved investigations',
820
+ '',
821
+ ...expectedReportDetails,
822
+ ...expectedReportFooter,
823
+ ].join('\n'),
824
+ fullReport: [
825
+ ...expectedReportHeading,
826
+ expectedReportRow,
827
+ '| pandas-stubs | 1 | 0 | 0 / 0 | Exposed typing issue | high | The model claims that the stub expectation is wrong. |',
828
+ '',
829
+ ...pandasDetails,
830
+ ...expectedReportDetails,
831
+ ].join('\n'),
832
+ });
833
+ });
834
+ test.each([
835
+ `https://github.com/microsoft/pyright/blob/main/file.ts#L1`,
836
+ `https://github.com/microsoft/pyright/blob/${'b'.repeat(40)}/file.ts#L1`,
837
+ `https://github.com/microsoft/pyright/blob/${headSha}/file.ts`,
838
+ `https://github.com/other/repo/blob/${headSha}/file.ts#L1`,
839
+ 'https://typing.python.org/en/latest/spec/overload.html#step-5',
840
+ ])('rejects likely PR attribution without exact head and line evidence: %s', (url) => {
841
+ const f = fixture();
842
+ const report = {
843
+ projects: [
844
+ {
845
+ ...f.report.projects[0],
846
+ attribution: 'likely-pr',
847
+ evidence: [{ url, detail: 'Insufficient attribution evidence.' }],
848
+ },
849
+ ],
850
+ };
851
+ expect(() => (0, mypyPrimerAnalysis_1.renderReport)(f.manifest, report, 100)).toThrow(new Error('PR attribution requires a line-pinned citation at the analyzed head for example'));
852
+ });
853
+ test.each([
854
+ { headRepository: repository, evidenceRepository: repository },
855
+ { headRepository: 'contributor/pyright', evidenceRepository: repository },
856
+ { headRepository: 'contributor/pyright', evidenceRepository: 'contributor/pyright' },
857
+ ])('accepts $evidenceRepository evidence for a PR from $headRepository', ({ headRepository, evidenceRepository }) => {
858
+ const f = fixture(sample, headRepository);
859
+ const url = `https://github.com/${evidenceRepository}/blob/${headSha}/file.ts#L1-L5`;
860
+ const report = {
861
+ projects: [
862
+ {
863
+ ...f.report.projects[0],
864
+ attribution: 'likely-pr',
865
+ evidence: [{ url, detail: 'Changed branch.' }],
866
+ },
867
+ ],
868
+ };
869
+ const details = [...expectedReportDetails.slice(0, -1), `- Changed branch.: <${url}>`, '']
870
+ .join('\n')
871
+ .replace('PR attribution:** Not established.', 'PR attribution:** Likely caused by the PR.');
872
+ expect((0, mypyPrimerAnalysis_1.renderReport)(f.manifest, report, 100)).toStrictEqual({
873
+ body: expectedReport.body.replace(expectedReportDetails.join('\n'), details),
874
+ fullReport: expectedReport.fullReport.replace(expectedReportDetails.join('\n'), details),
875
+ });
876
+ });
877
+ test.each([
878
+ `https://github.com/contributor/pyright/blob/${'b'.repeat(40)}/file.ts#L1`,
879
+ `https://github.com/contributor/pyright/blob/${headSha}/file.ts`,
880
+ `https://github.com/contributor/pyright-extra/blob/${headSha}/file.ts#L1`,
881
+ `https://github.com/other/pyright/blob/${headSha}/file.ts#L1`,
882
+ ])('rejects mismatched attribution evidence for a fork PR: %s', (url) => {
883
+ const f = fixture(sample, 'contributor/pyright');
884
+ const report = {
885
+ projects: [
886
+ {
887
+ ...f.report.projects[0],
888
+ attribution: 'likely-pr',
889
+ evidence: [{ url, detail: 'Insufficient attribution evidence.' }],
890
+ },
891
+ ],
892
+ };
893
+ expect(() => (0, mypyPrimerAnalysis_1.renderReport)(f.manifest, report, 100)).toThrow(new Error('PR attribution requires a line-pinned citation at the analyzed head for example'));
894
+ });
895
+ test.each(['before', 'after', 'impact', 'attribution'])('requires the %s investigation field', (field) => {
896
+ const f = fixture();
897
+ const report = { projects: [{ ...f.report.projects[0], [field]: undefined }] };
898
+ expect(() => (0, mypyPrimerAnalysis_1.renderReport)(f.manifest, report, 100)).toThrow(new Error(`Expected nonempty text of at most ${field === 'attribution' ? 2000 : 1200} characters`));
899
+ });
900
+ test('rejects unsupported PR attribution classifications', () => {
901
+ const f = fixture();
902
+ f.report.projects[0].attribution = 'definitely-caused-by-pr';
903
+ expect(() => (0, mypyPrimerAnalysis_1.renderReport)(f.manifest, f.report, 100)).toThrow(new Error('Invalid PR attribution for example'));
904
+ });
905
+ test('uses a short non-blocking notice only for canonical SymPy-only changes', () => {
906
+ const f = fixture(sample
907
+ .replace(/example/g, 'sympy')
908
+ .replace('https://github.com/sympy/project', 'https://github.com/sympy/sympy'));
909
+ f.report.projects[0].name = 'sympy';
910
+ expect((0, mypyPrimerAnalysis_1.renderReport)(f.manifest, f.report, 100)).toStrictEqual({
911
+ body: [
912
+ ...expectedReportProvenance,
913
+ '**Only SymPy changed.** These differences are treated as non-blocking primer noise; no other project changed.',
914
+ '',
915
+ 'Recorded changes: 1 added / 1 removed diagnostic headers; 1 added / 1 removed detail lines.',
916
+ '',
917
+ ...expectedReportFooter,
918
+ ].join('\n'),
919
+ fullReport: expectedReport.fullReport.replace(/example/g, 'sympy'),
920
+ });
921
+ expect(() => (0, mypyPrimerAnalysis_1.renderReport)(f.manifest, { projects: [] }, 100)).toThrow('The analysis must cover every changed project exactly once');
922
+ f.report.projects[0].explanation = '';
923
+ expect(() => (0, mypyPrimerAnalysis_1.renderReport)(f.manifest, f.report, 100)).toThrow('Expected nonempty text');
924
+ });
925
+ test('keeps normal analysis for noncanonical SymPy and mixed-project changes', () => {
926
+ const f = fixture();
927
+ const sympy = { ...f.report.projects[0], name: 'sympy' };
928
+ const manifest = { ...f.manifest, projects: [{ ...f.manifest.projects[0], name: 'sympy' }] };
929
+ expect((0, mypyPrimerAnalysis_1.renderReport)(manifest, { projects: [sympy] }, 100)).toStrictEqual({
930
+ body: expectedReport.body.replace(/example/g, 'sympy'),
931
+ fullReport: expectedReport.fullReport.replace(/example/g, 'sympy'),
932
+ });
933
+ manifest.projects[0].url = 'https://github.com/sympy/sympy';
934
+ manifest.projects.push(f.manifest.projects[0]);
935
+ const rows = [expectedReportRow.replace('example', 'sympy'), expectedReportRow];
936
+ const details = [
937
+ ...expectedReportDetails.map((line) => line.replace('example', 'sympy')),
938
+ ...expectedReportDetails,
939
+ ];
940
+ expect((0, mypyPrimerAnalysis_1.renderReport)(manifest, { projects: [sympy, ...f.report.projects] }, 100)).toStrictEqual({
941
+ body: [
942
+ ...expectedReportProvenance,
943
+ incompleteOverview,
944
+ '',
945
+ '## Unresolved investigations',
946
+ '',
947
+ ...details,
948
+ ...expectedReportFooter,
949
+ ].join('\n'),
950
+ fullReport: [...expectedReportHeading, ...rows, '', ...details].join('\n'),
951
+ });
952
+ });
953
+ test('labels preview reports and refuses to send them through the publisher', async () => {
954
+ const f = fixture();
955
+ const manifest = { ...f.manifest, preview: true };
956
+ const asPreview = (content) => content
957
+ .replace('<!-- pyright-primer-analysis:', '<!-- pyright-primer-analysis-preview:')
958
+ .replace('## mypy_primer analysis\n\n', '## mypy_primer analysis preview\n\n**Preview only. No PR comment was posted.**\n\n');
959
+ expect((0, mypyPrimerAnalysis_1.renderReport)(manifest, f.report, 100)).toStrictEqual({
960
+ body: asPreview(expectedReport.body),
961
+ fullReport: asPreview(expectedReport.fullReport),
962
+ });
963
+ await expect((0, mypyPrimerAnalysis_1.publishAnalysis)(f.request, manifest, f.output(), 100, (0, path_1.join)(directory, 'report.md'), false)).rejects.toThrow('Preview analyses cannot publish PR comments');
964
+ expect(f.request.mock.calls).toStrictEqual([]);
965
+ });
966
+ test.each([1799, 1800, 1801, 2143])('previews a %i-character explanation without losing the full report', (length) => {
967
+ const f = fixture();
968
+ const original = f.report.projects[0].explanation;
969
+ const explanation = 'x'.repeat(length);
970
+ f.report.projects[0].explanation = explanation;
971
+ const notice = length > 1800 ? '\n\n**Explanation truncated; see the full report artifact linked below.**' : '';
972
+ expect((0, mypyPrimerAnalysis_1.renderReport)(f.manifest, f.report, 100)).toStrictEqual({
973
+ body: expectedReport.body.replace(original, 'x'.repeat(Math.min(length, 1800)) + notice),
974
+ fullReport: expectedReport.fullReport.replace(original, explanation),
975
+ });
976
+ });
977
+ test.each([
978
+ ['@', '&#64;'],
979
+ ['|', '\\|'],
980
+ ['\\', '\\\\'],
981
+ ['\u{1f642}', '\u{1f642}'],
982
+ ])('truncates before escaping and preserves the last Unicode code point: %s', (character, escaped) => {
983
+ const f = fixture();
984
+ const original = f.report.projects[0].explanation;
985
+ f.report.projects[0].explanation = '@'.repeat(1799) + character + ' Omitted qualification.';
986
+ const preview = '&#64;'.repeat(1799) + escaped;
987
+ expect((0, mypyPrimerAnalysis_1.renderReport)(f.manifest, f.report, 100)).toStrictEqual({
988
+ body: expectedReport.body.replace(original, preview + '\n\n**Explanation truncated; see the full report artifact linked below.**'),
989
+ fullReport: expectedReport.fullReport.replace(original, preview + ' Omitted qualification.'),
990
+ });
991
+ });
992
+ test.each([undefined, null, 42, '', ' \n '])('rejects an invalid explanation: %s', (explanation) => {
993
+ const f = fixture();
994
+ const report = { projects: [{ ...f.report.projects[0], explanation }] };
995
+ expect(() => (0, mypyPrimerAnalysis_1.renderReport)(f.manifest, report, 100)).toThrow('Expected nonempty text');
996
+ });
997
+ test('keeps other field limits and the overall input bound', async () => {
998
+ const f = fixture();
999
+ for (const [field, value] of [
1000
+ ['summary', 'x'.repeat(241)],
1001
+ ['unresolved', 'x'.repeat(801)],
1002
+ ['explanation', 'x'.repeat(1000001)],
1003
+ ['before', 'x'.repeat(1201)],
1004
+ ['after', 'x'.repeat(1201)],
1005
+ ['impact', 'x'.repeat(1201)],
1006
+ ]) {
1007
+ const report = { projects: [{ ...f.report.projects[0], [field]: value }] };
1008
+ expect(() => (0, mypyPrimerAnalysis_1.renderReport)(f.manifest, report, 100)).toThrow('Expected nonempty text');
1009
+ }
1010
+ f.report.projects[0].explanation = 'x'.repeat(1000000);
1011
+ await expect((0, mypyPrimerAnalysis_1.publishAnalysis)(f.request, f.manifest, f.output(), 100, (0, path_1.join)(directory, 'report.md'), false)).rejects.toThrow('at most 1000000 characters');
1012
+ expect(f.request).not.toHaveBeenCalled();
1013
+ });
1014
+ test('publishes an oversized explanation preview and saves its complete text', async () => {
1015
+ const f = fixture();
1016
+ const original = f.report.projects[0].explanation;
1017
+ const preview = 'x'.repeat(1800);
1018
+ const explanation = preview + ' Qualification that must remain in the full artifact.';
1019
+ f.report.projects[0].explanation = explanation;
1020
+ const path = (0, path_1.join)(directory, 'report.md');
1021
+ await expect((0, mypyPrimerAnalysis_1.publishAnalysis)(f.request, f.manifest, f.output(), 100, path, false)).resolves.toBe('Published: advisory analysis of every changed project');
1022
+ expect((0, fs_1.readFileSync)(path, 'utf8')).toBe(expectedReport.fullReport.replace(original, explanation));
1023
+ const calls = f.request.mock.calls.filter(([route]) => /^(POST|PATCH) /.test(route));
1024
+ expect(calls).toStrictEqual([
1025
+ [
1026
+ 'POST /repos/{owner}/{repo}/issues/{issue_number}/comments',
1027
+ {
1028
+ owner: 'microsoft',
1029
+ repo: 'pyright',
1030
+ issue_number: 7,
1031
+ body: expectedReport.body.replace(original, preview + '\n\n**Explanation truncated; see the full report artifact linked below.**'),
1032
+ },
1033
+ ],
1034
+ ]);
1035
+ });
1036
+ test('long explanations still allow compact fallback without losing projects or artifact text', () => {
1037
+ const f = fixture();
1038
+ const names = Array.from({ length: 40 }, (_, index) => `project_${index}`);
1039
+ const manifest = {
1040
+ ...f.manifest,
1041
+ projects: names.map((name) => ({ ...f.manifest.projects[0], name })),
1042
+ };
1043
+ const explanation = 'x'.repeat(2143);
1044
+ const report = { projects: names.map((name) => ({ ...f.report.projects[0], name, explanation })) };
1045
+ const result = (0, mypyPrimerAnalysis_1.renderReport)(manifest, report, 100);
1046
+ expect(result.body.length).toBeLessThanOrEqual(60000);
1047
+ expect(result).toStrictEqual(expectedLargeReport(names.length, explanation));
1048
+ });
1049
+ test('large reports retain every project in the comment and full artifact', () => {
1050
+ const f = fixture();
1051
+ const names = Array.from({ length: 40 }, (_, index) => `project_${index}`);
1052
+ const manifest = {
1053
+ ...f.manifest,
1054
+ projects: names.map((name) => ({ ...f.manifest.projects[0], name })),
1055
+ };
1056
+ const report = {
1057
+ projects: names.map((name) => ({ ...f.report.projects[0], name, explanation: 'x'.repeat(1800) })),
1058
+ };
1059
+ const result = (0, mypyPrimerAnalysis_1.renderReport)(manifest, report, 100);
1060
+ expect(result.body.length).toBeLessThan(60000);
1061
+ expect(result.fullReport.length).toBeGreaterThan(60000);
1062
+ expect(result).toStrictEqual(expectedLargeReport(names.length, 'x'.repeat(1800)));
1063
+ });
1064
+ test('rejects missing projects, unsupported classifications, and uncited certainty', () => {
1065
+ const f = fixture();
1066
+ expect(() => (0, mypyPrimerAnalysis_1.renderReport)(f.manifest, { projects: [] }, 100)).toThrow('every changed project');
1067
+ f.report.projects.push(f.report.projects[0]);
1068
+ expect(() => (0, mypyPrimerAnalysis_1.renderReport)(f.manifest, f.report, 100)).toThrow('every changed project');
1069
+ f.report.projects.pop();
1070
+ f.report.projects[0].assessment = 'definitely-safe';
1071
+ expect(() => (0, mypyPrimerAnalysis_1.renderReport)(f.manifest, f.report, 100)).toThrow('Invalid assessment');
1072
+ f.report.projects[0].assessment = 'expected-improvement';
1073
+ expect(() => (0, mypyPrimerAnalysis_1.renderReport)(f.manifest, f.report, 100)).toThrow('cited evidence');
1074
+ });
1075
+ test('escapes report text and rejects unsafe evidence links', () => {
1076
+ const f = fixture();
1077
+ f.report.projects[0].summary = '<script> @someone | injected row';
1078
+ const escapedSummary = '\\<script\\> &#64;someone \\| injected row';
1079
+ expect((0, mypyPrimerAnalysis_1.renderReport)(f.manifest, f.report, 100)).toStrictEqual({
1080
+ body: expectedReport.body.replace(/The argument error changed its wording\./g, escapedSummary),
1081
+ fullReport: expectedReport.fullReport.replace(/The argument error changed its wording\./g, escapedSummary),
1082
+ });
1083
+ const report = {
1084
+ projects: [
1085
+ { ...f.report.projects[0], evidence: [{ url: 'https://github.com@evil.example/', detail: 'Unsafe' }] },
1086
+ ],
1087
+ };
1088
+ expect(() => (0, mypyPrimerAnalysis_1.renderReport)(f.manifest, report, 100)).toThrow('Unsupported evidence URL');
1089
+ });
1090
+ test.each([
1091
+ { name: 'missing items', output: {} },
1092
+ { name: 'empty output', output: { items: [] } },
1093
+ {
1094
+ name: 'no-op claiming the report is prepared',
1095
+ output: { items: [{ type: 'noop', message: 'The required report has been prepared for publication.' }] },
1096
+ },
1097
+ { name: 'incomplete-only output', output: { items: [{ type: 'report_incomplete', details: 'No evidence' }] } },
1098
+ { name: 'missing payload', output: { items: [{ type: 'publish_primer_analysis' }] } },
1099
+ { name: 'malformed payload', output: { items: [{ type: 'publish_primer_analysis', report: '{' }] } },
1100
+ {
1101
+ name: 'omitted project',
1102
+ output: { items: [{ type: 'publish_primer_analysis', report: '{"projects":[]}' }] },
1103
+ },
1104
+ ])('report completion rejects $name before GitHub access', async ({ output }) => {
1105
+ const f = fixture();
1106
+ expect(() => (0, mypyPrimerAnalysis_1.validateSubmittedReport)(f.manifest, output, 100)).toThrow();
1107
+ await expect((0, mypyPrimerAnalysis_1.publishAnalysis)(f.request, f.manifest, output, 100, (0, path_1.join)(directory, 'report.md'), false)).rejects.toThrow();
1108
+ expect(f.request).not.toHaveBeenCalled();
1109
+ });
1110
+ test('report completion rejects duplicate and oversized submissions', () => {
1111
+ const f = fixture();
1112
+ const output = f.output();
1113
+ expect(() => (0, mypyPrimerAnalysis_1.validateSubmittedReport)(f.manifest, { items: [...output.items, ...output.items] }, 100)).toThrow('Expected 1 to 1 primer analysis submissions; found 2');
1114
+ output.items[0].report = ' '.repeat(1000001);
1115
+ expect(() => (0, mypyPrimerAnalysis_1.validateSubmittedReport)(f.manifest, output, 100)).toThrow('Expected nonempty text of at most 1000000 characters');
1116
+ });
1117
+ test('assembles per-project submissions larger than the single-call transport limit', () => {
1118
+ const f = fixture();
1119
+ const names = Array.from({ length: 11 }, (_, index) => `project_${index}`);
1120
+ const manifest = {
1121
+ ...f.manifest,
1122
+ projects: names.map((name) => ({ ...f.manifest.projects[0], name })),
1123
+ };
1124
+ const report = {
1125
+ projects: names.map((name) => ({
1126
+ ...f.report.projects[0],
1127
+ name,
1128
+ assessment: 'possible-regression',
1129
+ attribution: 'likely-pr',
1130
+ explanation: 'x'.repeat(2143),
1131
+ evidence: [
1132
+ {
1133
+ url: `https://github.com/${repository}/blob/${headSha}/example.py#L1`,
1134
+ detail: 'The analyzed implementation.',
1135
+ },
1136
+ ],
1137
+ })),
1138
+ };
1139
+ const output = {
1140
+ items: report.projects.map((project) => ({
1141
+ type: 'publish_primer_analysis',
1142
+ report: JSON.stringify({ projects: [project] }),
1143
+ })),
1144
+ };
1145
+ expect(Buffer.byteLength(JSON.stringify(report), 'utf8')).toBeGreaterThan(10240);
1146
+ for (const item of output.items) {
1147
+ expect(Buffer.byteLength(item.report, 'utf8')).toBeLessThanOrEqual(10240);
1148
+ }
1149
+ const path = (0, path_1.join)(directory, 'agent_output.json');
1150
+ (0, fs_1.writeFileSync)(path, JSON.stringify(output));
1151
+ expect((0, mypyPrimerAnalysis_1.validateSubmittedReportFile)(manifest, path, 100)).toStrictEqual((0, mypyPrimerAnalysis_1.renderReport)(manifest, report, 100));
1152
+ });
1153
+ test('retains compatibility with a single report covering multiple projects', () => {
1154
+ const f = fixture([sample, sample.replace(/example/g, 'second')].join('\n'));
1155
+ f.report.projects.push({ ...f.report.projects[0], name: 'second' });
1156
+ expect((0, mypyPrimerAnalysis_1.validateSubmittedReport)(f.manifest, f.output(), 100)).toStrictEqual((0, mypyPrimerAnalysis_1.renderReport)(f.manifest, f.report, 100));
1157
+ });
1158
+ test('publishes per-project submissions only after every project is present', async () => {
1159
+ const f = fixture([sample, sample.replace(/example/g, 'second')].join('\n'));
1160
+ f.report.projects.push({ ...f.report.projects[0], name: 'second' });
1161
+ const output = f.output(true);
1162
+ const path = (0, path_1.join)(directory, 'report.md');
1163
+ await expect((0, mypyPrimerAnalysis_1.publishAnalysis)(f.request, f.manifest, { items: output.items.slice(0, 1) }, 100, path, false)).rejects.toThrow('every changed project exactly once');
1164
+ expect(f.request).not.toHaveBeenCalled();
1165
+ expect(await (0, mypyPrimerAnalysis_1.publishAnalysis)(f.request, f.manifest, output, 100, path, false)).toBe('Published: advisory analysis of every changed project');
1166
+ const expected = (0, mypyPrimerAnalysis_1.renderReport)(f.manifest, f.report, 100);
1167
+ expect((0, fs_1.readFileSync)(path, 'utf8')).toBe(expected.fullReport);
1168
+ expect(f.request).toHaveBeenLastCalledWith('POST /repos/{owner}/{repo}/issues/{issue_number}/comments', {
1169
+ owner: 'microsoft',
1170
+ repo: 'pyright',
1171
+ issue_number: 7,
1172
+ body: expected.body,
1173
+ });
1174
+ });
1175
+ test('rejects invalid per-project payloads and evidence before GitHub access', async () => {
1176
+ const f = fixture([sample, sample.replace(/example/g, 'second')].join('\n'));
1177
+ f.report.projects.push({ ...f.report.projects[0], name: 'second' });
1178
+ const [first, second] = f.report.projects;
1179
+ const invalidReports = [
1180
+ [JSON.stringify({ projects: [first] }), 'every changed project exactly once'],
1181
+ [JSON.stringify({ projects: [{ ...second, name: 'unexpected' }] }), 'every changed project exactly once'],
1182
+ [JSON.stringify({ projects: [] }), 'exactly one project'],
1183
+ [JSON.stringify(f.report), 'exactly one project'],
1184
+ ['{}', 'Expected a JSON array'],
1185
+ ['{', 'JSON'],
1186
+ [
1187
+ JSON.stringify({ projects: [{ ...second, assessment: 'possible-regression' }] }),
1188
+ 'Expected cited evidence',
1189
+ ],
1190
+ [
1191
+ JSON.stringify({
1192
+ projects: [
1193
+ {
1194
+ ...second,
1195
+ attribution: 'likely-pr',
1196
+ evidence: [
1197
+ {
1198
+ url: `https://github.com/${repository}/blob/main/example.py#L1`,
1199
+ detail: 'Not the analyzed head.',
1200
+ },
1201
+ ],
1202
+ },
1203
+ ],
1204
+ }),
1205
+ 'PR attribution requires a line-pinned citation',
1206
+ ],
1207
+ ];
1208
+ for (const [report, error] of invalidReports) {
1209
+ const output = f.output(true);
1210
+ output.items[1].report = report;
1211
+ expect(() => (0, mypyPrimerAnalysis_1.validateSubmittedReport)(f.manifest, output, 100)).toThrow(error);
1212
+ await expect((0, mypyPrimerAnalysis_1.publishAnalysis)(f.request, f.manifest, output, 100, (0, path_1.join)(directory, 'report.md'), false)).rejects.toThrow(error);
1213
+ }
1214
+ expect(f.request).not.toHaveBeenCalled();
1215
+ });
1216
+ test.each([10000, 10001])('enforces the combined bound for 100 submissions of %s characters', (length) => {
1217
+ const f = fixture();
1218
+ const names = Array.from({ length: 100 }, (_, index) => `project_${index}`);
1219
+ f.manifest.projects = names.map((name) => ({ ...f.manifest.projects[0], name }));
1220
+ f.report.projects = names.map((name) => {
1221
+ const project = { ...f.report.projects[0], name, explanation: '' };
1222
+ project.explanation = 'x'.repeat(length - JSON.stringify({ projects: [project] }).length);
1223
+ return project;
1224
+ });
1225
+ const output = f.output(true);
1226
+ expect(output.items.map((item) => Buffer.byteLength(item.report, 'utf8'))).toStrictEqual(Array.from({ length: 100 }, () => length));
1227
+ expect(length).toBeLessThanOrEqual(10240);
1228
+ if (length === 10000) {
1229
+ expect((0, mypyPrimerAnalysis_1.validateSubmittedReport)(f.manifest, output, 100)).toStrictEqual((0, mypyPrimerAnalysis_1.renderReport)(f.manifest, f.report, 100));
1230
+ }
1231
+ else {
1232
+ expect(() => (0, mypyPrimerAnalysis_1.validateSubmittedReport)(f.manifest, output, 100)).toThrow('The combined primer analysis report exceeds 1000000 characters');
1233
+ }
1234
+ });
1235
+ test('report completion reuses evidence and project validation', () => {
1236
+ const f = fixture();
1237
+ f.report.projects[0].assessment = 'expected-improvement';
1238
+ expect(() => (0, mypyPrimerAnalysis_1.validateSubmittedReport)(f.manifest, f.output(), 100)).toThrow('evidence');
1239
+ f.report.projects[0].assessment = 'needs-review';
1240
+ f.report.projects.push(f.report.projects[0]);
1241
+ expect(() => (0, mypyPrimerAnalysis_1.validateSubmittedReport)(f.manifest, f.output(), 100)).toThrow();
1242
+ });
1243
+ test('report completion accepts an honest needs-review report without evidence', () => {
1244
+ const f = fixture();
1245
+ const path = (0, path_1.join)(directory, 'agent_output.json');
1246
+ (0, fs_1.writeFileSync)(path, JSON.stringify(f.output()));
1247
+ expect((0, mypyPrimerAnalysis_1.validateSubmittedReportFile)(f.manifest, path, 100)).toStrictEqual(expectedReport);
1248
+ expect(f.request).not.toHaveBeenCalled();
1249
+ });
1250
+ test('report completion requires a bounded regular JSON output file', () => {
1251
+ const f = fixture();
1252
+ const path = (0, path_1.join)(directory, 'agent_output.json');
1253
+ expect(() => (0, mypyPrimerAnalysis_1.validateSubmittedReportFile)(f.manifest, path, 100)).toThrow('ENOENT');
1254
+ expect(() => (0, mypyPrimerAnalysis_1.validateSubmittedReportFile)(f.manifest, directory, 100)).toThrow('bounded regular input file');
1255
+ (0, fs_1.writeFileSync)(path, '');
1256
+ expect(() => (0, mypyPrimerAnalysis_1.validateSubmittedReportFile)(f.manifest, path, 100)).toThrow();
1257
+ (0, fs_1.writeFileSync)(path, ' '.repeat(8 * 1024 * 1024 + 1));
1258
+ expect(() => (0, mypyPrimerAnalysis_1.validateSubmittedReportFile)(f.manifest, path, 100)).toThrow('bounded regular input file');
1259
+ });
1260
+ test('report completion preserves typing specification citations in serialized reports', () => {
1261
+ const f = fixture();
1262
+ const url = 'https://typing.python.org/en/latest/spec/typeddict.html';
1263
+ const report = {
1264
+ projects: [
1265
+ {
1266
+ ...f.report.projects[0],
1267
+ evidence: [{ url, detail: 'TypedDict specification' }],
1268
+ },
1269
+ ],
1270
+ };
1271
+ const output = { items: [{ type: 'publish_primer_analysis', report: JSON.stringify(report) }] };
1272
+ const path = (0, path_1.join)(directory, 'agent_output.json');
1273
+ (0, fs_1.writeFileSync)(path, JSON.stringify(output));
1274
+ const originalDetails = expectedReportDetails.join('\n');
1275
+ const details = [
1276
+ ...expectedReportDetails.slice(0, -1),
1277
+ '- TypedDict specification: <https://typing.python.org/en/latest/spec/typeddict.html>',
1278
+ '',
1279
+ ].join('\n');
1280
+ expect((0, mypyPrimerAnalysis_1.validateSubmittedReportFile)(f.manifest, path, 100)).toStrictEqual({
1281
+ body: expectedReport.body.replace(originalDetails, details),
1282
+ fullReport: expectedReport.fullReport.replace(originalDetails, details),
1283
+ });
1284
+ });
1285
+ test('compiled completion step rejects no-op output and accepts a submitted report', () => {
1286
+ const workflow = (0, yaml_1.parseDocument)((0, fs_1.readFileSync)((0, path_1.join)(__dirname, '../../../../.github/workflows/mypy-primer-analysis.lock.yml'), 'utf8'));
1287
+ const steps = workflow.getIn(['jobs', 'agent', 'steps'], true);
1288
+ if (!(0, yaml_1.isSeq)(steps)) {
1289
+ throw new Error('Missing agent steps');
1290
+ }
1291
+ const step = steps.items.find((item) => (0, yaml_1.isMap)(item) && item.get('id') === 'require_primer_report');
1292
+ if (!(0, yaml_1.isMap)(step)) {
1293
+ throw new Error('Missing report completion step');
1294
+ }
1295
+ const script = step.getIn(['with', 'script']);
1296
+ if (typeof script !== 'string') {
1297
+ throw new Error('Missing report completion script');
1298
+ }
1299
+ const f = fixture();
1300
+ const actionsDir = (0, path_1.join)(directory, 'gh-aw', 'actions');
1301
+ const path = (0, path_1.join)(directory, 'agent_output.json');
1302
+ (0, fs_1.mkdirSync)(actionsDir, { recursive: true });
1303
+ (0, fs_1.writeFileSync)((0, path_1.join)(actionsDir, 'primer-manifest.json'), JSON.stringify(f.manifest));
1304
+ const notice = jest.fn();
1305
+ const run = () => (0, vm_1.runInNewContext)(script, {
1306
+ require: (name) => {
1307
+ if (name === 'fs') {
1308
+ return { readFileSync: fs_1.readFileSync };
1309
+ }
1310
+ if (name === 'path') {
1311
+ return { join: path_1.join };
1312
+ }
1313
+ if (name === (0, path_1.join)(actionsDir, 'mypyPrimerAnalysis.ts')) {
1314
+ return { validateSubmittedReportFile: mypyPrimerAnalysis_1.validateSubmittedReportFile };
1315
+ }
1316
+ throw new Error(`Unexpected module: ${name}`);
1317
+ },
1318
+ process: { env: { RUNNER_TEMP: directory, PRIMER_AGENT_OUTPUT: path } },
1319
+ context: { runId: 100 },
1320
+ core: { notice },
1321
+ });
1322
+ (0, fs_1.writeFileSync)(path, JSON.stringify({ items: [{ type: 'noop', message: 'Report prepared' }], errors: [] }));
1323
+ expect(run).toThrow('Submit publish_primer_analysis, not noop or report_incomplete');
1324
+ expect(notice).not.toHaveBeenCalled();
1325
+ (0, fs_1.writeFileSync)(path, JSON.stringify(f.output()));
1326
+ run();
1327
+ expect(notice).toHaveBeenCalledWith('A complete primer report was submitted; threat detection and publisher checks still apply');
1328
+ expect(f.request).not.toHaveBeenCalled();
1329
+ });
1330
+ test('staged reports never mutate GitHub', async () => {
1331
+ const f = fixture();
1332
+ const path = (0, path_1.join)(directory, 'report.md');
1333
+ await expect((0, mypyPrimerAnalysis_1.publishAnalysis)(f.request, f.manifest, f.output(), 100, path, true)).resolves.toBe('Staged: report saved without posting a comment');
1334
+ expect((0, fs_1.readFileSync)(path, 'utf8')).toBe(expectedReport.fullReport);
1335
+ expect(f.request.mock.calls.some(([route]) => /^(POST|PATCH) /.test(route))).toBe(false);
1336
+ });
1337
+ test('requires a definitive staged flag from this attempt of the trusted activation job', () => {
1338
+ const activation = { repository, run_id: 100, run_attempt: '1', staged: true };
1339
+ expect((0, mypyPrimerAnalysis_1.getStagedMode)(activation, repository, 100, 1)).toBe(true);
1340
+ expect((0, mypyPrimerAnalysis_1.getStagedMode)({ ...activation, staged: false }, repository, 100, 1)).toBe(false);
1341
+ expect(() => (0, mypyPrimerAnalysis_1.getStagedMode)({ ...activation, staged: undefined }, repository, 100, 1)).toThrow('metadata');
1342
+ expect(() => (0, mypyPrimerAnalysis_1.getStagedMode)({ ...activation, staged: 'true' }, repository, 100, 1)).toThrow('metadata');
1343
+ expect(() => (0, mypyPrimerAnalysis_1.getStagedMode)(activation, repository, 101, 1)).toThrow('metadata');
1344
+ expect(() => (0, mypyPrimerAnalysis_1.getStagedMode)(activation, repository, 100, 2)).toThrow('metadata');
1345
+ expect(() => (0, mypyPrimerAnalysis_1.getStagedMode)(activation, 'different/repository', 100, 1)).toThrow('metadata');
1346
+ });
1347
+ test('rechecks the head before posting', async () => {
1348
+ const f = fixture();
1349
+ f.pr.head.sha = 'b'.repeat(40);
1350
+ await expect((0, mypyPrimerAnalysis_1.publishAnalysis)(f.request, f.manifest, f.output(), 100, (0, path_1.join)(directory, 'report.md'), false)).resolves.toBe('Skipped: the PR is closed or its head changed during analysis');
1351
+ expect(f.request.mock.calls.some(([route]) => /^(POST|PATCH) /.test(route))).toBe(false);
1352
+ });
1353
+ test('posts only to the verified PR and does not replace raw primer comments', async () => {
1354
+ const f = fixture();
1355
+ f.comments.push({ id: 55, user: { login: 'github-actions[bot]' }, body: 'Diff from mypy_primer' });
1356
+ await expect((0, mypyPrimerAnalysis_1.publishAnalysis)(f.request, f.manifest, f.output(), 100, (0, path_1.join)(directory, 'report.md'), false)).resolves.toBe('Published: advisory analysis of every changed project');
1357
+ const calls = f.request.mock.calls.filter(([route]) => /^(POST|PATCH) /.test(route));
1358
+ expect(calls).toStrictEqual([
1359
+ [
1360
+ 'POST /repos/{owner}/{repo}/issues/{issue_number}/comments',
1361
+ { owner: 'microsoft', repo: 'pyright', issue_number: 7, body: expectedReport.body },
1362
+ ],
1363
+ ]);
1364
+ });
1365
+ test('updates an existing report instead of duplicating it', async () => {
1366
+ const f = fixture();
1367
+ f.comments.push({
1368
+ id: 55,
1369
+ user: { login: 'github-actions[bot]' },
1370
+ body: `<!-- pyright-primer-analysis:${headSha}:42:1 -->`,
1371
+ });
1372
+ await expect((0, mypyPrimerAnalysis_1.publishAnalysis)(f.request, f.manifest, f.output(), 100, (0, path_1.join)(directory, 'report.md'), false)).resolves.toBe('Published: advisory analysis of every changed project');
1373
+ const calls = f.request.mock.calls.filter(([route]) => /^(POST|PATCH) /.test(route));
1374
+ expect(calls).toStrictEqual([
1375
+ [
1376
+ 'PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}',
1377
+ { owner: 'microsoft', repo: 'pyright', comment_id: 55, body: expectedReport.body },
1378
+ ],
1379
+ ]);
1380
+ });
1381
+ test('rechecks the head after listing existing comments', async () => {
1382
+ const f = fixture();
1383
+ const original = f.request.getMockImplementation();
1384
+ f.request.mockImplementation(async (route, parameters) => {
1385
+ if (route === 'GET /repos/{owner}/{repo}/issues/{issue_number}/comments') {
1386
+ f.pr.head.sha = 'b'.repeat(40);
1387
+ }
1388
+ return original(route, parameters);
1389
+ });
1390
+ await expect((0, mypyPrimerAnalysis_1.publishAnalysis)(f.request, f.manifest, f.output(), 100, (0, path_1.join)(directory, 'report.md'), false)).resolves.toBe('Skipped: the PR changed while existing comments were being read');
1391
+ expect(f.request.mock.calls.some(([route]) => /^(POST|PATCH) /.test(route))).toBe(false);
1392
+ });
1393
+ test('does not overwrite a newer report for the same head', async () => {
1394
+ const f = fixture();
1395
+ f.comments.push({
1396
+ id: 55,
1397
+ user: { login: 'github-actions[bot]' },
1398
+ body: `<!-- pyright-primer-analysis:${headSha}:43:1 -->`,
1399
+ });
1400
+ await expect((0, mypyPrimerAnalysis_1.publishAnalysis)(f.request, f.manifest, f.output(), 100, (0, path_1.join)(directory, 'report.md'), false)).resolves.toBe('Skipped: a newer primer analysis has already been posted for this commit');
1401
+ expect(f.request.mock.calls.some(([route]) => /^(POST|PATCH) /.test(route))).toBe(false);
1402
+ });
1403
+ test.each(['workflow_run', 'workflow_dispatch'])('compiled collector accepts %s source context', async (eventName) => {
1404
+ const workflow = (0, yaml_1.parseDocument)((0, fs_1.readFileSync)((0, path_1.join)(__dirname, '../../../../.github/workflows/mypy-primer-analysis.lock.yml'), 'utf8'));
1405
+ const steps = workflow.getIn(['jobs', 'collect', 'steps'], true);
1406
+ if (!(0, yaml_1.isSeq)(steps)) {
1407
+ throw new Error('Missing collection steps');
1408
+ }
1409
+ const step = steps.items.find((item) => (0, yaml_1.isMap)(item) && item.get('id') === 'source');
1410
+ if (!(0, yaml_1.isMap)(step)) {
1411
+ throw new Error('Missing source validation step');
1412
+ }
1413
+ const script = step.getIn(['with', 'script']);
1414
+ if (typeof script !== 'string') {
1415
+ throw new Error('Missing source validation script');
1416
+ }
1417
+ const f = fixture();
1418
+ if (eventName === 'workflow_run') {
1419
+ (0, fs_1.mkdirSync)((0, path_1.join)(directory, 'primer-context'));
1420
+ (0, fs_1.writeFileSync)((0, path_1.join)(directory, 'primer-context', 'primer-analysis-context.json'), JSON.stringify(f.source));
1421
+ }
1422
+ const result = await (0, vm_1.runInNewContext)(`(async () => { ${script} })()`, {
1423
+ require: (name) => {
1424
+ if (name === 'fs') {
1425
+ return { readFileSync: fs_1.readFileSync, writeFileSync: fs_1.writeFileSync };
1426
+ }
1427
+ if (name === 'path') {
1428
+ return { join: path_1.join };
1429
+ }
1430
+ if (name === './build/ci/mypyPrimerAnalysis.ts') {
1431
+ return { loadSource: mypyPrimerAnalysis_1.loadSource };
1432
+ }
1433
+ throw new Error(`Unexpected module: ${name}`);
1434
+ },
1435
+ process: {
1436
+ env: {
1437
+ RUNNER_TEMP: directory,
1438
+ PRIMER_EVENT_NAME: eventName,
1439
+ PRIMER_RUN_ID: eventName === 'workflow_dispatch' ? '42' : 'ignored',
1440
+ PRIMER_RUN_ATTEMPT: eventName === 'workflow_dispatch' ? '1' : 'ignored',
1441
+ },
1442
+ },
1443
+ github: { request: f.request },
1444
+ context: { repo: { owner: 'microsoft', repo: 'pyright' } },
1445
+ });
1446
+ expect(result).toBe(42);
1447
+ expect(JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(directory, 'primer-source.json'), 'utf8'))).toStrictEqual(f.source);
1448
+ });
1449
+ test('compiled manual preview is read-only and isolated from publication', async () => {
1450
+ const workflow = (0, yaml_1.parseDocument)((0, fs_1.readFileSync)((0, path_1.join)(__dirname, '../../../../.github/workflows/mypy-primer-analysis.lock.yml'), 'utf8'));
1451
+ const inputs = workflow.getIn(['on', 'workflow_dispatch', 'inputs'], true);
1452
+ const preview = workflow.getIn(['jobs', 'preview'], true);
1453
+ if (!(0, yaml_1.isMap)(inputs) || !(0, yaml_1.isMap)(preview)) {
1454
+ throw new Error('Missing manual preview configuration');
1455
+ }
1456
+ expect(inputs.toJSON()).toStrictEqual({
1457
+ aw_context: {
1458
+ default: '',
1459
+ description: 'Agent caller context (used internally by Agentic Workflows).',
1460
+ required: false,
1461
+ type: 'string',
1462
+ },
1463
+ 'primer-run-id': {
1464
+ description: 'Successful Run mypy_primer on PR run ID to preview, including merged PRs.',
1465
+ required: true,
1466
+ type: 'string',
1467
+ },
1468
+ 'primer-run-attempt': {
1469
+ description: 'Current attempt of the recorded primer run.',
1470
+ required: true,
1471
+ default: '1',
1472
+ type: 'string',
1473
+ },
1474
+ });
1475
+ expect(String(workflow.getIn(['jobs', 'publish_primer_analysis', 'if'])).trim()).toBe("(!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'publish_primer_analysis') && (github.event_name != 'workflow_dispatch')");
1476
+ expect(String(preview.get('if')).trim()).toBe("${{ !cancelled() && github.event_name == 'workflow_dispatch' && needs.agent.result == 'success' && needs.detection.result == 'success' && needs.detection.outputs.detection_conclusion == 'success' }}");
1477
+ const permissions = preview.get('permissions');
1478
+ const steps = preview.get('steps');
1479
+ if (!(0, yaml_1.isMap)(permissions) || !(0, yaml_1.isSeq)(steps)) {
1480
+ throw new Error('Missing preview permissions or steps');
1481
+ }
1482
+ expect(permissions.toJSON()).toStrictEqual({ contents: 'read', actions: 'read' });
1483
+ const renderStep = steps.items.find((item) => (0, yaml_1.isMap)(item) && item.get('id') === 'render_preview');
1484
+ if (!(0, yaml_1.isMap)(renderStep)) {
1485
+ throw new Error('Missing preview rendering step');
1486
+ }
1487
+ const script = renderStep.getIn(['with', 'script']);
1488
+ if (typeof script !== 'string') {
1489
+ throw new Error('Missing preview rendering script');
1490
+ }
1491
+ const f = fixture();
1492
+ const manifestPath = (0, path_1.join)(directory, 'primer-input', 'manifest.json');
1493
+ const outputPath = (0, path_1.join)(directory, 'primer-preview-output', 'agent_output.json');
1494
+ (0, fs_1.mkdirSync)((0, path_1.join)(directory, 'primer-input'));
1495
+ (0, fs_1.mkdirSync)((0, path_1.join)(directory, 'primer-preview-output'));
1496
+ (0, fs_1.writeFileSync)(outputPath, JSON.stringify(f.output()));
1497
+ const summary = { addRaw: jest.fn().mockReturnThis(), write: jest.fn().mockResolvedValue(undefined) };
1498
+ const run = () => (0, vm_1.runInNewContext)(`(async () => { ${script} })()`, {
1499
+ require: (name) => {
1500
+ if (name === 'fs') {
1501
+ return { mkdirSync: fs_1.mkdirSync, readFileSync: fs_1.readFileSync, writeFileSync: fs_1.writeFileSync };
1502
+ }
1503
+ if (name === 'path') {
1504
+ return { join: path_1.join };
1505
+ }
1506
+ if (name === './build/ci/mypyPrimerAnalysis.ts') {
1507
+ return { validateSubmittedReportFile: mypyPrimerAnalysis_1.validateSubmittedReportFile };
1508
+ }
1509
+ throw new Error(`Unexpected module: ${name}`);
1510
+ },
1511
+ process: { env: { RUNNER_TEMP: directory } },
1512
+ context: { runId: 100 },
1513
+ core: { summary },
1514
+ });
1515
+ (0, fs_1.writeFileSync)(manifestPath, JSON.stringify(f.manifest));
1516
+ await expect(run()).rejects.toThrow('Expected a trusted manual-preview manifest');
1517
+ expect(summary.addRaw.mock.calls).toStrictEqual([]);
1518
+ (0, fs_1.writeFileSync)(manifestPath, JSON.stringify({ ...f.manifest, preview: true }));
1519
+ await run();
1520
+ const asPreview = (content) => content
1521
+ .replace('<!-- pyright-primer-analysis:', '<!-- pyright-primer-analysis-preview:')
1522
+ .replace('## mypy_primer analysis\n\n', '## mypy_primer analysis preview\n\n**Preview only. No PR comment was posted.**\n\n');
1523
+ expect((0, fs_1.readFileSync)((0, path_1.join)(directory, 'primer-preview', 'comment.md'), 'utf8')).toBe(asPreview(expectedReport.body));
1524
+ expect((0, fs_1.readFileSync)((0, path_1.join)(directory, 'primer-preview', 'full-report.md'), 'utf8')).toBe(asPreview(expectedReport.fullReport));
1525
+ expect(summary.addRaw.mock.calls).toStrictEqual([[asPreview(expectedReport.body)]]);
1526
+ expect(summary.write.mock.calls).toStrictEqual([[]]);
1527
+ expect(f.request.mock.calls).toStrictEqual([]);
1528
+ });
1529
+ test('compiled ingestion permits typing citations without expanding network access', () => {
1530
+ const source = (0, yaml_1.parseDocument)((0, fs_1.readFileSync)((0, path_1.join)(__dirname, '../../../../.github/workflows/mypy-primer-analysis.md'), 'utf8').split(/^---\r?$/m)[1]);
1531
+ const networkDomains = source.getIn(['network', 'allowed'], true);
1532
+ const outputDomains = source.getIn(['safe-outputs', 'allowed-domains'], true);
1533
+ if (!(0, yaml_1.isSeq)(networkDomains) || !(0, yaml_1.isSeq)(outputDomains)) {
1534
+ throw new Error('Missing network or safe-output domain configuration');
1535
+ }
1536
+ expect(networkDomains.toJSON()).toStrictEqual(['defaults', 'github']);
1537
+ expect(outputDomains.toJSON()).toStrictEqual(['typing.python.org']);
1538
+ const workflow = (0, yaml_1.parseDocument)((0, fs_1.readFileSync)((0, path_1.join)(__dirname, '../../../../.github/workflows/mypy-primer-analysis.lock.yml'), 'utf8'));
1539
+ const steps = workflow.getIn(['jobs', 'agent', 'steps'], true);
1540
+ if (!(0, yaml_1.isSeq)(steps)) {
1541
+ throw new Error('Missing agent steps');
1542
+ }
1543
+ const ingestion = steps.items.find((item) => (0, yaml_1.isMap)(item) && item.get('id') === 'collect_output');
1544
+ if (!(0, yaml_1.isMap)(ingestion)) {
1545
+ throw new Error('Missing safe-output ingestion step');
1546
+ }
1547
+ expect(ingestion.getIn(['env', 'GH_AW_ALLOWED_DOMAINS'])).toBe([
1548
+ '*.githubusercontent.com',
1549
+ 'api.snapcraft.io',
1550
+ 'archive.ubuntu.com',
1551
+ 'azure.archive.ubuntu.com',
1552
+ 'codeload.github.com',
1553
+ 'crl.geotrust.com',
1554
+ 'crl.globalsign.com',
1555
+ 'crl.identrust.com',
1556
+ 'crl.sectigo.com',
1557
+ 'crl.thawte.com',
1558
+ 'crl.usertrust.com',
1559
+ 'crl.verisign.com',
1560
+ 'crl3.digicert.com',
1561
+ 'crl4.digicert.com',
1562
+ 'crls.ssl.com',
1563
+ 'docs.github.com',
1564
+ 'github-cloud.githubusercontent.com',
1565
+ 'github-cloud.s3.amazonaws.com',
1566
+ 'github.blog',
1567
+ 'github.com',
1568
+ 'github.githubassets.com',
1569
+ 'json-schema.org',
1570
+ 'json.schemastore.org',
1571
+ 'keyserver.ubuntu.com',
1572
+ 'lfs.github.com',
1573
+ 'localhost', // gh-aw includes this implicitly when safe-output domains are configured.
1574
+ 'objects.githubusercontent.com',
1575
+ 'ocsp.digicert.com',
1576
+ 'ocsp.geotrust.com',
1577
+ 'ocsp.globalsign.com',
1578
+ 'ocsp.identrust.com',
1579
+ 'ocsp.sectigo.com',
1580
+ 'ocsp.ssl.com',
1581
+ 'ocsp.thawte.com',
1582
+ 'ocsp.usertrust.com',
1583
+ 'ocsp.verisign.com',
1584
+ 'packagecloud.io',
1585
+ 'packages.cloud.google.com',
1586
+ 'packages.microsoft.com',
1587
+ 'patch-diff.githubusercontent.com',
1588
+ 'patchdiff.githubusercontent.com',
1589
+ 'ppa.launchpad.net',
1590
+ 'raw.githubusercontent.com',
1591
+ 's.symcb.com',
1592
+ 's.symcd.com',
1593
+ 'security.ubuntu.com',
1594
+ 'ts-crl.ws.symantec.com',
1595
+ 'ts-ocsp.ws.symantec.com',
1596
+ 'typing.python.org',
1597
+ 'www.googleapis.com',
1598
+ ].join(','));
1599
+ });
1600
+ test('compiled workflow runtime matches the compiler resolution lock', () => {
1601
+ const content = (0, fs_1.readFileSync)((0, path_1.join)(__dirname, '../../../../.github/workflows/mypy-primer-analysis.lock.yml'), 'utf8');
1602
+ const metadata = (0, yaml_1.parseDocument)(content.split('\n')[0].replace('# gh-aw-metadata: ', ''));
1603
+ const compilerVersion = metadata.get('compiler_version');
1604
+ expect(compilerVersion).toMatch(/^v\d+\.\d+\.\d+$/);
1605
+ const actionsLock = (0, yaml_1.parseDocument)((0, fs_1.readFileSync)((0, path_1.join)(__dirname, '../../../../.github/aw/actions-lock.json'), 'utf8'));
1606
+ const runtimeActions = [...content.matchAll(/^\s+uses: (github\/gh-aw-actions\/[^@\s]+)@(\S+)/gm)];
1607
+ expect(runtimeActions.length).toBeGreaterThan(0);
1608
+ for (const [, repository, sha] of runtimeActions) {
1609
+ const key = `${repository}@${compilerVersion}`;
1610
+ const resolvedSha = actionsLock.getIn(['entries', key, 'sha']);
1611
+ expect(resolvedSha).toMatch(/^[a-f0-9]{40}$/);
1612
+ expect(actionsLock.getIn(['entries', key, 'repo'])).toBe(repository);
1613
+ expect(actionsLock.getIn(['entries', key, 'version'])).toBe(compilerVersion);
1614
+ expect(sha).toBe(resolvedSha);
1615
+ }
1616
+ });
1617
+ test('compiled workflow keeps the agent read-only and gates publication on threat detection', () => {
1618
+ const workflow = (0, yaml_1.parseDocument)((0, fs_1.readFileSync)((0, path_1.join)(__dirname, '../../../../.github/workflows/mypy-primer-analysis.lock.yml'), 'utf8'));
1619
+ for (const job of ['agent', 'collect']) {
1620
+ expect(workflow.getIn(['jobs', job, 'permissions', 'pull-requests'])).toBe('read');
1621
+ expect(workflow.getIn(['jobs', job, 'permissions', 'contents'])).toBe('read');
1622
+ }
1623
+ expect(workflow.getIn(['jobs', 'safe_outputs', 'permissions', 'issues'])).not.toBe('write');
1624
+ expect(workflow.getIn(['jobs', 'conclusion', 'permissions', 'issues'])).not.toBe('write');
1625
+ const agentSteps = workflow.getIn(['jobs', 'agent', 'steps'], true);
1626
+ expect(String(agentSteps)).toContain('--deny-tool write');
1627
+ expect(String(agentSteps)).toContain('--deny-tool shell');
1628
+ const publishingSteps = workflow.getIn(['jobs', 'publish_primer_analysis', 'steps'], true);
1629
+ expect(String(publishingSteps)).toContain('Threat detection did not explicitly allow publication');
1630
+ expect(String(publishingSteps)).toContain('needs.detection.outputs.detection_conclusion');
1631
+ expect(String(publishingSteps)).toContain('getStagedMode(activation,');
1632
+ expect(String(publishingSteps)).toContain('primer-activation');
1633
+ });
1634
+ test('compiled workflow exposes GitHub and safe outputs through MCP without CLI-only instructions', () => {
1635
+ const source = (0, yaml_1.parseDocument)((0, fs_1.readFileSync)((0, path_1.join)(__dirname, '../../../../.github/workflows/mypy-primer-analysis.md'), 'utf8').split(/^---\r?$/m)[1]);
1636
+ expect(source.getIn(['tools', 'cli-proxy'])).toBe(false);
1637
+ expect(source.getIn(['tools', 'bash'])).toBe(false);
1638
+ expect(source.getIn(['tools', 'edit'])).toBe(false);
1639
+ expect(source.getIn(['tools', 'github', 'read-only'])).toBe(true);
1640
+ expect(source.get('max-ai-credits')).toBe(200);
1641
+ expect(source.getIn(['engine', 'model'])).toBe('gpt-5.6-sol');
1642
+ expect(source.getIn(['safe-outputs', 'jobs', 'publish-primer-analysis', 'max'])).toBe(100);
1643
+ expect(source.getIn(['safe-outputs', 'noop'])).toBe(false);
1644
+ expect(source.getIn(['sandbox', 'agent', 'token-steering'])).toBe(false);
1645
+ expect(source.getIn(['safe-outputs', 'threat-detection', 'engine', 'model'])).toBe('detection');
1646
+ expect(source.getIn(['safe-outputs', 'threat-detection', 'engine', 'version'])).toBe('1.0.80');
1647
+ expect(source.getIn(['engine', 'version'])).toBe('1.0.80');
1648
+ expect(source.getIn(['engine', 'harness'])).toBe('mypyPrimerCopilotHarness.cjs');
1649
+ const content = (0, fs_1.readFileSync)((0, path_1.join)(__dirname, '../../../../.github/workflows/mypy-primer-analysis.lock.yml'), 'utf8');
1650
+ const metadata = (0, yaml_1.parseDocument)(content.split('\n')[0].replace('# gh-aw-metadata: ', ''));
1651
+ expect(metadata.get('compiler_version')).toBe('v0.88.7');
1652
+ expect(metadata.getIn(['engine_versions', 'copilot'])).toBe('1.0.80');
1653
+ expect(content.includes('mcp_cli_tools')).toBe(false);
1654
+ expect(content.includes('GH_AW_MCP_CLI_SERVERS')).toBe(false);
1655
+ const workflow = (0, yaml_1.parseDocument)(content);
1656
+ const agentSteps = workflow.getIn(['jobs', 'agent', 'steps'], true);
1657
+ if (!(0, yaml_1.isSeq)(agentSteps)) {
1658
+ throw new Error('Missing agent steps');
1659
+ }
1660
+ const getStep = (id) => {
1661
+ const step = agentSteps.items.find((item) => (0, yaml_1.isMap)(item) && item.get('id') === id);
1662
+ if (!(0, yaml_1.isMap)(step)) {
1663
+ throw new Error(`Missing agent step: ${id}`);
1664
+ }
1665
+ return step;
1666
+ };
1667
+ const normalizeScript = (script) => {
1668
+ if (typeof script !== 'string') {
1669
+ throw new Error('Missing workflow script');
1670
+ }
1671
+ return script.replace(/\r\n/g, '\n').trim();
1672
+ };
1673
+ const gateway = getStep('start-mcp-gateway').get('run');
1674
+ expect(gateway).toContain('export GH_AW_ENGINE="copilot"');
1675
+ expect(gateway).toContain('"github": {');
1676
+ expect(gateway).toContain('"safeoutputs": {');
1677
+ expect(gateway).toContain('"GITHUB_READ_ONLY": "1"');
1678
+ const install = agentSteps.items.find((item) => (0, yaml_1.isMap)(item) && String(item.get('run')).includes('install_copilot_cli.sh'));
1679
+ if (!(0, yaml_1.isMap)(install)) {
1680
+ throw new Error('Missing Copilot installation step');
1681
+ }
1682
+ expect(install.get('run')).toContain('install_copilot_cli.sh" 1.0.80');
1683
+ const preflight = getStep('stage_mcp_preflight');
1684
+ expect(preflight.get('continue-on-error')).toBeUndefined();
1685
+ expect(preflight.get('if')).toBeUndefined();
1686
+ expect(normalizeScript(preflight.getIn(['with', 'script']))).toBe([
1687
+ "const fs = require('fs');",
1688
+ "const path = require('path');",
1689
+ "for (const file of ['mypyPrimerMcp.ts', 'mypyPrimerCopilotHarness.cjs', 'mypyPrimerAnalysis.ts']) {",
1690
+ ' fs.copyFileSync(',
1691
+ " path.join('/tmp/gh-aw/primer-input', file),",
1692
+ " path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions', file)",
1693
+ ' );',
1694
+ '}',
1695
+ 'fs.copyFileSync(',
1696
+ " '/tmp/gh-aw/primer-input/manifest.json',",
1697
+ " path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions', 'primer-manifest.json')",
1698
+ ');',
1699
+ ].join('\n'));
1700
+ expect(agentSteps.items.indexOf(preflight)).toBeLessThan(agentSteps.items.indexOf(getStep('agentic_execution')));
1701
+ expect(getStep('agentic_execution').get('if')).toBeUndefined();
1702
+ const collectSteps = workflow.getIn(['jobs', 'collect', 'steps'], true);
1703
+ if (!(0, yaml_1.isSeq)(collectSteps)) {
1704
+ throw new Error('Missing collection steps');
1705
+ }
1706
+ const prepare = collectSteps.items.find((item) => (0, yaml_1.isMap)(item) && item.get('id') === 'prepare');
1707
+ if (!(0, yaml_1.isMap)(prepare)) {
1708
+ throw new Error('Missing preparation step');
1709
+ }
1710
+ expect(prepare.getIn(['env', 'PRIMER_PREVIEW'])).toBe("${{ github.event_name == 'workflow_dispatch' }}");
1711
+ expect(normalizeScript(prepare.getIn(['with', 'script']))).toBe([
1712
+ "const fs = require('fs');",
1713
+ "const path = require('path');",
1714
+ "const { prepareAnalysis } = require('./build/ci/mypyPrimerAnalysis.ts');",
1715
+ "const source = JSON.parse(fs.readFileSync(path.join(process.env.RUNNER_TEMP, 'primer-source.json'), 'utf8'));",
1716
+ "const folder = path.join(process.env.RUNNER_TEMP, 'primer-input');",
1717
+ "const manifest = await prepareAnalysis(github.request.bind(github), `${context.repo.owner}/${context.repo.repo}`, source, path.join(folder, 'raw'), process.env.PRIMER_PREVIEW === 'true');",
1718
+ 'if (!manifest) {',
1719
+ " core.notice('Skipping analysis: the PR is closed or its head has changed');",
1720
+ " core.setOutput('has_changes', 'false');",
1721
+ ' return;',
1722
+ '}',
1723
+ "fs.writeFileSync(path.join(folder, 'manifest.json'), JSON.stringify(manifest, null, 2));",
1724
+ "fs.copyFileSync('./build/ci/mypyPrimerMcp.ts', path.join(folder, 'mypyPrimerMcp.ts'));",
1725
+ "fs.copyFileSync('./build/ci/mypyPrimerCopilotHarness.cjs', path.join(folder, 'mypyPrimerCopilotHarness.cjs'));",
1726
+ "fs.copyFileSync('./build/ci/mypyPrimerAnalysis.ts', path.join(folder, 'mypyPrimerAnalysis.ts'));",
1727
+ "core.setOutput('has_changes', String(manifest.projects.length > 0));",
1728
+ 'core.notice(`${manifest.projects.length} changed projects; all eight shards accounted for`);',
1729
+ ].join('\n'));
1730
+ const reportCheck = getStep('require_primer_report');
1731
+ expect(reportCheck.get('continue-on-error')).toBeUndefined();
1732
+ expect(reportCheck.get('if')).toBe("${{ !cancelled() && steps.agentic_execution.outcome == 'success' }}");
1733
+ expect(reportCheck.getIn(['env', 'PRIMER_AGENT_OUTPUT'])).toBe('/tmp/gh-aw/agent_output.json');
1734
+ expect(agentSteps.items.indexOf(getStep('agentic_execution'))).toBeLessThan(agentSteps.items.indexOf(getStep('collect_output')));
1735
+ expect(agentSteps.items.indexOf(getStep('collect_output'))).toBeLessThan(agentSteps.items.indexOf(reportCheck));
1736
+ const upload = agentSteps.items.find((item) => (0, yaml_1.isMap)(item) && item.getIn(['with', 'name']) === 'agent-output-fallback');
1737
+ if (!(0, yaml_1.isMap)(upload)) {
1738
+ throw new Error('Missing output artifact upload');
1739
+ }
1740
+ expect(upload.get('if')).toBe('always()');
1741
+ expect(agentSteps.items.indexOf(reportCheck)).toBeLessThan(agentSteps.items.indexOf(upload));
1742
+ const configStep = agentSteps.items.find((item) => (0, yaml_1.isMap)(item) && item.getIn(['env', 'GH_AW_SAFE_OUTPUTS_CONFIG']) !== undefined);
1743
+ if (!(0, yaml_1.isMap)(configStep)) {
1744
+ throw new Error('Missing safe outputs configuration');
1745
+ }
1746
+ const safeOutputConfig = (0, yaml_1.parseDocument)(String(configStep.getIn(['env', 'GH_AW_SAFE_OUTPUTS_CONFIG'])));
1747
+ expect(safeOutputConfig.has('noop')).toBe(false);
1748
+ expect(safeOutputConfig.has('publish-primer-analysis')).toBe(true);
1749
+ expect(safeOutputConfig.getIn(['publish-primer-analysis', 'max'])).toBe(100);
1750
+ const execution = getStep('agentic_execution').get('run');
1751
+ expect(getStep('agentic_execution').getIn(['env', 'COPILOT_MODEL'])).toBe('gpt-5.6-sol');
1752
+ const awfConfig = /^printf '%s\\n' '(.+)' > "\$\{RUNNER_TEMP\}\/gh-aw\/awf-config\.json"$/m.exec(String(execution));
1753
+ if (!awfConfig) {
1754
+ throw new Error('Missing generated firewall configuration');
1755
+ }
1756
+ const proxy = (0, yaml_1.parseDocument)(awfConfig[1]);
1757
+ expect(proxy.getIn(['apiProxy', 'enableTokenSteering'])).toBe(false);
1758
+ expect(proxy.getIn(['apiProxy', 'maxAiCredits'])).toBe(200);
1759
+ expect(proxy.getIn(['apiProxy', 'maxRuns'])).toBe(30);
1760
+ expect(execution).toContain('/gh-aw/actions/mypyPrimerCopilotHarness.cjs"');
1761
+ expect(execution).toContain('export GH_AW_MCP_CONFIG="$HOME/.copilot/mcp-config.json"');
1762
+ expect(execution).toContain('--allow-tool github');
1763
+ expect(execution).toContain('--allow-tool safeoutputs');
1764
+ expect(execution).toContain('--deny-tool shell');
1765
+ expect(execution).toContain('--deny-tool write');
1766
+ expect(execution).not.toContain('--allow-tool shell');
1767
+ expect(execution).not.toContain('--allow-tool write');
1768
+ const detectionSteps = workflow.getIn(['jobs', 'detection', 'steps'], true);
1769
+ if (!(0, yaml_1.isSeq)(detectionSteps)) {
1770
+ throw new Error('Missing detection steps');
1771
+ }
1772
+ const detection = detectionSteps.items.find((item) => (0, yaml_1.isMap)(item) && item.getIn(['env', 'COPILOT_MODEL']) !== undefined);
1773
+ if (!(0, yaml_1.isMap)(detection)) {
1774
+ throw new Error('Missing threat detection model configuration');
1775
+ }
1776
+ expect(detection.getIn(['env', 'COPILOT_MODEL'])).toBe('detection');
1777
+ });
1778
+ });
1779
+ //# sourceMappingURL=mypyPrimerAnalysis.test.js.map