@oneuptime/common 11.5.12 → 11.5.13
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/Models/DatabaseModels/AIInsight.ts +30 -0
- package/Models/DatabaseModels/Project.ts +30 -0
- package/Models/DatabaseModels/TelemetryException.ts +128 -0
- package/Server/API/AIAgentDataAPI.ts +129 -5
- package/Server/API/AIInvestigationAPI.ts +87 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1784640000000-AddExceptionTriageAndPrivacyColumns.ts +56 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Services/AIRunService.ts +8 -0
- package/Server/Services/TelemetryExceptionService.ts +320 -24
- package/Server/Utils/AI/SRE/Insights/Detectors/ExceptionSpikeDetector.ts +15 -4
- package/Server/Utils/AI/SRE/Insights/Detectors/NewExceptionDetector.ts +15 -4
- package/Server/Utils/AI/SRE/Insights/InsightScanner.ts +222 -24
- package/Server/Utils/AI/SRE/Insights/InsightTriageRunner.ts +225 -9
- package/Server/Utils/AI/SRE/SubjectCodeFixRun.ts +39 -0
- package/Server/Utils/AI/SRE/TelemetryImprovementTaskTrigger.ts +114 -0
- package/Server/Utils/Telemetry/ExceptionSanitizer.ts +222 -0
- package/Tests/Server/Services/TelemetryExceptionCodeFixRun.test.ts +205 -1
- package/Tests/Server/Utils/AI/Insights/InsightScanner.test.ts +150 -141
- package/Tests/Server/Utils/AI/Insights/InsightTriageRunner.test.ts +277 -1
- package/Tests/Server/Utils/ExceptionSanitizer.test.ts +70 -0
- package/Tests/Server/Utils/InsightTriageClassification.test.ts +57 -0
- package/Tests/Server/Utils/TelemetryImprovementTaskTrigger.test.ts +141 -0
- package/Tests/UI/Components/ModelTableSelectFromColumns.test.ts +259 -0
- package/Types/AI/CodeFixTaskContext.ts +6 -0
- package/Types/AI/CodeFixTaskType.ts +38 -1
- package/Types/AI/ExceptionAIClassification.ts +27 -0
- package/Types/Log/LogScrubPatternType.ts +7 -0
- package/Types/Trace/TraceScrubPatternType.ts +7 -0
- package/UI/Components/ModelTable/BaseModelTable.tsx +15 -46
- package/UI/Components/ModelTable/SelectFromColumns.ts +129 -0
- package/build/dist/Models/DatabaseModels/AIInsight.js +31 -0
- package/build/dist/Models/DatabaseModels/AIInsight.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Project.js +31 -0
- package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
- package/build/dist/Models/DatabaseModels/TelemetryException.js +134 -0
- package/build/dist/Models/DatabaseModels/TelemetryException.js.map +1 -1
- package/build/dist/Server/API/AIAgentDataAPI.js +97 -8
- package/build/dist/Server/API/AIAgentDataAPI.js.map +1 -1
- package/build/dist/Server/API/AIInvestigationAPI.js +55 -0
- package/build/dist/Server/API/AIInvestigationAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784640000000-AddExceptionTriageAndPrivacyColumns.js +33 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784640000000-AddExceptionTriageAndPrivacyColumns.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/AIRunService.js +9 -2
- package/build/dist/Server/Services/AIRunService.js.map +1 -1
- package/build/dist/Server/Services/TelemetryExceptionService.js +263 -22
- package/build/dist/Server/Services/TelemetryExceptionService.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/ExceptionSpikeDetector.js +12 -4
- package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/ExceptionSpikeDetector.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/NewExceptionDetector.js +12 -4
- package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/NewExceptionDetector.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/Insights/InsightScanner.js +193 -22
- package/build/dist/Server/Utils/AI/SRE/Insights/InsightScanner.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/Insights/InsightTriageRunner.js +178 -9
- package/build/dist/Server/Utils/AI/SRE/Insights/InsightTriageRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/SubjectCodeFixRun.js +37 -0
- package/build/dist/Server/Utils/AI/SRE/SubjectCodeFixRun.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/TelemetryImprovementTaskTrigger.js +98 -0
- package/build/dist/Server/Utils/AI/SRE/TelemetryImprovementTaskTrigger.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/ExceptionSanitizer.js +149 -0
- package/build/dist/Server/Utils/Telemetry/ExceptionSanitizer.js.map +1 -0
- package/build/dist/Types/AI/CodeFixTaskType.js +38 -1
- package/build/dist/Types/AI/CodeFixTaskType.js.map +1 -1
- package/build/dist/Types/AI/ExceptionAIClassification.js +28 -0
- package/build/dist/Types/AI/ExceptionAIClassification.js.map +1 -0
- package/build/dist/Types/Log/LogScrubPatternType.js +7 -0
- package/build/dist/Types/Log/LogScrubPatternType.js.map +1 -1
- package/build/dist/Types/Trace/TraceScrubPatternType.js +7 -0
- package/build/dist/Types/Trace/TraceScrubPatternType.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +12 -37
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/SelectFromColumns.js +82 -0
- package/build/dist/UI/Components/ModelTable/SelectFromColumns.js.map +1 -0
- package/package.json +1 -1
|
@@ -33,15 +33,16 @@ import { describe, expect, test, afterEach, beforeEach } from "@jest/globals";
|
|
|
33
33
|
* broken project, detector or insight never stops the rest), only NEWLY
|
|
34
34
|
* created insights are routed — refreshed rows keep their status — and a
|
|
35
35
|
* new insight always leaves the defensive Detected state in the same tick:
|
|
36
|
-
*
|
|
37
|
-
*
|
|
36
|
+
* ActionRequired, with its triage enqueued. TRIAGE GATES THE FIX: the
|
|
37
|
+
* scanner NEVER calls fix routing — the automatic fix decision moved to
|
|
38
|
+
* the triage runner, which routes a fix only on a code-fault verdict.
|
|
38
39
|
*
|
|
39
40
|
* Plus the self-heal sweep: a row an earlier tick created but never routed
|
|
40
41
|
* (pod death, failed status write) is stranded in Detected forever — the
|
|
41
42
|
* refresh path never re-routes it, and its fingerprint stays pinned. The
|
|
42
43
|
* sweep re-routes it on the next tick, runs BEFORE the detectors (so this
|
|
43
44
|
* tick's own rows cannot be routed twice), never breaks the scan when it
|
|
44
|
-
* fails, and cannot double-create work (the
|
|
45
|
+
* fails, and cannot double-create work (the triage dedupe holds).
|
|
45
46
|
*/
|
|
46
47
|
|
|
47
48
|
const projectId: ObjectID = ObjectID.generate();
|
|
@@ -301,29 +302,7 @@ describe("InsightScanner — routing newly created insights", () => {
|
|
|
301
302
|
jest.restoreAllMocks();
|
|
302
303
|
});
|
|
303
304
|
|
|
304
|
-
test("
|
|
305
|
-
const insight: AIInsight = fakeInsight();
|
|
306
|
-
const fixAiRunId: ObjectID = ObjectID.generate();
|
|
307
|
-
jest
|
|
308
|
-
.spyOn(InsightStore, "upsertCandidates")
|
|
309
|
-
.mockResolvedValue(emptyUpsertResult({ created: [insight] }));
|
|
310
|
-
routeInsightFix.mockResolvedValue({ fixAiRunId });
|
|
311
|
-
|
|
312
|
-
await InsightScanner.scanProjectForInsights(fakeProject());
|
|
313
|
-
|
|
314
|
-
expect(updateOneById).toHaveBeenCalledWith(
|
|
315
|
-
expect.objectContaining({
|
|
316
|
-
id: insight.id,
|
|
317
|
-
data: expect.objectContaining({
|
|
318
|
-
status: AIInsightStatus.FixOpened,
|
|
319
|
-
fixAiRunId,
|
|
320
|
-
}),
|
|
321
|
-
props: expect.objectContaining({ isRoot: true }),
|
|
322
|
-
}),
|
|
323
|
-
);
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
test("no fix opened → status ActionRequired", async () => {
|
|
305
|
+
test("a new insight lands on ActionRequired — the scanner NEVER calls fix routing (triage gates the fix)", async () => {
|
|
327
306
|
const insight: AIInsight = fakeInsight();
|
|
328
307
|
jest
|
|
329
308
|
.spyOn(InsightStore, "upsertCandidates")
|
|
@@ -331,6 +310,7 @@ describe("InsightScanner — routing newly created insights", () => {
|
|
|
331
310
|
|
|
332
311
|
await InsightScanner.scanProjectForInsights(fakeProject());
|
|
333
312
|
|
|
313
|
+
expect(routeInsightFix).not.toHaveBeenCalled();
|
|
334
314
|
expect(updateOneById).toHaveBeenCalledWith(
|
|
335
315
|
expect.objectContaining({
|
|
336
316
|
id: insight.id,
|
|
@@ -340,18 +320,7 @@ describe("InsightScanner — routing newly created insights", () => {
|
|
|
340
320
|
props: expect.objectContaining({ isRoot: true }),
|
|
341
321
|
}),
|
|
342
322
|
);
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
test("the project (carrying its enableInsightFixTasks flag) is handed to fix routing — the flag gate lives there", async () => {
|
|
346
|
-
const insight: AIInsight = fakeInsight();
|
|
347
|
-
const project: Project = fakeProject();
|
|
348
|
-
jest
|
|
349
|
-
.spyOn(InsightStore, "upsertCandidates")
|
|
350
|
-
.mockResolvedValue(emptyUpsertResult({ created: [insight] }));
|
|
351
|
-
|
|
352
|
-
await InsightScanner.scanProjectForInsights(project);
|
|
353
|
-
|
|
354
|
-
expect(routeInsightFix).toHaveBeenCalledWith({ insight, project });
|
|
323
|
+
expect(enqueueInsightTriage).toHaveBeenCalledWith({ insight });
|
|
355
324
|
});
|
|
356
325
|
|
|
357
326
|
test("triage run enqueued → triageAiRunId persisted", async () => {
|
|
@@ -418,27 +387,6 @@ describe("InsightScanner — routing newly created insights", () => {
|
|
|
418
387
|
expect(create).not.toHaveBeenCalled();
|
|
419
388
|
});
|
|
420
389
|
|
|
421
|
-
test("fix routing rejecting (contract breach) degrades to ActionRequired and still triages", async () => {
|
|
422
|
-
const insight: AIInsight = fakeInsight();
|
|
423
|
-
jest
|
|
424
|
-
.spyOn(InsightStore, "upsertCandidates")
|
|
425
|
-
.mockResolvedValue(emptyUpsertResult({ created: [insight] }));
|
|
426
|
-
routeInsightFix.mockRejectedValue(new Error("routing blew up"));
|
|
427
|
-
|
|
428
|
-
await expect(
|
|
429
|
-
InsightScanner.scanProjectForInsights(fakeProject()),
|
|
430
|
-
).resolves.toBeUndefined();
|
|
431
|
-
|
|
432
|
-
expect(updateOneById).toHaveBeenCalledWith(
|
|
433
|
-
expect.objectContaining({
|
|
434
|
-
data: expect.objectContaining({
|
|
435
|
-
status: AIInsightStatus.ActionRequired,
|
|
436
|
-
}),
|
|
437
|
-
}),
|
|
438
|
-
);
|
|
439
|
-
expect(enqueueInsightTriage).toHaveBeenCalledWith({ insight });
|
|
440
|
-
});
|
|
441
|
-
|
|
442
390
|
test("triage rejecting (contract breach) does not fail the scan — the status was already routed", async () => {
|
|
443
391
|
const insight: AIInsight = fakeInsight();
|
|
444
392
|
jest
|
|
@@ -473,9 +421,14 @@ describe("InsightScanner — routing newly created insights", () => {
|
|
|
473
421
|
InsightScanner.scanProjectForInsights(fakeProject()),
|
|
474
422
|
).resolves.toBeUndefined();
|
|
475
423
|
|
|
476
|
-
expect(
|
|
477
|
-
expect(
|
|
478
|
-
expect.objectContaining({
|
|
424
|
+
expect(enqueueInsightTriage).toHaveBeenCalledWith({ insight: second });
|
|
425
|
+
expect(updateOneById).toHaveBeenCalledWith(
|
|
426
|
+
expect.objectContaining({
|
|
427
|
+
id: second.id,
|
|
428
|
+
data: expect.objectContaining({
|
|
429
|
+
status: AIInsightStatus.ActionRequired,
|
|
430
|
+
}),
|
|
431
|
+
}),
|
|
479
432
|
);
|
|
480
433
|
});
|
|
481
434
|
});
|
|
@@ -486,14 +439,42 @@ describe("InsightScanner — the self-heal sweep for insights stranded in Detect
|
|
|
486
439
|
let routeInsightFix: jest.SpyInstance;
|
|
487
440
|
let enqueueInsightTriage: jest.SpyInstance;
|
|
488
441
|
|
|
442
|
+
/*
|
|
443
|
+
* The sweep now issues THREE findBy queries per tick (Detected rows,
|
|
444
|
+
* untriaged ActionRequired rows, unrouted code-fault rows) — the mock
|
|
445
|
+
* dispatches on the queried status/classification so each test can feed
|
|
446
|
+
* exactly one stranded shape.
|
|
447
|
+
*/
|
|
448
|
+
let detectedStranded: Array<AIInsight>;
|
|
449
|
+
let untriagedStranded: Array<AIInsight>;
|
|
450
|
+
let unroutedCodeFaults: Array<AIInsight>;
|
|
451
|
+
|
|
489
452
|
beforeEach(() => {
|
|
453
|
+
detectedStranded = [];
|
|
454
|
+
untriagedStranded = [];
|
|
455
|
+
unroutedCodeFaults = [];
|
|
456
|
+
|
|
490
457
|
// No candidates this tick: only the sweep is under test.
|
|
491
458
|
jest
|
|
492
459
|
.spyOn(InsightDetectors, "getAllDetectors")
|
|
493
460
|
.mockReturnValue([fakeDetector({ candidates: [] })]);
|
|
494
461
|
findBy = jest
|
|
495
462
|
.spyOn(AIInsightService, "findBy")
|
|
496
|
-
.
|
|
463
|
+
.mockImplementation((findByArgs: unknown): Promise<Array<AIInsight>> => {
|
|
464
|
+
const query: Record<string, unknown> =
|
|
465
|
+
(findByArgs as { query: Record<string, unknown> }).query || {};
|
|
466
|
+
|
|
467
|
+
if (query["status"] === AIInsightStatus.Detected) {
|
|
468
|
+
return Promise.resolve(detectedStranded);
|
|
469
|
+
}
|
|
470
|
+
if (query["classification"]) {
|
|
471
|
+
return Promise.resolve(unroutedCodeFaults);
|
|
472
|
+
}
|
|
473
|
+
if (query["triageCompletedAt"]) {
|
|
474
|
+
return Promise.resolve(untriagedStranded);
|
|
475
|
+
}
|
|
476
|
+
return Promise.resolve([]);
|
|
477
|
+
});
|
|
497
478
|
updateOneById = jest
|
|
498
479
|
.spyOn(AIInsightService, "updateOneById")
|
|
499
480
|
.mockResolvedValue(undefined);
|
|
@@ -511,7 +492,7 @@ describe("InsightScanner — the self-heal sweep for insights stranded in Detect
|
|
|
511
492
|
|
|
512
493
|
test("a Detected row left over from an earlier tick is swept up and re-routed to ActionRequired (and triaged)", async () => {
|
|
513
494
|
const stranded: AIInsight = fakeInsight();
|
|
514
|
-
|
|
495
|
+
detectedStranded = [stranded];
|
|
515
496
|
|
|
516
497
|
await InsightScanner.scanProjectForInsights(fakeProject());
|
|
517
498
|
|
|
@@ -526,9 +507,8 @@ describe("InsightScanner — the self-heal sweep for insights stranded in Detect
|
|
|
526
507
|
props: expect.objectContaining({ isRoot: true }),
|
|
527
508
|
}),
|
|
528
509
|
);
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
);
|
|
510
|
+
// Fix routing is NOT part of scan-time routing — triage gates the fix.
|
|
511
|
+
expect(routeInsightFix).not.toHaveBeenCalled();
|
|
532
512
|
expect(updateOneById).toHaveBeenCalledWith(
|
|
533
513
|
expect.objectContaining({
|
|
534
514
|
id: stranded.id,
|
|
@@ -541,34 +521,21 @@ describe("InsightScanner — the self-heal sweep for insights stranded in Detect
|
|
|
541
521
|
expect(enqueueInsightTriage).toHaveBeenCalledWith({ insight: stranded });
|
|
542
522
|
});
|
|
543
523
|
|
|
544
|
-
test("a swept row whose fix routing opens a run lands on FixOpened + fixAiRunId — the full routing path, not a status patch", async () => {
|
|
545
|
-
const stranded: AIInsight = fakeInsight();
|
|
546
|
-
const fixAiRunId: ObjectID = ObjectID.generate();
|
|
547
|
-
findBy.mockResolvedValue([stranded]);
|
|
548
|
-
routeInsightFix.mockResolvedValue({ fixAiRunId });
|
|
549
|
-
|
|
550
|
-
await InsightScanner.scanProjectForInsights(fakeProject());
|
|
551
|
-
|
|
552
|
-
expect(updateOneById).toHaveBeenCalledWith(
|
|
553
|
-
expect.objectContaining({
|
|
554
|
-
id: stranded.id,
|
|
555
|
-
data: expect.objectContaining({
|
|
556
|
-
status: AIInsightStatus.FixOpened,
|
|
557
|
-
fixAiRunId,
|
|
558
|
-
}),
|
|
559
|
-
}),
|
|
560
|
-
);
|
|
561
|
-
});
|
|
562
|
-
|
|
563
524
|
test("the sweep runs BEFORE the detectors — so rows created by THIS tick are routed exactly once, never twice", async () => {
|
|
564
525
|
const callOrder: Array<string> = [];
|
|
565
526
|
const created: AIInsight = fakeInsight();
|
|
566
527
|
|
|
567
|
-
findBy.mockImplementation(
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
528
|
+
findBy.mockImplementation(
|
|
529
|
+
(findByArgs: unknown): Promise<Array<AIInsight>> => {
|
|
530
|
+
const query: Record<string, unknown> =
|
|
531
|
+
(findByArgs as { query: Record<string, unknown> }).query || {};
|
|
532
|
+
if (query["status"] === AIInsightStatus.Detected) {
|
|
533
|
+
callOrder.push("sweep");
|
|
534
|
+
}
|
|
535
|
+
// Nothing stranded: the row below does not exist yet at sweep time.
|
|
536
|
+
return Promise.resolve([]);
|
|
537
|
+
},
|
|
538
|
+
);
|
|
572
539
|
jest.spyOn(InsightDetectors, "getAllDetectors").mockReturnValue([
|
|
573
540
|
{
|
|
574
541
|
insightType: AIInsightType.NewException,
|
|
@@ -585,10 +552,8 @@ describe("InsightScanner — the self-heal sweep for insights stranded in Detect
|
|
|
585
552
|
await InsightScanner.scanProjectForInsights(fakeProject());
|
|
586
553
|
|
|
587
554
|
expect(callOrder).toEqual(["sweep", "detect"]);
|
|
588
|
-
expect(
|
|
589
|
-
expect(
|
|
590
|
-
expect.objectContaining({ insight: created }),
|
|
591
|
-
);
|
|
555
|
+
expect(enqueueInsightTriage).toHaveBeenCalledTimes(1);
|
|
556
|
+
expect(enqueueInsightTriage).toHaveBeenCalledWith({ insight: created });
|
|
592
557
|
});
|
|
593
558
|
|
|
594
559
|
test("a sweep failure never breaks the scan — detectors still run and this tick's new insights still route", async () => {
|
|
@@ -606,15 +571,13 @@ describe("InsightScanner — the self-heal sweep for insights stranded in Detect
|
|
|
606
571
|
).resolves.toBeUndefined();
|
|
607
572
|
|
|
608
573
|
expect(upsert).toHaveBeenCalledTimes(1);
|
|
609
|
-
expect(
|
|
610
|
-
expect.objectContaining({ insight: created }),
|
|
611
|
-
);
|
|
574
|
+
expect(enqueueInsightTriage).toHaveBeenCalledWith({ insight: created });
|
|
612
575
|
});
|
|
613
576
|
|
|
614
577
|
test("one stranded row failing to re-route does not stop the next one", async () => {
|
|
615
578
|
const first: AIInsight = fakeInsight();
|
|
616
579
|
const second: AIInsight = fakeInsight();
|
|
617
|
-
|
|
580
|
+
detectedStranded = [first, second];
|
|
618
581
|
updateOneById
|
|
619
582
|
.mockRejectedValueOnce(new Error("write conflict"))
|
|
620
583
|
.mockResolvedValue(undefined);
|
|
@@ -623,31 +586,99 @@ describe("InsightScanner — the self-heal sweep for insights stranded in Detect
|
|
|
623
586
|
InsightScanner.scanProjectForInsights(fakeProject()),
|
|
624
587
|
).resolves.toBeUndefined();
|
|
625
588
|
|
|
626
|
-
expect(
|
|
589
|
+
expect(enqueueInsightTriage).toHaveBeenCalledWith({ insight: second });
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
test("stranded shape 2: an ActionRequired insight with no triage verdict gets its triage re-enqueued (capped on failed attempts)", async () => {
|
|
593
|
+
const untriaged: AIInsight = fakeInsight();
|
|
594
|
+
untriagedStranded = [untriaged];
|
|
595
|
+
|
|
596
|
+
// Zero failed attempts: the re-enqueue proceeds.
|
|
597
|
+
const countBy: jest.SpyInstance = jest
|
|
598
|
+
.spyOn(AIRunService, "countBy")
|
|
599
|
+
.mockResolvedValue({
|
|
600
|
+
toNumber: () => {
|
|
601
|
+
return 0;
|
|
602
|
+
},
|
|
603
|
+
} as never);
|
|
604
|
+
const triageAiRunId: ObjectID = ObjectID.generate();
|
|
605
|
+
enqueueInsightTriage.mockResolvedValue({ triageAiRunId });
|
|
606
|
+
|
|
607
|
+
await InsightScanner.scanProjectForInsights(fakeProject());
|
|
608
|
+
|
|
609
|
+
expect(enqueueInsightTriage).toHaveBeenCalledWith({ insight: untriaged });
|
|
610
|
+
expect(updateOneById).toHaveBeenCalledWith(
|
|
611
|
+
expect.objectContaining({
|
|
612
|
+
id: untriaged.id,
|
|
613
|
+
data: expect.objectContaining({ triageAiRunId }),
|
|
614
|
+
}),
|
|
615
|
+
);
|
|
616
|
+
|
|
617
|
+
// Three failed attempts: the insight is left alone (no retry storm).
|
|
618
|
+
enqueueInsightTriage.mockClear();
|
|
619
|
+
countBy.mockResolvedValue({
|
|
620
|
+
toNumber: () => {
|
|
621
|
+
return 3;
|
|
622
|
+
},
|
|
623
|
+
} as never);
|
|
624
|
+
|
|
625
|
+
await InsightScanner.scanProjectForInsights(fakeProject());
|
|
626
|
+
|
|
627
|
+
expect(enqueueInsightTriage).not.toHaveBeenCalled();
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
test("stranded shape 3: a code-fault verdict whose fix never routed is re-routed with a CONDITIONAL FixOpened flip", async () => {
|
|
631
|
+
const unrouted: AIInsight = {
|
|
632
|
+
...fakeInsight(),
|
|
633
|
+
insightType: AIInsightType.NewException,
|
|
634
|
+
} as AIInsight;
|
|
635
|
+
unroutedCodeFaults = [unrouted];
|
|
636
|
+
|
|
637
|
+
const fixAiRunId: ObjectID = ObjectID.generate();
|
|
638
|
+
routeInsightFix.mockResolvedValue({ fixAiRunId });
|
|
639
|
+
const flipStatus: jest.SpyInstance = jest
|
|
640
|
+
.spyOn(AIInsightService, "updateOneBy")
|
|
641
|
+
.mockResolvedValue(undefined as never);
|
|
642
|
+
|
|
643
|
+
await InsightScanner.scanProjectForInsights(fakeProject());
|
|
644
|
+
|
|
627
645
|
expect(routeInsightFix).toHaveBeenCalledWith(
|
|
628
|
-
expect.objectContaining({ insight:
|
|
646
|
+
expect.objectContaining({ insight: unrouted }),
|
|
647
|
+
);
|
|
648
|
+
expect(flipStatus).toHaveBeenCalledWith(
|
|
649
|
+
expect.objectContaining({
|
|
650
|
+
query: expect.objectContaining({
|
|
651
|
+
status: AIInsightStatus.ActionRequired,
|
|
652
|
+
}),
|
|
653
|
+
data: expect.objectContaining({
|
|
654
|
+
status: AIInsightStatus.FixOpened,
|
|
655
|
+
fixAiRunId,
|
|
656
|
+
}),
|
|
657
|
+
}),
|
|
629
658
|
);
|
|
630
659
|
});
|
|
631
660
|
});
|
|
632
661
|
|
|
633
662
|
/*
|
|
634
|
-
* The safety proof for the
|
|
635
|
-
* run (the pod died after the run was
|
|
636
|
-
*
|
|
637
|
-
*
|
|
663
|
+
* The safety proof for the post-triage fix path: routing a fix for an
|
|
664
|
+
* insight whose exception ALREADY has a run (the pod died after the run was
|
|
665
|
+
* created but before the insight status write; or triage re-ran) cannot
|
|
666
|
+
* create a second one. The dedupe lives inside the creation path itself —
|
|
667
|
+
* this exercises the REAL InsightFixRouting against it, exactly as the
|
|
668
|
+
* triage runner invokes it on a code-fault verdict.
|
|
638
669
|
*/
|
|
639
|
-
describe("
|
|
670
|
+
describe("InsightFixRouting — a dedupe rejection from the creation path is a quiet no-fix", () => {
|
|
640
671
|
afterEach(() => {
|
|
641
672
|
jest.restoreAllMocks();
|
|
642
673
|
});
|
|
643
674
|
|
|
644
|
-
test("the creation path's per-(exception, recipe) dedupe rejection
|
|
645
|
-
const
|
|
675
|
+
test("the creation path's per-(exception, recipe) dedupe rejection yields an empty result and stamps nothing", async () => {
|
|
676
|
+
const insight: AIInsight = {
|
|
646
677
|
id: ObjectID.generate(),
|
|
647
678
|
projectId,
|
|
648
679
|
insightType: AIInsightType.NewException,
|
|
649
680
|
telemetryExceptionId: ObjectID.generate(),
|
|
650
|
-
status: AIInsightStatus.
|
|
681
|
+
status: AIInsightStatus.ActionRequired,
|
|
651
682
|
} as unknown as AIInsight;
|
|
652
683
|
|
|
653
684
|
const optedInProject: Project = {
|
|
@@ -665,24 +696,14 @@ describe("InsightScanner — re-routing a stranded insight cannot double-create
|
|
|
665
696
|
runsToday: 0,
|
|
666
697
|
};
|
|
667
698
|
|
|
668
|
-
jest
|
|
669
|
-
.spyOn(InsightDetectors, "getAllDetectors")
|
|
670
|
-
.mockReturnValue([fakeDetector({ candidates: [] })]);
|
|
671
|
-
jest
|
|
672
|
-
.spyOn(AIInsightService, "findBy")
|
|
673
|
-
.mockResolvedValue([stranded] as unknown as Array<AIInsight>);
|
|
674
|
-
const updateOneById: jest.SpyInstance = jest
|
|
675
|
-
.spyOn(AIInsightService, "updateOneById")
|
|
676
|
-
.mockResolvedValue(undefined);
|
|
677
|
-
jest.spyOn(InsightTriage, "enqueueInsightTriage").mockResolvedValue({});
|
|
678
699
|
jest.spyOn(FixRunBudget, "getBudgetStatus").mockResolvedValue(budget);
|
|
679
700
|
jest
|
|
680
701
|
.spyOn(TelemetryExceptionService, "getAIFixReadiness")
|
|
681
702
|
.mockResolvedValue({ ready: true, checks: [] });
|
|
682
703
|
|
|
683
704
|
/*
|
|
684
|
-
* The dedupe:
|
|
685
|
-
*
|
|
705
|
+
* The dedupe: a fix run for this exception is still non-terminal, so
|
|
706
|
+
* the creation path refuses a second one.
|
|
686
707
|
*/
|
|
687
708
|
const create: jest.SpyInstance = jest
|
|
688
709
|
.spyOn(TelemetryExceptionService, "createCodeFixRunForException")
|
|
@@ -693,28 +714,16 @@ describe("InsightScanner — re-routing a stranded insight cannot double-create
|
|
|
693
714
|
);
|
|
694
715
|
const stamp: jest.SpyInstance = jest.spyOn(AIRunService, "updateOneById");
|
|
695
716
|
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
717
|
+
const result: { fixAiRunId?: ObjectID | undefined } =
|
|
718
|
+
await InsightFixRouting.routeInsightFix({
|
|
719
|
+
insight,
|
|
720
|
+
project: optedInProject,
|
|
721
|
+
});
|
|
699
722
|
|
|
700
|
-
// The creation path was asked exactly once — and it refused.
|
|
723
|
+
// The creation path was asked exactly once — and it refused, quietly.
|
|
701
724
|
expect(create).toHaveBeenCalledTimes(1);
|
|
702
|
-
|
|
725
|
+
expect(result.fixAiRunId).toBeUndefined();
|
|
726
|
+
// No second run exists, so nothing was stamped.
|
|
703
727
|
expect(stamp).not.toHaveBeenCalled();
|
|
704
|
-
expect(updateOneById).toHaveBeenCalledWith(
|
|
705
|
-
expect.objectContaining({
|
|
706
|
-
id: stranded.id,
|
|
707
|
-
data: expect.objectContaining({
|
|
708
|
-
status: AIInsightStatus.ActionRequired,
|
|
709
|
-
}),
|
|
710
|
-
}),
|
|
711
|
-
);
|
|
712
|
-
expect(updateOneById).not.toHaveBeenCalledWith(
|
|
713
|
-
expect.objectContaining({
|
|
714
|
-
data: expect.objectContaining({
|
|
715
|
-
status: AIInsightStatus.FixOpened,
|
|
716
|
-
}),
|
|
717
|
-
}),
|
|
718
|
-
);
|
|
719
728
|
});
|
|
720
729
|
});
|