返回 DeepSeek-Reasonix
extension_generation_test.go
根目录 / desktop / extension_generation_test.go
1 package main
2
3 import (
4 "context"
5 "os"
6 "path/filepath"
7 "strings"
8 "testing"
9
10 "reasonix/internal/taskcatalog"
11 )
12
13 func TestExtensionGenerationBumpsOnMCPMutationSites(t *testing.T) {
14 // Guard mutation sites that publish shared Host / config changes while
15 // controller builds may still be running off the lifecycle lock.
16 entries, err := os.ReadDir(".")
17 if err != nil {
18 t.Fatal(err)
19 }
20 var sources []string
21 for _, entry := range entries {
22 name := entry.Name()
23 if entry.IsDir() || !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") {
24 continue
25 }
26 raw, readErr := os.ReadFile(filepath.Join(".", name))
27 if readErr != nil {
28 t.Fatal(readErr)
29 }
30 sources = append(sources, string(raw))
31 }
32 src := strings.Join(sources, "\n")
33 for _, name := range []string{
34 "func (a *App) InstallMCPServer",
35 "func (a *App) UpdateMCPServer",
36 "func (a *App) RemoveMCPServer",
37 "func (a *App) ReconnectMCPServer",
38 "func (a *App) ClearMCPServerAuthentication",
39 } {
40 idx := strings.Index(src, name)
41 if idx < 0 {
42 t.Fatalf("missing %s", name)
43 }
44 rest := src[idx:]
45 next := strings.Index(rest[len(name):], "\nfunc (a *App) ")
46 body := rest
47 if next >= 0 {
48 body = rest[:len(name)+next]
49 }
50 if !strings.Contains(body, "a.bumpExtensionGeneration()") && !strings.Contains(body, "saveDesktopMCPServerAndBump") {
51 t.Fatalf("%s does not bump extensionGeneration", name)
52 }
53 }
54 }
55
56 func TestControllerPublicationRejectsMutationThatCompletesAfterBoot(t *testing.T) {
57 app := NewApp()
58 generation := app.currentExtensionGeneration()
59
60 // Model a build that has finished extension boot while an MCP mutation is
61 // queued. The writer must finish and bump the generation before publication.
62 app.extensionBuildMu.RLock()
63 mutationLocked := make(chan struct{})
64 releaseMutation := make(chan struct{})
65 go func() {
66 unlock := app.lockMCPMutation("test-publication-fence")
67 close(mutationLocked)
68 <-releaseMutation
69 unlock()
70 }()
71 app.extensionBuildMu.RUnlock()
72 <-mutationLocked
73
74 publication := make(chan bool, 1)
75 go func() {
76 unlock, ok := app.lockTabControllerPublication(generation, "global", "")
77 if ok {
78 unlock()
79 }
80 publication <- ok
81 }()
82 close(releaseMutation)
83 if <-publication {
84 t.Fatal("controller published after an MCP mutation changed its extension generation")
85 }
86 }
87
88 func TestTaskActionProjectKeepsAllowlistedRoot(t *testing.T) {
89 root := t.TempDir()
90 app := &App{
91 ctx: context.Background(),
92 tabs: map[string]*WorkspaceTab{
93 "active": {ID: "active", Scope: "project", WorkspaceRoot: root},
94 },
95 activeTabID: "active",
96 }
97 key := taskcatalog.ProjectKey(root)
98 project, err := app.taskActionProject(key)
99 if err != nil {
100 t.Fatal(err)
101 }
102 if abs, err := filepath.Abs(root); err == nil {
103 root = abs
104 }
105 if project.Root != root {
106 t.Fatalf("root = %q, want allowlisted %q", project.Root, root)
107 }
108 }
109
109 lines GO