File size: 4,135 Bytes
ec03da9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
import { describe, expect, it, vi } from 'vitest';
import {
  formatBootProgressLabel,
  resolveBootProgressBar,
  softWaitRatio,
  softStepRatio,
  withTimeout,
  waitForBackendHealthy,
  startProgressTicker,
  withSoftStepProgress,
} from '../src/ui/BootProgress.js';

describe('BootProgress helpers', () => {
  it('formatBootProgressLabel prefers statusText then k/n label', () => {
    expect(formatBootProgressLabel({ statusText: '1/3 Encoding…' })).toBe('1/3 Encoding…');
    expect(formatBootProgressLabel({ step: 2, total: 4, label: 'UMAP' })).toBe('2/4 UMAP');
    expect(formatBootProgressLabel({ label: 'Connecting…' })).toBe('Connecting…');
    expect(formatBootProgressLabel(null)).toBe('');
  });

  it('resolveBootProgressBar maps ratio and indeterminate', () => {
    expect(resolveBootProgressBar({ ratio: 0.66 })).toEqual({
      pct: 66,
      indeterminate: false,
    });
    expect(resolveBootProgressBar({ step: 1, total: 4 })).toEqual({
      pct: 25,
      indeterminate: false,
    });
    expect(resolveBootProgressBar({
      statusText: 'Connecting…',
      indeterminate: true,
    })).toEqual({ pct: 0, indeterminate: true });
  });

  it('softWaitRatio climbs then caps under 1', () => {
    expect(softWaitRatio(0)).toBe(0);
    expect(softWaitRatio(45_000, 90_000)).toBeCloseTo(0.5);
    expect(softWaitRatio(200_000, 90_000)).toBe(0.92);
  });

  it('softStepRatio stays inside the step band and climbs', () => {
    const early = softStepRatio(1, 3, 0, 30_000);
    const mid = softStepRatio(1, 3, 15_000, 30_000);
    const late = softStepRatio(1, 3, 60_000, 30_000);
    expect(early).toBeCloseTo(0);
    expect(mid).toBeGreaterThan(early);
    expect(mid).toBeLessThan(1 / 3);
    expect(late).toBeGreaterThan(mid);
    expect(late).toBeLessThan(1 / 3);
  });

  it('withTimeout resolves fallback when promise is slow', async () => {
    const slow = new Promise(() => {});
    const result = await withTimeout(slow, 20, { ok: false });
    expect(result).toEqual({ ok: false });
  });

  it('withTimeout returns promise value when fast', async () => {
    const result = await withTimeout(Promise.resolve(42), 100, 0);
    expect(result).toBe(42);
  });

  it('startProgressTicker fires while a hung await is in flight', async () => {
    const ticks = [];
    const stop = startProgressTicker(() => ticks.push(Date.now()), 40);
    await new Promise((r) => setTimeout(r, 130));
    stop();
    expect(ticks.length).toBeGreaterThanOrEqual(3);
  });

  it('waitForBackendHealthy polls until ok and reports live progress during hung checks', async () => {
    let n = 0;
    const ticks = [];
    const health = await waitForBackendHealthy({
      attemptTimeoutMs: 80,
      gapMs: 10,
      tickMs: 30,
      maxWaitMs: 3000,
      checkHealth: async () => {
        n += 1;
        // First two attempts hang past abort window → ticker must still move.
        if (n < 3) {
          await new Promise(() => {});
        }
        return { ok: true, data: { model: 'x' } };
      },
      onProgress: (p) => ticks.push({ text: p.statusText, ratio: p.ratio }),
    });
    expect(health.ok).toBe(true);
    expect(n).toBeGreaterThanOrEqual(3);
    expect(ticks.length).toBeGreaterThanOrEqual(4);
    expect(ticks[0].text).toMatch(/Waiting for backend/);
    const ratios = ticks.map((t) => t.ratio);
    expect(Math.max(...ratios)).toBeGreaterThan(ratios[0]);
  });

  it('withSoftStepProgress advances ratio while work runs', async () => {
    const ticks = [];
    const result = await withSoftStepProgress(
      {
        step: 2,
        total: 4,
        label: 'UMAP',
        expectMs: 200,
        tickMs: 30,
        onProgress: (p) => ticks.push(p.ratio),
      },
      async () => {
        await new Promise((r) => setTimeout(r, 100));
        return 'done';
      },
    );
    expect(result).toBe('done');
    expect(ticks.length).toBeGreaterThanOrEqual(2);
    expect(ticks[0]).toBeGreaterThanOrEqual(0.25);
    expect(ticks[0]).toBeLessThan(0.5);
    expect(Math.max(...ticks)).toBeGreaterThan(ticks[0]);
    expect(Math.max(...ticks)).toBeLessThan(0.5);
  });
});