| 1 | //go:build darwin |
| 2 | |
| 3 | package main |
| 4 | |
| 5 | import ( |
| 6 | "os" |
| 7 | "path/filepath" |
| 8 | "strings" |
| 9 | "testing" |
| 10 | |
| 11 | "reasonix/internal/repair" |
| 12 | ) |
| 13 | |
| 14 | func TestMacUpdateHandoffParserRequiresOwnerPID(t *testing.T) { |
| 15 | base := []string{ |
| 16 | "-to-version", "v2", |
| 17 | "-created-at", "2026-07-28T00:00:00Z", |
| 18 | "-transaction-id", strings.Repeat("a", 64), |
| 19 | } |
| 20 | if _, err := parseMacUpdateHandoffArgs(base); err == nil || !strings.Contains(err.Error(), "missing required") { |
| 21 | t.Fatalf("handoff without -owner-pid parsed: %v", err) |
| 22 | } |
| 23 | cfg, err := parseMacUpdateHandoffArgs(append(base, "-owner-pid", "4242")) |
| 24 | if err != nil || cfg.OwnerPID != 4242 { |
| 25 | t.Fatalf("cfg=%+v err=%v", cfg, err) |
| 26 | } |
| 27 | } |
| 28 | |
| 29 | func TestMacUpdateHandoffRejectsOwnerPIDMismatchBeforeTouchingBundle(t *testing.T) { |
| 30 | root := t.TempDir() |
| 31 | oldApp := filepath.Join(root, "Reasonix.app") |
| 32 | newApp := filepath.Join(root, "staging", "Reasonix.app") |
| 33 | pending := filepath.Join(root, "pending.json") |
| 34 | logPath := filepath.Join(root, "update.log") |
| 35 | for _, dir := range []string{oldApp, newApp} { |
| 36 | if err := os.MkdirAll(dir, 0o700); err != nil { |
| 37 | t.Fatal(err) |
| 38 | } |
| 39 | } |
| 40 | if err := os.WriteFile(filepath.Join(oldApp, "marker"), []byte("old"), 0o600); err != nil { |
| 41 | t.Fatal(err) |
| 42 | } |
| 43 | if err := os.WriteFile(pending, []byte("pending"), 0o600); err != nil { |
| 44 | t.Fatal(err) |
| 45 | } |
| 46 | tx := &repair.UpdateTransaction{ |
| 47 | ToVersion: "v2", |
| 48 | CreatedAt: "2026-07-28T00:00:00Z", |
| 49 | TargetKind: "app-bundle", |
| 50 | TargetPath: oldApp, |
| 51 | BackupPath: oldApp + ".reasonix-update-backup", |
| 52 | HandoffAppPath: newApp, |
| 53 | HandoffStagingPath: filepath.Dir(newApp), |
| 54 | HandoffOwnerPID: 99999999, |
| 55 | } |
| 56 | installMacHandoffTestDeps(t, tx, pending, logPath, nil) |
| 57 | cfg := macHandoffConfigFor(tx) |
| 58 | cfg.OwnerPID = 12345 |
| 59 | if code := runMacUpdateHandoff(cfg); code == 0 { |
| 60 | t.Fatal("handoff accepted a wait pid that differs from the prepared transaction") |
| 61 | } |
| 62 | if got, err := os.ReadFile(filepath.Join(oldApp, "marker")); err != nil || string(got) != "old" { |
| 63 | t.Fatalf("installed bundle changed: %q, %v", got, err) |
| 64 | } |
| 65 | if _, err := os.Stat(pending); err != nil { |
| 66 | t.Fatalf("pending transaction was cleared on identity mismatch: %v", err) |
| 67 | } |
| 68 | logData, err := os.ReadFile(logPath) |
| 69 | if err != nil || !strings.Contains(string(logData), "does not match handoff identity") { |
| 70 | t.Fatalf("handoff log = %q err=%v", logData, err) |
| 71 | } |
| 72 | } |
| 73 |