@startsimpli/integrations 0.2.16 → 0.2.17
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/package.json +1 -1
- package/src/__tests__/corpus-llm-timeout.test.ts +91 -0
- package/src/__tests__/corpus-runs.test.ts +631 -0
- package/src/__tests__/corpus-tracker.test.tsx +254 -1
- package/src/components/pages/TrackerPage.tsx +326 -2
- package/src/corpus/client.ts +57 -1
- package/src/corpus/entities.ts +79 -0
- package/src/corpus/extract.ts +85 -8
- package/src/corpus/fixtures.ts +34 -0
- package/src/corpus/index.ts +29 -0
- package/src/corpus/llm.ts +32 -3
- package/src/corpus/runs.ts +563 -0
- package/src/corpus/types.ts +176 -2
|
@@ -18,6 +18,7 @@ import type {
|
|
|
18
18
|
CommitmentQuery,
|
|
19
19
|
CommitmentTrackerApi,
|
|
20
20
|
CorpusPage,
|
|
21
|
+
ExtractionRun,
|
|
21
22
|
} from '../corpus/types';
|
|
22
23
|
|
|
23
24
|
const NOW = Date.parse('2026-08-10T12:00:00Z');
|
|
@@ -74,6 +75,8 @@ function makeApi(options: {
|
|
|
74
75
|
appliedFilters?: string[];
|
|
75
76
|
onQuery?: (query: CommitmentQuery) => void;
|
|
76
77
|
extract?: CommitmentTrackerApi['extract'];
|
|
78
|
+
startExtraction?: CommitmentTrackerApi['startExtraction'];
|
|
79
|
+
getExtractionRun?: CommitmentTrackerApi['getExtractionRun'];
|
|
77
80
|
} = {}): CommitmentTrackerApi {
|
|
78
81
|
const rows = options.rows ?? ROWS;
|
|
79
82
|
return {
|
|
@@ -90,6 +93,8 @@ function makeApi(options: {
|
|
|
90
93
|
};
|
|
91
94
|
},
|
|
92
95
|
...(options.extract ? { extract: options.extract } : {}),
|
|
96
|
+
...(options.startExtraction ? { startExtraction: options.startExtraction } : {}),
|
|
97
|
+
...(options.getExtractionRun ? { getExtractionRun: options.getExtractionRun } : {}),
|
|
93
98
|
};
|
|
94
99
|
}
|
|
95
100
|
|
|
@@ -279,6 +284,8 @@ describe('TrackerPage — what is outstanding', () => {
|
|
|
279
284
|
describe('TrackerPage — running the extraction', () => {
|
|
280
285
|
const RESULT = {
|
|
281
286
|
documentsProcessed: 16,
|
|
287
|
+
documentIds: [],
|
|
288
|
+
modelFailure: null,
|
|
282
289
|
totalDocuments: 16,
|
|
283
290
|
nextOffset: null,
|
|
284
291
|
fromRules: 24,
|
|
@@ -343,7 +350,7 @@ describe('TrackerPage — running the extraction', () => {
|
|
|
343
350
|
|
|
344
351
|
describe('TrackerPage — the run reports its own precision work', () => {
|
|
345
352
|
const BASE = {
|
|
346
|
-
documentsProcessed: 16, totalDocuments: 16, nextOffset: null,
|
|
353
|
+
documentsProcessed: 16, documentIds: [], modelFailure: null, totalDocuments: 16, nextOffset: null,
|
|
347
354
|
fromRules: 67, fromModel: 35, ungrounded: 2, unconfirmed: 59, verified: true,
|
|
348
355
|
created: 102, merged: 0, resolved: 0, needsReview: [], droppedOutOfScope: 0,
|
|
349
356
|
provider: { provider: 'anthropic', model: 'claude-sonnet-5', live: true },
|
|
@@ -376,3 +383,249 @@ describe('TrackerPage — the run reports its own precision work', () => {
|
|
|
376
383
|
expect(screen.queryByText(/proposed and dropped/)).toBeNull();
|
|
377
384
|
});
|
|
378
385
|
});
|
|
386
|
+
|
|
387
|
+
// ---------------------------------------------------------------------------
|
|
388
|
+
// a sweep is a JOB, and the screen watches it (bd startsim-768w.38.28)
|
|
389
|
+
// ---------------------------------------------------------------------------
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* The button used to block on a synchronous call that could not possibly finish.
|
|
393
|
+
* Now it starts a run and watches it, so the four things a person needs while a
|
|
394
|
+
* six-minute job is going have to be on screen: that it started, how far it has
|
|
395
|
+
* got, that it can be left alone, and — the one that was missing — whether the
|
|
396
|
+
* model actually answered.
|
|
397
|
+
*/
|
|
398
|
+
describe('TrackerPage — watching a queued sweep', () => {
|
|
399
|
+
const TOTALS = {
|
|
400
|
+
documentsProcessed: 0, fromRules: 0, fromModel: 0, ungrounded: 0, unconfirmed: 0,
|
|
401
|
+
created: 0, merged: 0, resolved: 0, droppedOutOfScope: 0, modelCalls: 0, modelFailures: 0,
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
function run(overrides: Partial<ExtractionRun> = {}): ExtractionRun {
|
|
405
|
+
return {
|
|
406
|
+
runId: 'sweep-1',
|
|
407
|
+
scope: SCOPE,
|
|
408
|
+
status: 'running',
|
|
409
|
+
cursor: 0,
|
|
410
|
+
totalDocuments: 17,
|
|
411
|
+
totals: { ...TOTALS },
|
|
412
|
+
processedIds: [],
|
|
413
|
+
modelIncompleteIds: [],
|
|
414
|
+
needsReview: [],
|
|
415
|
+
provider: { provider: 'anthropic', model: 'claude-sonnet-5', live: true },
|
|
416
|
+
startedAt: '2026-08-10T12:00:00Z',
|
|
417
|
+
updatedAt: '2026-08-10T12:00:00Z',
|
|
418
|
+
finishedAt: null,
|
|
419
|
+
leaseOwner: 'driver-a',
|
|
420
|
+
leaseUntil: '2026-08-10T12:05:00Z',
|
|
421
|
+
error: null,
|
|
422
|
+
lastModelFailure: null,
|
|
423
|
+
history: [],
|
|
424
|
+
...overrides,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
it('starts a run and says what it is doing, without waiting for it', async () => {
|
|
429
|
+
// Nothing is running when the page opens, which is the only state in which
|
|
430
|
+
// the control is a START rather than a watch.
|
|
431
|
+
let started = false;
|
|
432
|
+
const startExtraction = vi.fn(async () => {
|
|
433
|
+
started = true;
|
|
434
|
+
return run({ status: 'queued' });
|
|
435
|
+
});
|
|
436
|
+
const getExtractionRun = vi.fn(async () =>
|
|
437
|
+
started ? run({ status: 'running', cursor: 4, totals: { ...TOTALS, documentsProcessed: 4 } }) : null,
|
|
438
|
+
);
|
|
439
|
+
render(
|
|
440
|
+
<TrackerPage
|
|
441
|
+
api={makeApi({ startExtraction, getExtractionRun })}
|
|
442
|
+
scope={SCOPE}
|
|
443
|
+
now={NOW}
|
|
444
|
+
/>,
|
|
445
|
+
);
|
|
446
|
+
await userEvent.click(await screen.findByRole('button', { name: /Pull from meetings/i }));
|
|
447
|
+
|
|
448
|
+
expect(startExtraction).toHaveBeenCalledWith({ scope: SCOPE });
|
|
449
|
+
// A fraction of something real, not a spinner. Six minutes of nothing is
|
|
450
|
+
// indistinguishable from a hang.
|
|
451
|
+
expect(await screen.findByText(/4 of 17 meetings/)).toBeInTheDocument();
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it('says the work carries on if you leave, because it does', async () => {
|
|
455
|
+
render(
|
|
456
|
+
<TrackerPage
|
|
457
|
+
api={makeApi({
|
|
458
|
+
startExtraction: async () => run({ status: 'queued' }),
|
|
459
|
+
getExtractionRun: async () => run({ cursor: 2 }),
|
|
460
|
+
})}
|
|
461
|
+
scope={SCOPE}
|
|
462
|
+
now={NOW}
|
|
463
|
+
/>,
|
|
464
|
+
);
|
|
465
|
+
await userEvent.click(await screen.findByRole('button', { name: /Pull from meetings/i }));
|
|
466
|
+
expect(await screen.findByText(/keeps going if you close this page/i)).toBeInTheDocument();
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it('picks up a sweep that was already running when the page opened', async () => {
|
|
470
|
+
render(
|
|
471
|
+
<TrackerPage
|
|
472
|
+
api={makeApi({
|
|
473
|
+
startExtraction: async () => run(),
|
|
474
|
+
getExtractionRun: async () => run({ cursor: 9 }),
|
|
475
|
+
})}
|
|
476
|
+
scope={SCOPE}
|
|
477
|
+
now={NOW}
|
|
478
|
+
/>,
|
|
479
|
+
);
|
|
480
|
+
// Nobody pressed anything. The run exists, so the screen shows it.
|
|
481
|
+
expect(await screen.findByText(/9 of 17 meetings/)).toBeInTheDocument();
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it('SAYS the model did not answer, instead of reporting nothing found', async () => {
|
|
485
|
+
// The lie this bead exists to kill: `fromModel: 0` with no explanation reads
|
|
486
|
+
// exactly like "the model had nothing to add", and that is how a week of
|
|
487
|
+
// rules-only runs got reported as model runs.
|
|
488
|
+
render(
|
|
489
|
+
<TrackerPage
|
|
490
|
+
api={makeApi({
|
|
491
|
+
startExtraction: async () => run(),
|
|
492
|
+
getExtractionRun: async () =>
|
|
493
|
+
run({
|
|
494
|
+
status: 'succeeded',
|
|
495
|
+
cursor: 17,
|
|
496
|
+
finishedAt: '2026-08-10T12:06:00Z',
|
|
497
|
+
leaseUntil: null,
|
|
498
|
+
totals: { ...TOTALS, documentsProcessed: 17, fromRules: 61, fromModel: 0, created: 61, modelCalls: 5, modelFailures: 12 },
|
|
499
|
+
modelIncompleteIds: Array.from({ length: 12 }, (_, index) => `doc_${index}`),
|
|
500
|
+
lastModelFailure: { stage: 'extract', message: 'Request timed out after 60000ms', timedOut: true },
|
|
501
|
+
}),
|
|
502
|
+
})}
|
|
503
|
+
scope={SCOPE}
|
|
504
|
+
now={NOW}
|
|
505
|
+
/>,
|
|
506
|
+
);
|
|
507
|
+
expect(await screen.findByText(/the model did not answer/i)).toBeInTheDocument();
|
|
508
|
+
expect(screen.getByText(/timed out/i)).toBeInTheDocument();
|
|
509
|
+
expect(screen.getByText(/12 meetings/)).toBeInTheDocument();
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
it('offers to resume a sweep whose driver went away, rather than spinning', async () => {
|
|
513
|
+
render(
|
|
514
|
+
<TrackerPage
|
|
515
|
+
api={makeApi({
|
|
516
|
+
startExtraction: async () => run(),
|
|
517
|
+
getExtractionRun: async () =>
|
|
518
|
+
run({ status: 'running', cursor: 9, leaseUntil: '2026-08-10T11:00:00Z' }),
|
|
519
|
+
})}
|
|
520
|
+
scope={SCOPE}
|
|
521
|
+
now={NOW}
|
|
522
|
+
/>,
|
|
523
|
+
);
|
|
524
|
+
expect(await screen.findByText(/stopped/i)).toBeInTheDocument();
|
|
525
|
+
expect(await screen.findByRole('button', { name: /Resume/i })).toBeInTheDocument();
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
it('picks a stalled sweep back up without anybody pressing anything', async () => {
|
|
529
|
+
// A sweep is about ten minutes and the token that starts it lives thirty, so
|
|
530
|
+
// a run begun late in a session stops mid-way with its cursor intact. All it
|
|
531
|
+
// needs is a live bearer, and asking a person to click for that is asking
|
|
532
|
+
// them to do the machine's bookkeeping. The SERVER still owns the cursor —
|
|
533
|
+
// this only re-presents a credential.
|
|
534
|
+
const startExtraction = vi.fn(async () => run({ cursor: 9 }));
|
|
535
|
+
render(
|
|
536
|
+
<TrackerPage
|
|
537
|
+
api={makeApi({
|
|
538
|
+
startExtraction,
|
|
539
|
+
getExtractionRun: async () =>
|
|
540
|
+
run({ status: 'running', cursor: 9, leaseUntil: '2026-08-10T11:00:00Z' }),
|
|
541
|
+
})}
|
|
542
|
+
scope={SCOPE}
|
|
543
|
+
now={NOW}
|
|
544
|
+
pollMs={5}
|
|
545
|
+
maxAutoResumes={2}
|
|
546
|
+
/>,
|
|
547
|
+
);
|
|
548
|
+
await screen.findByText(/stopped/i);
|
|
549
|
+
await new Promise((resolve) => setTimeout(resolve, 120));
|
|
550
|
+
// Bounded: two, not one per poll for ever.
|
|
551
|
+
expect(startExtraction.mock.calls.length).toBe(2);
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
it('does NOT auto-resume a sweep that failed — it said why, and that should be read', async () => {
|
|
555
|
+
const startExtraction = vi.fn(async () => run());
|
|
556
|
+
render(
|
|
557
|
+
<TrackerPage
|
|
558
|
+
api={makeApi({
|
|
559
|
+
startExtraction,
|
|
560
|
+
getExtractionRun: async () =>
|
|
561
|
+
run({ status: 'failed', cursor: 9, leaseUntil: null, error: 'entities responded 502' }),
|
|
562
|
+
})}
|
|
563
|
+
scope={SCOPE}
|
|
564
|
+
now={NOW}
|
|
565
|
+
pollMs={5}
|
|
566
|
+
/>,
|
|
567
|
+
);
|
|
568
|
+
await screen.findByText(/502/);
|
|
569
|
+
await new Promise((resolve) => setTimeout(resolve, 80));
|
|
570
|
+
expect(startExtraction).not.toHaveBeenCalled();
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
it('reports a sweep that failed, with the reason and what it kept', async () => {
|
|
574
|
+
render(
|
|
575
|
+
<TrackerPage
|
|
576
|
+
api={makeApi({
|
|
577
|
+
startExtraction: async () => run(),
|
|
578
|
+
getExtractionRun: async () =>
|
|
579
|
+
run({
|
|
580
|
+
status: 'failed',
|
|
581
|
+
cursor: 9,
|
|
582
|
+
leaseUntil: null,
|
|
583
|
+
finishedAt: '2026-08-10T12:04:00Z',
|
|
584
|
+
error: 'api/v1/entities responded 502',
|
|
585
|
+
totals: { ...TOTALS, documentsProcessed: 9, created: 31 },
|
|
586
|
+
}),
|
|
587
|
+
})}
|
|
588
|
+
scope={SCOPE}
|
|
589
|
+
now={NOW}
|
|
590
|
+
/>,
|
|
591
|
+
);
|
|
592
|
+
expect(await screen.findByText(/502/)).toBeInTheDocument();
|
|
593
|
+
// Partial progress is progress and the screen has to say so, or somebody
|
|
594
|
+
// starts over by hand.
|
|
595
|
+
expect(screen.getByText(/9 of 17/)).toBeInTheDocument();
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
it('stops polling once the run is over, and does not loop', async () => {
|
|
599
|
+
// The same failure this component already shipped once, in a new place: a
|
|
600
|
+
// poll whose dependencies churn is a request every render. A finished run is
|
|
601
|
+
// a record, not a subscription.
|
|
602
|
+
const getExtractionRun = vi.fn(async () =>
|
|
603
|
+
run({ status: 'succeeded', cursor: 17, finishedAt: '2026-08-10T12:06:00Z', leaseUntil: null }),
|
|
604
|
+
);
|
|
605
|
+
render(
|
|
606
|
+
<TrackerPage
|
|
607
|
+
api={makeApi({ startExtraction: async () => run(), getExtractionRun })}
|
|
608
|
+
scope={SCOPE}
|
|
609
|
+
now={NOW}
|
|
610
|
+
pollMs={5}
|
|
611
|
+
/>,
|
|
612
|
+
);
|
|
613
|
+
await screen.findByText('Chase the inventor for comments on 2148');
|
|
614
|
+
await new Promise((resolve) => setTimeout(resolve, 120));
|
|
615
|
+
// 24 polls would have happened at 5ms if it had not stopped.
|
|
616
|
+
expect(getExtractionRun.mock.calls.length).toBeLessThanOrEqual(2);
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
it('still works on a deployment that only has the synchronous call', async () => {
|
|
620
|
+
const extract = vi.fn(async () => ({
|
|
621
|
+
documentsProcessed: 1, documentIds: ['doc_1'], modelFailure: null, totalDocuments: 1,
|
|
622
|
+
nextOffset: null, fromRules: 1, fromModel: 0, ungrounded: 0, unconfirmed: 0, verified: true,
|
|
623
|
+
created: 1, merged: 0, resolved: 0, needsReview: [], droppedOutOfScope: 0,
|
|
624
|
+
provider: { provider: 'anthropic', model: 'claude-sonnet-5', live: true },
|
|
625
|
+
}));
|
|
626
|
+
render(<TrackerPage api={makeApi({ extract })} scope={SCOPE} now={NOW} />);
|
|
627
|
+
await userEvent.click(await screen.findByRole('button', { name: /Pull from meetings/i }));
|
|
628
|
+
expect(extract).toHaveBeenCalled();
|
|
629
|
+
expect(await screen.findByText(/Read 1 meeting/)).toBeInTheDocument();
|
|
630
|
+
});
|
|
631
|
+
});
|