返回 DeepSeek-Reasonix
session_organization_admission_test.go
根目录 / desktop / session_organization_admission_test.go
1 package main
2
3 import (
4 "bytes"
5 "errors"
6 "os"
7 "path/filepath"
8 "reflect"
9 "testing"
10 "time"
11
12 "reasonix/desktop/internal/workspacestate"
13 "reasonix/internal/agent"
14 "reasonix/internal/config"
15 "reasonix/internal/sessioncatalog"
16 )
17
18 func TestOrganizationAdmissionRejectsAdoptionAfterResolution(t *testing.T) {
19 path := filepath.Join(t.TempDir(), "registry.json")
20 registry := workspacestate.NewStore(path)
21 ctx := t.Context()
22 id := workspacestate.GlobalWorkspaceID
23 if err := registry.EnsureWorkspace(ctx, workspacestate.Workspace{ID: id}); err != nil {
24 t.Fatal(err)
25 }
26 if err := registry.AttachSession(ctx, "", id, "canonical", ""); err != nil {
27 t.Fatal(err)
28 }
29 o, _, err := registry.UpdateOrganization(ctx, id, nil, func(o *workspacestate.Organization) error {
30 return applyOrganizationMutation(o, SessionOrganizationMutation{Kind: "create-group", GroupID: "g", Title: "Group"}, "", "")
31 })
32 if err != nil {
33 t.Fatal(err)
34 }
35 // Force the ordering without timing hooks: resolve an as-yet unadopted
36 // source, commit its adoption, then enter the group mutation transaction.
37 source := &SessionSourceRef{HostID: localDesktopHostID, Path: filepath.Join(t.TempDir(), "old.jsonl")}
38 source.SourceKey = desktopSourceKey(source.Path, "")
39 target := SessionTarget{Source: source, SessionPath: source.Path}
40 if err := registry.RecordSource(ctx, workspacestate.SourceMapping{SourceKey: source.SourceKey, Path: source.Path,
41 WorkspaceID: id, SessionID: "canonical", Fingerprint: "verified-source"}, workspacestate.Presentation{}); err != nil {
42 t.Fatal(err)
43 }
44 before, err := os.ReadFile(path)
45 if err != nil {
46 t.Fatal(err)
47 }
48 _, applied, err := registry.UpdateOrganizationWithState(ctx, id, &o.Revision, func(state *workspacestate.State, o *workspacestate.Organization) error {
49 return applyResolvedOrganizationMutation(state, id, o, []SessionTarget{target},
50 SessionOrganizationMutation{Kind: "set-group", GroupID: "g"}, "source\x00local\x00"+source.SourceKey, "")
51 })
52 if !errors.Is(err, workspacestate.ErrMutationConflict) || applied {
53 t.Fatalf("adopted source admitted: applied=%v err=%v", applied, err)
54 }
55 after, err := os.ReadFile(path)
56 if err != nil || !bytes.Equal(before, after) {
57 t.Fatalf("rejected mutation changed authoritative registry: %v", err)
58 }
59 }
60
61 func TestGroupMutationsAdmitOnlyExplicitSourceAndPreserveUngrouping(t *testing.T) {
62 isolateDesktopUserDirs(t)
63 app := NewApp()
64 t.Cleanup(app.closeSessionServices)
65 app.ctx = t.Context()
66 dir := config.SessionDir()
67 if err := os.MkdirAll(dir, 0700); err != nil {
68 t.Fatal(err)
69 }
70 catalog, err := sessioncatalog.Open(t.Context(), sessioncatalog.Options{Path: filepath.Join(t.TempDir(), "catalog.sqlite"), MetadataOnly: true, StartPaused: true})
71 if err != nil {
72 t.Fatal(err)
73 }
74 app.sessionCatalog.Store(catalog)
75 t.Cleanup(func() { app.desktopSessions.readSnapshots.close(); app.stopSessionCatalog(time.Second) })
76 paths := []string{filepath.Join(dir, "selected.jsonl"), filepath.Join(dir, "unrelated.jsonl")}
77 for i, path := range paths {
78 // Metadata identifies the source. Group commands need no transcript
79 // parsing or execution recovery, even when bodies cannot be decoded.
80 if err := os.WriteFile(path, []byte("unread authoritative body\n"), 0600); err != nil {
81 t.Fatal(err)
82 }
83 if err := agent.SaveBranchMeta(path, agent.BranchMeta{Scope: "global", TopicID: "shared"}); err != nil {
84 t.Fatal(err)
85 }
86 if err := catalog.UpsertSession(t.Context(), sessioncatalog.SessionRecord{Path: path, Directory: dir, Scope: "global",
87 TopicID: "shared", TopicTitle: "Shared", LastActivityAt: int64(i + 1), OrdinaryVisible: true, Health: sessioncatalog.HealthOK}); err != nil {
88 t.Fatal(err)
89 }
90 }
91 workspace := SessionOrganizationWorkspace{Scope: "global"}
92 organization, err := app.GetSessionOrganization(workspace)
93 if err != nil {
94 t.Fatal(err)
95 }
96 mutate := func(m SessionOrganizationMutation) {
97 t.Helper()
98 organization, err = app.UpdateSessionOrganization(workspace, organization.Revision, m)
99 if err != nil || !organization.Applied {
100 t.Fatalf("mutation %+v: %+v %v", m, organization, err)
101 }
102 }
103 mutate(SessionOrganizationMutation{Kind: "create-group", GroupID: "g", Title: "Group"})
104 load := func() workspacestate.Organization {
105 t.Helper()
106 state, err := app.workspaceRegistry().Load(t.Context())
107 if err != nil {
108 t.Fatal(err)
109 }
110 return *state.Workspaces[desktopWorkspaceID("global", "")].Organization
111 }
112 if got := load(); len(got.Imported) != 0 || len(got.Order) != 0 {
113 t.Fatalf("group creation enumerated unrelated sources: %+v", got)
114 }
115 mutate(SessionOrganizationMutation{Kind: "set-group", GroupID: "g", Target: &SessionSelector{SessionPath: paths[0]}})
116 key := "source\x00local\x00" + desktopSourceKey(paths[0], "")
117 if got := load(); len(got.Imported) != 1 || !got.Imported[key] || !reflect.DeepEqual(got.Groups[0].Members, []string{key}) {
118 t.Fatalf("path selector did not admit the exact physical source: %+v", got)
119 }
120 stale, err := app.UpdateSessionOrganization(workspace, organization.Revision-1, SessionOrganizationMutation{Kind: "set-group", GroupID: "g", Target: &SessionSelector{SessionPath: paths[1]}})
121 if err != nil || stale.Applied || len(load().Imported) != 1 {
122 t.Fatalf("stale CAS admitted a new source: %+v %v", stale, err)
123 }
124 _, err = app.UpdateSessionOrganization(workspace, organization.Revision, SessionOrganizationMutation{Kind: "set-group", GroupID: "g",
125 Target: &SessionSelector{SessionPath: filepath.Join(dir, "missing.jsonl")}})
126 if err == nil || len(load().Imported) != 1 {
127 t.Fatal("missing source entered organization")
128 }
129 mutate(SessionOrganizationMutation{Kind: "rename-group", GroupID: "g", Title: "Renamed"})
130 if len(load().Imported) != 1 {
131 t.Fatal("rename enumerated unrelated sources")
132 }
133 mutate(SessionOrganizationMutation{Kind: "set-group", Target: &SessionSelector{Source: &SessionSourceRef{HostID: localDesktopHostID, Path: paths[0]}}})
134 if got := load(); !got.Imported[key] || len(got.Groups[0].Members) != 0 {
135 t.Fatalf("explicit ungrouping lost its import fence: %+v", got)
136 }
137 // Discovering an old topic-wide assignment later must not undo the user's
138 // explicit choice. The unrelated source can still inherit that preference.
139 if err := updateProjectsFile(func(f *desktopProjectFile) (bool, error) {
140 f.GlobalGroups = []desktopGroup{{ID: "g", Title: "Old title", TopicIDs: []string{"shared"}}}
141 return true, nil
142 }); err != nil {
143 t.Fatal(err)
144 }
145 if _, err := app.GetSessionOrganization(workspace); err != nil {
146 t.Fatal(err)
147 }
148 got := load()
149 unrelated := "source\x00local\x00" + desktopSourceKey(paths[1], "")
150 if got.Groups[0].Title != "Renamed" || !reflect.DeepEqual(got.Groups[0].Members, []string{unrelated}) || !got.Imported[key] {
151 t.Fatalf("late preference import overwrote an explicit choice: %+v", got)
152 }
153 }
154
154 lines GO