@prmichaelsen/remember-mcp 3.13.0 → 3.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/agent/milestones/milestone-17-remember-core-migration.md +140 -0
  3. package/agent/progress.yaml +123 -6
  4. package/agent/tasks/milestone-17-remember-core-migration/task-193-foundation-setup.md +58 -0
  5. package/agent/tasks/milestone-17-remember-core-migration/task-194-migrate-relationship-tools.md +47 -0
  6. package/agent/tasks/milestone-17-remember-core-migration/task-195-migrate-preference-tools.md +34 -0
  7. package/agent/tasks/milestone-17-remember-core-migration/task-196-migrate-memory-tools.md +46 -0
  8. package/agent/tasks/milestone-17-remember-core-migration/task-197-migrate-space-confirmation-tools.md +49 -0
  9. package/agent/tasks/milestone-17-remember-core-migration/task-198-migrate-space-search-moderate.md +46 -0
  10. package/agent/tasks/milestone-17-remember-core-migration/task-199-migrate-delete-memory.md +43 -0
  11. package/agent/tasks/milestone-17-remember-core-migration/task-200-code-cleanup-verification.md +52 -0
  12. package/dist/core-services.d.ts +25 -0
  13. package/dist/server-factory.js +3208 -3978
  14. package/dist/server.js +3708 -4474
  15. package/dist/tools/confirm-publish-moderation.spec.d.ts +3 -2
  16. package/dist/tools/create-memory.d.ts +1 -1
  17. package/dist/tools/query-space.d.ts +1 -1
  18. package/dist/tools/search-space.d.ts +10 -14
  19. package/jest.config.js +11 -0
  20. package/package.json +3 -1
  21. package/src/core-services.ts +50 -0
  22. package/src/tools/confirm-publish-moderation.spec.ts +120 -176
  23. package/src/tools/confirm.ts +70 -1035
  24. package/src/tools/create-memory.ts +16 -67
  25. package/src/tools/create-relationship.ts +13 -181
  26. package/src/tools/delete-memory.ts +7 -72
  27. package/src/tools/delete-relationship.ts +7 -91
  28. package/src/tools/deny.ts +4 -14
  29. package/src/tools/find-similar.ts +16 -110
  30. package/src/tools/get-preferences.ts +3 -8
  31. package/src/tools/moderate.spec.ts +65 -81
  32. package/src/tools/moderate.ts +18 -121
  33. package/src/tools/publish.ts +7 -204
  34. package/src/tools/query-space.ts +28 -140
  35. package/src/tools/retract.ts +7 -185
  36. package/src/tools/revise.ts +4 -136
  37. package/src/tools/search-relationship.ts +17 -116
  38. package/src/tools/search-space.ts +58 -304
  39. package/src/tools/set-preference.ts +3 -8
  40. package/src/tools/update-memory.ts +22 -190
  41. package/src/tools/update-relationship.ts +16 -90
  42. package/src/v2-smoke.e2e.ts +3 -2
  43. package/dist/collections/composite-ids.d.ts +0 -106
  44. package/dist/collections/core-infrastructure.spec.d.ts +0 -11
  45. package/dist/collections/dot-notation.d.ts +0 -106
  46. package/dist/collections/tracking-arrays.d.ts +0 -176
  47. package/dist/constants/content-types.d.ts +0 -61
  48. package/dist/services/confirmation-token.service.d.ts +0 -99
  49. package/dist/services/confirmation-token.service.spec.d.ts +0 -5
  50. package/dist/services/preferences-database.service.d.ts +0 -22
  51. package/dist/services/space-config.service.d.ts +0 -23
  52. package/dist/services/space-config.service.spec.d.ts +0 -2
  53. package/src/collections/composite-ids.ts +0 -193
  54. package/src/collections/core-infrastructure.spec.ts +0 -353
  55. package/src/collections/dot-notation.ts +0 -212
  56. package/src/collections/tracking-arrays.ts +0 -298
  57. package/src/constants/content-types.ts +0 -490
  58. package/src/services/confirmation-token.service.spec.ts +0 -254
  59. package/src/services/confirmation-token.service.ts +0 -328
  60. package/src/services/preferences-database.service.ts +0 -120
  61. package/src/services/space-config.service.spec.ts +0 -102
  62. package/src/services/space-config.service.ts +0 -79
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * Tests for moderation status wiring in the publish flow.
3
3
  *
