返回 DeepSeek-Reasonix
update_legacy_test.go
根目录 / internal / repair / update_legacy_test.go
1 package repair
2
3 import (
4 "os"
5 "path/filepath"
6 "runtime"
7 "strings"
8 "testing"
9 "time"
10
11 "reasonix/internal/installlayout"
12 )
13
14 type supersededUpdateFixture struct {
15 home string
16 root string
17 target string
18 active string
19 backup string
20 pendingBody []byte
21 transaction *UpdateTransaction
22 running string
23 }
24
25 type supersededAppUpdateFixture struct {
26 home string
27 target string
28 backup string
29 marker string
30 pendingBody []byte
31 transaction *UpdateTransaction
32 }
33
34 func prepareSupersededAppUpdateFixture(t *testing.T, withBackup, withBackupIdentity bool) supersededAppUpdateFixture {
35 t.Helper()
36 home := t.TempDir()
37 t.Setenv("REASONIX_HOME", home)
38 root := t.TempDir()
39 if resolved, err := filepath.EvalSymlinks(root); err == nil {
40 root = resolved
41 }
42 target := filepath.Join(root, "Reasonix.app")
43 marker := filepath.Join(target, "Contents", "Resources", "release.txt")
44 executable := filepath.Join(target, "Contents", "MacOS", "reasonix-desktop")
45 for _, path := range []string{marker, executable} {
46 if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
47 t.Fatal(err)
48 }
49 if err := os.WriteFile(path, []byte("healthy current app"), 0o700); err != nil {
50 t.Fatal(err)
51 }
52 }
53 backup := target + ".reasonix-update-backup"
54 if withBackup {
55 backupMarker := filepath.Join(backup, "Contents", "Resources", "release.txt")
56 if err := os.MkdirAll(filepath.Dir(backupMarker), 0o755); err != nil {
57 t.Fatal(err)
58 }
59 if err := os.WriteFile(backupMarker, []byte("preserved previous app"), 0o600); err != nil {
60 t.Fatal(err)
61 }
62 }
63 originalExecutable := repairExecutable
64 repairExecutable = func() (string, error) { return executable, nil }
65 t.Cleanup(func() { repairExecutable = originalExecutable })
66 tx := &UpdateTransaction{
67 SchemaVersion: updateTransactionVersion,
68 FromVersion: "v1.19.5",
69 ToVersion: "v1.19.7",
70 Platform: runtime.GOOS + "/amd64",
71 TargetKind: "app-bundle",
72 TargetPath: target,
73 BackupPath: backup,
74 CreatedAt: time.Now().UTC().Format(time.RFC3339Nano),
75 }
76 if withBackupIdentity {
77 var err error
78 tx.BackupTreeID, err = repairPlanTreeContentStateID(backup)
79 if err != nil {
80 t.Fatal(err)
81 }
82 }
83 if err := createPendingUpdate(tx); err != nil {
84 t.Fatal(err)
85 }
86 pendingBody, err := os.ReadFile(PendingUpdatePath())
87 if err != nil {
88 t.Fatal(err)
89 }
90 return supersededAppUpdateFixture{
91 home: home, target: target, backup: backup, marker: marker,
92 pendingBody: pendingBody, transaction: tx,
93 }
94 }
95
96 func TestArchiveSupersededPendingAppBundleUpdatePreservesLegacyBackup(t *testing.T) {
97 fixture := prepareSupersededAppUpdateFixture(t, true, false)
98 archived, err := ArchiveSupersededPendingAppBundleUpdate("v1.19.5")
99 if err != nil || !archived {
100 t.Fatalf("archived=%v err=%v", archived, err)
101 }
102 if _, err := os.Lstat(PendingUpdatePath()); !os.IsNotExist(err) {
103 t.Fatalf("pending transaction survived: %v", err)
104 }
105 if _, err := os.Lstat(fixture.backup); !os.IsNotExist(err) {
106 t.Fatalf("public rollback path survived: %v", err)
107 }
108 backups, err := filepath.Glob(fixture.backup + ".reasonix-retired-*")
109 if err != nil || len(backups) != 1 {
110 t.Fatalf("archived backups=%v err=%v", backups, err)
111 }
112 if body, err := os.ReadFile(filepath.Join(backups[0], "Contents", "Resources", "release.txt")); err != nil || string(body) != "preserved previous app" {
113 t.Fatalf("archived backup body=%q err=%v", body, err)
114 }
115 entries, err := os.ReadDir(filepath.Join(fixture.home, "repair", "legacy-updates"))
116 if err != nil || len(entries) != 1 {
117 t.Fatalf("transaction archives=%v err=%v", entries, err)
118 }
119 if body, err := os.ReadFile(fixture.marker); err != nil || string(body) != "healthy current app" {
120 t.Fatalf("running app changed=%q err=%v", body, err)
121 }
122 }
123
124 func TestArchiveSupersededPendingAppBundleUpdateHealsMissingBackup(t *testing.T) {
125 fixture := prepareSupersededAppUpdateFixture(t, false, false)
126 fixture.transaction.BackupTreeID = strings.Repeat("a", 64)
127 if err := overwritePendingUpdateForTest(fixture.transaction); err != nil {
128 t.Fatal(err)
129 }
130 archived, err := ArchiveSupersededPendingAppBundleUpdate("v1.19.5")
131 if err != nil || !archived {
132 t.Fatalf("archived=%v err=%v", archived, err)
133 }
134 if _, err := os.Lstat(PendingUpdatePath()); !os.IsNotExist(err) {
135 t.Fatalf("missing-backup transaction survived: %v", err)
136 }
137 }
138
139 func TestArchiveSupersededPendingAppBundleUpdateLeavesModernRollbackForHealthCommit(t *testing.T) {
140 prepareSupersededAppUpdateFixture(t, true, true)
141 archived, err := ArchiveSupersededPendingAppBundleUpdate("v1.19.7")
142 if err != nil || archived {
143 t.Fatalf("modern transaction archived=%v err=%v", archived, err)
144 }
145 if _, err := ReadPendingUpdate(); err != nil {
146 t.Fatalf("modern pending transaction changed: %v", err)
147 }
148 }
149
150 func TestArchiveSupersededPendingAppBundleUpdateLeavesCurrentProcessHandoff(t *testing.T) {
151 fixture := prepareSupersededAppUpdateFixture(t, false, false)
152 fixture.transaction.HandoffOwnerPID = os.Getpid()
153 fixture.transaction.HandoffStagingPath = filepath.Join(filepath.Dir(fixture.target), "handoff-stage")
154 fixture.transaction.HandoffAppPath = filepath.Join(fixture.transaction.HandoffStagingPath, "Reasonix.app")
155 fixture.transaction.HandoffAppTreeID = strings.Repeat("b", 64)
156 fixture.transaction.HandoffStagingTreeID = strings.Repeat("c", 64)
157 if err := overwritePendingUpdateForTest(fixture.transaction); err != nil {
158 t.Fatal(err)
159 }
160 archived, err := ArchiveSupersededPendingAppBundleUpdate("v1.19.5")
161 if err != nil || archived {
162 t.Fatalf("current handoff archived=%v err=%v", archived, err)
163 }
164 if _, err := readPendingUpdateUnchecked(); err != nil {
165 t.Fatalf("current handoff transaction changed: %v", err)
166 }
167 }
168
169 func TestArchiveSupersededPendingAppBundleUpdateRejectsUnrelatedOlderVersion(t *testing.T) {
170 fixture := prepareSupersededAppUpdateFixture(t, true, false)
171 archived, err := ArchiveSupersededPendingAppBundleUpdate("v1.19.4")
172 if err == nil || archived {
173 t.Fatalf("older running version archived=%v err=%v", archived, err)
174 }
175 assertPendingTransactionUnchanged(t, fixture.pendingBody)
176 }
177
178 func TestArchiveSupersededPendingAppBundleUpdateRestoresStateOnTargetDrift(t *testing.T) {
179 fixture := prepareSupersededAppUpdateFixture(t, true, false)
180 originalHook := supersededAppUpdateAfterBackupArchive
181 supersededAppUpdateAfterBackupArchive = func(string) {
182 if err := os.WriteFile(fixture.marker, []byte("changed concurrently"), 0o600); err != nil {
183 t.Fatal(err)
184 }
185 }
186 t.Cleanup(func() { supersededAppUpdateAfterBackupArchive = originalHook })
187 archived, err := ArchiveSupersededPendingAppBundleUpdate("v1.19.5")
188 if err == nil || archived {
189 t.Fatalf("drifted target archived=%v err=%v", archived, err)
190 }
191 if _, err := ReadPendingUpdate(); err != nil {
192 t.Fatalf("pending transaction was not restored: %v", err)
193 }
194 if body, err := os.ReadFile(filepath.Join(fixture.backup, "Contents", "Resources", "release.txt")); err != nil || string(body) != "preserved previous app" {
195 t.Fatalf("rollback backup was not restored=%q err=%v", body, err)
196 }
197 }
198
199 func TestArchiveSupersededPendingAppBundleUpdatePreservesRecreatedBackupPath(t *testing.T) {
200 fixture := prepareSupersededAppUpdateFixture(t, true, false)
201 originalHook := supersededAppUpdateAfterBackupArchive
202 supersededAppUpdateAfterBackupArchive = func(string) {
203 marker := filepath.Join(fixture.backup, "Contents", "Resources", "release.txt")
204 if err := os.MkdirAll(filepath.Dir(marker), 0o755); err != nil {
205 t.Fatal(err)
206 }
207 if err := os.WriteFile(marker, []byte("concurrently recreated backup"), 0o600); err != nil {
208 t.Fatal(err)
209 }
210 }
211 t.Cleanup(func() { supersededAppUpdateAfterBackupArchive = originalHook })
212
213 archived, err := ArchiveSupersededPendingAppBundleUpdate("v1.19.5")
214 if err == nil || archived {
215 t.Fatalf("recreated backup archived=%v err=%v", archived, err)
216 }
217 if _, err := ReadPendingUpdate(); err != nil {
218 t.Fatalf("pending transaction was not restored: %v", err)
219 }
220 if body, err := os.ReadFile(filepath.Join(fixture.backup, "Contents", "Resources", "release.txt")); err != nil || string(body) != "concurrently recreated backup" {
221 t.Fatalf("recreated public backup changed=%q err=%v", body, err)
222 }
223 backups, err := filepath.Glob(fixture.backup + ".reasonix-retired-*")
224 if err != nil || len(backups) != 1 {
225 t.Fatalf("preserved archived backups=%v err=%v", backups, err)
226 }
227 if body, err := os.ReadFile(filepath.Join(backups[0], "Contents", "Resources", "release.txt")); err != nil || string(body) != "preserved previous app" {
228 t.Fatalf("archived original backup changed=%q err=%v", body, err)
229 }
230 }
231
232 func prepareSupersededUpdateFixture(t *testing.T, pendingVersion, runningVersion string) supersededUpdateFixture {
233 t.Helper()
234 home := t.TempDir()
235 t.Setenv("REASONIX_HOME", home)
236 root := t.TempDir()
237 originalExecutable := repairExecutable
238 t.Cleanup(func() { repairExecutable = originalExecutable })
239
240 target := filepath.Join(root, installlayout.DesktopBinaryName())
241 if err := os.WriteFile(target, []byte("old desktop"), 0o700); err != nil {
242 t.Fatal(err)
243 }
244 guardName := "reasonix-guard"
245 if runtime.GOOS == "windows" {
246 guardName += ".exe"
247 }
248 guard := filepath.Join(root, guardName)
249 if err := os.WriteFile(guard, []byte("old guard"), 0o700); err != nil {
250 t.Fatal(err)
251 }
252 repairExecutable = func() (string, error) { return guard, nil }
253 tx, err := PrepareFileUpdate("v1.18.0", pendingVersion, target, guard)
254 if err != nil {
255 t.Fatal(err)
256 }
257 pendingBody, err := os.ReadFile(PendingUpdatePath())
258 if err != nil {
259 t.Fatal(err)
260 }
261
262 runningVersion = canonicalSemver(runningVersion)
263 activeDir := filepath.Join(root, filepath.FromSlash(installlayout.VersionDirRelative(runningVersion)))
264 if err := os.MkdirAll(activeDir, 0o755); err != nil {
265 t.Fatal(err)
266 }
267 active := filepath.Join(activeDir, installlayout.DesktopBinaryName())
268 if err := os.WriteFile(active, []byte("healthy versioned desktop"), 0o700); err != nil {
269 t.Fatal(err)
270 }
271 if err := installlayout.WriteCurrent(root, installlayout.CurrentPointer{
272 ActiveVersion: runningVersion,
273 ActiveDir: installlayout.VersionDirRelative(runningVersion),
274 }); err != nil {
275 t.Fatal(err)
276 }
277 repairExecutable = func() (string, error) { return active, nil }
278
279 return supersededUpdateFixture{
280 home: home, root: root, target: target, active: active,
281 backup: tx.BackupPath, pendingBody: pendingBody, transaction: tx,
282 running: runningVersion,
283 }
284 }
285
286 func TestArchiveSupersededPendingFileUpdateHealsGuardDirectoryMismatch(t *testing.T) {
287 fixture := prepareSupersededUpdateFixture(t, "v1.19.1", "v1.20.0")
288 if _, err := ReadPendingUpdate(); err == nil || !strings.Contains(err.Error(), "outside the current Guard installation") {
289 t.Fatalf("ReadPendingUpdate error = %v, want the reported Guard-directory mismatch", err)
290 }
291
292 archived, err := ArchiveSupersededPendingFileUpdate(fixture.running, fixture.root)
293 if err != nil || !archived {
294 t.Fatalf("archived=%v err=%v", archived, err)
295 }
296 if _, err := os.Lstat(PendingUpdatePath()); !os.IsNotExist(err) {
297 t.Fatalf("pending transaction survived: %v", err)
298 }
299 entries, err := os.ReadDir(filepath.Join(fixture.home, "repair", "legacy-updates"))
300 if err != nil || len(entries) != 1 {
301 t.Fatalf("archive entries=%v err=%v", entries, err)
302 }
303 archivedBody, err := os.ReadFile(filepath.Join(fixture.home, "repair", "legacy-updates", entries[0].Name()))
304 if err != nil || string(archivedBody) != string(fixture.pendingBody) {
305 t.Fatalf("archived transaction changed: err=%v\n got=%q\nwant=%q", err, archivedBody, fixture.pendingBody)
306 }
307 if body, err := os.ReadFile(fixture.backup); err != nil || string(body) != "old desktop" {
308 t.Fatalf("rollback backup=%q err=%v", body, err)
309 }
310 if len(fixture.transaction.Files) != 2 {
311 t.Fatalf("release-unit files=%d, want desktop and Guard", len(fixture.transaction.Files))
312 }
313 if body, err := os.ReadFile(fixture.transaction.Files[1].BackupPath); err != nil || string(body) != "old guard" {
314 t.Fatalf("Guard rollback backup=%q err=%v", body, err)
315 }
316 if body, err := os.ReadFile(fixture.active); err != nil || string(body) != "healthy versioned desktop" {
317 t.Fatalf("active desktop=%q err=%v", body, err)
318 }
319 }
320
321 func TestArchiveSupersededPendingFileUpdateAcceptsSameVersionReinstall(t *testing.T) {
322 fixture := prepareSupersededUpdateFixture(t, "v1.19.3", "v1.19.3")
323 archived, err := ArchiveSupersededPendingFileUpdate(fixture.running, fixture.root)
324 if err != nil || !archived {
325 t.Fatalf("same-version healthy reinstall archived=%v err=%v", archived, err)
326 }
327 }
328
329 func TestArchiveSupersededPendingFileUpdateRejectsOlderRunningVersion(t *testing.T) {
330 fixture := prepareSupersededUpdateFixture(t, "v1.20.0", "v1.19.3")
331 if archived, err := ArchiveSupersededPendingFileUpdate(fixture.running, fixture.root); err == nil || archived {
332 t.Fatalf("archived=%v err=%v", archived, err)
333 }
334 assertPendingTransactionUnchanged(t, fixture.pendingBody)
335 }
336
337 func TestArchiveSupersededPendingFileUpdateRejectsForeignInstall(t *testing.T) {
338 fixture := prepareSupersededUpdateFixture(t, "v1.19.1", "v1.20.0")
339 if archived, err := ArchiveSupersededPendingFileUpdate(fixture.running, t.TempDir()); err == nil || archived {
340 t.Fatalf("archived=%v err=%v", archived, err)
341 }
342 assertPendingTransactionUnchanged(t, fixture.pendingBody)
343 }
344
345 func TestArchiveSupersededPendingFileUpdateRejectsNonActiveExecutable(t *testing.T) {
346 fixture := prepareSupersededUpdateFixture(t, "v1.19.1", "v1.20.0")
347 other := filepath.Join(filepath.Dir(fixture.active), "other-desktop")
348 if err := os.WriteFile(other, []byte("other"), 0o700); err != nil {
349 t.Fatal(err)
350 }
351 repairExecutable = func() (string, error) { return other, nil }
352 if archived, err := ArchiveSupersededPendingFileUpdate(fixture.running, fixture.root); err == nil || archived {
353 t.Fatalf("archived=%v err=%v", archived, err)
354 }
355 assertPendingTransactionUnchanged(t, fixture.pendingBody)
356 }
357
358 func TestArchiveSupersededPendingFileUpdateRejectsPlatformMismatch(t *testing.T) {
359 fixture := prepareSupersededUpdateFixture(t, "v1.19.1", "v1.20.0")
360 changed := *fixture.transaction
361 changed.Platform = "foreign/amd64"
362 if err := overwritePendingUpdateForTest(&changed); err != nil {
363 t.Fatal(err)
364 }
365 changedBody, err := os.ReadFile(PendingUpdatePath())
366 if err != nil {
367 t.Fatal(err)
368 }
369 if archived, err := ArchiveSupersededPendingFileUpdate(fixture.running, fixture.root); err == nil || archived {
370 t.Fatalf("archived=%v err=%v", archived, err)
371 }
372 assertPendingTransactionUnchanged(t, changedBody)
373 }
374
375 func TestArchiveSupersededPendingFileUpdateRejectsNestedLegacyTarget(t *testing.T) {
376 fixture := prepareSupersededUpdateFixture(t, "v1.19.1", "v1.20.0")
377 nestedDir := filepath.Join(fixture.root, "other")
378 if err := os.MkdirAll(nestedDir, 0o755); err != nil {
379 t.Fatal(err)
380 }
381 nestedTarget := filepath.Join(nestedDir, installlayout.DesktopBinaryName())
382 changed := *fixture.transaction
383 changed.TargetPath = nestedTarget
384 changed.Files = append([]UpdateTransactionFile(nil), fixture.transaction.Files...)
385 for i := range changed.Files {
386 changed.Files[i].TargetPath = nestedTarget
387 }
388 if err := overwritePendingUpdateForTest(&changed); err != nil {
389 t.Fatal(err)
390 }
391 changedBody, err := os.ReadFile(PendingUpdatePath())
392 if err != nil {
393 t.Fatal(err)
394 }
395 if archived, err := ArchiveSupersededPendingFileUpdate(fixture.running, fixture.root); err == nil || archived {
396 t.Fatalf("archived=%v err=%v", archived, err)
397 }
398 assertPendingTransactionUnchanged(t, changedBody)
399 }
400
401 func TestArchiveSupersededPendingFileUpdateDetectsConcurrentReplacement(t *testing.T) {
402 fixture := prepareSupersededUpdateFixture(t, "v1.19.1", "v1.20.0")
403 originalAcquire := acquirePendingUpdateLock
404 attempted := make(chan struct{})
405 release := make(chan struct{})
406 acquirePendingUpdateLock = func() (func(), error) {
407 close(attempted)
408 <-release
409 return func() {}, nil
410 }
411 t.Cleanup(func() { acquirePendingUpdateLock = originalAcquire })
412
413 type outcome struct {
414 archived bool
415 err error
416 }
417 done := make(chan outcome, 1)
418 go func() {
419 archived, err := ArchiveSupersededPendingFileUpdate(fixture.running, fixture.root)
420 done <- outcome{archived: archived, err: err}
421 }()
422 <-attempted
423 changed := *fixture.transaction
424 changed.CreatedAt = time.Now().UTC().Add(time.Second).Format(time.RFC3339Nano)
425 if err := overwritePendingUpdateForTest(&changed); err != nil {
426 t.Fatal(err)
427 }
428 close(release)
429 result := <-done
430 if result.err == nil || result.archived || !strings.Contains(result.err.Error(), "transaction changed while waiting") {
431 t.Fatalf("archived=%v err=%v", result.archived, result.err)
432 }
433 current, err := readPendingUpdateUnchecked()
434 if err != nil || UpdateTransactionID(current) != UpdateTransactionID(&changed) {
435 t.Fatalf("concurrent transaction was not preserved: current=%+v err=%v", current, err)
436 }
437 }
438
439 func TestArchiveSupersededPendingFileUpdateRestoresLateConcurrentRewrite(t *testing.T) {
440 fixture := prepareSupersededUpdateFixture(t, "v1.19.1", "v1.20.0")
441 changed := *fixture.transaction
442 changed.CreatedAt = time.Now().UTC().Add(2 * time.Second).Format(time.RFC3339Nano)
443 originalHook := supersededUpdateBeforeArchive
444 supersededUpdateBeforeArchive = func(string) {
445 if err := overwritePendingUpdateForTest(&changed); err != nil {
446 t.Fatalf("rewrite pending transaction: %v", err)
447 }
448 }
449 t.Cleanup(func() { supersededUpdateBeforeArchive = originalHook })
450
451 archived, err := ArchiveSupersededPendingFileUpdate(fixture.running, fixture.root)
452 if err == nil || archived || !strings.Contains(err.Error(), "transaction changed before archival") {
453 t.Fatalf("archived=%v err=%v", archived, err)
454 }
455 current, readErr := readPendingUpdateUnchecked()
456 if readErr != nil || UpdateTransactionID(current) != UpdateTransactionID(&changed) {
457 t.Fatalf("late replacement was not restored: current=%+v err=%v", current, readErr)
458 }
459 entries, readDirErr := os.ReadDir(filepath.Join(fixture.home, "repair", "legacy-updates"))
460 if readDirErr != nil || len(entries) != 0 {
461 t.Fatalf("unexpected archive after restored rewrite: entries=%v err=%v", entries, readDirErr)
462 }
463 }
464
465 func TestArchiveSupersededPendingFileUpdateSerializesConcurrentRecovery(t *testing.T) {
466 fixture := prepareSupersededUpdateFixture(t, "v1.19.1", "v1.20.0")
467 start := make(chan struct{})
468 type outcome struct {
469 archived bool
470 err error
471 }
472 done := make(chan outcome, 2)
473 for range 2 {
474 go func() {
475 <-start
476 archived, err := ArchiveSupersededPendingFileUpdate(fixture.running, fixture.root)
477 done <- outcome{archived: archived, err: err}
478 }()
479 }
480 close(start)
481 archivedCount := 0
482 for range 2 {
483 result := <-done
484 if result.err != nil {
485 t.Fatalf("concurrent recovery error: %v", result.err)
486 }
487 if result.archived {
488 archivedCount++
489 }
490 }
491 if archivedCount != 1 {
492 t.Fatalf("archived count=%d, want exactly one", archivedCount)
493 }
494 entries, err := os.ReadDir(filepath.Join(fixture.home, "repair", "legacy-updates"))
495 if err != nil || len(entries) != 1 {
496 t.Fatalf("archive entries=%v err=%v", entries, err)
497 }
498 }
499
500 func TestArchiveSupersededPendingFileUpdateRejectsMalformedTransaction(t *testing.T) {
501 t.Setenv("REASONIX_HOME", t.TempDir())
502 body := `{"schemaVersion":1,"toVersion":`
503 writePendingUpdateRaw(t, body)
504 if archived, err := ArchiveSupersededPendingFileUpdate("v1.20.0", t.TempDir()); err == nil || archived {
505 t.Fatalf("archived=%v err=%v", archived, err)
506 }
507 got, err := os.ReadFile(PendingUpdatePath())
508 if err != nil || string(got) != body {
509 t.Fatalf("malformed transaction changed: got=%q err=%v", got, err)
510 }
511 }
512
513 func assertPendingTransactionUnchanged(t *testing.T, want []byte) {
514 t.Helper()
515 got, err := os.ReadFile(PendingUpdatePath())
516 if err != nil || string(got) != string(want) {
517 t.Fatalf("pending transaction changed: err=%v\n got=%q\nwant=%q", err, got, want)
518 }
519 }
520
521 func TestSupersededUpdateFixtureUsesCurrentPlatform(t *testing.T) {
522 fixture := prepareSupersededUpdateFixture(t, "v1.19.1", "v1.20.0")
523 if !strings.HasPrefix(fixture.transaction.Platform, runtime.GOOS+"/") {
524 t.Fatalf("transaction platform=%q, want %s", fixture.transaction.Platform, runtime.GOOS)
525 }
526 }
527
527 lines GO