|
4 | 4 | package main |
5 | 5 |
|
6 | 6 | import ( |
| 7 | + "context" |
| 8 | + "crypto/sha256" |
| 9 | + "encoding/hex" |
| 10 | + "net/http" |
| 11 | + "net/http/httptest" |
| 12 | + "path/filepath" |
| 13 | + "sync" |
7 | 14 | "testing" |
8 | 15 | "time" |
9 | 16 | ) |
@@ -65,22 +72,144 @@ func TestProgressHubDropsWhenFull(t *testing.T) { |
65 | 72 | // TestEmitInstallProgressPublishesToHub verifies the executor helper both |
66 | 73 | // notifies and feeds the hub. |
67 | 74 | func TestEmitInstallProgressPublishesToHub(t *testing.T) { |
68 | | - e := &Executor{progress: newProgressHub()} |
| 75 | + var gotParams map[string]any |
| 76 | + e := &Executor{ |
| 77 | + progress: newProgressHub(), |
| 78 | + emit: func(_ string, params any) { |
| 79 | + gotParams, _ = params.(map[string]any) |
| 80 | + }, |
| 81 | + } |
69 | 82 | ch, cancel := e.progress.subscribe("ollama") |
70 | 83 | defer cancel() |
71 | 84 |
|
72 | | - e.emitInstallProgress("ollama", "installing", 75) |
| 85 | + e.emitInstallProgress("ollama", "downloading", 42) |
73 | 86 |
|
| 87 | + if gotParams["stage"] != "downloading" || gotParams["percent"] != 42 { |
| 88 | + t.Fatalf("unexpected notification params: %+v", gotParams) |
| 89 | + } |
74 | 90 | select { |
75 | 91 | case ev := <-ch: |
76 | | - if ev.Op != "install" || ev.Stage != "installing" || ev.Percent != 75 { |
| 92 | + if ev.Op != "install" || ev.Stage != "downloading" || ev.Percent != 42 { |
77 | 93 | t.Fatalf("unexpected event: %+v", ev) |
78 | 94 | } |
79 | 95 | case <-time.After(time.Second): |
80 | 96 | t.Fatal("emitInstallProgress did not publish to the hub") |
81 | 97 | } |
82 | 98 | } |
83 | 99 |
|
| 100 | +// TestEmitInstallProgressOmitsIndeterminatePercent verifies an unmeasurable |
| 101 | +// install step (percent 0) reaches the wire without a percent, so the UI shows |
| 102 | +// the stage alone instead of a made-up number. |
| 103 | +func TestEmitInstallProgressOmitsIndeterminatePercent(t *testing.T) { |
| 104 | + var gotParams map[string]any |
| 105 | + e := &Executor{ |
| 106 | + progress: newProgressHub(), |
| 107 | + emit: func(_ string, params any) { |
| 108 | + gotParams, _ = params.(map[string]any) |
| 109 | + }, |
| 110 | + } |
| 111 | + ch, cancel := e.progress.subscribe("ollama") |
| 112 | + defer cancel() |
| 113 | + |
| 114 | + e.emitInstallProgress("ollama", "installing", 0) |
| 115 | + |
| 116 | + if gotParams["engine"] != "ollama" || gotParams["stage"] != "installing" { |
| 117 | + t.Fatalf("unexpected notification params: %+v", gotParams) |
| 118 | + } |
| 119 | + if _, ok := gotParams["percent"]; ok { |
| 120 | + t.Fatalf("indeterminate install progress carried a percent: %+v", gotParams) |
| 121 | + } |
| 122 | + select { |
| 123 | + case ev := <-ch: |
| 124 | + if ev.Stage != "installing" || ev.Percent != 0 { |
| 125 | + t.Fatalf("unexpected hub event: %+v", ev) |
| 126 | + } |
| 127 | + case <-time.After(time.Second): |
| 128 | + t.Fatal("emitInstallProgress did not publish to the hub") |
| 129 | + } |
| 130 | +} |
| 131 | + |
| 132 | +// TestInstallFlowReportsOnlyMeasurablePercents drives a full fetch+run install |
| 133 | +// and checks that only measurable (download) and terminal frames carry a |
| 134 | +// percent; the verified and installing steps are indeterminate. |
| 135 | +func TestInstallFlowReportsOnlyMeasurablePercents(t *testing.T) { |
| 136 | + payload := []byte("engine payload") |
| 137 | + sum := sha256.Sum256(payload) |
| 138 | + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { |
| 139 | + _, _ = w.Write(payload) |
| 140 | + })) |
| 141 | + defer srv.Close() |
| 142 | + bin := filepath.Join(t.TempDir(), "engine"+exeExt()) |
| 143 | + m := &Manifest{ |
| 144 | + Engine: "fake", DisplayName: "Fake", ManifestVersion: 1, |
| 145 | + Platforms: map[string]Platform{hostKey(): { |
| 146 | + Detect: []string{bin}, |
| 147 | + Install: &Install{ |
| 148 | + Fetch: &Fetch{URL: srv.URL, SHA256: hex.EncodeToString(sum[:])}, |
| 149 | + Run: []string{fakeEngineBin, "touch", bin}, |
| 150 | + Mode: "user", |
| 151 | + }, |
| 152 | + Runtime: Runtime{Bin: fakeEngineBin}, |
| 153 | + }}, |
| 154 | + } |
| 155 | + |
| 156 | + var mu sync.Mutex |
| 157 | + var frames []map[string]any |
| 158 | + reg := NewRegistry() |
| 159 | + reg.engines[m.Engine] = m |
| 160 | + ex := NewExecutor(reg, NewReporter(nil), func(method string, params any) { |
| 161 | + if method != "engine:install-progress" { |
| 162 | + return |
| 163 | + } |
| 164 | + p, _ := params.(map[string]any) |
| 165 | + mu.Lock() |
| 166 | + frames = append(frames, p) |
| 167 | + mu.Unlock() |
| 168 | + }, t.TempDir()) |
| 169 | + ex.detectTimeout = 2 * time.Second |
| 170 | + |
| 171 | + if err := ex.Install(context.Background(), m.Engine); err != nil { |
| 172 | + t.Fatalf("install: %v", err) |
| 173 | + } |
| 174 | + |
| 175 | + mu.Lock() |
| 176 | + defer mu.Unlock() |
| 177 | + stages := map[string]bool{} |
| 178 | + measuredDownload := false |
| 179 | + for _, f := range frames { |
| 180 | + stage, _ := f["stage"].(string) |
| 181 | + stages[stage] = true |
| 182 | + pct, hasPct := f["percent"] |
| 183 | + switch stage { |
| 184 | + case "downloading": |
| 185 | + if hasPct { |
| 186 | + if p, _ := pct.(int); p <= 0 { |
| 187 | + t.Errorf("downloading frame carried a non-positive percent: %+v", f) |
| 188 | + } |
| 189 | + measuredDownload = true |
| 190 | + } |
| 191 | + case "verified", "installing": |
| 192 | + if hasPct { |
| 193 | + t.Errorf("%s frame carried a percent: %+v", stage, f) |
| 194 | + } |
| 195 | + case "done": |
| 196 | + if pct != 100 { |
| 197 | + t.Errorf("done frame percent = %v, want 100", pct) |
| 198 | + } |
| 199 | + default: |
| 200 | + t.Errorf("unexpected install stage %q: %+v", stage, f) |
| 201 | + } |
| 202 | + } |
| 203 | + if !measuredDownload { |
| 204 | + t.Errorf("no downloading frame carried a measured percent; got %+v", frames) |
| 205 | + } |
| 206 | + for _, want := range []string{"downloading", "verified", "installing", "done"} { |
| 207 | + if !stages[want] { |
| 208 | + t.Errorf("missing %q frame; got %+v", want, frames) |
| 209 | + } |
| 210 | + } |
| 211 | +} |
| 212 | + |
84 | 213 | // TestEmitPullProgressNotifiesAndPublishes verifies the pull helper emits the |
85 | 214 | // local engine:pull-progress notification (with op/message) and also feeds the |
86 | 215 | // hub, so a local pull surfaces progress like a remote pull. |
|
0 commit comments