4
- * Verifies that executePublishMemory() sets moderation_status
5
- * based on SpaceConfig.require_moderation for each destination.
4
+ * After migration to remember-core, the publish confirmation logic
5
+ * is handled by SpaceService.confirm(). These tests verify the adapter
6
+ * correctly delegates and formats responses.
6
7
  */
7
8
  export {};
8
9
  //# sourceMappingURL=confirm-publish-moderation.spec.d.ts.map
@@ -24,7 +24,7 @@ export declare const createMemoryTool: {
24
24
  type: {
25
25
  type: string;
26
26
  description: string;
27
- default: ContentType;
27
+ default: import("@prmichaelsen/remember-core").ContentType;
28
28
  };
29
29
  weight: {
30
30
  type: string;
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import type { Tool } from '@modelcontextprotocol/sdk/types.js';
8
8
  import type { AuthContext } from '../types/auth.js';
9
- import type { ModerationFilter } from './search-space.js';
9
+ export type ModerationFilter = 'approved' | 'pending' | 'rejected' | 'removed' | 'all';
10
10
  /**
11
11
  * Tool definition for remember_query_space
12
12
  */
@@ -12,6 +12,16 @@ import type { AuthContext } from '../types/auth.js';
12
12
  */
13
13
  export declare const searchSpaceTool: Tool;
14
14
  export type ModerationFilter = 'approved' | 'pending' | 'rejected' | 'removed' | 'all';
15
+ /**
16
+ * Build the moderation status filter for a Weaviate collection query.
17
+ * @deprecated Kept for test compatibility — logic now lives in remember-core SpaceService
18
+ */
19
+ export declare function buildModerationFilter(collection: any, moderationFilter?: ModerationFilter): any | null;
20
+ /**
21
+ * Build base filters applied to all space/group collection queries.
22
+ * @deprecated Kept for test compatibility — logic now lives in remember-core SpaceService
23
+ */
24
+ export declare function buildBaseFilters(collection: any, args: SearchSpaceArgs): any[];
15
25
  interface SearchSpaceArgs {
16
26
  query: string;
17
27
  spaces?: string[];
@@ -28,20 +38,6 @@ interface SearchSpaceArgs {
28
38
  limit?: number;
29
39
  offset?: number;
30
40
  }
31
- /**
32
- * Build the moderation status filter for a Weaviate collection query.
33
- *
34
- * - 'approved' (default): matches approved OR null (backward compat for pre-moderation memories)
35
- * - 'pending'/'rejected'/'removed': matches that specific status
36
- * - 'all': no moderation filter applied
37
- */
38
- export declare function buildModerationFilter(collection: any, moderationFilter?: ModerationFilter): any | null;
39
- /**
40
- * Build base filters applied to all space/group collection queries.
41
- * Excludes soft-deleted memories and optionally filters by content type, tags, weight, and date.
42
- * Includes moderation status filter (default: approved/null only).
43
- */
44
- export declare function buildBaseFilters(collection: any, args: SearchSpaceArgs): any[];
45
41
  /**
46
42
  * Handle remember_search_space tool execution
47
43
  */
package/jest.config.js CHANGED
@@ -19,7 +19,12 @@ export default {
19
19
  moduleNameMapper: {
20
20
  '^@/(.*)$': '<rootDir>/src/$1',
21
21
  '^(\\.{1,2}/.*)\\.js$': '$1',
22
+ '^@prmichaelsen/remember-core$': '<rootDir>/node_modules/@prmichaelsen/remember-core/dist/index.js',
23
+ '^@prmichaelsen/remember-core/(.*)$': '<rootDir>/node_modules/@prmichaelsen/remember-core/dist/$1/index.js',
22
24
  },
25
+ transformIgnorePatterns: [
26
+ 'node_modules/(?!(@prmichaelsen/remember-core)/)',
27
+ ],
23
28
  transform: {
24
29
  '^.+\\.ts$': [
25
30
  'ts-jest',
@@ -27,5 +32,11 @@ export default {
27
32
  useESM: true,
28
33
  },
29
34
  ],
35
+ 'node_modules/@prmichaelsen/remember-core/.+\\.js$': [
36
+ 'ts-jest',
37
+ {
38
+ useESM: true,
39
+ },
40
+ ],
30
41
  },
31
42
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prmichaelsen/remember-mcp",
3
- "version": "3.13.0",
3
+ "version": "3.14.1",
4
4
  "description": "Multi-tenant memory system MCP server with vector search and relationships",
5
5
  "main": "dist/server.js",
6
6
  "type": "module",
@@ -50,8 +50,10 @@
50
50
  "@modelcontextprotocol/sdk": "^1.0.4",
51
51
  "@prmichaelsen/firebase-admin-sdk-v8": "^2.2.0",
52
52
  "@prmichaelsen/mcp-auth": "^7.0.4",
53
+ "@prmichaelsen/remember-core": "^0.16.0",
53
54
  "@prmichaelsen/remember-mcp": "^2.7.3",
54
55
  "dotenv": "^16.4.5",
56
+ "uuid": "^13.0.0",
55
57
  "weaviate-client": "^3.2.0"
56
58
  },
57
59
  "devDependencies": {
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Bridge module: initializes remember-core services from remember-mcp infrastructure.
3
+ *
4
+ * Core services are scoped per-user (MemoryService, RelationshipService, SpaceService)
5
+ * except PreferencesDatabaseService and ConfirmationTokenService which are singletons.
6
+ */
7
+
8
+ import {
9
+ MemoryService,
10
+ RelationshipService,
11
+ SpaceService,
12
+ PreferencesDatabaseService,
13
+ ConfirmationTokenService,
14
+ createLogger,
15
+ } from '@prmichaelsen/remember-core';
16
+ import type { Logger } from '@prmichaelsen/remember-core';
17
+ import { getWeaviateClient } from './weaviate/client.js';
18
+ import { getMemoryCollection } from './weaviate/schema.js';
19
+
20
+ export interface CoreServices {
21
+ memory: MemoryService;
22
+ relationship: RelationshipService;
23
+ space: SpaceService;
24
+ preferences: PreferencesDatabaseService;
25
+ token: ConfirmationTokenService;
26
+ }
27
+
28
+ // Singletons — shared across all user scopes
29
+ const coreLogger: Logger = createLogger('info');
30
+ const tokenService = new ConfirmationTokenService(coreLogger);
31
+ const preferencesService = new PreferencesDatabaseService(coreLogger);
32
+
33
+ /**
34
+ * Create core services scoped to a specific user.
35
+ * Call after databases have been initialized (initWeaviateClient + initFirestore).
36
+ */
37
+ export function createCoreServices(userId: string): CoreServices {
38
+ const collection = getMemoryCollection(userId);
39
+ const weaviateClient = getWeaviateClient();
40
+
41
+ return {
42
+ memory: new MemoryService(collection, userId, coreLogger),
43
+ relationship: new RelationshipService(collection, userId, coreLogger),
44
+ space: new SpaceService(weaviateClient, collection, userId, tokenService, coreLogger),
45
+ preferences: preferencesService,
46
+ token: tokenService,
47
+ };
48
+ }
49
+
50
+ export { coreLogger, tokenService, preferencesService };
@@ -1,34 +1,22 @@
1
1
  /**
2
2
  * Tests for moderation status wiring in the publish flow.
3
3
  *
4
- * Verifies that executePublishMemory() sets moderation_status
5
- * based on SpaceConfig.require_moderation for each destination.
4
+ * After migration to remember-core, the publish confirmation logic
5
+ * is handled by SpaceService.confirm(). These tests verify the adapter
6
+ * correctly delegates and formats responses.
6
7
  */
7
8
 
8
9
  import { handleConfirm } from './confirm.js';
9
10
 
10
- // ─── Mocks (factories only — configured in beforeEach) ──────
11
+ // ─── Mocks ──────────────────────────────────────────────────
12
+
13
+ jest.mock('../core-services.js', () => ({
14
+ createCoreServices: jest.fn(),
15
+ }));
11
16
 
12
17
  jest.mock('../weaviate/client.js', () => ({
13
18
  getWeaviateClient: jest.fn(),
14
19
  getMemoryCollectionName: jest.fn((userId: string) => `Memory_users_${userId}`),
15
- fetchMemoryWithAllProperties: jest.fn(),
16
- }));
17
-
18
- jest.mock('../weaviate/space-schema.js', () => ({
19
- ensurePublicCollection: jest.fn(),
20
- }));
21
-
22
- jest.mock('../services/confirmation-token.service.js', () => ({
23
- confirmationTokenService: { confirmRequest: jest.fn() },
24
- }));
25
-
26
- jest.mock('../services/space-config.service.js', () => ({
27
- getSpaceConfig: jest.fn(),
28
- }));
29
-
30
- jest.mock('../utils/logger.js', () => ({
31
- logger: { info: jest.fn(), debug: jest.fn(), warn: jest.fn(), error: jest.fn() },
32
20
  }));
33
21
 
34
22
  jest.mock('../utils/debug.js', () => ({
@@ -41,200 +29,156 @@ jest.mock('../utils/debug.js', () => ({
41
29
  })),
42
30
  }));
43
31
 
44
- jest.mock('../collections/dot-notation.js', () => ({
45
- CollectionType: { GROUPS: 'groups' },
46
- getCollectionName: jest.fn((_: string, id: string) => `Memory_groups_${id}`),
47
- }));
48
-
49
- jest.mock('../collections/composite-ids.js', () => ({
50
- generateCompositeId: jest.fn((u: string, m: string) => `${u}.${m}`),
51
- parseCompositeId: jest.fn(),
52
- }));
53
-
54
- jest.mock('../collections/tracking-arrays.js', () => ({
55
- addToSpaceIds: jest.fn(),
56
- addToGroupIds: jest.fn(),
57
- removeFromSpaceIds: jest.fn(),
58
- removeFromGroupIds: jest.fn(),
59
- getPublishedLocations: jest.fn(),
60
- }));
61
-
62
32
  jest.mock('../utils/error-handler.js', () => ({
63
33
  handleToolError: jest.fn(),
64
34
  }));
65
35
 
66
- // ─── Import mocked modules ──────────────────────────────────
67
-
68
- import { getWeaviateClient, fetchMemoryWithAllProperties } from '../weaviate/client.js';
69
- import { ensurePublicCollection } from '../weaviate/space-schema.js';
70
- import { confirmationTokenService } from '../services/confirmation-token.service.js';
71
- import { getSpaceConfig } from '../services/space-config.service.js';
72
-
73
- const mockGetWeaviateClient = getWeaviateClient as jest.MockedFunction<any>;
74
- const mockFetchMemory = fetchMemoryWithAllProperties as jest.MockedFunction<any>;
75
- const mockEnsurePublicCollection = ensurePublicCollection as jest.MockedFunction<any>;
76
- const mockConfirmRequest = confirmationTokenService.confirmRequest as jest.MockedFunction<any>;
77
- const mockGetSpaceConfig = getSpaceConfig as jest.MockedFunction<any>;
78
-
79
- // ─── Shared per-test mock state ──────────────────────────────
80
-
81
- let spaceInsert: jest.Mock;
82
- let spaceUpdate: jest.Mock;
83
- let groupInsert: jest.Mock;
84
- let groupUpdate: jest.Mock;
85
- let userUpdate: jest.Mock;
86
-
87
- const ORIGINAL_MEMORY = {
88
- properties: {
89
- user_id: 'user-1',
90
- content: 'Test memory',
91
- content_type: 'text',
92
- tags: ['test'],
93
- space_ids: [],
94
- group_ids: [],
95
- },
96
- };
97
-
98
- function makePublishRequest(overrides: Record<string, any> = {}) {
99
- return {
100
- request_id: 'req-1',
101
- userId: 'user-1',
102
- action: 'publish_memory' as const,
103
- payload: { memory_id: 'mem-1', spaces: [], groups: [], ...overrides },
104
- createdAt: Date.now(),
105
- expiresAt: Date.now() + 300_000,
106
- status: 'confirmed',
107
- };
108
- }
36
+ import { createCoreServices } from '../core-services.js';
37
+ const mockCreateCoreServices = createCoreServices as jest.MockedFunction<any>;
38
+
39
+ // ─── Shared mock state ──────────────────────────────────────
40
+
41
+ let mockConfirm: jest.Mock;
42
+ let mockValidateToken: jest.Mock;
109
43
 
110
44
  // ─── Tests ───────────────────────────────────────────────────
111
45
 
112
46
  describe('publish moderation wiring', () => {
113
47
  beforeEach(() => {
114
48
  jest.clearAllMocks();
49
+ mockConfirm = jest.fn();
50
+ mockValidateToken = jest.fn();
115
51
 
116
- // Fresh mock functions per test
117
- spaceInsert = jest.fn().mockResolvedValue(undefined);
118
- spaceUpdate = jest.fn().mockResolvedValue(undefined);
119
- groupInsert = jest.fn().mockResolvedValue(undefined);
120
- groupUpdate = jest.fn().mockResolvedValue(undefined);
121
- userUpdate = jest.fn().mockResolvedValue(undefined);
122
-
123
- // Weaviate client: route collections by name
124
- mockGetWeaviateClient.mockReturnValue({
125
- collections: {
126
- get: jest.fn().mockImplementation((name: string) => {
127
- if (name.startsWith('Memory_groups_')) {
128
- return { data: { insert: groupInsert, update: groupUpdate } };
129
- }
130
- // User collection
131
- return { data: { insert: jest.fn(), update: userUpdate } };
132
- }),
133
- },
134
- });
135
-
136
- // Space collection via ensurePublicCollection
137
- mockEnsurePublicCollection.mockResolvedValue({
138
- data: { insert: spaceInsert, update: spaceUpdate },
139
- });
140
-
141
- // fetchMemoryWithAllProperties: 1st call = original, subsequent = null (not already published)
142
- mockFetchMemory
143
- .mockResolvedValueOnce(ORIGINAL_MEMORY)
144
- .mockResolvedValue(null);
145
-
146
- // Default: unmoderated
147
- mockGetSpaceConfig.mockResolvedValue({
148
- require_moderation: false,
149
- default_write_mode: 'owner_only',
52
+ mockCreateCoreServices.mockReturnValue({
53
+ space: { confirm: mockConfirm },
54
+ token: { validateToken: mockValidateToken, confirmRequest: jest.fn() },
150
55
  });
151
56
  });
152
57
 
153
58
  describe('spaces publication', () => {
154
59
  it('sets moderation_status to approved for unmoderated space', async () => {
155
- mockConfirmRequest.mockResolvedValue(makePublishRequest({ spaces: ['public'] }));
156
-
157
- await handleConfirm({ token: 'tok-1' }, 'user-1');
158
-
159
- expect(mockGetSpaceConfig).toHaveBeenCalledWith('public', 'space');
160
- expect(spaceInsert).toHaveBeenCalledTimes(1);
161
- expect(spaceInsert.mock.calls[0][0].properties.moderation_status).toBe('approved');
60
+ mockValidateToken.mockResolvedValue({ action: 'publish_memory' });
61
+ mockConfirm.mockResolvedValue({
62
+ action: 'publish_memory',
63
+ success: true,
64
+ composite_id: 'user-1.mem-1',
65
+ published_to: ['spaces: public'],
66
+ space_ids: ['public'],
67
+ group_ids: [],
68
+ });
69
+
70
+ const result = JSON.parse(await handleConfirm({ token: 'tok-1' }, 'user-1'));
71
+
72
+ expect(result.success).toBe(true);
73
+ expect(result.published_to).toContain('spaces: public');
74
+ expect(mockConfirm).toHaveBeenCalledWith({ token: 'tok-1' });
162
75
  });
163
76
 
164
77
  it('sets moderation_status to pending for moderated space', async () => {
165
- mockConfirmRequest.mockResolvedValue(makePublishRequest({ spaces: ['moderated-space'] }));
166
- mockGetSpaceConfig.mockResolvedValue({ require_moderation: true, default_write_mode: 'owner_only' });
167
-
168
- await handleConfirm({ token: 'tok-2' }, 'user-1');
169
-
170
- expect(mockGetSpaceConfig).toHaveBeenCalledWith('moderated-space', 'space');
171
- expect(spaceInsert).toHaveBeenCalledTimes(1);
172
- expect(spaceInsert.mock.calls[0][0].properties.moderation_status).toBe('pending');
78
+ mockValidateToken.mockResolvedValue({ action: 'publish_memory' });
79
+ mockConfirm.mockResolvedValue({
80
+ action: 'publish_memory',
81
+ success: true,
82
+ composite_id: 'user-1.mem-1',
83
+ published_to: ['spaces: moderated-space'],
84
+ space_ids: ['moderated-space'],
85
+ group_ids: [],
86
+ });
87
+
88
+ const result = JSON.parse(await handleConfirm({ token: 'tok-2' }, 'user-1'));
89
+
90
+ expect(result.success).toBe(true);
91
+ expect(mockConfirm).toHaveBeenCalledWith({ token: 'tok-2' });
173
92
  });
174
93
 
175
94
  it('sets pending if any of multiple spaces requires moderation', async () => {
176
- mockConfirmRequest.mockResolvedValue(makePublishRequest({ spaces: ['open', 'strict'] }));
177
- mockGetSpaceConfig
178
- .mockResolvedValueOnce({ require_moderation: false, default_write_mode: 'owner_only' })
179
- .mockResolvedValueOnce({ require_moderation: true, default_write_mode: 'owner_only' });
180
-
181
- await handleConfirm({ token: 'tok-3' }, 'user-1');
182
-
183
- expect(spaceInsert).toHaveBeenCalledTimes(1);
184
- expect(spaceInsert.mock.calls[0][0].properties.moderation_status).toBe('pending');
95
+ mockValidateToken.mockResolvedValue({ action: 'publish_memory' });
96
+ mockConfirm.mockResolvedValue({
97
+ action: 'publish_memory',
98
+ success: true,
99
+ composite_id: 'user-1.mem-1',
100
+ published_to: ['spaces: open, strict'],
101
+ space_ids: ['open', 'strict'],
102
+ group_ids: [],
103
+ });
104
+
105
+ const result = JSON.parse(await handleConfirm({ token: 'tok-3' }, 'user-1'));
106
+
107
+ expect(result.success).toBe(true);
108
+ expect(result.space_ids).toContain('open');
109
+ expect(result.space_ids).toContain('strict');
185
110
  });
186
111
  });
187
112
 
188
113
  describe('groups publication', () => {
189
114
  it('sets moderation_status to approved for unmoderated group', async () => {
190
- mockConfirmRequest.mockResolvedValue(makePublishRequest({ groups: ['team-alpha'] }));
191
-
192
- await handleConfirm({ token: 'tok-4' }, 'user-1');
193
-
194
- expect(mockGetSpaceConfig).toHaveBeenCalledWith('team-alpha', 'group');
195
- expect(groupInsert).toHaveBeenCalledTimes(1);
196
- expect(groupInsert.mock.calls[0][0].properties.moderation_status).toBe('approved');
115
+ mockValidateToken.mockResolvedValue({ action: 'publish_memory' });
116
+ mockConfirm.mockResolvedValue({
117
+ action: 'publish_memory',
118
+ success: true,
119
+ composite_id: 'user-1.mem-1',
120
+ published_to: ['group: team-alpha'],
121
+ space_ids: [],
122
+ group_ids: ['team-alpha'],
123
+ });
124
+
125
+ const result = JSON.parse(await handleConfirm({ token: 'tok-4' }, 'user-1'));
126
+
127
+ expect(result.success).toBe(true);
128
+ expect(result.group_ids).toContain('team-alpha');
197
129
  });
198
130
 
199
131
  it('sets moderation_status to pending for moderated group', async () => {
200
- mockConfirmRequest.mockResolvedValue(makePublishRequest({ groups: ['strict-group'] }));
201
- mockGetSpaceConfig.mockResolvedValue({ require_moderation: true, default_write_mode: 'owner_only' });
202
-
203
- await handleConfirm({ token: 'tok-5' }, 'user-1');
204
-
205
- expect(mockGetSpaceConfig).toHaveBeenCalledWith('strict-group', 'group');
206
- expect(groupInsert).toHaveBeenCalledTimes(1);
207
- expect(groupInsert.mock.calls[0][0].properties.moderation_status).toBe('pending');
132
+ mockValidateToken.mockResolvedValue({ action: 'publish_memory' });
133
+ mockConfirm.mockResolvedValue({
134
+ action: 'publish_memory',
135
+ success: true,
136
+ composite_id: 'user-1.mem-1',
137
+ published_to: ['group: strict-group'],
138
+ space_ids: [],
139
+ group_ids: ['strict-group'],
140
+ });
141
+
142
+ const result = JSON.parse(await handleConfirm({ token: 'tok-5' }, 'user-1'));
143
+
144
+ expect(result.success).toBe(true);
145
+ expect(result.group_ids).toContain('strict-group');
208
146
  });
209
147
 
210
148
  it('sets independent moderation status per group', async () => {
211
- mockConfirmRequest.mockResolvedValue(makePublishRequest({ groups: ['open-group', 'strict-group'] }));
212
- // Reset fetchMemory: 1st = original, 2nd = null (open-group), 3rd = null (strict-group)
213
- mockFetchMemory.mockReset();
214
- mockFetchMemory
215
- .mockResolvedValueOnce(ORIGINAL_MEMORY)
216
- .mockResolvedValue(null);
217
-
218
- mockGetSpaceConfig
219
- .mockResolvedValueOnce({ require_moderation: false, default_write_mode: 'owner_only' })
220
- .mockResolvedValueOnce({ require_moderation: true, default_write_mode: 'owner_only' });
221
-
222
- await handleConfirm({ token: 'tok-6' }, 'user-1');
223
-
224
- expect(groupInsert).toHaveBeenCalledTimes(2);
225
- expect(groupInsert.mock.calls[0][0].properties.moderation_status).toBe('approved');
226
- expect(groupInsert.mock.calls[1][0].properties.moderation_status).toBe('pending');
149
+ mockValidateToken.mockResolvedValue({ action: 'publish_memory' });
150
+ mockConfirm.mockResolvedValue({
151
+ action: 'publish_memory',
152
+ success: true,
153
+ composite_id: 'user-1.mem-1',
154
+ published_to: ['group: open-group', 'group: strict-group'],
155
+ space_ids: [],
156
+ group_ids: ['open-group', 'strict-group'],
157
+ });
158
+
159
+ const result = JSON.parse(await handleConfirm({ token: 'tok-6' }, 'user-1'));
160
+
161
+ expect(result.success).toBe(true);
162
+ expect(result.group_ids).toEqual(['open-group', 'strict-group']);
227
163
  });
228
164
  });
229
165
 
230
166
  describe('default behavior', () => {
231
167
  it('defaults to approved when getSpaceConfig returns defaults', async () => {
232
- mockConfirmRequest.mockResolvedValue(makePublishRequest({ spaces: ['unknown-space'] }));
233
-
234
- await handleConfirm({ token: 'tok-7' }, 'user-1');
235
-
236
- expect(spaceInsert).toHaveBeenCalledTimes(1);
237
- expect(spaceInsert.mock.calls[0][0].properties.moderation_status).toBe('approved');
168
+ mockValidateToken.mockResolvedValue({ action: 'publish_memory' });
169
+ mockConfirm.mockResolvedValue({
170
+ action: 'publish_memory',
171
+ success: true,
172
+ composite_id: 'user-1.mem-1',
173
+ published_to: ['spaces: unknown-space'],
174
+ space_ids: ['unknown-space'],
175
+ group_ids: [],
176
+ });
177
+
178
+ const result = JSON.parse(await handleConfirm({ token: 'tok-7' }, 'user-1'));
179
+
180
+ expect(result.success).toBe(true);
181
+ expect(result.published_to).toBeDefined();
238
182
  });
239
183
  });
240
184
  });