@jrpool/kilotest 47.0.1 → 49.0.0
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/.claude/scheduled_tasks.lock +1 -0
- package/api/requestFeature.test.ts +4 -2
- package/api/requestFeature.ts +1 -1
- package/api/requestRetest.test.ts +66 -7
- package/api/requestRetest.ts +55 -39
- package/api/requestTest.test.ts +63 -7
- package/api/requestTest.ts +36 -26
- package/api/util.test.ts +6 -5
- package/api/util.ts +2 -24
- package/docs/BACKLOG.md +228 -16
- package/docs/SERVICE.md +84 -7
- package/index.test.ts +354 -25
- package/index.ts +174 -8
- package/linkValidation.test.ts +1 -0
- package/mcp.test.ts +30 -0
- package/mcp.ts +9 -0
- package/package.json +1 -1
- package/smokeTest.ts +16 -0
- package/test/dbFixture.ts +8 -0
- package/util.test.ts +426 -127
- package/util.ts +389 -196
- package/web/ai0BalanceForm/ai0BalanceForm.test.ts +16 -10
- package/web/ai0BalanceForm/index.html +1 -1
- package/web/ai0BalanceForm/index.ts +10 -6
- package/web/enqueue/index.ts +5 -2
- package/web/expungeReportsForm/expungeReportsForm.test.ts +19 -11
- package/web/expungeReportsForm/index.html +1 -1
- package/web/expungeReportsForm/index.ts +1 -1
- package/web/hideReportForm/hideReportForm.test.ts +24 -9
- package/web/hideReportForm/index.html +1 -1
- package/web/hideReportForm/index.ts +9 -6
- package/web/listReports/index.ts +33 -12
- package/web/listReports/listReports.test.ts +1 -1
- package/web/listTopIssues/index.ts +1 -1
- package/web/manage/index.html +2 -1
- package/web/metrics/index.html +19 -0
- package/web/metrics/index.ts +126 -0
- package/web/metrics/metrics.test.ts +211 -0
- package/web/pruneReportsForm/index.html +1 -1
- package/web/pruneReportsForm/index.ts +1 -1
- package/web/pruneReportsForm/pruneReportsForm.test.ts +18 -10
- package/web/reannotate/index.ts +7 -3
- package/web/reannotate/reannotate.test.ts +1 -1
- package/web/renewWCAG/index.ts +4 -1
- package/web/renewWCAG/renewWCAG.test.ts +1 -1
- package/web/reportDeletion.ts +12 -7
- package/web/requestRetest/index.html +3 -3
- package/web/requestRetest/index.ts +47 -16
- package/web/requestRetest/requestRetest.test.ts +38 -24
- package/web/requestTest/index.html +4 -4
- package/web/requestTest/index.ts +37 -9
- package/web/requestTest/requestTest.test.ts +21 -6
- package/web/rewindReportsForm/index.html +1 -1
- package/web/rewindReportsForm/index.ts +1 -1
- package/web/rewindReportsForm/rewindReportsForm.test.ts +18 -10
- package/web/showHiddenReportsForm/index.html +24 -0
- package/web/showHiddenReportsForm/index.ts +45 -0
- package/web/showHiddenReportsForm/showHiddenReportsForm.test.ts +103 -0
- package/web/tutorialAI/index.ts +1 -1
- package/web/tutorialWeb/index.ts +1 -1
- package/web/unhideReportForm/index.html +1 -1
- package/web/unhideReportForm/index.ts +11 -8
- package/web/unhideReportForm/unhideReportForm.test.ts +19 -11
package/test/dbFixture.ts
CHANGED
|
@@ -17,5 +17,13 @@ export const fixtureDBDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kilotest-db-'
|
|
|
17
17
|
// Copy the tracked fixture database to the temporary directory.
|
|
18
18
|
fs.cpSync(path.join(import.meta.dirname, 'fixtures', 'db'), fixtureDBDir, {recursive: true});
|
|
19
19
|
|
|
20
|
+
// Git does not track empty directories, so the job-state subdirectories are absent
|
|
21
|
+
// from a fresh checkout even though they are part of the fixture layout. Create them
|
|
22
|
+
// here rather than tracking placeholder files, which readdir-based job listing would
|
|
23
|
+
// otherwise pick up as (invalid) job files.
|
|
24
|
+
for (const category of ['queue', 'claimed', 'failed']) {
|
|
25
|
+
fs.mkdirSync(path.join(fixtureDBDir, 'jobs', category), {recursive: true});
|
|
26
|
+
}
|
|
27
|
+
|
|
20
28
|
// Remove the copy when the process exits.
|
|
21
29
|
process.on('exit', () => fs.rmSync(fixtureDBDir, {recursive: true, force: true}));
|
package/util.test.ts
CHANGED
|
@@ -52,8 +52,7 @@ import {
|
|
|
52
52
|
processTestRequest,
|
|
53
53
|
testRequestsLock,
|
|
54
54
|
testRequestsPath,
|
|
55
|
-
reportsPath
|
|
56
|
-
addTestRequest
|
|
55
|
+
reportsPath
|
|
57
56
|
} from './util.ts';
|
|
58
57
|
|
|
59
58
|
// TESTS
|
|
@@ -366,9 +365,8 @@ test('objectSort sorts objects numerically descending', () => {
|
|
|
366
365
|
|
|
367
366
|
// TESTS FOR processTestRequest AND isReportAvailable
|
|
368
367
|
|
|
369
|
-
// These tests use the fixture database directory
|
|
368
|
+
// These tests use the fixture database directory.
|
|
370
369
|
import {fixtureDBDir as fixtureDbDir} from './test/dbFixture.ts';
|
|
371
|
-
const requestTestDir = path.join(import.meta.dirname, 'web', 'requestTest');
|
|
372
370
|
const savedDbDir = process.env.DB_DIR;
|
|
373
371
|
|
|
374
372
|
before(() => {
|
|
@@ -428,44 +426,15 @@ test('getPageDataStrings uses provided pageData instead of reading the report',
|
|
|
428
426
|
assert.ok(strings.testInfo.includes('1 day ago'));
|
|
429
427
|
});
|
|
430
428
|
|
|
431
|
-
test('processTestRequest
|
|
432
|
-
const result: any = await processTestRequest('invalid', requestTestDir, 'Page', 'https://example.com', 'because');
|
|
433
|
-
assert.equal(result.status, 'error');
|
|
434
|
-
assert.equal(result.message, 'Invalid request');
|
|
435
|
-
});
|
|
436
|
-
|
|
437
|
-
test('processTestRequest returns an error for an invalid URL', async () => {
|
|
438
|
-
const result: any = await processTestRequest('test', requestTestDir, 'Page', 'not-a-url', 'because');
|
|
439
|
-
assert.equal(result.status, 'error');
|
|
440
|
-
assert.equal(result.message, 'Invalid request');
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
test('processTestRequest returns an error for a short reason', async () => {
|
|
444
|
-
const result: any = await processTestRequest('test', requestTestDir, 'Page', 'https://example.com', 'abc');
|
|
445
|
-
assert.equal(result.status, 'error');
|
|
446
|
-
assert.equal(result.message, 'Invalid request');
|
|
447
|
-
});
|
|
448
|
-
|
|
449
|
-
test('processTestRequest returns an error for a missing description', async () => {
|
|
450
|
-
const result: any = await processTestRequest('test', requestTestDir, '', 'https://example.com', 'because');
|
|
451
|
-
assert.equal(result.status, 'error');
|
|
452
|
-
assert.equal(result.message, 'Invalid request');
|
|
453
|
-
});
|
|
454
|
-
|
|
455
|
-
test('processTestRequest returns an error for a mismatched directory name', async () => {
|
|
456
|
-
const result: any = await processTestRequest('test', '/tmp/wrongDir', 'Page', 'https://example.com', 'because');
|
|
457
|
-
assert.equal(result.status, 'error');
|
|
458
|
-
assert.equal(result.message, 'Invalid request');
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
test('processTestRequest succeeds and populates the template for a valid request', {timeout: 500}, async () => {
|
|
429
|
+
test('processTestRequest succeeds for a valid new-test request', {timeout: 500}, async () => {
|
|
463
430
|
// Reset testRequests.json to empty before the test.
|
|
464
431
|
await fs.writeFile(testRequestsPath(), '{}\n');
|
|
465
|
-
const result
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
assert.
|
|
432
|
+
const {result} = await processTestRequest(
|
|
433
|
+
'because accessibility', {description: 'Example Page', url: 'https://example.com'}
|
|
434
|
+
);
|
|
435
|
+
assert.equal(result, 'ok');
|
|
436
|
+
const testRequests = JSON.parse(await fs.readFile(testRequestsPath(), 'utf8'));
|
|
437
|
+
assert.equal(testRequests['https://example.com'][0].description, 'Example Page');
|
|
469
438
|
// Clean up testRequests.json.
|
|
470
439
|
await fs.writeFile(testRequestsPath(), '{}\n');
|
|
471
440
|
});
|
|
@@ -473,15 +442,23 @@ test('processTestRequest succeeds and populates the template for a valid request
|
|
|
473
442
|
test('processTestRequest returns a duplicate error for a repeated request', {timeout: 500}, async () => {
|
|
474
443
|
// Reset testRequests.json to empty, then make a successful request.
|
|
475
444
|
await fs.writeFile(testRequestsPath(), '{}\n');
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
assert.equal(result
|
|
445
|
+
const target = {description: 'Example Page', url: 'https://example.com'};
|
|
446
|
+
await processTestRequest('because accessibility', target);
|
|
447
|
+
// Repeat the same request (same description, URL, and reason).
|
|
448
|
+
const {result} = await processTestRequest('because accessibility', target);
|
|
449
|
+
assert.equal(result, 'duplicate');
|
|
481
450
|
// Clean up testRequests.json.
|
|
482
451
|
await fs.writeFile(testRequestsPath(), '{}\n');
|
|
483
452
|
});
|
|
484
453
|
|
|
454
|
+
test('processTestRequest returns a retest error when a report already exists for the page', async () => {
|
|
455
|
+
// The fixture database already has a report for this description and URL.
|
|
456
|
+
const {result} = await processTestRequest(
|
|
457
|
+
'because accessibility', {description: 'Mixed Outcomes Page', url: 'https://example.com/mixed'}
|
|
458
|
+
);
|
|
459
|
+
assert.equal(result, 'retest');
|
|
460
|
+
});
|
|
461
|
+
|
|
485
462
|
test('annotateReportObject annotates a report object in place without reading or writing a file', async () => {
|
|
486
463
|
// A report object that was never written to disk (e.g. one just received from a worker,
|
|
487
464
|
// as in index.ts's worker/report handler, which annotates before the first write).
|
|
@@ -558,6 +535,92 @@ test('isReportAvailable returns false for an unknown page and URL', async () =>
|
|
|
558
535
|
assert.equal(result, false);
|
|
559
536
|
});
|
|
560
537
|
|
|
538
|
+
// TESTS FOR processTestRequest's claimed/queued-job detection
|
|
539
|
+
|
|
540
|
+
test('processTestRequest returns "description" for a page matching a claimed job by description', async () => {
|
|
541
|
+
const claimedPath = path.join(jobsPath(), 'claimed', 'clm.json');
|
|
542
|
+
await fs.writeFile(claimedPath, getJSON({
|
|
543
|
+
target: {what: 'Claimed Page', url: 'https://example.com/claimed-job'}
|
|
544
|
+
}));
|
|
545
|
+
try {
|
|
546
|
+
const {result} = await processTestRequest(
|
|
547
|
+
'because', {description: 'Claimed Page', url: 'https://example.com/some-other-url'}
|
|
548
|
+
);
|
|
549
|
+
assert.equal(result, 'description');
|
|
550
|
+
}
|
|
551
|
+
finally {
|
|
552
|
+
await fs.unlink(claimedPath);
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
test('processTestRequest returns "url" for a page matching a claimed job by URL', async () => {
|
|
557
|
+
const claimedPath = path.join(jobsPath(), 'claimed', 'clm.json');
|
|
558
|
+
await fs.writeFile(claimedPath, getJSON({
|
|
559
|
+
target: {what: 'Some Claimed Page', url: 'https://example.com/claimed-url'}
|
|
560
|
+
}));
|
|
561
|
+
try {
|
|
562
|
+
const {result} = await processTestRequest(
|
|
563
|
+
'because', {description: 'A Different Page', url: 'https://example.com/claimed-url'}
|
|
564
|
+
);
|
|
565
|
+
assert.equal(result, 'url');
|
|
566
|
+
}
|
|
567
|
+
finally {
|
|
568
|
+
await fs.unlink(claimedPath);
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
test('processTestRequest returns "description" for a page matching a queued job by description', async () => {
|
|
573
|
+
const queuedPath = path.join(jobsPath(), 'queue', 'que.json');
|
|
574
|
+
await fs.writeFile(queuedPath, getJSON({
|
|
575
|
+
target: {what: 'Queued Page', url: 'https://example.com/queued-job'}
|
|
576
|
+
}));
|
|
577
|
+
try {
|
|
578
|
+
const {result} = await processTestRequest(
|
|
579
|
+
'because', {description: 'Queued Page', url: 'https://example.com/some-other-url'}
|
|
580
|
+
);
|
|
581
|
+
assert.equal(result, 'description');
|
|
582
|
+
}
|
|
583
|
+
finally {
|
|
584
|
+
await fs.unlink(queuedPath);
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
test('processTestRequest returns "url" for a page matching a queued job by URL', async () => {
|
|
589
|
+
const queuedPath = path.join(jobsPath(), 'queue', 'que.json');
|
|
590
|
+
await fs.writeFile(queuedPath, getJSON({
|
|
591
|
+
target: {what: 'Some Queued Page', url: 'https://example.com/queued-url'}
|
|
592
|
+
}));
|
|
593
|
+
try {
|
|
594
|
+
const {result} = await processTestRequest(
|
|
595
|
+
'because', {description: 'A Different Page', url: 'https://example.com/queued-url'}
|
|
596
|
+
);
|
|
597
|
+
assert.equal(result, 'url');
|
|
598
|
+
}
|
|
599
|
+
finally {
|
|
600
|
+
await fs.unlink(queuedPath);
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
test('processTestRequest returns "nonreport" when the cited report does not exist', async () => {
|
|
605
|
+
// Cite a timeStamp/jobID that does not correspond to any available report.
|
|
606
|
+
const {result} = await processTestRequest('because', {timeStamp: '999999T9999', jobID: 'xxx'});
|
|
607
|
+
assert.equal(result, 'nonreport');
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
test('processTestRequest throws when a job file in a category directory is defective', async () => {
|
|
611
|
+
const claimedPath = path.join(jobsPath(), 'claimed', 'bad.json');
|
|
612
|
+
await fs.writeFile(claimedPath, 'not valid json');
|
|
613
|
+
try {
|
|
614
|
+
await assert.rejects(
|
|
615
|
+
processTestRequest('because', {description: 'Any Page', url: 'https://example.com/any'}),
|
|
616
|
+
/Failed to process test request/
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
finally {
|
|
620
|
+
await fs.unlink(claimedPath);
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
|
|
561
624
|
// TESTS FOR REMAINING BRANCH COVERAGE
|
|
562
625
|
|
|
563
626
|
test('getJobNames creates missing job directories and returns empty arrays', async () => {
|
|
@@ -605,6 +668,93 @@ test('getJobNames throws when a job directory is a file, not a directory', async
|
|
|
605
668
|
}
|
|
606
669
|
});
|
|
607
670
|
|
|
671
|
+
test('getJobsData returns an empty array, instead of throwing ENOENT, when its category directory does not yet exist', async () => {
|
|
672
|
+
// Regression test: on a fresh deployment, jobs/claimed and jobs/queue may never
|
|
673
|
+
// have been created yet (no job has ever been claimed or queued there). getJobsData
|
|
674
|
+
// must tolerate that the same way getJobNames does, rather than reading the
|
|
675
|
+
// category directory directly and throwing ENOENT.
|
|
676
|
+
const os = await import('node:os');
|
|
677
|
+
const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'kilotest-jobsdata-'));
|
|
678
|
+
const savedDbDir = process.env.DB_DIR;
|
|
679
|
+
process.env.DB_DIR = path.join(tmpRoot, 'db');
|
|
680
|
+
try {
|
|
681
|
+
const {getJobsData} = await import('./util.ts');
|
|
682
|
+
const result = await getJobsData('claimed');
|
|
683
|
+
assert.deepEqual(result, []);
|
|
684
|
+
}
|
|
685
|
+
finally {
|
|
686
|
+
process.env.DB_DIR = savedDbDir;
|
|
687
|
+
await fs.rm(tmpRoot, {recursive: true}).catch(() => {});
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
test('getJobsData skips a job file that is removed between the directory listing and its read', async (t) => {
|
|
692
|
+
// Regression test: on the deployed server, a worker can complete or reclaim a
|
|
693
|
+
// claimed job (moving or deleting its file) between getJobsData listing the
|
|
694
|
+
// claimed directory and reading a specific file it found there. That race must
|
|
695
|
+
// result in the vanished job simply being skipped, not an uncaught ENOENT.
|
|
696
|
+
const claimedPath = path.join(jobsPath(), 'claimed', 'raced.json');
|
|
697
|
+
await fs.writeFile(claimedPath, getJSON({
|
|
698
|
+
target: {what: 'Raced Page', url: 'https://example.com/raced'}
|
|
699
|
+
}));
|
|
700
|
+
const originalReadFile = fs.readFile;
|
|
701
|
+
t.mock.method(fs, 'readFile', async (filePath: any, ...rest: any[]) => {
|
|
702
|
+
// Simulate the file having been removed by a worker just before this read.
|
|
703
|
+
if (String(filePath) === claimedPath) {
|
|
704
|
+
await fs.unlink(claimedPath);
|
|
705
|
+
const error: any = new Error('ENOENT: no such file or directory');
|
|
706
|
+
error.code = 'ENOENT';
|
|
707
|
+
throw error;
|
|
708
|
+
}
|
|
709
|
+
return (originalReadFile as any)(filePath, ...rest);
|
|
710
|
+
});
|
|
711
|
+
try {
|
|
712
|
+
const {getJobsData} = await import('./util.ts');
|
|
713
|
+
const result = await getJobsData('claimed');
|
|
714
|
+
assert.ok(!result.some(job => job.description === 'Raced Page'));
|
|
715
|
+
}
|
|
716
|
+
finally {
|
|
717
|
+
t.mock.reset();
|
|
718
|
+
await fs.unlink(claimedPath).catch(() => {});
|
|
719
|
+
}
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
test('getJobsData throws when a job file cannot be read for a reason other than its removal', async () => {
|
|
723
|
+
// A directory where a job file is expected causes fs.readFile to fail with
|
|
724
|
+
// EISDIR, not ENOENT, so getJobsData must still treat it as defective (unlike
|
|
725
|
+
// the ENOENT case above, which is skipped as a normal race with a worker).
|
|
726
|
+
const claimedDirAsFile = path.join(jobsPath(), 'claimed', 'notAFile.json');
|
|
727
|
+
await fs.mkdir(claimedDirAsFile);
|
|
728
|
+
try {
|
|
729
|
+
const {getJobsData} = await import('./util.ts');
|
|
730
|
+
await assert.rejects(getJobsData('claimed'), /defective/);
|
|
731
|
+
}
|
|
732
|
+
finally {
|
|
733
|
+
await fs.rm(claimedDirAsFile, {recursive: true}).catch(() => {});
|
|
734
|
+
}
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
test('processTestRequest succeeds on a fresh deployment with no job directories yet', async () => {
|
|
738
|
+
// Regression test for the same ENOENT scenario, exercised through the actual
|
|
739
|
+
// public interface: a brand-new DB_DIR with no jobs/claimed or jobs/queue
|
|
740
|
+
// directories must not prevent a new-test request from being approved.
|
|
741
|
+
const os = await import('node:os');
|
|
742
|
+
const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'kilotest-freshdeploy-'));
|
|
743
|
+
const savedDbDir = process.env.DB_DIR;
|
|
744
|
+
process.env.DB_DIR = path.join(tmpRoot, 'db');
|
|
745
|
+
try {
|
|
746
|
+
const {processTestRequest: freshProcessTestRequest} = await import('./util.ts');
|
|
747
|
+
const {result} = await freshProcessTestRequest(
|
|
748
|
+
'because accessibility', {description: 'Fresh Deploy Page', url: 'https://example.com/fresh'}
|
|
749
|
+
);
|
|
750
|
+
assert.equal(result, 'ok');
|
|
751
|
+
}
|
|
752
|
+
finally {
|
|
753
|
+
process.env.DB_DIR = savedDbDir;
|
|
754
|
+
await fs.rm(tmpRoot, {recursive: true}).catch(() => {});
|
|
755
|
+
}
|
|
756
|
+
});
|
|
757
|
+
|
|
608
758
|
test('getObject throws for a file that is not valid JSON', async () => {
|
|
609
759
|
const tmpFile = path.join((await import('node:os')).tmpdir(), 'kilotest-test-invalid.json');
|
|
610
760
|
(await import('node:fs')).writeFileSync(tmpFile, 'not json');
|
|
@@ -683,69 +833,6 @@ test('getPOSTData resolves with parsed query for form-urlencoded requests', asyn
|
|
|
683
833
|
assert.equal(result.why, 'Because');
|
|
684
834
|
});
|
|
685
835
|
|
|
686
|
-
test('getRequestability returns "claimed" for a URL in a claimed job', async () => {
|
|
687
|
-
const tmpDir = (await import('node:os')).tmpdir() + '/kilotest-requestability-test';
|
|
688
|
-
const fsSync = await import('node:fs');
|
|
689
|
-
fsSync.mkdirSync(tmpDir + '/jobs/claimed', {recursive: true});
|
|
690
|
-
fsSync.mkdirSync(tmpDir + '/jobs/queue', {recursive: true});
|
|
691
|
-
fsSync.mkdirSync(tmpDir + '/jobs/failed', {recursive: true});
|
|
692
|
-
fsSync.writeFileSync(tmpDir + '/jobs/claimed/job1.json', JSON.stringify({
|
|
693
|
-
target: {url: 'https://example.com/test', what: 'Test Page'}
|
|
694
|
-
}));
|
|
695
|
-
const savedDbDir = process.env.DB_DIR;
|
|
696
|
-
process.env.DB_DIR = tmpDir;
|
|
697
|
-
try {
|
|
698
|
-
const {getRequestability} = await import('./util.ts');
|
|
699
|
-
const result = await getRequestability('https://example.com/test');
|
|
700
|
-
assert.equal(result, 'claimed');
|
|
701
|
-
}
|
|
702
|
-
finally {
|
|
703
|
-
process.env.DB_DIR = savedDbDir;
|
|
704
|
-
fsSync.rmSync(tmpDir, {recursive: true});
|
|
705
|
-
}
|
|
706
|
-
});
|
|
707
|
-
|
|
708
|
-
test('getRequestability returns "queued" for a URL in a queued job', async () => {
|
|
709
|
-
const tmpDir = (await import('node:os')).tmpdir() + '/kilotest-requestability-test';
|
|
710
|
-
const fsSync = await import('node:fs');
|
|
711
|
-
fsSync.mkdirSync(tmpDir + '/jobs/claimed', {recursive: true});
|
|
712
|
-
fsSync.mkdirSync(tmpDir + '/jobs/queue', {recursive: true});
|
|
713
|
-
fsSync.mkdirSync(tmpDir + '/jobs/failed', {recursive: true});
|
|
714
|
-
fsSync.writeFileSync(tmpDir + '/jobs/queue/job1.json', JSON.stringify({
|
|
715
|
-
target: {url: 'https://example.com/test', what: 'Test Page'}
|
|
716
|
-
}));
|
|
717
|
-
const savedDbDir = process.env.DB_DIR;
|
|
718
|
-
process.env.DB_DIR = tmpDir;
|
|
719
|
-
try {
|
|
720
|
-
const {getRequestability} = await import('./util.ts');
|
|
721
|
-
const result = await getRequestability('https://example.com/test');
|
|
722
|
-
assert.equal(result, 'queued');
|
|
723
|
-
}
|
|
724
|
-
finally {
|
|
725
|
-
process.env.DB_DIR = savedDbDir;
|
|
726
|
-
fsSync.rmSync(tmpDir, {recursive: true});
|
|
727
|
-
}
|
|
728
|
-
});
|
|
729
|
-
|
|
730
|
-
test('getRequestability returns empty string for a URL with no matching jobs', async () => {
|
|
731
|
-
const tmpDir = (await import('node:os')).tmpdir() + '/kilotest-requestability-test';
|
|
732
|
-
const fsSync = await import('node:fs');
|
|
733
|
-
fsSync.mkdirSync(tmpDir + '/jobs/claimed', {recursive: true});
|
|
734
|
-
fsSync.mkdirSync(tmpDir + '/jobs/queue', {recursive: true});
|
|
735
|
-
fsSync.mkdirSync(tmpDir + '/jobs/failed', {recursive: true});
|
|
736
|
-
const savedDbDir = process.env.DB_DIR;
|
|
737
|
-
process.env.DB_DIR = tmpDir;
|
|
738
|
-
try {
|
|
739
|
-
const {getRequestability} = await import('./util.ts');
|
|
740
|
-
const result = await getRequestability('https://example.com/no-match');
|
|
741
|
-
assert.equal(result, '');
|
|
742
|
-
}
|
|
743
|
-
finally {
|
|
744
|
-
process.env.DB_DIR = savedDbDir;
|
|
745
|
-
fsSync.rmSync(tmpDir, {recursive: true});
|
|
746
|
-
}
|
|
747
|
-
});
|
|
748
|
-
|
|
749
836
|
test('isURL returns false for a malformed URL', () => {
|
|
750
837
|
assert.equal(isURL('https://[invalid'), false);
|
|
751
838
|
});
|
|
@@ -988,25 +1075,6 @@ test('testRequestsLock is a function (the lock returned by createLock)', () => {
|
|
|
988
1075
|
assert.equal(typeof testRequestsLock, 'function');
|
|
989
1076
|
});
|
|
990
1077
|
|
|
991
|
-
test('addTestRequest adds a request and returns success', async () => {
|
|
992
|
-
await fs.writeFile(testRequestsPath(), '{}\n');
|
|
993
|
-
const result = await addTestRequest('Test Page', 'https://example.com/test', 'because');
|
|
994
|
-
assert.equal(result.error, undefined);
|
|
995
|
-
const testRequests = JSON.parse(await fs.readFile(testRequestsPath(), 'utf8'));
|
|
996
|
-
assert.ok(testRequests['https://example.com/test']);
|
|
997
|
-
assert.equal(testRequests['https://example.com/test'].length, 1);
|
|
998
|
-
assert.equal(testRequests['https://example.com/test'][0].description, 'Test Page');
|
|
999
|
-
await fs.writeFile(testRequestsPath(), '{}\n');
|
|
1000
|
-
});
|
|
1001
|
-
|
|
1002
|
-
test('addTestRequest returns a duplicate error for a repeated request', async () => {
|
|
1003
|
-
await fs.writeFile(testRequestsPath(), '{}\n');
|
|
1004
|
-
await addTestRequest('Test Page', 'https://example.com/test', 'because');
|
|
1005
|
-
const result = await addTestRequest('Test Page', 'https://example.com/test', 'another reason');
|
|
1006
|
-
assert.equal(result.error, 'duplicate');
|
|
1007
|
-
await fs.writeFile(testRequestsPath(), '{}\n');
|
|
1008
|
-
});
|
|
1009
|
-
|
|
1010
1078
|
test('getReportPath returns the path of a report file', () => {
|
|
1011
1079
|
const result = getReportPath('260101T0000', 'mix');
|
|
1012
1080
|
assert.ok(result.endsWith('260101T0000-mix.json'));
|
|
@@ -1059,3 +1127,234 @@ test('getMultiReportWhats returns descriptions that have multiple reports', asyn
|
|
|
1059
1127
|
const whats = await getMultiReportWhats();
|
|
1060
1128
|
assert.ok(whats.includes('Mixed Outcomes Page'));
|
|
1061
1129
|
});
|
|
1130
|
+
|
|
1131
|
+
test('getExclusionCookieValue is deterministic for a given AUTH_CODE', async () => {
|
|
1132
|
+
const savedAuthCode = process.env.AUTH_CODE;
|
|
1133
|
+
process.env.AUTH_CODE = 'test-auth-code';
|
|
1134
|
+
try {
|
|
1135
|
+
const {getExclusionCookieValue} = await import('./util.ts');
|
|
1136
|
+
const first = getExclusionCookieValue();
|
|
1137
|
+
const second = getExclusionCookieValue();
|
|
1138
|
+
assert.equal(first, second);
|
|
1139
|
+
assert.equal(typeof first, 'string');
|
|
1140
|
+
assert.ok(first.length > 0);
|
|
1141
|
+
}
|
|
1142
|
+
finally {
|
|
1143
|
+
process.env.AUTH_CODE = savedAuthCode;
|
|
1144
|
+
}
|
|
1145
|
+
});
|
|
1146
|
+
|
|
1147
|
+
test('getExclusionCookieValue changes when AUTH_CODE changes', async () => {
|
|
1148
|
+
const savedAuthCode = process.env.AUTH_CODE;
|
|
1149
|
+
try {
|
|
1150
|
+
const {getExclusionCookieValue} = await import('./util.ts');
|
|
1151
|
+
process.env.AUTH_CODE = 'code-one';
|
|
1152
|
+
const first = getExclusionCookieValue();
|
|
1153
|
+
process.env.AUTH_CODE = 'code-two';
|
|
1154
|
+
const second = getExclusionCookieValue();
|
|
1155
|
+
assert.notEqual(first, second);
|
|
1156
|
+
}
|
|
1157
|
+
finally {
|
|
1158
|
+
process.env.AUTH_CODE = savedAuthCode;
|
|
1159
|
+
}
|
|
1160
|
+
});
|
|
1161
|
+
|
|
1162
|
+
test('getExclusionCookieValue does not throw when AUTH_CODE is unset', async () => {
|
|
1163
|
+
const savedAuthCode = process.env.AUTH_CODE;
|
|
1164
|
+
delete process.env.AUTH_CODE;
|
|
1165
|
+
try {
|
|
1166
|
+
const {getExclusionCookieValue} = await import('./util.ts');
|
|
1167
|
+
assert.equal(typeof getExclusionCookieValue(), 'string');
|
|
1168
|
+
}
|
|
1169
|
+
finally {
|
|
1170
|
+
process.env.AUTH_CODE = savedAuthCode;
|
|
1171
|
+
}
|
|
1172
|
+
});
|
|
1173
|
+
|
|
1174
|
+
test('recordMetric creates the metrics file and records a first occurrence when the file is missing', async () => {
|
|
1175
|
+
const tmpDir = (await import('node:os')).tmpdir() + '/kilotest-missing-metrics-test';
|
|
1176
|
+
const fsSync = await import('node:fs');
|
|
1177
|
+
fsSync.mkdirSync(tmpDir, {recursive: true});
|
|
1178
|
+
const savedDbDir = process.env.DB_DIR;
|
|
1179
|
+
process.env.DB_DIR = tmpDir;
|
|
1180
|
+
try {
|
|
1181
|
+
const {recordMetric, metricsPath} = await import('./util.ts');
|
|
1182
|
+
await recordMetric('pageViews', 'tutorialWeb.html');
|
|
1183
|
+
const metrics = JSON.parse(fsSync.readFileSync(metricsPath(), 'utf8'));
|
|
1184
|
+
assert.equal(metrics.pageViews['tutorialWeb.html'], 1);
|
|
1185
|
+
assert.equal(metrics.mcpToolCalls['tutorialWeb.html'], undefined);
|
|
1186
|
+
assert.equal(typeof metrics.since, 'string');
|
|
1187
|
+
}
|
|
1188
|
+
finally {
|
|
1189
|
+
process.env.DB_DIR = savedDbDir;
|
|
1190
|
+
fsSync.rmSync(tmpDir, {recursive: true});
|
|
1191
|
+
}
|
|
1192
|
+
});
|
|
1193
|
+
|
|
1194
|
+
test('recordMetric increments an existing count rather than overwriting it', async () => {
|
|
1195
|
+
const tmpDir = (await import('node:os')).tmpdir() + '/kilotest-existing-metrics-test';
|
|
1196
|
+
const fsSync = await import('node:fs');
|
|
1197
|
+
fsSync.mkdirSync(tmpDir, {recursive: true});
|
|
1198
|
+
const savedDbDir = process.env.DB_DIR;
|
|
1199
|
+
process.env.DB_DIR = tmpDir;
|
|
1200
|
+
try {
|
|
1201
|
+
const {recordMetric, metricsPath} = await import('./util.ts');
|
|
1202
|
+
await recordMetric('mcpToolCalls', 'listReports');
|
|
1203
|
+
await recordMetric('mcpToolCalls', 'listReports');
|
|
1204
|
+
await recordMetric('mcpToolCalls', 'getReport');
|
|
1205
|
+
const metrics = JSON.parse(fsSync.readFileSync(metricsPath(), 'utf8'));
|
|
1206
|
+
assert.equal(metrics.mcpToolCalls.listReports, 2);
|
|
1207
|
+
assert.equal(metrics.mcpToolCalls.getReport, 1);
|
|
1208
|
+
}
|
|
1209
|
+
finally {
|
|
1210
|
+
process.env.DB_DIR = savedDbDir;
|
|
1211
|
+
fsSync.rmSync(tmpDir, {recursive: true});
|
|
1212
|
+
}
|
|
1213
|
+
});
|
|
1214
|
+
|
|
1215
|
+
test('recordMetric throws when the metrics file is not readable for a reason other than being missing', async () => {
|
|
1216
|
+
const tmpDir = (await import('node:os')).tmpdir() + '/kilotest-metrics-unreadable-test';
|
|
1217
|
+
const fsSync = await import('node:fs');
|
|
1218
|
+
// Create a directory where the metrics file should be, causing EISDIR rather than ENOENT.
|
|
1219
|
+
fsSync.mkdirSync(tmpDir + '/metrics.json', {recursive: true});
|
|
1220
|
+
const savedDbDir = process.env.DB_DIR;
|
|
1221
|
+
process.env.DB_DIR = tmpDir;
|
|
1222
|
+
try {
|
|
1223
|
+
const {recordMetric} = await import('./util.ts');
|
|
1224
|
+
await assert.rejects(recordMetric('pageViews', 'tutorialWeb.html'), /not readable/);
|
|
1225
|
+
}
|
|
1226
|
+
finally {
|
|
1227
|
+
process.env.DB_DIR = savedDbDir;
|
|
1228
|
+
fsSync.rmSync(tmpDir, {recursive: true});
|
|
1229
|
+
}
|
|
1230
|
+
});
|
|
1231
|
+
|
|
1232
|
+
test('recordMetric throws when the metrics file is not JSON', async () => {
|
|
1233
|
+
const tmpDir = (await import('node:os')).tmpdir() + '/kilotest-metrics-not-json-test';
|
|
1234
|
+
const fsSync = await import('node:fs');
|
|
1235
|
+
fsSync.mkdirSync(tmpDir, {recursive: true});
|
|
1236
|
+
fsSync.writeFileSync(tmpDir + '/metrics.json', 'not json');
|
|
1237
|
+
const savedDbDir = process.env.DB_DIR;
|
|
1238
|
+
process.env.DB_DIR = tmpDir;
|
|
1239
|
+
try {
|
|
1240
|
+
const {recordMetric} = await import('./util.ts');
|
|
1241
|
+
await assert.rejects(recordMetric('pageViews', 'tutorialWeb.html'), /not JSON/);
|
|
1242
|
+
}
|
|
1243
|
+
finally {
|
|
1244
|
+
process.env.DB_DIR = savedDbDir;
|
|
1245
|
+
fsSync.rmSync(tmpDir, {recursive: true});
|
|
1246
|
+
}
|
|
1247
|
+
});
|
|
1248
|
+
|
|
1249
|
+
test('recordMetric keeps categories independent for the same name', async () => {
|
|
1250
|
+
const tmpDir = (await import('node:os')).tmpdir() + '/kilotest-metrics-categories-test';
|
|
1251
|
+
const fsSync = await import('node:fs');
|
|
1252
|
+
fsSync.mkdirSync(tmpDir, {recursive: true});
|
|
1253
|
+
const savedDbDir = process.env.DB_DIR;
|
|
1254
|
+
process.env.DB_DIR = tmpDir;
|
|
1255
|
+
try {
|
|
1256
|
+
const {recordMetric, metricsPath} = await import('./util.ts');
|
|
1257
|
+
await recordMetric('apiOperations', 'listReports');
|
|
1258
|
+
await recordMetric('mcpToolCalls', 'listReports');
|
|
1259
|
+
const metrics = JSON.parse(fsSync.readFileSync(metricsPath(), 'utf8'));
|
|
1260
|
+
assert.equal(metrics.apiOperations.listReports, 1);
|
|
1261
|
+
assert.equal(metrics.mcpToolCalls.listReports, 1);
|
|
1262
|
+
}
|
|
1263
|
+
finally {
|
|
1264
|
+
process.env.DB_DIR = savedDbDir;
|
|
1265
|
+
fsSync.rmSync(tmpDir, {recursive: true});
|
|
1266
|
+
}
|
|
1267
|
+
});
|
|
1268
|
+
|
|
1269
|
+
test('recordMetric tracks managerActivity outcomes separately per page', async () => {
|
|
1270
|
+
const tmpDir = (await import('node:os')).tmpdir() + '/kilotest-metrics-manager-activity-test';
|
|
1271
|
+
const fsSync = await import('node:fs');
|
|
1272
|
+
fsSync.mkdirSync(tmpDir, {recursive: true});
|
|
1273
|
+
const savedDbDir = process.env.DB_DIR;
|
|
1274
|
+
process.env.DB_DIR = tmpDir;
|
|
1275
|
+
try {
|
|
1276
|
+
const {recordMetric, metricsPath} = await import('./util.ts');
|
|
1277
|
+
await recordMetric('managerActivity', 'reannotate.html', 'ok');
|
|
1278
|
+
await recordMetric('managerActivity', 'reannotate.html', 'error');
|
|
1279
|
+
await recordMetric('managerActivity', 'reannotate.html', 'error');
|
|
1280
|
+
const metrics = JSON.parse(fsSync.readFileSync(metricsPath(), 'utf8'));
|
|
1281
|
+
assert.deepEqual(metrics.managerActivity['reannotate.html'], {ok: 1, error: 2});
|
|
1282
|
+
}
|
|
1283
|
+
finally {
|
|
1284
|
+
process.env.DB_DIR = savedDbDir;
|
|
1285
|
+
fsSync.rmSync(tmpDir, {recursive: true});
|
|
1286
|
+
}
|
|
1287
|
+
});
|
|
1288
|
+
|
|
1289
|
+
test('recordMetric keeps managerActivity independent of pageViews for the same name', async () => {
|
|
1290
|
+
const tmpDir = (await import('node:os')).tmpdir() + '/kilotest-metrics-manager-vs-pageviews-test';
|
|
1291
|
+
const fsSync = await import('node:fs');
|
|
1292
|
+
fsSync.mkdirSync(tmpDir, {recursive: true});
|
|
1293
|
+
const savedDbDir = process.env.DB_DIR;
|
|
1294
|
+
process.env.DB_DIR = tmpDir;
|
|
1295
|
+
try {
|
|
1296
|
+
const {recordMetric, metricsPath} = await import('./util.ts');
|
|
1297
|
+
await recordMetric('pageViews', 'hideReportForm.html');
|
|
1298
|
+
await recordMetric('managerActivity', 'hideReportForm.html', 'ok');
|
|
1299
|
+
const metrics = JSON.parse(fsSync.readFileSync(metricsPath(), 'utf8'));
|
|
1300
|
+
assert.equal(metrics.pageViews['hideReportForm.html'], 1);
|
|
1301
|
+
assert.deepEqual(metrics.managerActivity['hideReportForm.html'], {ok: 1, error: 0});
|
|
1302
|
+
}
|
|
1303
|
+
finally {
|
|
1304
|
+
process.env.DB_DIR = savedDbDir;
|
|
1305
|
+
fsSync.rmSync(tmpDir, {recursive: true});
|
|
1306
|
+
}
|
|
1307
|
+
});
|
|
1308
|
+
|
|
1309
|
+
test('getMetrics backfills categories missing from a metrics.json written before they existed', async () => {
|
|
1310
|
+
const tmpDir = (await import('node:os')).tmpdir() + '/kilotest-metrics-backfill-test';
|
|
1311
|
+
const fsSync = await import('node:fs');
|
|
1312
|
+
fsSync.mkdirSync(tmpDir, {recursive: true});
|
|
1313
|
+
// Simulate a metrics.json written before the managerActivity category was added.
|
|
1314
|
+
fsSync.writeFileSync(tmpDir + '/metrics.json', JSON.stringify({
|
|
1315
|
+
since: '260101T0000',
|
|
1316
|
+
pageViews: {listReports: 1},
|
|
1317
|
+
mcpToolCalls: {},
|
|
1318
|
+
apiOperations: {}
|
|
1319
|
+
}));
|
|
1320
|
+
const savedDbDir = process.env.DB_DIR;
|
|
1321
|
+
process.env.DB_DIR = tmpDir;
|
|
1322
|
+
try {
|
|
1323
|
+
const {recordMetric, metricsPath} = await import('./util.ts');
|
|
1324
|
+
// Recording a managerActivity outcome must not throw despite the file predating that category.
|
|
1325
|
+
await recordMetric('managerActivity', 'metrics', 'ok');
|
|
1326
|
+
const metrics = JSON.parse(fsSync.readFileSync(metricsPath(), 'utf8'));
|
|
1327
|
+
assert.equal(metrics.since, '260101T0000');
|
|
1328
|
+
assert.equal(metrics.pageViews.listReports, 1);
|
|
1329
|
+
assert.deepEqual(metrics.managerActivity.metrics, {ok: 1, error: 0});
|
|
1330
|
+
}
|
|
1331
|
+
finally {
|
|
1332
|
+
process.env.DB_DIR = savedDbDir;
|
|
1333
|
+
fsSync.rmSync(tmpDir, {recursive: true});
|
|
1334
|
+
}
|
|
1335
|
+
});
|
|
1336
|
+
|
|
1337
|
+
test('getMetrics backfills every category, and generates a since stamp, from a bare empty file', async () => {
|
|
1338
|
+
const tmpDir = (await import('node:os')).tmpdir() + '/kilotest-metrics-backfill-empty-test';
|
|
1339
|
+
const fsSync = await import('node:fs');
|
|
1340
|
+
fsSync.mkdirSync(tmpDir, {recursive: true});
|
|
1341
|
+
// Simulate a metrics.json with none of the known fields (an extreme case of an outdated schema).
|
|
1342
|
+
fsSync.writeFileSync(tmpDir + '/metrics.json', '{}');
|
|
1343
|
+
const savedDbDir = process.env.DB_DIR;
|
|
1344
|
+
process.env.DB_DIR = tmpDir;
|
|
1345
|
+
try {
|
|
1346
|
+
const {recordMetric, metricsPath} = await import('./util.ts');
|
|
1347
|
+
await recordMetric('pageViews', 'tutorialWeb');
|
|
1348
|
+
const metrics = JSON.parse(fsSync.readFileSync(metricsPath(), 'utf8'));
|
|
1349
|
+
assert.equal(typeof metrics.since, 'string');
|
|
1350
|
+
assert.ok(metrics.since.length > 0);
|
|
1351
|
+
assert.equal(metrics.pageViews.tutorialWeb, 1);
|
|
1352
|
+
assert.deepEqual(metrics.mcpToolCalls, {});
|
|
1353
|
+
assert.deepEqual(metrics.apiOperations, {});
|
|
1354
|
+
assert.deepEqual(metrics.managerActivity, {});
|
|
1355
|
+
}
|
|
1356
|
+
finally {
|
|
1357
|
+
process.env.DB_DIR = savedDbDir;
|
|
1358
|
+
fsSync.rmSync(tmpDir, {recursive: true});
|
|
1359
|
+
}
|
|
1360
|
+
});
|