@zzzen/pyright-internal 1.2.0-dev.20260830 → 1.2.0-dev.20260913

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 (100) hide show
  1. package/dist/analyzer/checker.d.ts +2 -1
  2. package/dist/analyzer/checker.js +78 -2
  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/parseTreeUtils.js +12 -13
  11. package/dist/analyzer/parseTreeUtils.js.map +1 -1
  12. package/dist/analyzer/service.d.ts +3 -1
  13. package/dist/analyzer/service.js +19 -10
  14. package/dist/analyzer/service.js.map +1 -1
  15. package/dist/analyzer/typeEvaluator.js +56 -379
  16. package/dist/analyzer/typeEvaluator.js.map +1 -1
  17. package/dist/common/fileSystem.d.ts +1 -0
  18. package/dist/common/fileSystem.js.map +1 -1
  19. package/dist/common/realFileSystem.d.ts +1 -0
  20. package/dist/common/realFileSystem.js +12 -0
  21. package/dist/common/realFileSystem.js.map +1 -1
  22. package/dist/fileSystemMapping.d.ts +1 -1
  23. package/dist/fileSystemMapping.js +3 -0
  24. package/dist/fileSystemMapping.js.map +1 -1
  25. package/dist/languageService/completionProvider.d.ts +2 -1
  26. package/dist/languageService/completionProvider.js +4 -1
  27. package/dist/languageService/completionProvider.js.map +1 -1
  28. package/dist/languageService/fileWatcherDynamicFeature.d.ts +6 -4
  29. package/dist/languageService/fileWatcherDynamicFeature.js +19 -15
  30. package/dist/languageService/fileWatcherDynamicFeature.js.map +1 -1
  31. package/dist/languageService/hoverProvider.d.ts +1 -1
  32. package/dist/languageService/hoverProvider.js +13 -8
  33. package/dist/languageService/hoverProvider.js.map +1 -1
  34. package/dist/languageService/tooltipUtils.d.ts +2 -1
  35. package/dist/languageService/tooltipUtils.js +15 -17
  36. package/dist/languageService/tooltipUtils.js.map +1 -1
  37. package/dist/localization/localize.d.ts +1 -0
  38. package/dist/localization/localize.js +1 -0
  39. package/dist/localization/localize.js.map +1 -1
  40. package/dist/localization/package.nls.cs.json +5 -4
  41. package/dist/localization/package.nls.de.json +5 -4
  42. package/dist/localization/package.nls.en-us.json +1 -0
  43. package/dist/localization/package.nls.es.json +5 -4
  44. package/dist/localization/package.nls.fr.json +5 -4
  45. package/dist/localization/package.nls.it.json +5 -4
  46. package/dist/localization/package.nls.ja.json +5 -4
  47. package/dist/localization/package.nls.ko.json +5 -4
  48. package/dist/localization/package.nls.pl.json +5 -4
  49. package/dist/localization/package.nls.pt-br.json +5 -4
  50. package/dist/localization/package.nls.qps-ploc.json +1 -0
  51. package/dist/localization/package.nls.ru.json +5 -4
  52. package/dist/localization/package.nls.tr.json +5 -4
  53. package/dist/localization/package.nls.zh-cn.json +5 -4
  54. package/dist/localization/package.nls.zh-tw.json +5 -4
  55. package/dist/parser/parser.js +7 -1
  56. package/dist/parser/parser.js.map +1 -1
  57. package/dist/parser/tokenizer.js +3 -5
  58. package/dist/parser/tokenizer.js.map +1 -1
  59. package/dist/readonlyAugmentedFileSystem.d.ts +1 -0
  60. package/dist/readonlyAugmentedFileSystem.js +3 -0
  61. package/dist/readonlyAugmentedFileSystem.js.map +1 -1
  62. package/dist/tests/checker.test.js +8 -0
  63. package/dist/tests/checker.test.js.map +1 -1
  64. package/dist/tests/fileSystemMapping.test.js +3 -0
  65. package/dist/tests/fileSystemMapping.test.js.map +1 -1
  66. package/dist/tests/fourslash/hover.builtinDocstrings.fourslash.js +5 -5
  67. package/dist/tests/fourslash/hover.fourslash.js +1 -1
  68. package/dist/tests/fourslash/hover.import.django.view.fourslash.js +1 -1
  69. package/dist/tests/fourslash/hover.plainText.fourslash.js +1 -1
  70. package/dist/tests/harness/vfs/filesystem.d.ts +1 -0
  71. package/dist/tests/harness/vfs/filesystem.js +3 -0
  72. package/dist/tests/harness/vfs/filesystem.js.map +1 -1
  73. package/dist/tests/hoverProvider.test.js +2 -2
  74. package/dist/tests/importResolver.test.js +5 -0
  75. package/dist/tests/importResolver.test.js.map +1 -1
  76. package/dist/tests/mypyPrimerAnalysis.test.d.ts +1 -0
  77. package/dist/tests/mypyPrimerAnalysis.test.js +633 -0
  78. package/dist/tests/mypyPrimerAnalysis.test.js.map +1 -0
  79. package/dist/tests/parseTreeUtils.test.js +41 -0
  80. package/dist/tests/parseTreeUtils.test.js.map +1 -1
  81. package/dist/tests/service.test.js +46 -0
  82. package/dist/tests/service.test.js.map +1 -1
  83. package/dist/tests/typeEvaluator1.test.js +13 -2
  84. package/dist/tests/typeEvaluator1.test.js.map +1 -1
  85. package/dist/tests/typeEvaluator6.test.js +155 -3
  86. package/dist/tests/typeEvaluator6.test.js.map +1 -1
  87. package/dist/tests/typeServer/stubGenerator.test.d.ts +1 -0
  88. package/dist/tests/typeServer/stubGenerator.test.js +73 -0
  89. package/dist/tests/typeServer/stubGenerator.test.js.map +1 -0
  90. package/dist/tests/typeUtils.test.js +15 -0
  91. package/dist/tests/typeUtils.test.js.map +1 -1
  92. package/dist/typeServer/stubGenerator.js +32 -7
  93. package/dist/typeServer/stubGenerator.js.map +1 -1
  94. package/dist/typeServer/typeServerFileSystem.d.ts +1 -0
  95. package/dist/typeServer/typeServerFileSystem.js +3 -0
  96. package/dist/typeServer/typeServerFileSystem.js.map +1 -1
  97. package/dist/typeServer/virtualFileOverlayFileSystem.d.ts +1 -0
  98. package/dist/typeServer/virtualFileOverlayFileSystem.js +3 -0
  99. package/dist/typeServer/virtualFileOverlayFileSystem.js.map +1 -1
  100. package/package.json +1 -1
