Repository navigation
Expand file tree
/
Copy pathorg-defaults-coverage.test.mjs
More file actions
1322 lines (1188 loc) · 61.6 KB
/
Copy pathorg-defaults-coverage.test.mjs
File metadata and controls
1322 lines (1188 loc) · 61.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Tests for scripts/org-defaults-coverage.mjs, the check that measures whether this account's
// default issue templates actually land, and for the workflow that runs it.
//
// WHAT THESE TESTS ARE FOR, said once so the individual cases read against it. The defect is not "a
// label is missing". It is that NOTHING EVER LOOKED. A default issue template promising `bug` files
// an unlabelled issue in a repository that never created `bug`, the form renders perfectly, the run
// is green, and the only statement anyone can make about coverage is an assumption. So every arm
// below asks one question: does this check still refuse to report coverage it did not measure?
//
// THE API IS A REAL SOCKET, NOT AN INJECTED FUNCTION. Each case stands up a `node:http` server, in
// the style of `test/environment-gate.test.mjs`, so the pagination, the classification and (where
// the CLI is driven as a child process) THE PROCESS EXIT STATUS are all in the measurement. The exit
// status is the whole mechanism by which a refusal reaches a maintainer as a red run.
//
// AND THE STUB RECORDS EVERY REQUEST IT IS SENT, because "this check only reads" is a claim about
// what it does rather than about what its source says, and the request log is the only place that
// claim can be checked.
import assert from 'node:assert/strict';
import { execFile } from 'node:child_process';
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
import { createServer } from 'node:http';
import { tmpdir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import test from 'node:test';
import { fileURLToPath } from 'node:url';
import { promisify } from 'node:util';
import {
TemplateError,
decide,
declaredLabels,
foldLabel,
issueTemplateOverride,
measure,
overridesOf,
readDefaultTemplates,
readTemplateDirectory,
renderReport,
} from '../scripts/org-defaults-coverage.mjs';
const HERE = dirname(fileURLToPath(import.meta.url));
const REPO = resolve(HERE, '..');
const SCRIPT = resolve(REPO, 'scripts/org-defaults-coverage.mjs');
const WORKFLOWS = '.github/workflows';
const CHECK_WORKFLOW = 'org-defaults-coverage.yml';
const run = promisify(execFile);
const ACCOUNT = 'cosyte';
const DEFAULTS_REPO = 'cosyte/.github';
const read = (relative) => readFileSync(join(REPO, relative), 'utf8');
// ---------------------------------------------------------------------------
// The labels the defaults declare, read without a YAML parser
// ---------------------------------------------------------------------------
const form = (labelsBlock) =>
`name: Bug report\ndescription: something broke\n${labelsBlock}body:\n - type: input\n id: version\n attributes:\n label: Package version\n`;
test('a flow sequence of double-quoted strings is the shape this repository ships', () => {
assert.deepEqual(declaredLabels('t.yml', form('labels: ["bug"]\n')), ['bug']);
});
test('a flow sequence of several entries, quoted either way or plain, reads as all of them', () => {
assert.deepEqual(declaredLabels('t.yml', form(`labels: ["bug", 'triage', needs-repro]\n`)), [
'bug',
'triage',
'needs-repro',
]);
});
test('a block sequence reads as the same list', () => {
assert.deepEqual(declaredLabels('t.yml', form('labels:\n - bug\n - "needs triage"\n')), ['bug', 'needs triage']);
});
test('an empty flow sequence declares no labels, which is not a refusal', () => {
assert.deepEqual(declaredLabels('t.yml', form('labels: []\n')), []);
});
test('a template with no `labels:` key at all declares no labels', () => {
assert.deepEqual(declaredLabels('t.yml', form('')), []);
});
test('a `labels:` nested inside `body:` is not mistaken for the top-level declaration', () => {
// Every issue form in this repository has an indented `label:` under `body:`, and one could just
// as easily have `labels:`. Column zero is what separates the declaration from the prose.
const text = 'name: Bug\nbody:\n - type: dropdown\n attributes:\n labels: ["not-a-declaration"]\n';
assert.deepEqual(declaredLabels('t.yml', text), []);
});
test('a scalar `labels:` is refused naming the file, not read as no labels', () => {
// The markdown-front-matter spelling. Reading it as "declares nothing" would leave a real
// obligation unmeasured while the run went green, which is the defect this check exists to end.
assert.throws(() => declaredLabels('bug.yml', form('labels: bug\n')), (error) => {
assert.ok(error instanceof TemplateError);
assert.equal(error.path, 'bug.yml');
assert.match(error.reason, /not a list of strings/);
return true;
});
});
test('a mapping `labels:` is refused naming the file', () => {
assert.throws(() => declaredLabels('bug.yml', form('labels: {bug: true}\n')), /not a list of strings/);
});
test('a second top-level `labels:` key is refused rather than resolved', () => {
assert.throws(() => declaredLabels('bug.yml', form('labels: ["bug"]\nlabels: ["other"]\n')), /more than once/);
});
test('an unterminated flow sequence is refused', () => {
assert.throws(() => declaredLabels('bug.yml', form('labels: ["bug"\n')), /cannot see the end of/);
});
test('a nested collection inside the flow sequence is refused', () => {
assert.throws(() => declaredLabels('bug.yml', form('labels: ["bug", ["nested"]]\n')), /nests a collection/);
});
test('a tab in the declaration is refused rather than guessed at', () => {
assert.throws(() => declaredLabels('bug.yml', form('labels:\t["bug"]\n')), /tab/);
});
test('`labels:` present with nothing under it is refused, being neither a list nor absent', () => {
assert.throws(() => declaredLabels('bug.yml', 'name: Bug\nlabels:\n'), /neither a list nor absent/);
});
test('a mapping item inside the block sequence is refused', () => {
assert.throws(() => declaredLabels('bug.yml', 'name: Bug\nlabels:\n bug: true\n'), /not a list item/);
});
test('an empty entry is refused, because an empty string is not a label', () => {
assert.throws(() => declaredLabels('bug.yml', form('labels: ["bug", ""]\n')), /not a label/);
});
test('a quoting form this reader does not read is refused rather than mis-unquoted', () => {
assert.throws(() => declaredLabels('bug.yml', form('labels: ["a\\"b"]\n')), /quoting form/);
});
// ---------------------------------------------------------------------------
// The KEY, not only its value: `[]` means "no obligation", so it has to be earned
// ---------------------------------------------------------------------------
//
// `[]` is the most dangerous answer this function can give. It says "this template declares no
// labels", the check then reports there is no obligation to measure and exits 0, and a promise
// GitHub is really making goes unmeasured behind a green run. So `[]` is allowed for exactly one
// reason - no top-level `labels` key is there - and every line that IS such a key in a spelling
// this reader cannot read is refused naming the file instead.
const BOM = String.fromCharCode(0xfeff);
const crlf = (/** @type {string} */ text) => text.replace(/\n/g, '\r\n');
test('a CRLF block sequence declares its label: YAML breaks a line on CRLF', () => {
// The spelling GitHub's own issue-form documentation uses, saved by an editor that writes CRLF.
// Splitting on LF alone left `labels:\r`, which matched no key and read as declaring nothing.
assert.deepEqual(declaredLabels('t.yml', crlf(form('labels:\n - bug\n'))), ['bug']);
});
test('a CRLF flow sequence declares its label', () => {
assert.deepEqual(declaredLabels('t.yml', crlf(form('labels: ["bug"]\n'))), ['bug']);
});
test('a lone carriage return breaks a line too, and does not hide the declaration', () => {
assert.deepEqual(declaredLabels('t.yml', form('labels:\n - bug\n').replace(/\n/g, '\r')), ['bug']);
});
test('a byte order mark opening the stream does not hide the first key', () => {
// YAML 1.2 permits a BOM at the start of a stream, so `<BOM>labels: ["bug"]` declares `bug`.
assert.deepEqual(declaredLabels('t.yml', `${BOM}labels: ["bug"]\nname: Bug report\n`), ['bug']);
});
test('a top-level labels key spelled in a form this reader does not read is refused, not answered as none', () => {
// Each of these is a `labels` key to YAML and to GitHub. None of them is a key this reader reads,
// and the answer it must never give for any of them is `[]`.
const spellings = [
'"labels": ["bug"]', // a quoted key
"'labels': ['bug']",
'labels : ["bug"]', // a space before the colon
'LABELS: ["bug"]', // a case this reader does not recognise
'Labels: ["bug"]',
'? labels', // YAML's explicit key form
`${BOM}labels: ["bug"]`, // a byte order mark somewhere other than the head of the stream
];
for (const spelling of spellings) {
assert.throws(
() => declaredLabels('bug.yml', `name: Bug\n${spelling}\nbody: []\n`),
(error) => {
assert.ok(error instanceof TemplateError, `${spelling} refused with ${error}`);
assert.equal(error.path, 'bug.yml');
assert.match(error.reason, /does not read/);
return true;
},
`${JSON.stringify(spelling)} was not refused`,
);
}
});
test('a flow collection at the document root is refused, because a labels key can hide inside one', () => {
assert.throws(() => declaredLabels('bug.yml', '{name: Bug, labels: [bug]}\n'), /flow collection/);
});
test('none of that refusal reaches an indented line, where a labels key is not a declaration', () => {
// The refusal scans column zero only. `body:` is full of indented keys, and one of them spelled
// `labels:` or `"labels":` declares nothing at the top level and must not red the check.
const text = 'name: Bug\nbody:\n - type: dropdown\n attributes:\n "labels": ["not-a-declaration"]\n';
assert.deepEqual(declaredLabels('t.yml', text), []);
});
test('config.yml contributes no labels and is still listed as a default file', () => {
const outcome = readDefaultTemplates([
{ name: 'bug_report.yml', text: form('labels: ["bug"]\n') },
{ name: 'config.yml', text: 'blank_issues_enabled: false\ncontact_links:\n - name: Docs\n' },
]);
assert.deepEqual(
outcome.templates.map((entry) => entry.path),
['.github/ISSUE_TEMPLATE/bug_report.yml', '.github/ISSUE_TEMPLATE/config.yml'],
);
assert.deepEqual(outcome.labels, [{ label: 'bug', templates: ['.github/ISSUE_TEMPLATE/bug_report.yml'] }]);
});
test('a markdown issue template is refused naming the file, not skipped', () => {
assert.throws(
() => readDefaultTemplates([{ name: 'legacy.md', text: '---\nlabels: bug\n---\n' }]),
(error) => {
assert.ok(error instanceof TemplateError);
assert.equal(error.path, '.github/ISSUE_TEMPLATE/legacy.md');
return true;
},
);
});
test('one label declared by two templates is reported once, naming both', () => {
const outcome = readDefaultTemplates([
{ name: 'a.yml', text: form('labels: ["bug"]\n') },
{ name: 'b.yml', text: form('labels: ["bug", "enhancement"]\n') },
]);
assert.deepEqual(outcome.labels, [
{ label: 'bug', templates: ['.github/ISSUE_TEMPLATE/a.yml', '.github/ISSUE_TEMPLATE/b.yml'] },
{ label: 'enhancement', templates: ['.github/ISSUE_TEMPLATE/b.yml'] },
]);
});
test("this repository's own default templates parse, and declare bug and enhancement", () => {
// The live obligation, read off the tracked files rather than restated. A template edit that this
// reader cannot read fails here rather than in a scheduled run a week later.
const found = readTemplateDirectory(join(REPO, '.github/ISSUE_TEMPLATE'));
assert.equal(found.failure, null, 'the default issue templates could not be read off disk');
const outcome = readDefaultTemplates(found.files);
assert.deepEqual(
outcome.labels.map((entry) => entry.label),
['bug', 'enhancement'],
);
});
test('an absent template folder is an answer, not a refusal: this account publishes no defaults', () => {
const found = readTemplateDirectory(join(REPO, '.github/NOT_A_TEMPLATE_FOLDER'));
assert.equal(found.failure, null);
assert.deepEqual(found.files, []);
});
test('an entry in the template folder that is not a regular file is refused naming it', () => {
const dir = mkdtempSync(join(tmpdir(), 'org-defaults-'));
mkdirSync(join(dir, 'nested'));
const found = readTemplateDirectory(dir);
assert.notEqual(found.failure, null);
assert.match(found.failure.reason, /not a regular file/);
assert.match(found.failure.path, /nested$/);
});
// ---------------------------------------------------------------------------
// The stub API
// ---------------------------------------------------------------------------
const entry = (name, type = 'file') => ({ name, type });
const TEMPLATE_FOLDER = entry('ISSUE_TEMPLATE', 'dir');
/** The default templates, as the fixtures declare them. */
const TEMPLATE_FILES = [
{ name: 'bug_report.yml', text: form('labels: ["bug"]\n') },
{ name: 'config.yml', text: 'blank_issues_enabled: false\n' },
{ name: 'feature_request.yml', text: form('labels: ["enhancement"]\n') },
];
const BOTH_LABELS = ['bug', 'enhancement'];
/**
* A stand-in for the REST API over a real socket.
*
* `repos` is the whole fixture account: each entry names a repository, the three folder listings the
* check reads, and the labels it holds. `null` for a listing is a 404, which is how a repository
* says it has no such folder.
*/
async function startApi({
account = ACCOUNT,
accountType = 'Organization',
repos = [],
publicRepos = null,
privateRepos = null,
omitPublicCount = false,
neverConverge = false,
status = {},
headers = {},
rawBody = {},
} = {}) {
/** @type {{method: string, path: string, search: string}[]} */
const requests = [];
const byName = new Map(repos.map((repository) => [repository.name, repository]));
const paged = (items, search) => {
const perPage = Number(search.get('per_page') || 30);
const page = Number(search.get('page') || 1);
return { status: 200, body: items.slice((page - 1) * perPage, page * perPage) };
};
const accountPath = accountType === 'User' ? `/users/${account}` : `/orgs/${account}`;
const routes = (pathname, search) => {
if (pathname === `${accountPath}/repos`) {
if (status.list) return { status: status.list, body: { message: 'nope' } };
if (rawBody.list !== undefined) return { status: 200, raw: rawBody.list };
if (neverConverge) {
// A full page for every page asked for, which is what a list endpoint whose paging is broken
// looks like from the outside. No walk terminates on a short page against it.
const perPage = Number(search.get('per_page') || 30);
return {
status: 200,
body: Array.from({ length: perPage }, (_, index) => ({ full_name: `cosyte/r${index}` })),
};
}
return paged(
repos.map((repository) => ({ name: repository.name.split('/')[1], full_name: repository.name })),
search,
);
}
if (pathname === accountPath) {
if (status.account) return { status: status.account, body: { message: 'nope' } };
if (rawBody.account !== undefined) return { status: 200, raw: rawBody.account };
return {
status: 200,
body: {
login: account,
...(omitPublicCount ? {} : { public_repos: publicRepos ?? repos.length }),
...(privateRepos === null ? {} : { owned_private_repos: privateRepos }),
},
};
}
const contents = /^\/repos\/([^/]+\/[^/]+)\/contents\/(.*)$/.exec(pathname);
if (contents) {
const [, name, path] = contents;
if (status.contents?.[name]) return { status: status.contents[name], body: { message: 'nope' } };
if (rawBody.contents?.[name] !== undefined) return { status: 200, raw: rawBody.contents[name] };
const repository = byName.get(name);
if (!repository) return { status: 404, body: { message: 'Not Found' } };
const key = path === '' ? 'root' : path === '.github' ? 'github' : path === 'docs' ? 'docs' : null;
if (key === null) return { status: 404, body: { message: 'Not Found' } };
const listing = repository[key];
if (listing === undefined || listing === null) return { status: 404, body: { message: 'Not Found' } };
if (listing === 'a-file') return { status: 200, body: { name: path, type: 'file', size: 12 } };
return { status: 200, body: listing };
}
const labels = /^\/repos\/([^/]+\/[^/]+)\/labels$/.exec(pathname);
if (labels) {
const name = labels[1];
if (status.labels?.[name]) {
return {
status: status.labels[name],
body: { message: headers.labels?.[name] ? 'API rate limit exceeded' : 'nope' },
headers: headers.labels?.[name],
};
}
if (rawBody.labels?.[name] !== undefined) return { status: 200, raw: rawBody.labels[name] };
const repository = byName.get(name);
if (!repository) return { status: 404, body: { message: 'Not Found' } };
return paged((repository.labels ?? []).map((label) => ({ name: label })), search);
}
const repo = /^\/repos\/([^/]+\/[^/]+)$/.exec(pathname);
if (repo) {
if (status.repo) return { status: status.repo, body: { message: 'nope' } };
if (rawBody.repo !== undefined) return { status: 200, raw: rawBody.repo };
return { status: 200, body: { full_name: repo[1], owner: { login: account, type: accountType } } };
}
return undefined;
};
const server = createServer((req, res) => {
const url = new URL(req.url ?? '/', 'http://127.0.0.1');
requests.push({ method: req.method ?? '', path: url.pathname, search: url.search });
const answer = routes(url.pathname, url.searchParams) ?? { status: 404, body: { message: 'Not Found' } };
res.writeHead(answer.status, { 'content-type': 'application/json', ...(answer.headers ?? {}) });
res.end(answer.raw !== undefined ? answer.raw : JSON.stringify(answer.body ?? null));
});
await new Promise((done) => server.listen(0, '127.0.0.1', done));
const address = server.address();
const base = `http://127.0.0.1:${typeof address === 'object' && address ? address.port : 0}`;
return { base, requests, close: () => new Promise((done) => server.close(() => done(undefined))) };
}
/** Stand the stub up, run the real measurement against it, tear the stub down. */
async function measureAgainst(stubOptions, options = {}) {
const api = await startApi(stubOptions);
try {
const measurement = await measure({
repository: options.repository ?? DEFAULTS_REPO,
templateFiles: options.templateFiles ?? TEMPLATE_FILES,
apiBase: api.base,
token: 'ghs_test_token_not_a_credential',
...options.measure,
});
return { ...decide(measurement), measurement, requests: api.requests };
} finally {
await api.close();
}
}
/** Run the CLI exactly as the workflow does, and return { code, stdout, stderr } separately. */
async function runCheck({ repository = DEFAULTS_REPO, api, args = [], env = {} } = {}) {
const argv = [SCRIPT, '--repository', repository, ...args];
if (api) argv.push('--api', api);
const childEnv = {
...process.env,
// Never inherit the surrounding run's own GitHub identity: these tests depend on nothing but
// what they pass in, and a real token must never be sent to a socket a test opened.
GH_TOKEN: 'ghs_test_token_not_a_credential',
GITHUB_TOKEN: '',
GITHUB_REPOSITORY: '',
GITHUB_API_URL: '',
GITHUB_STEP_SUMMARY: '',
...env,
};
try {
const { stdout, stderr } = await run(process.execPath, argv, { encoding: 'utf8', env: childEnv });
return { code: 0, stdout, stderr };
} catch (error) {
return { code: error.code, stdout: error.stdout ?? '', stderr: error.stderr ?? '' };
}
}
/** The fixture account that everything is covered in. */
const COVERED = [
{ name: DEFAULTS_REPO, github: [TEMPLATE_FOLDER, entry('workflows', 'dir')], root: [entry('README.md')], docs: null, labels: BOTH_LABELS },
{ name: 'cosyte/hl7', github: [entry('workflows', 'dir')], root: [entry('README.md')], docs: null, labels: BOTH_LABELS },
{ name: 'cosyte/x12', github: null, root: [entry('README.md')], docs: null, labels: [...BOTH_LABELS, 'chore'] },
];
// ---------------------------------------------------------------------------
// The passing arm, and what it is allowed to claim
// ---------------------------------------------------------------------------
test('every repository holding every declared label passes, and the outcome says covered', async () => {
const outcome = await measureAgainst({ repos: COVERED });
assert.equal(outcome.failing, false);
assert.equal(outcome.report.outcome, 'covered');
});
test('the passing report states the population it measured rather than asserting completeness', async () => {
const outcome = await measureAgainst({ repos: COVERED });
assert.equal(outcome.report.population.listed, 3);
assert.equal(outcome.report.population.reported, 3);
assert.match(renderReport(outcome.report), /Measured, not assumed/);
});
test('the check only ever issues GETs: it verifies, it never writes', async () => {
const outcome = await measureAgainst({ repos: COVERED });
assert.ok(outcome.requests.length > 0);
for (const request of outcome.requests) assert.equal(request.method, 'GET');
});
test('the account is derived from the run\'s repository, so a different owner measures that owner', async () => {
const repos = [
{ name: 'acme/.github', github: [TEMPLATE_FOLDER], root: [], docs: null, labels: BOTH_LABELS },
{ name: 'acme/widget', github: [], root: [], docs: null, labels: BOTH_LABELS },
];
const outcome = await measureAgainst({ account: 'acme', repos }, { repository: 'acme/.github' });
assert.equal(outcome.report.account.login, 'acme');
assert.deepEqual(
outcome.report.repositories.map((item) => item.name),
['acme/.github', 'acme/widget'],
);
// Nothing in the source names an account, so the same check under `acme` measures `acme`.
assert.ok(!readFileSync(SCRIPT, 'utf8').includes(`'${ACCOUNT}'`));
});
test('a user account is enumerated through the user endpoints rather than the org ones', async () => {
const repos = [{ name: 'noah/.github', github: [TEMPLATE_FOLDER], root: [], docs: null, labels: BOTH_LABELS }];
const outcome = await measureAgainst(
{ account: 'noah', accountType: 'User', repos },
{ repository: 'noah/.github' },
);
assert.equal(outcome.failing, false);
assert.equal(outcome.report.population.endpoint, '/users/noah/repos?type=owner');
});
// ---------------------------------------------------------------------------
// The obligation, and who it falls on
// ---------------------------------------------------------------------------
test('a missing label in one repository of several fails, naming that repository', async () => {
const repos = [
{ name: DEFAULTS_REPO, github: [TEMPLATE_FOLDER], root: [], docs: null, labels: BOTH_LABELS },
{ name: 'cosyte/hl7', github: [], root: [], docs: null, labels: BOTH_LABELS },
{ name: 'cosyte/x12', github: [], root: [], docs: null, labels: ['bug'] },
];
const outcome = await measureAgainst({ repos });
assert.equal(outcome.failing, true);
assert.equal(outcome.report.outcome, 'uncovered');
const enhancement = outcome.report.coverage.find((item) => item.label === 'enhancement');
assert.deepEqual(enhancement.missing, ['cosyte/x12']);
assert.match(outcome.report.summary, /cosyte\/x12/);
});
test('the repository the check runs in is inside the population it measures', async () => {
// The source clause names it first: the label "must be created in your `.github` repository and
// any repositories where the template will be used". Its own ISSUE_TEMPLATE folder is the
// defaults' folder, not an override of them.
const repos = [
{ name: DEFAULTS_REPO, github: [TEMPLATE_FOLDER], root: [], docs: null, labels: ['bug'] },
{ name: 'cosyte/hl7', github: [], root: [], docs: null, labels: BOTH_LABELS },
];
const outcome = await measureAgainst({ repos });
const self = outcome.report.repositories.find((item) => item.name === DEFAULTS_REPO);
assert.equal(self.state, 'default-in-effect');
assert.equal(self.defaults, true);
const enhancement = outcome.report.coverage.find((item) => item.label === 'enhancement');
assert.deepEqual(enhancement.missing, [DEFAULTS_REPO]);
assert.equal(outcome.failing, true);
});
test('a repository with its own ISSUE_TEMPLATE folder is overridden, and owes no label', async () => {
const repos = [
{ name: DEFAULTS_REPO, github: [TEMPLATE_FOLDER], root: [], docs: null, labels: BOTH_LABELS },
{ name: 'cosyte/x12', github: [TEMPLATE_FOLDER], root: [], docs: null, labels: [] },
];
const outcome = await measureAgainst({ repos });
const overridden = outcome.report.repositories.find((item) => item.name === 'cosyte/x12');
assert.equal(overridden.state, 'overridden');
assert.equal(outcome.failing, false);
for (const item of outcome.report.coverage) assert.deepEqual(item.missing, []);
});
test('an overridden repository names the folder that overrides it, so it reads as an override', async () => {
const repos = [
{ name: DEFAULTS_REPO, github: [TEMPLATE_FOLDER], root: [], docs: null, labels: BOTH_LABELS },
{ name: 'cosyte/x12', github: [TEMPLATE_FOLDER], root: [], docs: null, labels: [] },
];
const outcome = await measureAgainst({ repos });
const overridden = outcome.report.repositories.find((item) => item.name === 'cosyte/x12');
assert.equal(overridden.overriddenBy, '.github/ISSUE_TEMPLATE');
assert.match(renderReport(outcome.report), /overridden by `\.github\/ISSUE_TEMPLATE`/);
});
test('the override is per folder and all or nothing: no label is asked of an overridden repository', async () => {
// One file in that folder switches off every default template AND the default config.yml
// together, so there is no per-file arithmetic to get wrong.
const repos = [
{ name: DEFAULTS_REPO, github: [TEMPLATE_FOLDER], root: [], docs: null, labels: BOTH_LABELS },
{ name: 'cosyte/x12', github: [TEMPLATE_FOLDER], root: [], docs: null, labels: [] },
];
const outcome = await measureAgainst({ repos });
for (const item of outcome.report.coverage) {
assert.equal(item.required, 1, 'only the defaults repository is obligated here');
assert.ok(!item.missing.includes('cosyte/x12'));
}
assert.ok(!outcome.requests.some((request) => request.path === '/repos/cosyte/x12/labels'));
});
test('a repository defining another supported type reports that default as not in effect', async () => {
const repos = [
{ name: DEFAULTS_REPO, github: [TEMPLATE_FOLDER], root: [], docs: null, labels: BOTH_LABELS },
{ name: 'cosyte/hl7', github: [], root: [entry('CONTRIBUTING.md'), entry('SECURITY.md')], docs: null, labels: BOTH_LABELS },
];
const outcome = await measureAgainst({ repos });
const item = outcome.report.repositories.find((entryOf) => entryOf.name === 'cosyte/hl7');
assert.deepEqual(item.overrides, [
{ type: 'contributing guide', path: 'CONTRIBUTING.md' },
{ type: 'security policy', path: 'SECURITY.md' },
]);
// It still renders the default ISSUE TEMPLATES, so it still owes the labels.
assert.equal(item.state, 'default-in-effect');
assert.match(renderReport(outcome.report), /so the default of each is not in effect there/);
});
test('the documented order of precedence decides which file is named as the override', () => {
const found = overridesOf({
github: [entry('CONTRIBUTING.md')],
root: [entry('CONTRIBUTING.md')],
docs: [entry('CONTRIBUTING.md')],
});
assert.deepEqual(found, [{ type: 'contributing guide', path: '.github/CONTRIBUTING.md' }]);
});
test('a file at the ISSUE_TEMPLATE path is not the folder the clause names', () => {
// `.github/ISSUE_TEMPLATE.md` is the legacy single-template spelling. The clause that switches off
// every default names the FOLDER, so this repository still renders the defaults and still owes the
// labels; the report says the legacy file is there.
const entries = { github: [entry('ISSUE_TEMPLATE.md')], root: [], docs: null };
assert.equal(issueTemplateOverride(entries), null);
assert.deepEqual(overridesOf(entries), [
{ type: 'issue templates and config.yml', path: '.github/ISSUE_TEMPLATE.md' },
]);
});
test('a type the source stores in one place only is looked for in that one place', () => {
// "Discussion category forms must be in a folder called `.github/DISCUSSION_TEMPLATE`. Issue
// templates and their configuration file must be in a folder called `.github/ISSUE_TEMPLATE`. A
// `FUNDING.yml` file must be in the `.github` folder. All other supported files may be in the root
// of the repository, the `.github` folder, or the `docs` folder." The order of precedence is
// introduced as being for files that CAN be stored in more than one location, and these cannot.
const entries = {
github: [entry('workflows', 'dir')],
root: [entry('ISSUE_TEMPLATE', 'dir'), entry('FUNDING.yml'), entry('DISCUSSION_TEMPLATE', 'dir')],
docs: [entry('ISSUE_TEMPLATE', 'dir')],
};
assert.deepEqual(overridesOf(entries), []);
assert.equal(issueTemplateOverride(entries), null);
});
test('the report never says default-in-effect and not-in-effect about the same repository', async () => {
// An `ISSUE_TEMPLATE` folder at a repository's root is not a folder GitHub renders issue templates
// from, so the default IS in effect there and the label IS owed. Reporting it as an override would
// leave one row contradicting another, in a report whose whole job is to be readable after the
// fact, and the only way to silence that red would be to create a label nothing would ever apply.
const repos = [
{ name: DEFAULTS_REPO, github: [TEMPLATE_FOLDER], root: [], docs: null, labels: BOTH_LABELS },
{ name: 'cosyte/hl7', github: [], root: [entry('ISSUE_TEMPLATE', 'dir')], docs: null, labels: BOTH_LABELS },
];
const outcome = await measureAgainst({ repos });
const item = outcome.report.repositories.find((entryOf) => entryOf.name === 'cosyte/hl7');
assert.equal(item.state, 'default-in-effect');
assert.deepEqual(item.overrides, []);
assert.equal(outcome.failing, false);
for (const row of outcome.report.repositories) {
const claimsOverride = row.overrides.some((over) => over.type === 'issue templates and config.yml');
assert.ok(
!(row.state === 'default-in-effect' && claimsOverride),
`${row.name} is charged the label obligation while the report says its issue-template default is not in effect`,
);
}
});
// ---------------------------------------------------------------------------
// Near misses
// ---------------------------------------------------------------------------
test('a label differing only by letter case is a near miss, not a satisfied obligation', async () => {
const repos = [
{ name: DEFAULTS_REPO, github: [TEMPLATE_FOLDER], root: [], docs: null, labels: BOTH_LABELS },
{ name: 'cosyte/hl7', github: [], root: [], docs: null, labels: ['Bug', 'enhancement'] },
];
const outcome = await measureAgainst({ repos });
assert.equal(outcome.failing, true);
assert.deepEqual(outcome.report.nearMisses, [{ repository: 'cosyte/hl7', declared: 'bug', found: 'Bug' }]);
const bug = outcome.report.coverage.find((item) => item.label === 'bug');
assert.deepEqual(bug.missing, ['cosyte/hl7']);
});
test('a label differing only by surrounding whitespace is a near miss naming both spellings', async () => {
const repos = [
{ name: DEFAULTS_REPO, github: [TEMPLATE_FOLDER], root: [], docs: null, labels: BOTH_LABELS },
{ name: 'cosyte/hl7', github: [], root: [], docs: null, labels: [' bug ', 'enhancement'] },
];
const outcome = await measureAgainst({ repos });
assert.equal(outcome.failing, true);
assert.deepEqual(outcome.report.nearMisses, [{ repository: 'cosyte/hl7', declared: 'bug', found: ' bug ' }]);
const rendered = renderReport(outcome.report);
assert.match(rendered, /" bug "/);
assert.match(rendered, /"bug"/);
});
test('the near-miss fold is case and surrounding whitespace, and nothing wider', () => {
assert.equal(foldLabel(' Bug '), foldLabel('bug'));
assert.notEqual(foldLabel('bugs'), foldLabel('bug'));
assert.notEqual(foldLabel('b ug'), foldLabel('bug'));
});
// ---------------------------------------------------------------------------
// Pagination, exhausted rather than sampled
// ---------------------------------------------------------------------------
test('a label on the second page of results is never reported as absent', async () => {
const filler = Array.from({ length: 100 }, (_, index) => `filler-${index}`);
const repos = [
{ name: DEFAULTS_REPO, github: [TEMPLATE_FOLDER], root: [], docs: null, labels: BOTH_LABELS },
{ name: 'cosyte/hl7', github: [], root: [], docs: null, labels: [...filler, 'bug', 'enhancement'] },
];
const outcome = await measureAgainst({ repos });
assert.equal(outcome.failing, false, 'a label on page two was reported as missing');
const pages = outcome.requests.filter((request) => request.path === '/repos/cosyte/hl7/labels');
assert.ok(pages.length >= 2, `the label list was sampled, not exhausted: ${pages.length} page(s) read`);
});
test('a repository on the second page of the listing is measured like any other', async () => {
const filler = Array.from({ length: 100 }, (_, index) => ({
name: `cosyte/filler-${index}`,
github: [TEMPLATE_FOLDER],
root: [],
docs: null,
labels: [],
}));
const repos = [
{ name: DEFAULTS_REPO, github: [TEMPLATE_FOLDER], root: [], docs: null, labels: BOTH_LABELS },
...filler,
{ name: 'cosyte/zulu', github: [], root: [], docs: null, labels: ['bug'] },
];
const outcome = await measureAgainst({ repos });
assert.equal(outcome.report.population.pages, 2);
assert.equal(outcome.report.population.listed, 102);
assert.ok(outcome.report.repositories.some((item) => item.name === 'cosyte/zulu'));
const enhancement = outcome.report.coverage.find((item) => item.label === 'enhancement');
assert.deepEqual(enhancement.missing, ['cosyte/zulu']);
});
// ---------------------------------------------------------------------------
// An enumeration that cannot be shown to be the whole population
// ---------------------------------------------------------------------------
test('a listing shorter than the account\'s own count fails, naming the shortfall', async () => {
const outcome = await measureAgainst({ repos: COVERED, publicRepos: 7 });
assert.equal(outcome.failing, true);
assert.equal(outcome.report.outcome, 'incomplete-enumeration');
assert.match(outcome.report.summary, /reports 7 owned repositories/);
assert.match(outcome.report.summary, /listing returned 3/);
});
test('an incomplete listing reports NO coverage over the subset it did read', async () => {
const outcome = await measureAgainst({ repos: COVERED, publicRepos: 7 });
assert.deepEqual(outcome.report.coverage, []);
assert.deepEqual(outcome.report.repositories, []);
assert.ok(!/Measured, not assumed/.test(renderReport(outcome.report)));
});
test('private repositories the account reports are part of the count the listing must reach', async () => {
const outcome = await measureAgainst({ repos: COVERED, publicRepos: 3, privateRepos: 2 });
assert.equal(outcome.report.outcome, 'incomplete-enumeration');
assert.match(outcome.report.summary, /reports 5 owned repositories/);
});
test('an account record with no private count states the population it proved, rather than assuming', async () => {
const outcome = await measureAgainst({ repos: COVERED });
assert.equal(outcome.report.population.privateRepos, null);
assert.match(renderReport(outcome.report), /PUBLIC repositories/);
assert.match(renderReport(outcome.report), /outside this measurement/);
});
test('an account record with no repository count at all is an unreadable read, not a free pass', async () => {
const outcome = await measureAgainst({ repos: COVERED, omitPublicCount: true });
assert.equal(outcome.failing, true);
assert.equal(outcome.report.outcome, 'unreadable');
assert.match(outcome.report.summary, /public_repos/);
});
test('an empty listing fails rather than reporting full coverage over nothing', async () => {
const outcome = await measureAgainst({ repos: [], publicRepos: 0 });
assert.equal(outcome.failing, true);
assert.equal(outcome.report.outcome, 'empty-enumeration');
assert.deepEqual(outcome.report.coverage, []);
});
// ---------------------------------------------------------------------------
// Reads that do not answer
// ---------------------------------------------------------------------------
test('a permission denial on a label read fails, naming the repository and the read', async () => {
const repos = [
{ name: DEFAULTS_REPO, github: [TEMPLATE_FOLDER], root: [], docs: null, labels: BOTH_LABELS },
{ name: 'cosyte/hl7', github: [], root: [], docs: null, labels: BOTH_LABELS },
];
const outcome = await measureAgainst({ repos, status: { labels: { 'cosyte/hl7': 403 } } });
assert.equal(outcome.failing, true);
const failure = outcome.report.unreadable.find((item) => item.repository === 'cosyte/hl7');
assert.equal(failure.kind, 'permission');
assert.match(failure.read, /\/repos\/cosyte\/hl7\/labels/);
});
test('a repository whose read failed is classified as neither covered nor overridden', async () => {
const repos = [
{ name: DEFAULTS_REPO, github: [TEMPLATE_FOLDER], root: [], docs: null, labels: BOTH_LABELS },
{ name: 'cosyte/hl7', github: [], root: [], docs: null, labels: BOTH_LABELS },
];
const outcome = await measureAgainst({ repos, status: { labels: { 'cosyte/hl7': 403 } } });
const item = outcome.report.repositories.find((entryOf) => entryOf.name === 'cosyte/hl7');
assert.equal(item.state, 'unreadable');
for (const coverage of outcome.report.coverage) assert.ok(!coverage.missing.includes('cosyte/hl7'));
assert.equal(outcome.report.outcome, 'unreadable');
});
test('a rate-limited read is named as one rather than as a missing label', async () => {
const repos = [
{ name: DEFAULTS_REPO, github: [TEMPLATE_FOLDER], root: [], docs: null, labels: BOTH_LABELS },
{ name: 'cosyte/hl7', github: [], root: [], docs: null, labels: BOTH_LABELS },
];
const outcome = await measureAgainst({
repos,
status: { labels: { 'cosyte/hl7': 403 } },
headers: { labels: { 'cosyte/hl7': { 'x-ratelimit-remaining': '0' } } },
});
const failure = outcome.report.unreadable.find((item) => item.repository === 'cosyte/hl7');
assert.equal(failure.kind, 'rate-limit');
assert.match(failure.reason, /rate limit exhausted/);
});
test('a body that does not parse is a failed read, not an empty answer', async () => {
const repos = [
{ name: DEFAULTS_REPO, github: [TEMPLATE_FOLDER], root: [], docs: null, labels: BOTH_LABELS },
{ name: 'cosyte/hl7', github: [], root: [], docs: null, labels: BOTH_LABELS },
];
const outcome = await measureAgainst({ repos, rawBody: { labels: { 'cosyte/hl7': '{ not json' } } });
const failure = outcome.report.unreadable.find((item) => item.repository === 'cosyte/hl7');
assert.equal(failure.kind, 'unparseable');
assert.equal(outcome.failing, true);
});
test('a folder read answering with a file is refused rather than classified either way', async () => {
const repos = [
{ name: DEFAULTS_REPO, github: [TEMPLATE_FOLDER], root: [], docs: null, labels: BOTH_LABELS },
{ name: 'cosyte/hl7', github: 'a-file', root: [], docs: null, labels: BOTH_LABELS },
];
const outcome = await measureAgainst({ repos });
const failure = outcome.report.unreadable.find((item) => item.repository === 'cosyte/hl7');
assert.match(failure.reason, /file rather than a folder listing/);
assert.equal(outcome.report.repositories.find((item) => item.name === 'cosyte/hl7').state, 'unreadable');
});
test('a transport fault fails the run naming the read that could not be reached', async () => {
const api = await startApi({ repos: COVERED });
const base = api.base;
await api.close(); // Nothing is listening now: the very next read is a transport fault.
const outcome = decide(
await measure({ repository: DEFAULTS_REPO, templateFiles: TEMPLATE_FILES, apiBase: base, token: 'x' }),
);
assert.equal(outcome.failing, true);
assert.equal(outcome.report.outcome, 'unreadable');
assert.equal(outcome.report.unreadable[0].kind, 'transport');
assert.match(outcome.report.unreadable[0].read, /\/repos\/cosyte\/\.github/);
});
test('the listing itself failing to read leaves no coverage reported at all', async () => {
const outcome = await measureAgainst({ repos: COVERED, status: { list: 500 } });
assert.equal(outcome.report.outcome, 'unreadable');
assert.deepEqual(outcome.report.repositories, []);
assert.deepEqual(outcome.report.coverage, []);
});
test('a walk that never converges is refused rather than truncated', async () => {
// A list endpoint answering every page with a full page is the shape a broken paging implementation
// takes from the outside: no walk terminates on a short page against it. Refusing at the backstop
// is the fail-closed direction; treating the pages read so far as the whole account is not.
const outcome = await measureAgainst({ repos: COVERED, neverConverge: true });
assert.equal(outcome.failing, true);
assert.equal(outcome.report.outcome, 'unreadable');
assert.match(outcome.report.unreadable[0].reason, /did not finish paginating/);
assert.deepEqual(outcome.report.coverage, []);
});
test('a repository the obligation applies to whose labels were never read is reported unread', () => {
// Reached by construction rather than by a stub, because it is the state no read produces and the
// one a later edit could produce by accident: an obligated repository with no label list.
const outcome = decide({
repository: DEFAULTS_REPO,
account: { login: ACCOUNT, type: 'Organization' },
population: { listed: 1, reported: 1, pages: 1, endpoint: '/orgs/cosyte/repos?type=all', publicRepos: 1, privateRepos: null, countedFrom: '/orgs/cosyte' },
templates: [],
declaredLabels: [{ label: 'bug', templates: ['.github/ISSUE_TEMPLATE/bug_report.yml'] }],
observations: [{ name: 'cosyte/hl7', entries: { github: [], root: [], docs: null }, labels: null }],
failures: [],
});
assert.equal(outcome.failing, true);
assert.equal(outcome.report.repositories[0].state, 'unreadable');
assert.match(outcome.report.unreadable[0].reason, /were never read/);
});
// ---------------------------------------------------------------------------
// Templates that cannot be read, and templates that ask for nothing
// ---------------------------------------------------------------------------
test('an unparseable default template fails naming the file, before anything is enumerated', async () => {
const outcome = await measureAgainst(
{ repos: COVERED },
{ templateFiles: [{ name: 'bug_report.yml', text: form('labels: bug\n') }] },
);
assert.equal(outcome.failing, true);
assert.equal(outcome.report.outcome, 'unreadable-template');
assert.equal(outcome.report.templateFailure.path, '.github/ISSUE_TEMPLATE/bug_report.yml');
assert.deepEqual(outcome.report.coverage, []);
// Nothing was enumerated: the set of obligations was never established.
assert.equal(outcome.requests.length, 0);
});
test('CRLF templates carry a real obligation end to end, and an account missing it fails', async () => {
// The whole check over an account where NO repository holds `bug` or `enhancement`. Reading the
// key wrongly took the `no-label-obligation` branch here and exited 0, which is the green run over
// an unmeasured obligation this check exists to end.
const repos = [
{ name: DEFAULTS_REPO, github: [TEMPLATE_FOLDER], root: [], docs: null, labels: [] },
{ name: 'cosyte/hl7', github: [], root: [], docs: null, labels: [] },
];
const outcome = await measureAgainst(
{ repos },
{
templateFiles: [
{ name: 'bug_report.yml', text: crlf(form('labels:\n - bug\n')) },
{ name: 'feature_request.yml', text: crlf(form('labels:\n - enhancement\n')) },
],
},
);
assert.notEqual(outcome.report.outcome, 'no-label-obligation');
assert.equal(outcome.report.outcome, 'uncovered');
assert.equal(outcome.failing, true);
assert.deepEqual(
outcome.report.coverage.map((item) => item.label),
BOTH_LABELS,
);
for (const item of outcome.report.coverage) {
assert.deepEqual([...item.missing].sort(), [DEFAULTS_REPO, 'cosyte/hl7'].sort(), `${item.label} missing`);
}
});
test('no declared label reports no obligation to measure, and does not report full coverage', async () => {
const outcome = await measureAgainst(
{ repos: COVERED },
{ templateFiles: [{ name: 'bug_report.yml', text: form('labels: []\n') }] },
);
assert.equal(outcome.failing, false);
assert.equal(outcome.report.outcome, 'no-label-obligation');
assert.deepEqual(outcome.report.coverage, []);
const rendered = renderReport(outcome.report);
assert.match(rendered, /no label obligation to measure/);
assert.match(rendered, /not the same statement as every repository being covered/);
});
test('no declared label still enumerates, so an empty population is still a refusal', async () => {
const outcome = await measureAgainst(
{ repos: [], publicRepos: 0 },
{ templateFiles: [{ name: 'bug_report.yml', text: form('labels: []\n') }] },
);
assert.equal(outcome.failing, true);
assert.equal(outcome.report.outcome, 'empty-enumeration');
});
// ---------------------------------------------------------------------------
// The report is the product: the finding is reconstructable from it alone
// ---------------------------------------------------------------------------
test('the report names every repository enumerated, with its state', async () => {
const repos = [
{ name: DEFAULTS_REPO, github: [TEMPLATE_FOLDER], root: [], docs: null, labels: BOTH_LABELS },
{ name: 'cosyte/hl7', github: [], root: [], docs: null, labels: ['bug'] },
{ name: 'cosyte/x12', github: [TEMPLATE_FOLDER], root: [], docs: null, labels: [] },
{ name: 'cosyte/ccda', github: [], root: [], docs: null, labels: BOTH_LABELS },
];
const outcome = await measureAgainst({ repos, status: { labels: { 'cosyte/ccda': 500 } } });
assert.deepEqual(
outcome.report.repositories.map((item) => [item.name, item.state]),
[
['cosyte/.github', 'default-in-effect'],
['cosyte/ccda', 'unreadable'],
['cosyte/hl7', 'default-in-effect'],
['cosyte/x12', 'overridden'],
],
);
const rendered = renderReport(outcome.report);
for (const repository of repos) assert.ok(rendered.includes(repository.name), `${repository.name} is not in the report`);
});
test('the report carries every declared label and, per label, every repository missing it', async () => {
const repos = [
{ name: DEFAULTS_REPO, github: [TEMPLATE_FOLDER], root: [], docs: null, labels: [] },
{ name: 'cosyte/hl7', github: [], root: [], docs: null, labels: ['bug'] },
];
const outcome = await measureAgainst({ repos });
assert.deepEqual(outcome.report.coverage, [
{
label: 'bug',
declaredBy: ['.github/ISSUE_TEMPLATE/bug_report.yml'],
required: 2,
holding: 1,
missing: [DEFAULTS_REPO],
},
{
label: 'enhancement',
declaredBy: ['.github/ISSUE_TEMPLATE/feature_request.yml'],
required: 2,
holding: 0,
missing: [DEFAULTS_REPO, 'cosyte/hl7'],
},
]);
});
test('the finding survives a round trip through the written report, without re-running the check', async () => {
const repos = [
{ name: DEFAULTS_REPO, github: [TEMPLATE_FOLDER], root: [], docs: null, labels: BOTH_LABELS },
{ name: 'cosyte/hl7', github: [], root: [], docs: null, labels: ['bug'] },
];
const api = await startApi({ repos });
const dir = mkdtempSync(join(tmpdir(), 'org-defaults-report-'));
const reportPath = join(dir, 'report.json');
const summaryPath = join(dir, 'summary.md');
writeFileSync(summaryPath, '');
try {