返回 DeepSeek-Reasonix
update_handoff_darwin_smoke_test.go
根目录 / internal / repair / update_handoff_darwin_smoke_test.go
1 //go:build darwin
2
3 package repair
4
5 import (
6 "os"
7 "path/filepath"
8 "testing"
9 "time"
10 )
11
12 // TestDarwinApplicationsOrphanBackupLifecycleSmoke is opt-in because it probes
13 // the real App Management boundary under /Applications. It creates only a
14 // uniquely owned synthetic app directory, never touches an installed app, and
15 // verifies both the macOS no-replace quarantine rename and terminal cleanup.
16 //
17 // Run with:
18 //
19 // REASONIX_MACOS_APPLICATIONS_SMOKE=1 go test ./internal/repair \
20 // -run TestDarwinApplicationsOrphanBackupLifecycleSmoke -count=1
21 func TestDarwinApplicationsOrphanBackupLifecycleSmoke(t *testing.T) {
22 if os.Getenv("REASONIX_MACOS_APPLICATIONS_SMOKE") != "1" {
23 t.Skip("set REASONIX_MACOS_APPLICATIONS_SMOKE=1 to probe /Applications")
24 }
25
26 smokeRoot, err := os.MkdirTemp("/Applications", ".reasonix-updater-smoke-")
27 if err != nil {
28 t.Fatalf("create isolated /Applications smoke root (allow the terminal in App Management): %v", err)
29 }
30 owned, err := os.Lstat(smokeRoot)
31 if err != nil {
32 t.Fatal(err)
33 }
34 t.Cleanup(func() {
35 current, statErr := os.Lstat(smokeRoot)
36 if os.IsNotExist(statErr) {
37 return
38 }
39 if statErr != nil || !os.SameFile(owned, current) {
40 t.Errorf("preserving changed smoke root %s: %v", smokeRoot, statErr)
41 return
42 }
43 if removeErr := os.RemoveAll(smokeRoot); removeErr != nil {
44 t.Errorf("remove owned smoke root %s: %v", smokeRoot, removeErr)
45 }
46 })
47
48 t.Setenv("REASONIX_HOME", t.TempDir())
49 app := filepath.Join(smokeRoot, "Reasonix.app")
50 executable := filepath.Join(app, "Contents", "MacOS", "Reasonix")
51 backup := app + ".reasonix-update-backup"
52 staging, err := os.MkdirTemp("", "reasonix-mac-update-*")
53 if err != nil {
54 t.Fatal(err)
55 }
56 t.Cleanup(func() { _ = os.RemoveAll(staging) })
57 stagedApp := filepath.Join(staging, "Reasonix.app")
58 for _, dir := range []string{filepath.Dir(executable), backup, stagedApp} {
59 if err := os.MkdirAll(dir, 0o700); err != nil {
60 t.Fatal(err)
61 }
62 }
63 if err := os.WriteFile(executable, []byte("synthetic-current"), 0o700); err != nil {
64 t.Fatal(err)
65 }
66 if err := os.WriteFile(filepath.Join(backup, "marker"), []byte("orphan"), 0o600); err != nil {
67 t.Fatal(err)
68 }
69 if err := os.WriteFile(filepath.Join(stagedApp, "marker"), []byte("replacement"), 0o600); err != nil {
70 t.Fatal(err)
71 }
72
73 originalExecutable := repairExecutable
74 repairExecutable = func() (string, error) { return executable, nil }
75 t.Cleanup(func() { repairExecutable = originalExecutable })
76
77 tx, err := PrepareAppBundleUpdateHandoff(
78 "v1", "v2", app, backup, stagedApp, staging, os.Getpid(),
79 )
80 if err != nil {
81 t.Fatalf("prepare handoff through /Applications: %v", err)
82 }
83 if tx.OrphanedBackupPath == "" || tx.OrphanedBackupTreeID == "" {
84 t.Fatal("prepared transaction did not bind the quarantined backup")
85 }
86 if got, err := os.ReadFile(filepath.Join(tx.OrphanedBackupPath, "marker")); err != nil || string(got) != "orphan" {
87 t.Fatalf("quarantined backup marker = %q, %v", got, err)
88 }
89 if _, err := os.Lstat(backup); !os.IsNotExist(err) {
90 t.Fatalf("public backup path still exists after quarantine: %v", err)
91 }
92
93 if _, err := CancelPendingAppBundleUpdateHandoffExact(tx, 5*time.Second); err != nil {
94 t.Fatalf("finish smoke transaction: %v", err)
95 }
96 if _, err := os.Lstat(tx.OrphanedBackupPath); !os.IsNotExist(err) {
97 t.Fatalf("terminal cleanup retained the owned quarantine: %v", err)
98 }
99 if _, err := os.Lstat(PendingUpdatePath()); !os.IsNotExist(err) {
100 t.Fatalf("terminal cleanup retained pending-update.json: %v", err)
101 }
102 }
103
103 lines GO