@jrpool/kilotest 52.0.0 → 57.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.devin/config.local.json +4 -1
- package/.github/workflows/ci.yml +3 -0
- package/README.md +1 -1
- package/api/awaitTest.test.ts +114 -0
- package/api/awaitTest.ts +108 -0
- package/api/listReports.ts +2 -2
- package/api/orderNewTest.test.ts +213 -0
- package/api/orderNewTest.ts +145 -0
- package/api/orderRetest.test.ts +179 -0
- package/api/orderRetest.ts +119 -0
- package/api/requestFeature.test.ts +36 -0
- package/api/requestFeature.ts +26 -4
- package/api/{requestTest.test.ts → requestNewTest.test.ts} +57 -23
- package/api/{requestTest.ts → requestNewTest.ts} +31 -36
- package/api/requestRetest.test.ts +24 -6
- package/api/requestRetest.ts +10 -29
- package/api/routes.test.ts +7 -4
- package/api/routes.ts +45 -7
- package/api/schemas.ts +120 -10
- package/api/util.ts +59 -2
- package/api/version.ts +6 -2
- package/capability.md +9 -6
- package/docs/AI-TOOLS.md +3 -3
- package/docs/SERVICE.md +19 -8
- package/env.example +39 -0
- package/index.test.ts +146 -24
- package/index.ts +129 -36
- package/lhm.plugin.json +1 -1
- package/linkValidation.test.ts +2 -2
- package/llms-full.txt +35 -4
- package/llms.txt +6 -3
- package/mcp.test.ts +109 -12
- package/mcp.ts +146 -26
- package/memory/MEMORY.md +1 -0
- package/memory/plan_issue_111_instant_test.md +198 -0
- package/openapi.yaml +456 -9
- package/package.json +2 -1
- package/routes.test.ts +1 -1
- package/routing.test.ts +2 -2
- package/scripts/generate-openapi.ts +1 -1
- package/server.json +1 -1
- package/sitemap.xml +1 -1
- package/smokeTest.ts +2 -2
- package/util.test.ts +486 -2
- package/util.ts +276 -45
- package/web/listReports/index.html +1 -1
- package/web/listReports/listReports.test.ts +1 -1
- package/web/{requestTest → requestNewTest}/index.ts +10 -1
- package/web/{requestTest/requestTest.test.ts → requestNewTest/requestNewTest.test.ts} +18 -2
- package/web/{requestTestForm → requestNewTestForm}/index.html +1 -1
- package/web/{requestTestForm/requestTestForm.test.ts → requestNewTestForm/requestNewTestForm.test.ts} +6 -6
- package/web/requestRetest/index.ts +9 -0
- package/web/requestRetest/requestRetest.test.ts +14 -0
- package/web/tutorialAI/index.ts +25 -3
- package/web/tutorialAI/tutorialAI.test.ts +32 -0
- package/web/tutorialWeb/index.ts +25 -3
- package/web/tutorialWeb/tutorialWeb.test.ts +32 -0
- /package/web/{requestTest → requestNewTest}/index.html +0 -0
- /package/web/{requestTestForm → requestNewTestForm}/index.ts +0 -0
package/util.test.ts
CHANGED
|
@@ -21,17 +21,20 @@ for (const key of ['MANAGER_EMAIL', 'ALERT_API_HOST', 'ALERT_API_PATH', 'ALERT_A
|
|
|
21
21
|
|
|
22
22
|
import {
|
|
23
23
|
annotateReportObject,
|
|
24
|
+
awaitJob,
|
|
24
25
|
checkCommentDuplicate,
|
|
25
26
|
checkCommentLength,
|
|
26
27
|
checkLength,
|
|
27
28
|
createLock,
|
|
28
29
|
dbPath,
|
|
30
|
+
describeMax,
|
|
29
31
|
getAgoDays,
|
|
30
32
|
getAgoString,
|
|
31
33
|
getCountString,
|
|
32
34
|
getDateString,
|
|
33
35
|
getDateTime,
|
|
34
36
|
getEngineNamesString,
|
|
37
|
+
getEnvMax,
|
|
35
38
|
getIssue,
|
|
36
39
|
errorMessage,
|
|
37
40
|
getJSON,
|
|
@@ -52,6 +55,7 @@ import {
|
|
|
52
55
|
getWeightName,
|
|
53
56
|
hiddenReportsPath,
|
|
54
57
|
htmlSafe,
|
|
58
|
+
isAllowedRedirectTarget,
|
|
55
59
|
isAllowedReport,
|
|
56
60
|
isAllowedTarget,
|
|
57
61
|
isJobID,
|
|
@@ -63,6 +67,7 @@ import {
|
|
|
63
67
|
makeBreakable,
|
|
64
68
|
minifyURL,
|
|
65
69
|
objectSort,
|
|
70
|
+
orderJob,
|
|
66
71
|
processTestRequest,
|
|
67
72
|
testRequestsLock,
|
|
68
73
|
testRequestsPath,
|
|
@@ -268,6 +273,46 @@ test('getDateTime returns null for an invalid time stamp', () => {
|
|
|
268
273
|
assert.equal(getDateTime('invalid'), null);
|
|
269
274
|
});
|
|
270
275
|
|
|
276
|
+
test('getEnvMax returns the default when the variable is unset', () => {
|
|
277
|
+
delete process.env.TEST_ENV_MAX_VAR;
|
|
278
|
+
assert.equal(getEnvMax('TEST_ENV_MAX_VAR', 20), 20);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test('getEnvMax returns the default when the variable is empty', () => {
|
|
282
|
+
process.env.TEST_ENV_MAX_VAR = '';
|
|
283
|
+
assert.equal(getEnvMax('TEST_ENV_MAX_VAR', 20), 20);
|
|
284
|
+
delete process.env.TEST_ENV_MAX_VAR;
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test('getEnvMax returns the configured value when it is a positive integer', () => {
|
|
288
|
+
process.env.TEST_ENV_MAX_VAR = '50';
|
|
289
|
+
assert.equal(getEnvMax('TEST_ENV_MAX_VAR', 20), 50);
|
|
290
|
+
delete process.env.TEST_ENV_MAX_VAR;
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test('getEnvMax returns the default when the variable is not an integer', () => {
|
|
294
|
+
process.env.TEST_ENV_MAX_VAR = 'not-a-number';
|
|
295
|
+
assert.equal(getEnvMax('TEST_ENV_MAX_VAR', 20), 20);
|
|
296
|
+
delete process.env.TEST_ENV_MAX_VAR;
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test('getEnvMax returns 0 when the variable is explicitly 0, meaning no limit', () => {
|
|
300
|
+
process.env.TEST_ENV_MAX_VAR = '0';
|
|
301
|
+
assert.equal(getEnvMax('TEST_ENV_MAX_VAR', 20), 0);
|
|
302
|
+
delete process.env.TEST_ENV_MAX_VAR;
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test('getEnvMax returns the default when the variable is negative', () => {
|
|
306
|
+
process.env.TEST_ENV_MAX_VAR = '-5';
|
|
307
|
+
assert.equal(getEnvMax('TEST_ENV_MAX_VAR', 20), 20);
|
|
308
|
+
delete process.env.TEST_ENV_MAX_VAR;
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test('describeMax returns "no limit" for a cap of 0 and the number otherwise', () => {
|
|
312
|
+
assert.equal(describeMax(0), 'no limit');
|
|
313
|
+
assert.equal(describeMax(20), '20');
|
|
314
|
+
});
|
|
315
|
+
|
|
271
316
|
test('getIssue returns an issue ID for a known engine and rule', () => {
|
|
272
317
|
const issueID = getIssue('axe', 'link-name');
|
|
273
318
|
assert.ok(typeof issueID === 'string' || issueID === null);
|
|
@@ -451,6 +496,66 @@ test('isAllowedTarget returns true for a URL resolving to a public IPv6 address'
|
|
|
451
496
|
assert.equal(await isAllowedTarget('https://example.com/page', fakeLookup('2606:2800:220:1:248:1893:25c8:1946')), true);
|
|
452
497
|
});
|
|
453
498
|
|
|
499
|
+
test('isAllowedTarget returns false for a URL resolving to a TEST-NET-1 address', async () => {
|
|
500
|
+
delete process.env.ALLOW_INTERNAL_TARGETS;
|
|
501
|
+
assert.equal(await isAllowedTarget('https://example.com/page', fakeLookup('192.0.2.1')), false);
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
test('isAllowedTarget returns false for a URL resolving to a TEST-NET-2 address', async () => {
|
|
505
|
+
delete process.env.ALLOW_INTERNAL_TARGETS;
|
|
506
|
+
assert.equal(await isAllowedTarget('https://example.com/page', fakeLookup('198.51.100.1')), false);
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
test('isAllowedTarget returns false for a URL resolving to a TEST-NET-3 address', async () => {
|
|
510
|
+
delete process.env.ALLOW_INTERNAL_TARGETS;
|
|
511
|
+
assert.equal(await isAllowedTarget('https://example.com/page', fakeLookup('203.0.113.1')), false);
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
test('isAllowedTarget returns false for a URL resolving to a benchmarking (198.18/15) address', async () => {
|
|
515
|
+
delete process.env.ALLOW_INTERNAL_TARGETS;
|
|
516
|
+
assert.equal(await isAllowedTarget('https://example.com/page', fakeLookup('198.18.0.1')), false);
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
test('isAllowedTarget returns false for a URL resolving to a reserved 240/4 address', async () => {
|
|
520
|
+
delete process.env.ALLOW_INTERNAL_TARGETS;
|
|
521
|
+
assert.equal(await isAllowedTarget('https://example.com/page', fakeLookup('240.0.0.1')), false);
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
test('isAllowedTarget returns false for a URL resolving to the broadcast address', async () => {
|
|
525
|
+
delete process.env.ALLOW_INTERNAL_TARGETS;
|
|
526
|
+
assert.equal(await isAllowedTarget('https://example.com/page', fakeLookup('255.255.255.255')), false);
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
test('isAllowedTarget returns false for a URL resolving to a multicast address', async () => {
|
|
530
|
+
delete process.env.ALLOW_INTERNAL_TARGETS;
|
|
531
|
+
assert.equal(await isAllowedTarget('https://example.com/page', fakeLookup('224.0.0.1')), false);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
test('isAllowedTarget returns false for a URL resolving to a deprecated IPv6 site-local address', async () => {
|
|
535
|
+
delete process.env.ALLOW_INTERNAL_TARGETS;
|
|
536
|
+
assert.equal(await isAllowedTarget('https://example.com/page', fakeLookup('fec0::1')), false);
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
test('isAllowedTarget returns false for a URL resolving to an IPv6 multicast address', async () => {
|
|
540
|
+
delete process.env.ALLOW_INTERNAL_TARGETS;
|
|
541
|
+
assert.equal(await isAllowedTarget('https://example.com/page', fakeLookup('ff02::1')), false);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
test('isAllowedTarget returns false for a URL resolving to a 6to4 (2002::/16) address', async () => {
|
|
545
|
+
delete process.env.ALLOW_INTERNAL_TARGETS;
|
|
546
|
+
assert.equal(await isAllowedTarget('https://example.com/page', fakeLookup('2002::1')), false);
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
test('isAllowedTarget returns false for a URL resolving to a Teredo (2001::/32) address', async () => {
|
|
550
|
+
delete process.env.ALLOW_INTERNAL_TARGETS;
|
|
551
|
+
assert.equal(await isAllowedTarget('https://example.com/page', fakeLookup('2001::1')), false);
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
test('isAllowedTarget returns false for a URL resolving to an NAT64 (64:ff9b::/96) address', async () => {
|
|
555
|
+
delete process.env.ALLOW_INTERNAL_TARGETS;
|
|
556
|
+
assert.equal(await isAllowedTarget('https://example.com/page', fakeLookup('64:ff9b::10.0.0.5')), false);
|
|
557
|
+
});
|
|
558
|
+
|
|
454
559
|
test('isAllowedTarget returns false for a URL resolving to an address family net.isIP does not recognize', async () => {
|
|
455
560
|
delete process.env.ALLOW_INTERNAL_TARGETS;
|
|
456
561
|
assert.equal(await isAllowedTarget('https://example.com/page', async () => [{address: 'not-an-ip', family: 0}]), false);
|
|
@@ -482,6 +587,118 @@ test('isAllowedTarget returns true for a private-address target when ALLOW_INTER
|
|
|
482
587
|
assert.equal(await isAllowedTarget('https://192.168.1.1/page'), true);
|
|
483
588
|
});
|
|
484
589
|
|
|
590
|
+
// A fake fetch that returns a response whose url reflects any redirect,
|
|
591
|
+
// so isAllowedRedirectTarget tests need no real network access.
|
|
592
|
+
const fakeFetch = (finalURL: string) => async () => ({url: finalURL}) as Response;
|
|
593
|
+
|
|
594
|
+
test('isAllowedRedirectTarget returns true when the final URL resolves to a public address', async () => {
|
|
595
|
+
delete process.env.ALLOW_INTERNAL_TARGETS;
|
|
596
|
+
assert.equal(
|
|
597
|
+
await isAllowedRedirectTarget('https://example.com/page', fakeFetch('https://example.com/page'), fakeLookup('93.184.216.34')),
|
|
598
|
+
true
|
|
599
|
+
);
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
test('isAllowedRedirectTarget returns false when a redirect leads to a private address', async () => {
|
|
603
|
+
delete process.env.ALLOW_INTERNAL_TARGETS;
|
|
604
|
+
assert.equal(
|
|
605
|
+
await isAllowedRedirectTarget('https://example.com/page', fakeFetch('https://10.0.0.5/page'), fakeLookup('93.184.216.34')),
|
|
606
|
+
false
|
|
607
|
+
);
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
test('isAllowedRedirectTarget returns false when the fetch throws', async () => {
|
|
611
|
+
delete process.env.ALLOW_INTERNAL_TARGETS;
|
|
612
|
+
const fetchImpl = async () => { throw new Error('fetch failed'); };
|
|
613
|
+
assert.equal(await isAllowedRedirectTarget('https://example.com/page', fetchImpl), false);
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
test('isAllowedRedirectTarget returns true for any URL when ALLOW_INTERNAL_TARGETS is true, without fetching', async () => {
|
|
617
|
+
process.env.ALLOW_INTERNAL_TARGETS = 'true';
|
|
618
|
+
const fetchImpl = async () => { throw new Error('fetch should not be called'); };
|
|
619
|
+
assert.equal(await isAllowedRedirectTarget('https://192.168.1.1/page', fetchImpl), true);
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
test('isAllowedRedirectTarget returns false when a redirect leads to a TEST-NET-1 address', async () => {
|
|
623
|
+
delete process.env.ALLOW_INTERNAL_TARGETS;
|
|
624
|
+
assert.equal(
|
|
625
|
+
await isAllowedRedirectTarget('https://example.com/page', fakeFetch('https://192.0.2.1/page'), fakeLookup('93.184.216.34')),
|
|
626
|
+
false
|
|
627
|
+
);
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
test('isAllowedRedirectTarget returns false when a redirect leads to a TEST-NET-2 address', async () => {
|
|
631
|
+
delete process.env.ALLOW_INTERNAL_TARGETS;
|
|
632
|
+
assert.equal(
|
|
633
|
+
await isAllowedRedirectTarget('https://example.com/page', fakeFetch('https://198.51.100.1/page'), fakeLookup('93.184.216.34')),
|
|
634
|
+
false
|
|
635
|
+
);
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
test('isAllowedRedirectTarget returns false when a redirect leads to a TEST-NET-3 address', async () => {
|
|
639
|
+
delete process.env.ALLOW_INTERNAL_TARGETS;
|
|
640
|
+
assert.equal(
|
|
641
|
+
await isAllowedRedirectTarget('https://example.com/page', fakeFetch('https://203.0.113.1/page'), fakeLookup('93.184.216.34')),
|
|
642
|
+
false
|
|
643
|
+
);
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
test('isAllowedRedirectTarget returns false when a redirect leads to a benchmarking (198.18/15) address', async () => {
|
|
647
|
+
delete process.env.ALLOW_INTERNAL_TARGETS;
|
|
648
|
+
assert.equal(
|
|
649
|
+
await isAllowedRedirectTarget('https://example.com/page', fakeFetch('https://198.18.0.1/page'), fakeLookup('93.184.216.34')),
|
|
650
|
+
false
|
|
651
|
+
);
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
test('isAllowedRedirectTarget returns false when a redirect leads to a reserved 240/4 address', async () => {
|
|
655
|
+
delete process.env.ALLOW_INTERNAL_TARGETS;
|
|
656
|
+
assert.equal(
|
|
657
|
+
await isAllowedRedirectTarget('https://example.com/page', fakeFetch('https://240.0.0.1/page'), fakeLookup('93.184.216.34')),
|
|
658
|
+
false
|
|
659
|
+
);
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
test('isAllowedRedirectTarget returns false when a redirect leads to the broadcast address', async () => {
|
|
663
|
+
delete process.env.ALLOW_INTERNAL_TARGETS;
|
|
664
|
+
assert.equal(
|
|
665
|
+
await isAllowedRedirectTarget('https://example.com/page', fakeFetch('https://255.255.255.255/page'), fakeLookup('93.184.216.34')),
|
|
666
|
+
false
|
|
667
|
+
);
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
test('isAllowedRedirectTarget returns false when a redirect leads to a multicast address', async () => {
|
|
671
|
+
delete process.env.ALLOW_INTERNAL_TARGETS;
|
|
672
|
+
assert.equal(
|
|
673
|
+
await isAllowedRedirectTarget('https://example.com/page', fakeFetch('https://224.0.0.1/page'), fakeLookup('93.184.216.34')),
|
|
674
|
+
false
|
|
675
|
+
);
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
test('isAllowedRedirectTarget returns false when a redirect leads to a deprecated IPv6 site-local address', async () => {
|
|
679
|
+
delete process.env.ALLOW_INTERNAL_TARGETS;
|
|
680
|
+
assert.equal(
|
|
681
|
+
await isAllowedRedirectTarget('https://example.com/page', fakeFetch('https://[fec0::1]/page'), fakeLookup('93.184.216.34')),
|
|
682
|
+
false
|
|
683
|
+
);
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
test('isAllowedRedirectTarget returns false when a redirect leads to a 6to4 (2002::/16) address', async () => {
|
|
687
|
+
delete process.env.ALLOW_INTERNAL_TARGETS;
|
|
688
|
+
assert.equal(
|
|
689
|
+
await isAllowedRedirectTarget('https://example.com/page', fakeFetch('https://[2002::1]/page'), fakeLookup('93.184.216.34')),
|
|
690
|
+
false
|
|
691
|
+
);
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
test('isAllowedRedirectTarget returns false when a redirect leads to a Teredo (2001::/32) address', async () => {
|
|
695
|
+
delete process.env.ALLOW_INTERNAL_TARGETS;
|
|
696
|
+
assert.equal(
|
|
697
|
+
await isAllowedRedirectTarget('https://example.com/page', fakeFetch('https://[2001::1]/page'), fakeLookup('93.184.216.34')),
|
|
698
|
+
false
|
|
699
|
+
);
|
|
700
|
+
});
|
|
701
|
+
|
|
485
702
|
test('isAllowedReport returns true when every act has a public actualURL', async () => {
|
|
486
703
|
delete process.env.ALLOW_INTERNAL_TARGETS;
|
|
487
704
|
const report = {
|
|
@@ -673,12 +890,69 @@ test('processTestRequest returns a duplicate error for a repeated request', {tim
|
|
|
673
890
|
await fs.writeFile(testRequestsPath(), '{}\n');
|
|
674
891
|
});
|
|
675
892
|
|
|
676
|
-
test('processTestRequest returns a
|
|
893
|
+
test('processTestRequest returns a reportExists error when a report already exists for the page', async () => {
|
|
677
894
|
// The fixture database already has a report for this description and URL.
|
|
678
895
|
const {result} = await processTestRequest(
|
|
679
896
|
'because accessibility', {description: 'Mixed Outcomes Page', url: 'https://example.com/mixed'}
|
|
680
897
|
);
|
|
681
|
-
assert.equal(result, '
|
|
898
|
+
assert.equal(result, 'reportExists');
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
test('processTestRequest returns a queueFull error once the pending-request queue is full', {timeout: 500}, async () => {
|
|
902
|
+
// Fill testRequests.json with 20 pending requests, all for the same URL, so the total
|
|
903
|
+
// pending count (summed across all URLs' request arrays) reaches the cap directly,
|
|
904
|
+
// without needing 20 distinct URLs.
|
|
905
|
+
const filler = Array.from({length: 20}, (_, i) => ({
|
|
906
|
+
timeStamp: getNowStamp(), description: `Filler Page ${i}`, reason: 'because filler'
|
|
907
|
+
}));
|
|
908
|
+
await fs.writeFile(testRequestsPath(), getJSON({'https://example.com/filler': filler}));
|
|
909
|
+
// A further, distinct request is rejected as queueFull.
|
|
910
|
+
const {result} = await processTestRequest(
|
|
911
|
+
'because accessibility', {description: 'One Too Many Page', url: 'https://example.com/one-too-many'}
|
|
912
|
+
);
|
|
913
|
+
assert.equal(result, 'queueFull');
|
|
914
|
+
// Clean up testRequests.json.
|
|
915
|
+
await fs.writeFile(testRequestsPath(), '{}\n');
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
test('processTestRequest honors TEST_REQUEST_QUEUE_MAX when the cap is reached below the default', {timeout: 500}, async () => {
|
|
919
|
+
// With the cap lowered to 2, a single pending request already fills the queue.
|
|
920
|
+
process.env.TEST_REQUEST_QUEUE_MAX = '2';
|
|
921
|
+
try {
|
|
922
|
+
await fs.writeFile(testRequestsPath(), getJSON({
|
|
923
|
+
'https://example.com/filler': [
|
|
924
|
+
{timeStamp: getNowStamp(), description: 'Filler Page A', reason: 'because filler'},
|
|
925
|
+
{timeStamp: getNowStamp(), description: 'Filler Page B', reason: 'because filler'}
|
|
926
|
+
]
|
|
927
|
+
}));
|
|
928
|
+
const {result} = await processTestRequest(
|
|
929
|
+
'because accessibility', {description: 'One Too Many Page', url: 'https://example.com/one-too-many'}
|
|
930
|
+
);
|
|
931
|
+
assert.equal(result, 'queueFull');
|
|
932
|
+
}
|
|
933
|
+
finally {
|
|
934
|
+
delete process.env.TEST_REQUEST_QUEUE_MAX;
|
|
935
|
+
await fs.writeFile(testRequestsPath(), '{}\n');
|
|
936
|
+
}
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
test('processTestRequest never returns queueFull when TEST_REQUEST_QUEUE_MAX is 0 (no limit)', {timeout: 500}, async () => {
|
|
940
|
+
// With the cap set to 0, a queue already past the default size is still not full.
|
|
941
|
+
process.env.TEST_REQUEST_QUEUE_MAX = '0';
|
|
942
|
+
try {
|
|
943
|
+
const filler = Array.from({length: 25}, (_, i) => ({
|
|
944
|
+
timeStamp: getNowStamp(), description: `Filler Page ${i}`, reason: 'because filler'
|
|
945
|
+
}));
|
|
946
|
+
await fs.writeFile(testRequestsPath(), getJSON({'https://example.com/filler': filler}));
|
|
947
|
+
const {result} = await processTestRequest(
|
|
948
|
+
'because accessibility', {description: 'One More Page', url: 'https://example.com/one-more'}
|
|
949
|
+
);
|
|
950
|
+
assert.equal(result, 'ok');
|
|
951
|
+
}
|
|
952
|
+
finally {
|
|
953
|
+
delete process.env.TEST_REQUEST_QUEUE_MAX;
|
|
954
|
+
await fs.writeFile(testRequestsPath(), '{}\n');
|
|
955
|
+
}
|
|
682
956
|
});
|
|
683
957
|
|
|
684
958
|
test('annotateReportObject annotates a report object in place without reading or writing a file', async () => {
|
|
@@ -1580,3 +1854,213 @@ test('getMetrics backfills every category, and generates a since stamp, from a b
|
|
|
1580
1854
|
fsSync.rmSync(tmpDir, {recursive: true});
|
|
1581
1855
|
}
|
|
1582
1856
|
});
|
|
1857
|
+
|
|
1858
|
+
// TESTS FOR orderJob AND awaitJob (GitHub issue #111)
|
|
1859
|
+
|
|
1860
|
+
test('orderJob enqueues a new-page order directly into the job queue', async () => {
|
|
1861
|
+
const {result, description, url, jobID} = await orderJob(
|
|
1862
|
+
{description: 'Ordered Page', url: 'https://example.com/ordered'}, 'because accessibility matters here'
|
|
1863
|
+
) as any;
|
|
1864
|
+
assert.equal(result, 'ok');
|
|
1865
|
+
assert.equal(description, 'Ordered Page');
|
|
1866
|
+
assert.equal(url, 'https://example.com/ordered');
|
|
1867
|
+
assert.match(jobID, /^\d{6}T\d{4}-[a-z0-9]{3}$/);
|
|
1868
|
+
const queuedPath = path.join(jobsPath(), 'queue', `${jobID}.json`);
|
|
1869
|
+
try {
|
|
1870
|
+
const job = JSON.parse(await fs.readFile(queuedPath, 'utf8'));
|
|
1871
|
+
assert.equal(job.target.what, 'Ordered Page');
|
|
1872
|
+
assert.equal(job.target.url, 'https://example.com/ordered');
|
|
1873
|
+
assert.equal(job.sources.reason, 'because accessibility matters here');
|
|
1874
|
+
// No pending-approval inbox entry is created; the job goes straight to the queue.
|
|
1875
|
+
const testRequests = JSON.parse(await fs.readFile(testRequestsPath(), 'utf8'));
|
|
1876
|
+
assert.equal(testRequests['https://example.com/ordered'], undefined);
|
|
1877
|
+
}
|
|
1878
|
+
finally {
|
|
1879
|
+
await fs.unlink(queuedPath);
|
|
1880
|
+
}
|
|
1881
|
+
});
|
|
1882
|
+
|
|
1883
|
+
test('orderJob returns a reportExists error when a report already exists for the ordered page', async () => {
|
|
1884
|
+
const {result} = await orderJob(
|
|
1885
|
+
{description: 'Mixed Outcomes Page', url: 'https://example.com/mixed'}, 'because accessibility matters here'
|
|
1886
|
+
);
|
|
1887
|
+
assert.equal(result, 'reportExists');
|
|
1888
|
+
});
|
|
1889
|
+
|
|
1890
|
+
test('orderJob returns "description" for a page matching a claimed job by description', async () => {
|
|
1891
|
+
const claimedPath = path.join(jobsPath(), 'claimed', 'clm.json');
|
|
1892
|
+
await fs.writeFile(claimedPath, getJSON({
|
|
1893
|
+
target: {what: 'Claimed Order Page', url: 'https://example.com/claimed-order-job'}
|
|
1894
|
+
}));
|
|
1895
|
+
try {
|
|
1896
|
+
const {result} = await orderJob(
|
|
1897
|
+
{description: 'Claimed Order Page', url: 'https://example.com/some-other-order-url'},
|
|
1898
|
+
'because accessibility matters here'
|
|
1899
|
+
);
|
|
1900
|
+
assert.equal(result, 'description');
|
|
1901
|
+
}
|
|
1902
|
+
finally {
|
|
1903
|
+
await fs.unlink(claimedPath);
|
|
1904
|
+
}
|
|
1905
|
+
});
|
|
1906
|
+
|
|
1907
|
+
test('orderJob returns "url" for a page matching a queued job by URL', async () => {
|
|
1908
|
+
const queuedPath = path.join(jobsPath(), 'queue', 'que.json');
|
|
1909
|
+
await fs.writeFile(queuedPath, getJSON({
|
|
1910
|
+
target: {what: 'Some Queued Order Page', url: 'https://example.com/queued-order-url'}
|
|
1911
|
+
}));
|
|
1912
|
+
try {
|
|
1913
|
+
const {result} = await orderJob(
|
|
1914
|
+
{description: 'A Different Order Page', url: 'https://example.com/queued-order-url'},
|
|
1915
|
+
'because accessibility matters here'
|
|
1916
|
+
);
|
|
1917
|
+
assert.equal(result, 'url');
|
|
1918
|
+
}
|
|
1919
|
+
finally {
|
|
1920
|
+
await fs.unlink(queuedPath);
|
|
1921
|
+
}
|
|
1922
|
+
});
|
|
1923
|
+
|
|
1924
|
+
test('orderJob throws when a job file in a category directory is defective', async () => {
|
|
1925
|
+
const claimedPath = path.join(jobsPath(), 'claimed', 'bad.json');
|
|
1926
|
+
await fs.writeFile(claimedPath, 'not valid json');
|
|
1927
|
+
try {
|
|
1928
|
+
await assert.rejects(
|
|
1929
|
+
orderJob({description: 'Any Order Page', url: 'https://example.com/any-order'}, 'because accessibility matters here'),
|
|
1930
|
+
/Failed to order test/
|
|
1931
|
+
);
|
|
1932
|
+
}
|
|
1933
|
+
finally {
|
|
1934
|
+
await fs.unlink(claimedPath);
|
|
1935
|
+
}
|
|
1936
|
+
});
|
|
1937
|
+
|
|
1938
|
+
test('orderJob returns "nonreport" when a retest order cites a report that does not exist', async () => {
|
|
1939
|
+
const {result} = await orderJob({timeStamp: '999999T9999', jobID: 'xxx'}, 'because accessibility matters here');
|
|
1940
|
+
assert.equal(result, 'nonreport');
|
|
1941
|
+
});
|
|
1942
|
+
|
|
1943
|
+
test('orderJob returns "superseded" when a retest order cites a superseded report', async () => {
|
|
1944
|
+
// 260101T0000-mix is an earlier report of "Mixed Outcomes Page" than 260202T0000-new,
|
|
1945
|
+
// so it is superseded.
|
|
1946
|
+
const {result} = await orderJob(
|
|
1947
|
+
{timeStamp: '260101T0000', jobID: 'mix'}, 'because accessibility matters here'
|
|
1948
|
+
);
|
|
1949
|
+
assert.equal(result, 'superseded');
|
|
1950
|
+
});
|
|
1951
|
+
|
|
1952
|
+
test('orderJob enqueues a retest order for the latest report of a page', async () => {
|
|
1953
|
+
const {result, description, url, jobID} = await orderJob(
|
|
1954
|
+
{timeStamp: '260202T0000', jobID: 'new'}, 'because accessibility matters here'
|
|
1955
|
+
) as any;
|
|
1956
|
+
assert.equal(result, 'ok');
|
|
1957
|
+
assert.equal(description, 'Mixed Outcomes Page');
|
|
1958
|
+
assert.equal(url, 'https://example.com/mixed');
|
|
1959
|
+
const queuedPath = path.join(jobsPath(), 'queue', `${jobID}.json`);
|
|
1960
|
+
try {
|
|
1961
|
+
const job = JSON.parse(await fs.readFile(queuedPath, 'utf8'));
|
|
1962
|
+
assert.equal(job.target.what, 'Mixed Outcomes Page');
|
|
1963
|
+
}
|
|
1964
|
+
finally {
|
|
1965
|
+
await fs.unlink(queuedPath);
|
|
1966
|
+
}
|
|
1967
|
+
});
|
|
1968
|
+
|
|
1969
|
+
test('orderJob honors JOB_QUEUE_MAX when the cap is reached', async () => {
|
|
1970
|
+
process.env.JOB_QUEUE_MAX = '1';
|
|
1971
|
+
const fillerPath = path.join(jobsPath(), 'queue', 'fil.json');
|
|
1972
|
+
await fs.writeFile(fillerPath, getJSON({target: {what: 'Filler Queued Page', url: 'https://example.com/filler-queued'}}));
|
|
1973
|
+
try {
|
|
1974
|
+
const {result} = await orderJob(
|
|
1975
|
+
{description: 'One Too Many Order Page', url: 'https://example.com/one-too-many-order'},
|
|
1976
|
+
'because accessibility matters here'
|
|
1977
|
+
);
|
|
1978
|
+
assert.equal(result, 'queueFull');
|
|
1979
|
+
}
|
|
1980
|
+
finally {
|
|
1981
|
+
delete process.env.JOB_QUEUE_MAX;
|
|
1982
|
+
await fs.unlink(fillerPath);
|
|
1983
|
+
}
|
|
1984
|
+
});
|
|
1985
|
+
|
|
1986
|
+
test('orderJob never returns queueFull when JOB_QUEUE_MAX is 0 (no limit)', async () => {
|
|
1987
|
+
process.env.JOB_QUEUE_MAX = '0';
|
|
1988
|
+
const fillerPath = path.join(jobsPath(), 'queue', 'fi2.json');
|
|
1989
|
+
await fs.writeFile(fillerPath, getJSON({target: {what: 'Filler Queued Page Two', url: 'https://example.com/filler-queued-2'}}));
|
|
1990
|
+
try {
|
|
1991
|
+
const {result, jobID} = await orderJob(
|
|
1992
|
+
{description: 'One More Order Page', url: 'https://example.com/one-more-order'},
|
|
1993
|
+
'because accessibility matters here'
|
|
1994
|
+
) as any;
|
|
1995
|
+
assert.equal(result, 'ok');
|
|
1996
|
+
await fs.unlink(path.join(jobsPath(), 'queue', `${jobID}.json`));
|
|
1997
|
+
}
|
|
1998
|
+
finally {
|
|
1999
|
+
delete process.env.JOB_QUEUE_MAX;
|
|
2000
|
+
await fs.unlink(fillerPath);
|
|
2001
|
+
}
|
|
2002
|
+
});
|
|
2003
|
+
|
|
2004
|
+
test('awaitJob returns "completed" immediately when a report already exists', async () => {
|
|
2005
|
+
const outcome = await awaitJob('260101T0000', 'mix');
|
|
2006
|
+
assert.equal(outcome, 'completed');
|
|
2007
|
+
});
|
|
2008
|
+
|
|
2009
|
+
test('awaitJob returns "notFound" for an identifier naming neither a report nor a pending job', async () => {
|
|
2010
|
+
const outcome = await awaitJob('999999T9999', 'xxx');
|
|
2011
|
+
assert.equal(outcome, 'notFound');
|
|
2012
|
+
});
|
|
2013
|
+
|
|
2014
|
+
test('awaitJob returns "failed" when the job has moved to the failed directory', async () => {
|
|
2015
|
+
const failedPath = path.join(jobsPath(), 'failed', '260101T1111-flj.json');
|
|
2016
|
+
await fs.mkdir(path.dirname(failedPath), {recursive: true});
|
|
2017
|
+
await fs.writeFile(failedPath, getJSON({target: {what: 'Failed Await Page', url: 'https://example.com/failed-await'}}));
|
|
2018
|
+
try {
|
|
2019
|
+
const outcome = await awaitJob('260101T1111', 'flj');
|
|
2020
|
+
assert.equal(outcome, 'failed');
|
|
2021
|
+
}
|
|
2022
|
+
finally {
|
|
2023
|
+
await fs.unlink(failedPath);
|
|
2024
|
+
}
|
|
2025
|
+
});
|
|
2026
|
+
|
|
2027
|
+
test('awaitJob polls until a queued job completes, then returns "completed"', async () => {
|
|
2028
|
+
process.env.AWAIT_JOB_POLL_MS = '20';
|
|
2029
|
+
process.env.AWAIT_JOB_TIMEOUT_MS = '2000';
|
|
2030
|
+
const jobName = '260101T2222-plj';
|
|
2031
|
+
const queuedPath = path.join(jobsPath(), 'queue', `${jobName}.json`);
|
|
2032
|
+
const reportPath = path.join(reportsPath(), `${jobName}.json`);
|
|
2033
|
+
await fs.writeFile(queuedPath, getJSON({target: {what: 'Polled Page', url: 'https://example.com/polled'}}));
|
|
2034
|
+
// Simulate the job completing shortly after the wait begins.
|
|
2035
|
+
setTimeout(async () => {
|
|
2036
|
+
await fs.unlink(queuedPath).catch(() => {});
|
|
2037
|
+
await fs.writeFile(reportPath, getJSON({id: jobName, target: {what: 'Polled Page', url: 'https://example.com/polled'}}));
|
|
2038
|
+
}, 50);
|
|
2039
|
+
try {
|
|
2040
|
+
const outcome = await awaitJob('260101T2222', 'plj');
|
|
2041
|
+
assert.equal(outcome, 'completed');
|
|
2042
|
+
}
|
|
2043
|
+
finally {
|
|
2044
|
+
delete process.env.AWAIT_JOB_POLL_MS;
|
|
2045
|
+
delete process.env.AWAIT_JOB_TIMEOUT_MS;
|
|
2046
|
+
await fs.unlink(queuedPath).catch(() => {});
|
|
2047
|
+
await fs.unlink(reportPath).catch(() => {});
|
|
2048
|
+
}
|
|
2049
|
+
});
|
|
2050
|
+
|
|
2051
|
+
test('awaitJob returns "timedOut" when a claimed job neither completes nor fails before the deadline', async () => {
|
|
2052
|
+
process.env.AWAIT_JOB_POLL_MS = '10';
|
|
2053
|
+
process.env.AWAIT_JOB_TIMEOUT_MS = '50';
|
|
2054
|
+
const jobName = '260101T3333-toj';
|
|
2055
|
+
const claimedPath = path.join(jobsPath(), 'claimed', `${jobName}.json`);
|
|
2056
|
+
await fs.writeFile(claimedPath, getJSON({target: {what: 'Stuck Page', url: 'https://example.com/stuck'}}));
|
|
2057
|
+
try {
|
|
2058
|
+
const outcome = await awaitJob('260101T3333', 'toj');
|
|
2059
|
+
assert.equal(outcome, 'timedOut');
|
|
2060
|
+
}
|
|
2061
|
+
finally {
|
|
2062
|
+
delete process.env.AWAIT_JOB_POLL_MS;
|
|
2063
|
+
delete process.env.AWAIT_JOB_TIMEOUT_MS;
|
|
2064
|
+
await fs.unlink(claimedPath);
|
|
2065
|
+
}
|
|
2066
|
+
});
|