@startsimpli/integrations 0.2.16 → 0.2.18

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.
@@ -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
+ });