@@ -0,0 +1,633 @@
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 yaml_1 = require("yaml");
12
+ const mypyPrimerAnalysis_1 = require("../../../../build/ci/mypyPrimerAnalysis");
13
+ const repository = 'microsoft/pyright';
14
+ const headSha = 'a'.repeat(40);
15
+ const sample = [
16
+ 'example (https://github.com/example/project)',
17
+ '- .../projects/example/test.py',
18
+ '- .../projects/example/test.py:1:1 - error: Old message',
19
+ '- Details (reportArgumentType)',
20
+ '+ .../projects/example/test.py:1:1 - error: New message',
21
+ '+ Details (reportArgumentType)',
22
+ ].join('\n');
23
+ const expectedProject = {
24
+ name: 'example',
25
+ url: 'https://github.com/example/project',
26
+ shard: 0,
27
+ added: [
28
+ {
29
+ severity: 'error',
30
+ rule: 'unspecified',
31
+ text: '.../projects/example/test.py:1:1 - error: New message',
32
+ },
33
+ ],
34
+ removed: [
35
+ {
36
+ severity: 'error',
37
+ rule: 'unspecified',
38
+ text: '.../projects/example/test.py:1:1 - error: Old message',
39
+ },
40
+ ],
41
+ detailsAdded: [{ text: ' Details (reportArgumentType)', rule: 'reportArgumentType' }],
42
+ detailsRemoved: [{ text: ' Details (reportArgumentType)', rule: 'reportArgumentType' }],
43
+ };
44
+ // Reduced forms of the headerless pip and xarray changes in PR #11601's primer artifacts.
45
+ const detailOnlySample = [
46
+ 'example (https://github.com/example/project)',
47
+ '- \u00a0\u00a0Return type mismatch: override returns "Match[str] | None"',
48
+ '+ \u00a0\u00a0Return type mismatch: override returns "Unknown"',
49
+ '- Operator "+" not supported when expected type is "NDArray[Any]" (reportOperatorIssue)',
50
+ '+ Operator "+" not supported when expected type is "ndarray[_AnyShape, dtype[Any]]" (reportOperatorIssue)',
51
+ ].join('\n');
52
+ const expectedDetailOnlyProject = {
53
+ ...expectedProject,
54
+ added: [],
55
+ removed: [],
56
+ detailsAdded: [
57
+ { text: ' \u00a0\u00a0Return type mismatch: override returns "Unknown"', rule: 'unspecified' },
58
+ {
59
+ text: ' Operator "+" not supported when expected type is "ndarray[_AnyShape, dtype[Any]]" (reportOperatorIssue)',
60
+ rule: 'reportOperatorIssue',
61
+ },
62
+ ],
63
+ detailsRemoved: [
64
+ { text: ' \u00a0\u00a0Return type mismatch: override returns "Match[str] | None"', rule: 'unspecified' },
65
+ {
66
+ text: ' Operator "+" not supported when expected type is "NDArray[Any]" (reportOperatorIssue)',
67
+ rule: 'reportOperatorIssue',
68
+ },
69
+ ],
70
+ };
71
+ const expectedDetailOnlySummary = {
72
+ projects: [
73
+ {
74
+ name: 'example',
75
+ url: 'https://github.com/example/project',
76
+ shard: 0,
77
+ added: 0,
78
+ removed: 0,
79
+ detailLinesAdded: 2,
80
+ detailLinesRemoved: 2,
81
+ },
82
+ ],
83
+ groups: { example: {} },
84
+ detailGroups: {
85
+ example: {
86
+ unspecified: {
87
+ added: 1,
88
+ removed: 1,
89
+ examplesAdded: [' \u00a0\u00a0Return type mismatch: override returns "Unknown"'],
90
+ examplesRemoved: [' \u00a0\u00a0Return type mismatch: override returns "Match[str] | None"'],
91
+ },
92
+ reportOperatorIssue: {
93
+ added: 1,
94
+ removed: 1,
95
+ examplesAdded: [
96
+ ' Operator "+" not supported when expected type is "ndarray[_AnyShape, dtype[Any]]" (reportOperatorIssue)',
97
+ ],
98
+ examplesRemoved: [
99
+ ' Operator "+" not supported when expected type is "NDArray[Any]" (reportOperatorIssue)',
100
+ ],
101
+ },
102
+ },
103
+ },
104
+ };
105
+ const expectedReportHeading = [
106
+ `<!-- pyright-primer-analysis:${headSha}:42:1 -->`,
107
+ '## mypy_primer analysis',
108
+ '',
109
+ 'Advisory AI analysis of [primer run 42, attempt 1](https://github.com/microsoft/pyright/actions/runs/42)',
110
+ `for commit \`${headSha}\`. This is not an approval or proof of correctness.`,
111
+ '',
112
+ 'Added/removed counts are diagnostic headers; message rewrites can appear on both sides.',
113
+ 'Detail lines are counted separately, without assuming a location or association with nearby headers.',
114
+ '',
115
+ '| Project | Added | Removed | Detail lines + / - | Assessment | Confidence | Explanation |',
116
+ '| --- | ---: | ---: | ---: | --- | --- | --- |',
117
+ ];
118
+ const expectedReportRow = '| example | 1 | 1 | 1 / 1 | Needs human review | low | The argument error changed its wording. |';
119
+ const expectedReportDetails = [
120
+ '### example',
121
+ '',
122
+ 'The error remains at the same location. Its cause has not been established.',
123
+ '',
124
+ '**Unresolved / coverage limits:** Exact dependency revision unavailable.',
125
+ '',
126
+ '',
127
+ ];
128
+ const expectedReportFooter = [
129
+ '**Full evidence and limitations:** download `mypy-primer-analysis-report` from ' +
130
+ '[this analysis run](https://github.com/microsoft/pyright/actions/runs/100).',
131
+ 'The original raw primer comment is unchanged. Treat uncertainty and possible regressions as requests for human review.',
132
+ ];
133
+ const expectedReport = {
134
+ body: [
135
+ ...expectedReportHeading,
136
+ expectedReportRow,
137
+ '',
138
+ '<details>',
139
+ '<summary>Evidence and limitations by project</summary>',
140
+ '',
141
+ ...expectedReportDetails,
142
+ '</details>',
143
+ '',
144
+ ...expectedReportFooter,
145
+ ].join('\n'),
146
+ fullReport: [...expectedReportHeading, expectedReportRow, '', ...expectedReportDetails].join('\n'),
147
+ };
148
+ function fixture(contents = sample) {
149
+ const source = {
150
+ runId: 42,
151
+ runAttempt: 1,
152
+ headSha,
153
+ headRepository: repository,
154
+ headBranch: 'feature',
155
+ pullRequests: [7],
156
+ };
157
+ const manifest = { repository, source, prNumber: 7, ...(0, mypyPrimerAnalysis_1.summarizeDiffs)((0, mypyPrimerAnalysis_1.parseDiff)(contents, 0)) };
158
+ const pr = {
159
+ number: 7,
160
+ state: 'open',
161
+ base: { repo: { full_name: repository } },
162
+ head: { sha: headSha, ref: 'feature', repo: { full_name: repository } },
163
+ };
164
+ const run = {
165
+ id: 42,
166
+ run_attempt: 1,
167
+ event: 'pull_request',
168
+ path: '.github/workflows/mypy_primer_pr.yaml',
169
+ conclusion: 'success',
170
+ repository: { full_name: repository },
171
+ head_sha: headSha,
172
+ head_repository: { full_name: repository },
173
+ head_branch: 'feature',
174
+ pull_requests: [{ number: 7 }],
175
+ run_started_at: '2026-09-10T00:00:00Z',
176
+ };
177
+ const artifacts = [
178
+ 'mypy_primer_diffs_pr_number',
179
+ ...Array.from({ length: 8 }, (_, shard) => `mypy_primer_diffs_${shard}`),
180
+ ].map((name) => ({ name, expired: false, size_in_bytes: 100, created_at: '2026-09-10T00:01:00Z' }));
181
+ const comments = [];
182
+ const associated = [pr];
183
+ const request = jest.fn(async (route, parameters) => {
184
+ if (route === 'GET /repos/{owner}/{repo}/actions/runs/{run_id}') {
185
+ return { data: run };
186
+ }
187
+ if (route.endsWith('/artifacts')) {
188
+ return { data: { artifacts } };
189
+ }
190
+ if (route === 'GET /repos/{owner}/{repo}/pulls/{pull_number}') {
191
+ return { data: pr };
192
+ }
193
+ if (route === 'GET /repos/{owner}/{repo}/commits/{commit_sha}/pulls') {
194
+ return { data: associated };
195
+ }
196
+ if (route === 'GET /repos/{owner}/{repo}/issues/{issue_number}/comments') {
197
+ return { data: comments };
198
+ }
199
+ if (route.startsWith('POST ') || route.startsWith('PATCH ')) {
200
+ return { data: { id: 123 } };
201
+ }
202
+ throw new Error(`Unexpected route: ${route}`);
203
+ });
204
+ const report = {
205
+ projects: [
206
+ {
207
+ name: 'example',
208
+ assessment: 'needs-review',
209
+ confidence: 'low',
210
+ summary: 'The argument error changed its wording.',
211
+ explanation: 'The error remains at the same location. Its cause has not been established.',
212
+ unresolved: 'Exact dependency revision unavailable.',
213
+ evidence: [],
214
+ },
215
+ ],
216
+ };
217
+ const output = () => ({ items: [{ type: 'publish_primer_analysis', report: JSON.stringify(report) }] });
218
+ return { source, manifest, pr, run, artifacts, comments, associated, request, report, output };
219
+ }
220
+ describe('mypy_primer analysis', () => {
221
+ let directory;
222
+ beforeEach(() => {
223
+ directory = (0, fs_1.mkdtempSync)((0, path_1.join)((0, os_1.tmpdir)(), 'pyright-primer-analysis-'));
224
+ });
225
+ afterEach(() => {
226
+ (0, fs_1.rmSync)(directory, { recursive: true, force: true });
227
+ });
228
+ function writeShards(contents = sample) {
229
+ for (let shard = 0; shard < 8; shard++) {
230
+ const folder = (0, path_1.join)(directory, `mypy_primer_diffs_${shard}`);
231
+ (0, fs_1.mkdirSync)(folder);
232
+ (0, fs_1.writeFileSync)((0, path_1.join)(folder, `diff_${shard}.txt`), shard === 0 ? contents : '');
233
+ }
234
+ (0, fs_1.mkdirSync)((0, path_1.join)(directory, 'mypy_primer_diffs_pr_number'));
235
+ (0, fs_1.writeFileSync)((0, path_1.join)(directory, 'mypy_primer_diffs_pr_number', 'pr_number.txt'), '7\n');
236
+ }
237
+ test('counts diagnostic records, not file headings or continuation lines', () => {
238
+ expect((0, mypyPrimerAnalysis_1.parseDiff)(sample.replace(/\n/g, '\r\n'), 3)).toStrictEqual([{ ...expectedProject, shard: 3 }]);
239
+ });
240
+ test('preserves headerless details and groups only explicitly present rules', () => {
241
+ const projects = (0, mypyPrimerAnalysis_1.parseDiff)(detailOnlySample, 0);
242
+ expect(projects).toStrictEqual([expectedDetailOnlyProject]);
243
+ expect((0, mypyPrimerAnalysis_1.summarizeDiffs)(projects)).toStrictEqual(expectedDetailOnlySummary);
244
+ });
245
+ test.each(['+', '-'])('accepts one-sided %s detail changes without inventing diagnostics', (sign) => {
246
+ const contents = [
247
+ 'example (https://github.com/example/project)',
248
+ `${sign} Changed detail (reportArgumentType)`,
249
+ ].join('\n');
250
+ const detail = { text: ' Changed detail (reportArgumentType)', rule: 'reportArgumentType' };
251
+ expect((0, mypyPrimerAnalysis_1.parseDiff)(contents, 0)).toStrictEqual([
252
+ {
253
+ ...expectedProject,
254
+ added: [],
255
+ removed: [],
256
+ detailsAdded: sign === '+' ? [detail] : [],
257
+ detailsRemoved: sign === '-' ? [detail] : [],
258
+ },
259
+ ]);
260
+ });
261
+ test('does not attach detail changes to nearby headers or leak them across projects', () => {
262
+ const contents = [
263
+ sample.replace('- .../projects/example/test.py', '- Earlier detail (reportGeneralTypeIssues)\n- .../projects/example/test.py'),
264
+ '+ .../projects/example/other.py',
265
+ '+ other.py:3:4 - error: Nested description (reportOperatorIssue)',
266
+ 'second (https://github.com/example/second)',
267
+ '- Unlocated detail',
268
+ ].join('\n');
269
+ expect((0, mypyPrimerAnalysis_1.parseDiff)(contents, 0)).toStrictEqual([
270
+ {
271
+ ...expectedProject,
272
+ detailsAdded: [
273
+ ...expectedProject.detailsAdded,
274
+ {
275
+ text: ' other.py:3:4 - error: Nested description (reportOperatorIssue)',
276
+ rule: 'reportOperatorIssue',
277
+ },
278
+ ],
279
+ detailsRemoved: [
280
+ { text: ' Earlier detail (reportGeneralTypeIssues)', rule: 'reportGeneralTypeIssues' },
281
+ ...expectedProject.detailsRemoved,
282
+ ],
283
+ },
284
+ {
285
+ name: 'second',
286
+ url: 'https://github.com/example/second',
287
+ shard: 0,
288
+ added: [],
289
+ removed: [],
290
+ detailsAdded: [],
291
+ detailsRemoved: [{ text: ' Unlocated detail', rule: 'unspecified' }],
292
+ },
293
+ ]);
294
+ });
295
+ test('keeps the complete project inventory before bounded diagnostic examples', () => {
296
+ const diagnostics = (0, mypyPrimerAnalysis_1.parseDiff)(sample, 0);
297
+ diagnostics[0].added.push(...Array.from({ length: 20 }, () => diagnostics[0].added[0]));
298
+ diagnostics[0].detailsAdded.push(...Array.from({ length: 20 }, () => diagnostics[0].detailsAdded[0]));
299
+ const summary = (0, mypyPrimerAnalysis_1.summarizeDiffs)(diagnostics);
300
+ expect(summary).toStrictEqual({
301
+ projects: [
302
+ {
303
+ name: 'example',
304
+ url: 'https://github.com/example/project',
305
+ shard: 0,
306
+ added: 21,
307
+ removed: 1,
308
+ detailLinesAdded: 21,
309
+ detailLinesRemoved: 1,
310
+ },
311
+ ],
312
+ groups: {
313
+ example: {
314
+ unspecified: {
315
+ added: 21,
316
+ removed: 1,
317
+ examplesAdded: Array(3).fill('.../projects/example/test.py:1:1 - error: New message'),
318
+ examplesRemoved: ['.../projects/example/test.py:1:1 - error: Old message'],
319
+ },
320
+ },
321
+ },
322
+ detailGroups: {
323
+ example: {
324
+ reportArgumentType: {
325
+ added: 21,
326
+ removed: 1,
327
+ examplesAdded: Array(3).fill(' Details (reportArgumentType)'),
328
+ examplesRemoved: [' Details (reportArgumentType)'],
329
+ },
330
+ },
331
+ },
332
+ });
333
+ });
334
+ test('accepts empty shards and reconciles changed totals', () => {
335
+ expect((0, mypyPrimerAnalysis_1.parseDiff)('', 0)).toEqual([]);
336
+ const change = sample.replace(/^-.*\n/gm, '') +
337
+ '\n- 0 errors, 0 warnings, 0 informations\n+ 1 error, 0 warnings, 0 informations';
338
+ expect((0, mypyPrimerAnalysis_1.parseDiff)(change, 0)[0].added).toHaveLength(1);
339
+ expect(() => (0, mypyPrimerAnalysis_1.parseDiff)(change.replace('+ 1 error', '+ 2 errors'), 0)).toThrow('counts');
340
+ expect(() => (0, mypyPrimerAnalysis_1.parseDiff)(change.replace('- 0 errors, 0 warnings, 0 informations\n', ''), 0)).toThrow('Incomplete');
341
+ });
342
+ test('rejects malformed output instead of silently omitting projects', () => {
343
+ expect(() => (0, mypyPrimerAnalysis_1.parseDiff)('unexpected output', 0)).toThrow('Unrecognized');
344
+ expect(() => (0, mypyPrimerAnalysis_1.parseDiff)(sample.replace('Old message', 'Old message\nnot a diff line'), 0)).toThrow();
345
+ });
346
+ test.each(['+ unrecognized output', '- not a diagnostic header', '+ '])('rejects malformed signed output: %s', (line) => {
347
+ expect(() => (0, mypyPrimerAnalysis_1.parseDiff)(`${sample}\n${line}`, 0)).toThrow(new Error('Unrecognized diagnostic for example'));
348
+ });
349
+ test('detail changes cannot account for missing diagnostic headers in the totals', () => {
350
+ 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'));
351
+ });
352
+ test('requires all artifacts from the exact successful PR run and attempt', async () => {
353
+ const f = fixture();
354
+ await expect((0, mypyPrimerAnalysis_1.loadSource)(f.request, repository, f.source)).resolves.toEqual(f.source);
355
+ f.artifacts.pop();
356
+ await expect((0, mypyPrimerAnalysis_1.loadSource)(f.request, repository, f.source)).rejects.toThrow('Missing');
357
+ f.run.run_attempt = 2;
358
+ await expect((0, mypyPrimerAnalysis_1.loadSource)(f.request, repository, f.source)).rejects.toThrow('matching');
359
+ });
360
+ test('rejects expired, duplicate, old-attempt, and oversized artifacts', async () => {
361
+ for (const mutation of [
362
+ (f) => (f.artifacts[0].expired = true),
363
+ (f) => f.artifacts.push(f.artifacts[0]),
364
+ (f) => (f.artifacts[0].created_at = '2026-09-09T00:00:00Z'),
365
+ (f) => (f.artifacts[0].size_in_bytes = 9 * 1024 * 1024),
366
+ ]) {
367
+ const f = fixture();
368
+ mutation(f);
369
+ await expect((0, mypyPrimerAnalysis_1.loadSource)(f.request, repository, f.source)).rejects.toThrow('artifact');
370
+ }
371
+ });
372
+ test('rejects another workflow or repository', async () => {
373
+ const f = fixture();
374
+ f.run.path = '.github/workflows/other.yml';
375
+ await expect((0, mypyPrimerAnalysis_1.loadSource)(f.request, repository, f.source)).rejects.toThrow('matching');
376
+ f.run.path = '.github/workflows/mypy_primer_pr.yaml';
377
+ f.run.repository.full_name = 'someone/else';
378
+ await expect((0, mypyPrimerAnalysis_1.loadSource)(f.request, repository, f.source)).rejects.toThrow('matching');
379
+ });
380
+ test('prepares all shards and skips a superseded head', async () => {
381
+ writeShards();
382
+ const f = fixture();
383
+ await expect((0, mypyPrimerAnalysis_1.prepareAnalysis)(f.request, repository, f.source, directory)).resolves.toEqual(f.manifest);
384
+ f.pr.head.sha = 'b'.repeat(40);
385
+ await expect((0, mypyPrimerAnalysis_1.prepareAnalysis)(f.request, repository, f.source, directory)).resolves.toBeUndefined();
386
+ });
387
+ test('prepares and renders a detail-only project without treating zero headers as no changes', async () => {
388
+ writeShards(detailOnlySample);
389
+ const f = fixture(detailOnlySample);
390
+ const manifest = await (0, mypyPrimerAnalysis_1.prepareAnalysis)(f.request, repository, f.source, directory);
391
+ expect(manifest).toStrictEqual({
392
+ repository,
393
+ source: f.source,
394
+ prNumber: 7,
395
+ ...expectedDetailOnlySummary,
396
+ });
397
+ if (!manifest) {
398
+ throw new Error('Preparation unexpectedly skipped the detail-only project');
399
+ }
400
+ f.report.projects[0] = {
401
+ name: 'example',
402
+ assessment: 'needs-review',
403
+ confidence: 'low',
404
+ summary: 'Only diagnostic details changed.',
405
+ explanation: 'The return type changed from Match[str] | None to Unknown.',
406
+ unresolved: 'Diagnostic headers and locations are absent from the concise diff.',
407
+ evidence: [],
408
+ };
409
+ const expectedDetails = [
410
+ '### example',
411
+ '',
412
+ 'The return type changed from Match\\[str\\] \\| None to Unknown.',
413
+ '',
414
+ '**Unresolved / coverage limits:** Diagnostic headers and locations are absent from the concise diff.',
415
+ '',
416
+ '',
417
+ ];
418
+ const expectedRow = '| example | 0 | 0 | 2 / 2 | Needs human review | low | Only diagnostic details changed. |';
419
+ const expected = {
420
+ body: [
421
+ ...expectedReportHeading,
422
+ expectedRow,
423
+ '',
424
+ '<details>',
425
+ '<summary>Evidence and limitations by project</summary>',
426
+ '',
427
+ ...expectedDetails,
428
+ '</details>',
429
+ '',
430
+ ...expectedReportFooter,
431
+ ].join('\n'),
432
+ fullReport: [...expectedReportHeading, expectedRow, '', ...expectedDetails].join('\n'),
433
+ };
434
+ expect((0, mypyPrimerAnalysis_1.renderReport)(manifest, f.report, 100)).toStrictEqual(expected);
435
+ expect(() => (0, mypyPrimerAnalysis_1.renderReport)(manifest, { projects: [] }, 100)).toThrow(new Error('The analysis must cover every changed project exactly once'));
436
+ const path = (0, path_1.join)(directory, 'report.md');
437
+ await expect((0, mypyPrimerAnalysis_1.publishAnalysis)(f.request, manifest, f.output(), 100, path, true)).resolves.toBe('Staged: report saved without posting a comment');
438
+ expect((0, fs_1.readFileSync)(path, 'utf8')).toBe(expected.fullReport);
439
+ expect(f.request.mock.calls.filter(([route]) => /^(POST|PATCH) /.test(route))).toStrictEqual([]);
440
+ });
441
+ test('does not trust the PR number artifact', async () => {
442
+ writeShards();
443
+ const f = fixture();
444
+ f.source.pullRequests = [99];
445
+ await expect((0, mypyPrimerAnalysis_1.prepareAnalysis)(f.request, repository, f.source, directory)).rejects.toThrow('not associated');
446
+ (0, fs_1.writeFileSync)((0, path_1.join)(directory, 'mypy_primer_diffs_pr_number', 'pr_number.txt'), '7; echo unsafe');
447
+ await expect((0, mypyPrimerAnalysis_1.prepareAnalysis)(f.request, repository, f.source, directory)).rejects.toThrow('Invalid PR number');
448
+ });
449
+ test('accepts a uniquely associated fork but rejects ambiguous associations', async () => {
450
+ writeShards();
451
+ const f = fixture();
452
+ f.source.pullRequests = [];
453
+ f.source.headRepository = 'contributor/pyright';
454
+ f.pr.head.repo.full_name = 'contributor/pyright';
455
+ await expect((0, mypyPrimerAnalysis_1.prepareAnalysis)(f.request, repository, f.source, directory)).resolves.toBeDefined();
456
+ f.associated.push({ ...f.pr, number: 8 });
457
+ await expect((0, mypyPrimerAnalysis_1.prepareAnalysis)(f.request, repository, f.source, directory)).rejects.toThrow('unique');
458
+ });
459
+ test('rejects duplicate project sections and unexpected shard files', async () => {
460
+ writeShards(sample + '\n' + sample);
461
+ const f = fixture();
462
+ await expect((0, mypyPrimerAnalysis_1.prepareAnalysis)(f.request, repository, f.source, directory)).rejects.toThrow('duplicate project');
463
+ (0, fs_1.writeFileSync)((0, path_1.join)(directory, 'mypy_primer_diffs_0', 'unexpected.js'), '');
464
+ await expect((0, mypyPrimerAnalysis_1.prepareAnalysis)(f.request, repository, f.source, directory)).rejects.toThrow('Unexpected contents');
465
+ });
466
+ test('renders authoritative counts and preserves limitations in the full report', () => {
467
+ const f = fixture();
468
+ const result = (0, mypyPrimerAnalysis_1.renderReport)(f.manifest, f.report, 100);
469
+ expect(result).toStrictEqual(expectedReport);
470
+ });
471
+ test('large reports retain every project in the comment and full artifact', () => {
472
+ const f = fixture();
473
+ const names = Array.from({ length: 40 }, (_, index) => `project_${index}`);
474
+ const manifest = {
475
+ ...f.manifest,
476
+ projects: names.map((name) => ({ ...f.manifest.projects[0], name })),
477
+ };
478
+ const report = {
479
+ projects: names.map((name) => ({ ...f.report.projects[0], name, explanation: 'x'.repeat(1800) })),
480
+ };
481
+ const result = (0, mypyPrimerAnalysis_1.renderReport)(manifest, report, 100);
482
+ expect(result.body.length).toBeLessThan(60000);
483
+ expect(result.fullReport.length).toBeGreaterThan(60000);
484
+ const expectedRows = names.map((_, index) => `| project\\_${index} | 1 | 1 | 1 / 1 | Needs human review | low | The argument error changed its wording. |`);
485
+ const expectedDetails = names.flatMap((_, index) => [
486
+ `### project\\_${index}`,
487
+ '',
488
+ 'x'.repeat(1800),
489
+ '',
490
+ '**Unresolved / coverage limits:** Exact dependency revision unavailable.',
491
+ '',
492
+ '',
493
+ ]);
494
+ expect(result).toStrictEqual({
495
+ body: [...expectedReportHeading, ...expectedRows, '', ...expectedReportFooter].join('\n'),
496
+ fullReport: [...expectedReportHeading, ...expectedRows, '', ...expectedDetails].join('\n'),
497
+ });
498
+ });
499
+ test('rejects missing projects, unsupported classifications, and uncited certainty', () => {
500
+ const f = fixture();
501
+ expect(() => (0, mypyPrimerAnalysis_1.renderReport)(f.manifest, { projects: [] }, 100)).toThrow('every changed project');
502
+ f.report.projects.push(f.report.projects[0]);
503
+ expect(() => (0, mypyPrimerAnalysis_1.renderReport)(f.manifest, f.report, 100)).toThrow('every changed project');
504
+ f.report.projects.pop();
505
+ f.report.projects[0].assessment = 'definitely-safe';
506
+ expect(() => (0, mypyPrimerAnalysis_1.renderReport)(f.manifest, f.report, 100)).toThrow('Invalid assessment');
507
+ f.report.projects[0].assessment = 'expected-improvement';
508
+ expect(() => (0, mypyPrimerAnalysis_1.renderReport)(f.manifest, f.report, 100)).toThrow('cited evidence');
509
+ });
510
+ test('escapes report text and rejects unsafe evidence links', () => {
511
+ const f = fixture();
512
+ f.report.projects[0].summary = '<script> @someone | injected row';
513
+ const escapedSummary = '\\<script\\> &#64;someone \\| injected row';
514
+ expect((0, mypyPrimerAnalysis_1.renderReport)(f.manifest, f.report, 100)).toStrictEqual({
515
+ body: expectedReport.body.replace('The argument error changed its wording.', escapedSummary),
516
+ fullReport: expectedReport.fullReport.replace('The argument error changed its wording.', escapedSummary),
517
+ });
518
+ const report = {
519
+ projects: [
520
+ { ...f.report.projects[0], evidence: [{ url: 'https://github.com@evil.example/', detail: 'Unsafe' }] },
521
+ ],
522
+ };
523
+ expect(() => (0, mypyPrimerAnalysis_1.renderReport)(f.manifest, report, 100)).toThrow('Unsupported evidence URL');
524
+ });
525
+ test('staged reports never mutate GitHub', async () => {
526
+ const f = fixture();
527
+ const path = (0, path_1.join)(directory, 'report.md');
528
+ await expect((0, mypyPrimerAnalysis_1.publishAnalysis)(f.request, f.manifest, f.output(), 100, path, true)).resolves.toBe('Staged: report saved without posting a comment');
529
+ expect((0, fs_1.readFileSync)(path, 'utf8')).toBe(expectedReport.fullReport);
530
+ expect(f.request.mock.calls.some(([route]) => /^(POST|PATCH) /.test(route))).toBe(false);
531
+ });
532
+ test('requires a definitive staged flag from this attempt of the trusted activation job', () => {
533
+ const activation = { repository, run_id: 100, run_attempt: '1', staged: true };
534
+ expect((0, mypyPrimerAnalysis_1.getStagedMode)(activation, repository, 100, 1)).toBe(true);
535
+ expect((0, mypyPrimerAnalysis_1.getStagedMode)({ ...activation, staged: false }, repository, 100, 1)).toBe(false);
536
+ expect(() => (0, mypyPrimerAnalysis_1.getStagedMode)({ ...activation, staged: undefined }, repository, 100, 1)).toThrow('metadata');
537
+ expect(() => (0, mypyPrimerAnalysis_1.getStagedMode)({ ...activation, staged: 'true' }, repository, 100, 1)).toThrow('metadata');
538
+ expect(() => (0, mypyPrimerAnalysis_1.getStagedMode)(activation, repository, 101, 1)).toThrow('metadata');
539
+ expect(() => (0, mypyPrimerAnalysis_1.getStagedMode)(activation, repository, 100, 2)).toThrow('metadata');
540
+ expect(() => (0, mypyPrimerAnalysis_1.getStagedMode)(activation, 'different/repository', 100, 1)).toThrow('metadata');
541
+ });
542
+ test('rechecks the head before posting', async () => {
543
+ const f = fixture();
544
+ f.pr.head.sha = 'b'.repeat(40);
545
+ 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');
546
+ expect(f.request.mock.calls.some(([route]) => /^(POST|PATCH) /.test(route))).toBe(false);
547
+ });
548
+ test('posts only to the verified PR and does not replace raw primer comments', async () => {
549
+ const f = fixture();
550
+ f.comments.push({ id: 55, user: { login: 'github-actions[bot]' }, body: 'Diff from mypy_primer' });
551
+ 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');
552
+ const calls = f.request.mock.calls.filter(([route]) => /^(POST|PATCH) /.test(route));
553
+ expect(calls).toStrictEqual([
554
+ [
555
+ 'POST /repos/{owner}/{repo}/issues/{issue_number}/comments',
556
+ { owner: 'microsoft', repo: 'pyright', issue_number: 7, body: expectedReport.body },
557
+ ],
558
+ ]);
559
+ });
560
+ test('updates an existing report instead of duplicating it', async () => {
561
+ const f = fixture();
562
+ f.comments.push({
563
+ id: 55,
564
+ user: { login: 'github-actions[bot]' },
565
+ body: `<!-- pyright-primer-analysis:${headSha}:42:1 -->`,
566
+ });
567
+ 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');
568
+ const calls = f.request.mock.calls.filter(([route]) => /^(POST|PATCH) /.test(route));
569
+ expect(calls).toStrictEqual([
570
+ [
571
+ 'PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}',
572
+ { owner: 'microsoft', repo: 'pyright', comment_id: 55, body: expectedReport.body },
573
+ ],
574
+ ]);
575
+ });
576
+ test('rechecks the head after listing existing comments', async () => {
577
+ const f = fixture();
578
+ const original = f.request.getMockImplementation();
579
+ f.request.mockImplementation(async (route, parameters) => {
580
+ if (route === 'GET /repos/{owner}/{repo}/issues/{issue_number}/comments') {
581
+ f.pr.head.sha = 'b'.repeat(40);
582
+ }
583
+ return original(route, parameters);
584
+ });
585
+ 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');
586
+ expect(f.request.mock.calls.some(([route]) => /^(POST|PATCH) /.test(route))).toBe(false);
587
+ });
588
+ test('does not overwrite a newer report for the same head', async () => {
589
+ const f = fixture();
590
+ f.comments.push({
591
+ id: 55,
592
+ user: { login: 'github-actions[bot]' },
593
+ body: `<!-- pyright-primer-analysis:${headSha}:43:1 -->`,
594
+ });
595
+ 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');
596
+ expect(f.request.mock.calls.some(([route]) => /^(POST|PATCH) /.test(route))).toBe(false);
597
+ });
598
+ test('compiled workflow runtime matches the compiler resolution lock', () => {
599
+ const content = (0, fs_1.readFileSync)((0, path_1.join)(__dirname, '../../../../.github/workflows/mypy-primer-analysis.lock.yml'), 'utf8');
600
+ const metadata = (0, yaml_1.parseDocument)(content.split('\n')[0].replace('# gh-aw-metadata: ', ''));
601
+ const compilerVersion = metadata.get('compiler_version');
602
+ expect(compilerVersion).toMatch(/^v\d+\.\d+\.\d+$/);
603
+ const actionsLock = (0, yaml_1.parseDocument)((0, fs_1.readFileSync)((0, path_1.join)(__dirname, '../../../../.github/aw/actions-lock.json'), 'utf8'));
604
+ const runtimeActions = [...content.matchAll(/^\s+uses: (github\/gh-aw-actions\/[^@\s]+)@(\S+)/gm)];
605
+ expect(runtimeActions.length).toBeGreaterThan(0);
606
+ for (const [, repository, sha] of runtimeActions) {
607
+ const key = `${repository}@${compilerVersion}`;
608
+ const resolvedSha = actionsLock.getIn(['entries', key, 'sha']);
609
+ expect(resolvedSha).toMatch(/^[a-f0-9]{40}$/);
610
+ expect(actionsLock.getIn(['entries', key, 'repo'])).toBe(repository);
611
+ expect(actionsLock.getIn(['entries', key, 'version'])).toBe(compilerVersion);
612
+ expect(sha).toBe(resolvedSha);
613
+ }
614
+ });
615
+ test('compiled workflow keeps the agent read-only and gates publication on threat detection', () => {
616
+ const workflow = (0, yaml_1.parseDocument)((0, fs_1.readFileSync)((0, path_1.join)(__dirname, '../../../../.github/workflows/mypy-primer-analysis.lock.yml'), 'utf8'));
617
+ for (const job of ['agent', 'collect']) {
618
+ expect(workflow.getIn(['jobs', job, 'permissions', 'pull-requests'])).toBe('read');
619
+ expect(workflow.getIn(['jobs', job, 'permissions', 'contents'])).toBe('read');
620
+ }
621
+ expect(workflow.getIn(['jobs', 'safe_outputs', 'permissions', 'issues'])).not.toBe('write');
622
+ expect(workflow.getIn(['jobs', 'conclusion', 'permissions', 'issues'])).not.toBe('write');
623
+ const agentSteps = workflow.getIn(['jobs', 'agent', 'steps'], true);
624
+ expect(String(agentSteps)).toContain('--deny-tool write');
625
+ expect(String(agentSteps)).toContain('--deny-tool shell');
626
+ const publishingSteps = workflow.getIn(['jobs', 'publish_primer_analysis', 'steps'], true);
627
+ expect(String(publishingSteps)).toContain('Threat detection did not explicitly allow publication');
628
+ expect(String(publishingSteps)).toContain('needs.detection.outputs.detection_conclusion');
629
+ expect(String(publishingSteps)).toContain('getStagedMode(activation,');
630
+ expect(String(publishingSteps)).toContain('primer-activation');
631
+ });
632
+ });
633
+ //# sourceMappingURL=mypyPrimerAnalysis.test.js.map