@unifiedmemory/cli 1.3.13 → 1.3.14

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.
@@ -14,12 +14,6 @@ export function saveToken(tokenData) {
14
14
  // Ensure directory permissions are correct (in case it was created with wrong permissions)
15
15
  fs.chmodSync(TOKEN_DIR, 0o700);
16
16
 
17
- // Preserve existing organization context if not explicitly overwritten
18
- const existingData = getToken();
19
- if (existingData && existingData.selectedOrg && !tokenData.selectedOrg) {
20
- tokenData.selectedOrg = existingData.selectedOrg;
21
- }
22
-
23
17
  // Write token file with owner-only read/write permissions (0600)
24
18
  fs.writeFileSync(TOKEN_FILE, JSON.stringify(tokenData, null, 2));
25
19
  // Explicitly set file permissions (writeFileSync mode option doesn't always work with extended attributes)
@@ -46,31 +40,43 @@ export function clearToken() {
46
40
  }
47
41
 
48
42
  /**
49
- * Update the selected organization context
50
- * @param {Object|null} orgData - Organization data or null for personal context
43
+ * Get organization context from JWT claims
44
+ * JWT is the single source of truth for org context
45
+ * @returns {Object|null} Organization context or null if personal context
51
46
  */
52
- export function updateSelectedOrg(orgData) {
53
- const tokenData = getToken();
54
- if (!tokenData) {
55
- throw new Error('No token found. Please login first.');
47
+ export function getOrgContext() {
48
+ const token = getToken();
49
+
50
+ // Try nested 'o' object first (original OAuth token format)
51
+ if (token?.decoded?.o?.o_id) {
52
+ return {
53
+ id: token.decoded.o.o_id,
54
+ name: token.decoded.o.o_name,
55
+ slug: token.decoded.o.o_slug,
56
+ role: token.decoded.o.o_role
57
+ };
56
58
  }
57
59
 
58
- // Ensure directory permissions are correct
59
- if (fs.existsSync(TOKEN_DIR)) {
60
- fs.chmodSync(TOKEN_DIR, 0o700);
60
+ // Fall back to flat org_* claims (org-scoped token from Clerk)
61
+ if (token?.decoded?.org_id) {
62
+ return {
63
+ id: token.decoded.org_id,
64
+ name: token.decoded.org_name,
65
+ slug: token.decoded.org_slug,
66
+ role: token.decoded.org_role
67
+ };
61
68
  }
62
69
 
63
- tokenData.selectedOrg = orgData;
64
- // Write with owner-only read/write permissions (0600)
65
- fs.writeFileSync(TOKEN_FILE, JSON.stringify(tokenData, null, 2));
66
- fs.chmodSync(TOKEN_FILE, 0o600);
67
- }
70
+ // Fall back to selectedOrgId from login state parameter
71
+ // This is used when JWT doesn't have org claims (Clerk setActive limitation)
72
+ if (token?.selectedOrgId) {
73
+ return {
74
+ id: token.selectedOrgId,
75
+ name: token.selectedOrgName || null,
76
+ slug: null,
77
+ role: null
78
+ };
79
+ }
68
80
 
69
- /**
70
- * Get the currently selected organization context
71
- * @returns {Object|null} Organization data or null if personal context
72
- */
73
- export function getSelectedOrg() {
74
- const tokenData = getToken();
75
- return tokenData?.selectedOrg || null;
81
+ return null;
76
82
  }
@@ -1,11 +1,9 @@
1
- import { getToken, saveToken } from './token-storage.js';
1
+ import { getToken } from './token-storage.js';
2
2
  import { isTokenExpired, refreshAccessToken } from './token-refresh.js';
3
- import { getOrgScopedToken } from './clerk-api.js';
4
- import { parseJWT } from './jwt-utils.js';
5
- import { config } from './config.js';
6
3
 
7
4
  /**
8
5
  * Load token and refresh if expired
6
+ * JWT is the single source of truth - no recovery logic needed
9
7
  * @param {boolean} requireAuth - Whether to throw if not authenticated (default true)
10
8
  * @returns {Promise<Object|null>} - Token data or null
11
9
  * @throws {Error} - If not authenticated and requireAuth is true, or if refresh fails
@@ -46,86 +44,5 @@ export async function loadAndRefreshToken(requireAuth = true) {
46
44
  }
47
45
  }
48
46
 
49
- // Check if token has org context but lacks org claims in JWT
50
- // This can happen if getOrgScopedToken() failed during login
51
- if (tokenData.selectedOrg?.id && !tokenData.decoded?.o) {
52
- console.error('⚠️ Token missing org claims, attempting to fix...');
53
-
54
- // Check for sid in decoded token or preserved originalSid from login
55
- let sessionId = tokenData.decoded?.sid || tokenData.originalSid;
56
- let currentToken = tokenData.idToken || tokenData.accessToken;
57
-
58
- // If no sid, try to refresh first to get a new base token with sid
59
- if (!sessionId && tokenData.refresh_token) {
60
- console.error(' Refreshing to obtain session ID...');
61
- try {
62
- const tokenParams = {
63
- client_id: config.clerkClientId,
64
- refresh_token: tokenData.refresh_token,
65
- grant_type: 'refresh_token',
66
- };
67
-
68
- if (config.clerkClientSecret) {
69
- tokenParams.client_secret = config.clerkClientSecret;
70
- }
71
-
72
- const response = await fetch(`https://${config.clerkDomain}/oauth/token`, {
73
- method: 'POST',
74
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
75
- body: new URLSearchParams(tokenParams),
76
- });
77
-
78
- if (response.ok) {
79
- const newTokenData = await response.json();
80
- const refreshedToken = newTokenData.id_token || newTokenData.access_token;
81
- const refreshedDecoded = parseJWT(refreshedToken);
82
-
83
- sessionId = refreshedDecoded?.sid;
84
- currentToken = refreshedToken;
85
-
86
- // Update tokenData with refreshed values
87
- tokenData.accessToken = newTokenData.access_token;
88
- tokenData.idToken = newTokenData.id_token;
89
- tokenData.refresh_token = newTokenData.refresh_token || tokenData.refresh_token;
90
- tokenData.decoded = refreshedDecoded;
91
-
92
- if (sessionId) {
93
- console.error(' ✓ Obtained session ID');
94
- }
95
- }
96
- } catch (error) {
97
- console.error(` Could not refresh token: ${error.message}`);
98
- }
99
- }
100
-
101
- // Now try to get org-scoped token if we have sessionId
102
- if (sessionId) {
103
- try {
104
- const orgToken = await getOrgScopedToken(
105
- sessionId,
106
- tokenData.selectedOrg.id,
107
- currentToken
108
- );
109
-
110
- const decoded = parseJWT(orgToken.jwt);
111
- const updatedToken = {
112
- ...tokenData,
113
- idToken: orgToken.jwt,
114
- decoded: decoded,
115
- };
116
-
117
- saveToken(updatedToken);
118
- console.error('✓ Token updated with org claims');
119
- return updatedToken;
120
- } catch (error) {
121
- console.error(`⚠️ Could not get org-scoped token: ${error.message}`);
122
- // Continue with existing token - API calls may fail
123
- }
124
- } else {
125
- console.error('⚠️ Could not obtain session ID for org-scoped token');
126
- console.error(' Please run: um login');
127
- }
128
- }
129
-
130
47
  return tokenData;
131
48
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unifiedmemory/cli",
3
- "version": "1.3.13",
3
+ "version": "1.3.14",
4
4
  "description": "UnifiedMemory CLI - AI code assistant integration",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -221,7 +221,7 @@ describe('mcp-proxy', () => {
221
221
 
222
222
  const authContext = {
223
223
  decoded: { sub: 'user_123' },
224
- selectedOrg: { id: 'org_456' },
224
+ orgContext: { id: 'org_456' },
225
225
  };
226
226
 
227
227
  const projectContext = {
@@ -245,7 +245,7 @@ describe('mcp-proxy', () => {
245
245
  expect(capturedBody.params.arguments.headers['X-Org-Id']).toBe('org_456');
246
246
  });
247
247
 
248
- it('should fallback to user_id for org when no selectedOrg', async () => {
248
+ it('should not inject org when no orgContext (personal context)', async () => {
249
249
  let capturedBody = null;
250
250
 
251
251
  server.use(
@@ -263,7 +263,7 @@ describe('mcp-proxy', () => {
263
263
 
264
264
  const authContext = {
265
265
  decoded: { sub: 'user_123' },
266
- // No selectedOrg
266
+ // No orgContext - personal context
267
267
  };
268
268
 
269
269
  await callRemoteMCPTool(
@@ -274,9 +274,12 @@ describe('mcp-proxy', () => {
274
274
  null
275
275
  );
276
276
 
277
- // Org should fallback to user_id
278
- expect(capturedBody.params.arguments.pathParams.org).toBe('user_123');
279
- expect(capturedBody.params.arguments.headers['X-Org-Id']).toBe('user_123');
277
+ // No org injection for personal context - gateway handles this
278
+ expect(capturedBody.params.arguments.pathParams.org).toBeUndefined();
279
+ expect(capturedBody.params.arguments.headers['X-Org-Id']).toBeUndefined();
280
+ // User should still be injected
281
+ expect(capturedBody.params.arguments.pathParams.user).toBe('user_123');
282
+ expect(capturedBody.params.arguments.headers['X-User-Id']).toBe('user_123');
280
283
  });
281
284
 
282
285
  it('should return tool execution result', async () => {
@@ -44,17 +44,9 @@ vi.mock('../../lib/jwt-utils.js', () => ({
44
44
  }),
45
45
  }));
46
46
 
47
- vi.mock('../../lib/clerk-api.js', () => ({
48
- getOrgScopedToken: vi.fn().mockResolvedValue({
49
- jwt: 'org_scoped_jwt',
50
- object: 'token',
51
- }),
52
- }));
53
-
54
47
  // Import after mocking
55
48
  import { isTokenExpired, refreshAccessToken } from '../../lib/token-refresh.js';
56
49
  import { saveToken } from '../../lib/token-storage.js';
57
- import { getOrgScopedToken } from '../../lib/clerk-api.js';
58
50
 
59
51
  describe('isTokenExpired', () => {
60
52
  it('returns true for null tokenData', () => {
@@ -181,7 +173,7 @@ describe('refreshAccessToken', () => {
181
173
  );
182
174
  });
183
175
 
184
- it('preserves selectedOrg in refreshed token', async () => {
176
+ it('preserves selectedOrgId and selectedOrgName in refreshed token', async () => {
185
177
  global.fetch = vi.fn().mockResolvedValue({
186
178
  ok: true,
187
179
  json: () => Promise.resolve({
@@ -193,60 +185,42 @@ describe('refreshAccessToken', () => {
193
185
  }),
194
186
  });
195
187
 
196
- const selectedOrg = { id: 'org_456', name: 'Test Org', role: 'admin' };
197
- await refreshAccessToken({ refresh_token: 'rt_123', selectedOrg });
188
+ await refreshAccessToken({
189
+ refresh_token: 'rt_123',
190
+ selectedOrgId: 'org_456',
191
+ selectedOrgName: 'Test Org',
192
+ });
198
193
 
199
194
  expect(saveToken).toHaveBeenCalledWith(
200
195
  expect.objectContaining({
201
- selectedOrg: selectedOrg,
196
+ selectedOrgId: 'org_456',
197
+ selectedOrgName: 'Test Org',
202
198
  })
203
199
  );
204
200
  });
205
201
 
206
- it('gets org-scoped token when selectedOrg exists and refreshed token has sid', async () => {
202
+ it('preserves sessionId through refresh', async () => {
207
203
  global.fetch = vi.fn().mockResolvedValue({
208
204
  ok: true,
209
205
  json: () => Promise.resolve({
210
206
  access_token: 'new_access_token',
211
- id_token: 'new_id_token_with_sid',
207
+ id_token: 'new_id_token',
212
208
  refresh_token: 'new_refresh_token',
213
209
  token_type: 'Bearer',
214
210
  expires_in: 3600,
215
211
  }),
216
212
  });
217
213
 
218
- const selectedOrg = { id: 'org_456', name: 'Test Org', role: 'admin' };
219
- await refreshAccessToken({ refresh_token: 'rt_123', selectedOrg });
220
-
221
- expect(getOrgScopedToken).toHaveBeenCalledWith(
222
- 'sess_new_123',
223
- 'org_456',
224
- 'new_id_token_with_sid'
225
- );
226
- });
227
-
228
- it('continues with base token when getOrgScopedToken fails', async () => {
229
- global.fetch = vi.fn().mockResolvedValue({
230
- ok: true,
231
- json: () => Promise.resolve({
232
- access_token: 'new_access_token',
233
- id_token: 'new_id_token_with_sid',
234
- refresh_token: 'new_refresh_token',
235
- token_type: 'Bearer',
236
- expires_in: 3600,
237
- }),
214
+ await refreshAccessToken({
215
+ refresh_token: 'rt_123',
216
+ sessionId: 'sess_123',
238
217
  });
239
218
 
240
- // Make getOrgScopedToken fail
241
- vi.mocked(getOrgScopedToken).mockRejectedValueOnce(new Error('API error'));
242
-
243
- const selectedOrg = { id: 'org_456', name: 'Test Org', role: 'admin' };
244
-
245
- // Should not throw - continues with base token
246
- const result = await refreshAccessToken({ refresh_token: 'rt_123', selectedOrg });
247
-
248
- expect(result).toBeDefined();
249
- expect(result.idToken).toBe('new_id_token_with_sid'); // Falls back to base token
219
+ expect(saveToken).toHaveBeenCalledWith(
220
+ expect.objectContaining({
221
+ sessionId: 'sess_123',
222
+ })
223
+ );
250
224
  });
251
225
 
252
226
  it('throws error when OAuth refresh fails with 400', async () => {
@@ -132,7 +132,6 @@ describe('token-storage module behavior', () => {
132
132
  expect(typeof ts.saveToken).toBe('function');
133
133
  expect(typeof ts.getToken).toBe('function');
134
134
  expect(typeof ts.clearToken).toBe('function');
135
- expect(typeof ts.updateSelectedOrg).toBe('function');
136
- expect(typeof ts.getSelectedOrg).toBe('function');
135
+ expect(typeof ts.getOrgContext).toBe('function');
137
136
  });
138
137
  });
@@ -1,15 +1,15 @@
1
1
  /**
2
2
  * Unit tests for lib/token-validation.js
3
3
  *
4
- * Tests token loading, expiration checking, and org claims recovery.
4
+ * Tests token loading and expiration checking.
5
+ * The implementation is simple: load token, refresh if expired.
5
6
  */
6
7
 
7
8
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
8
9
 
9
- // Mock dependencies - using factory functions to avoid hoisting issues
10
+ // Mock dependencies
10
11
  vi.mock('../../lib/token-storage.js', () => ({
11
12
  getToken: vi.fn(),
12
- saveToken: vi.fn(),
13
13
  }));
14
14
 
15
15
  vi.mock('../../lib/token-refresh.js', () => ({
@@ -17,28 +17,10 @@ vi.mock('../../lib/token-refresh.js', () => ({
17
17
  refreshAccessToken: vi.fn(),
18
18
  }));
19
19
 
20
- vi.mock('../../lib/clerk-api.js', () => ({
21
- getOrgScopedToken: vi.fn(),
22
- }));
23
-
24
- vi.mock('../../lib/jwt-utils.js', () => ({
25
- parseJWT: vi.fn(),
26
- }));
27
-
28
- vi.mock('../../lib/config.js', () => ({
29
- config: {
30
- clerkDomain: 'clerk.test.com',
31
- clerkClientId: 'test_client_id',
32
- clerkClientSecret: 'test_client_secret',
33
- },
34
- }));
35
-
36
20
  // Import after mocking
37
21
  import { loadAndRefreshToken } from '../../lib/token-validation.js';
38
- import { getToken, saveToken } from '../../lib/token-storage.js';
22
+ import { getToken } from '../../lib/token-storage.js';
39
23
  import { isTokenExpired, refreshAccessToken } from '../../lib/token-refresh.js';
40
- import { getOrgScopedToken } from '../../lib/clerk-api.js';
41
- import { parseJWT } from '../../lib/jwt-utils.js';
42
24
 
43
25
  describe('loadAndRefreshToken', () => {
44
26
  const futureExp = Math.floor(Date.now() / 1000) + 3600;
@@ -130,238 +112,39 @@ describe('loadAndRefreshToken', () => {
130
112
  });
131
113
  });
132
114
 
133
- describe('org claims recovery', () => {
134
- it('recovers using decoded.sid when org claims missing', async () => {
135
- const tokenMissingOrgClaims = {
136
- idToken: 'token_no_org',
137
- decoded: { exp: futureExp, sid: 'sess_123' },
138
- selectedOrg: { id: 'org_456', name: 'Test Org' },
139
- };
140
-
141
- vi.mocked(getToken).mockReturnValue(tokenMissingOrgClaims);
142
- vi.mocked(getOrgScopedToken).mockResolvedValue({
143
- jwt: 'org_scoped_jwt',
144
- });
145
- vi.mocked(parseJWT).mockReturnValue({
146
- exp: futureExp,
147
- o: { o_id: 'org_456' },
148
- });
149
-
150
- await loadAndRefreshToken();
151
-
152
- expect(getOrgScopedToken).toHaveBeenCalledWith(
153
- 'sess_123',
154
- 'org_456',
155
- 'token_no_org'
156
- );
157
- expect(saveToken).toHaveBeenCalled();
158
- });
159
-
160
- it('recovers using originalSid when decoded.sid is missing', async () => {
161
- const tokenWithOriginalSid = {
162
- idToken: 'token_org_scoped',
163
- decoded: { exp: futureExp }, // No sid - org-scoped token
164
- selectedOrg: { id: 'org_456', name: 'Test Org' },
165
- originalSid: 'sess_original',
166
- };
167
-
168
- vi.mocked(getToken).mockReturnValue(tokenWithOriginalSid);
169
- vi.mocked(getOrgScopedToken).mockResolvedValue({
170
- jwt: 'new_org_scoped_jwt',
171
- });
172
- vi.mocked(parseJWT).mockReturnValue({
173
- exp: futureExp,
174
- o: { o_id: 'org_456' },
175
- });
176
-
177
- await loadAndRefreshToken();
178
-
179
- expect(getOrgScopedToken).toHaveBeenCalledWith(
180
- 'sess_original',
181
- 'org_456',
182
- 'token_org_scoped'
183
- );
184
- });
185
-
186
- it('skips recovery when token already has org claims', async () => {
187
- const tokenWithOrgClaims = {
188
- idToken: 'complete_token',
189
- decoded: { exp: futureExp, o: { o_id: 'org_456' } },
190
- selectedOrg: { id: 'org_456', name: 'Test Org' },
191
- };
192
-
193
- vi.mocked(getToken).mockReturnValue(tokenWithOrgClaims);
194
-
195
- const result = await loadAndRefreshToken();
196
-
197
- expect(getOrgScopedToken).not.toHaveBeenCalled();
198
- expect(result.decoded.o).toBeDefined();
199
- });
200
-
201
- it('skips recovery when no selectedOrg (personal account)', async () => {
202
- const personalToken = {
203
- idToken: 'personal_token',
204
- decoded: { exp: futureExp, sid: 'sess_123' },
205
- // No selectedOrg - personal account
206
- };
207
-
208
- vi.mocked(getToken).mockReturnValue(personalToken);
209
-
210
- await loadAndRefreshToken();
211
-
212
- expect(getOrgScopedToken).not.toHaveBeenCalled();
213
- });
214
-
215
- it('skips recovery when selectedOrg has no id', async () => {
216
- const tokenWithEmptyOrg = {
217
- idToken: 'token_empty_org',
218
- decoded: { exp: futureExp, sid: 'sess_123' },
219
- selectedOrg: {}, // Empty org object
220
- };
221
-
222
- vi.mocked(getToken).mockReturnValue(tokenWithEmptyOrg);
223
-
224
- await loadAndRefreshToken();
225
-
226
- expect(getOrgScopedToken).not.toHaveBeenCalled();
227
- });
228
-
229
- it('continues gracefully when recovery fails', async () => {
230
- const brokenToken = {
231
- idToken: 'broken_token',
232
- decoded: { exp: futureExp, sid: 'sess_123' },
233
- selectedOrg: { id: 'org_456', name: 'Test Org' },
234
- };
235
-
236
- vi.mocked(getToken).mockReturnValue(brokenToken);
237
- vi.mocked(getOrgScopedToken).mockRejectedValue(new Error('API error'));
238
-
239
- // Should not throw, just log warning and return token
240
- const result = await loadAndRefreshToken();
241
-
242
- expect(result).toBeDefined();
243
- expect(result.idToken).toBe('broken_token');
244
- });
245
-
246
- it('saves updated token after successful recovery', async () => {
247
- const tokenMissingOrgClaims = {
248
- idToken: 'token_no_org',
249
- decoded: { exp: futureExp, sid: 'sess_123' },
250
- selectedOrg: { id: 'org_456', name: 'Test Org' },
251
- refresh_token: 'rt_123',
252
- };
253
-
254
- vi.mocked(getToken).mockReturnValue(tokenMissingOrgClaims);
255
- vi.mocked(getOrgScopedToken).mockResolvedValue({
256
- jwt: 'org_scoped_jwt',
257
- });
258
- vi.mocked(parseJWT).mockReturnValue({
259
- exp: futureExp,
260
- o: { o_id: 'org_456' },
261
- });
262
-
263
- await loadAndRefreshToken();
264
-
265
- expect(saveToken).toHaveBeenCalledWith(
266
- expect.objectContaining({
267
- idToken: 'org_scoped_jwt',
268
- })
269
- );
270
- });
271
- });
272
-
273
- describe('OAuth refresh fallback for missing sid', () => {
274
- const originalFetch = global.fetch;
275
-
276
- afterEach(() => {
277
- global.fetch = originalFetch;
278
- });
279
-
280
- it('attempts OAuth refresh when no sid/originalSid but has refresh_token', async () => {
281
- const tokenNoSid = {
282
- idToken: 'token_no_sid',
283
- decoded: { exp: futureExp }, // No sid
284
- selectedOrg: { id: 'org_456', name: 'Test Org' },
115
+ describe('valid token handling', () => {
116
+ it('returns token data when not expired', async () => {
117
+ const validToken = {
118
+ idToken: 'valid_token',
119
+ decoded: { exp: futureExp, sub: 'user_123' },
285
120
  refresh_token: 'rt_123',
286
- // No originalSid either
287
121
  };
288
122
 
289
- vi.mocked(getToken).mockReturnValue(tokenNoSid);
290
-
291
- // Mock fetch for OAuth refresh
292
- global.fetch = vi.fn().mockResolvedValue({
293
- ok: true,
294
- json: () => Promise.resolve({
295
- access_token: 'new_at',
296
- id_token: 'new_idt_with_sid',
297
- refresh_token: 'new_rt',
298
- }),
299
- });
300
-
301
- // parseJWT returns token with sid after refresh
302
- vi.mocked(parseJWT)
303
- .mockReturnValueOnce({ exp: futureExp, sid: 'sess_new' }) // First call: parse refreshed token
304
- .mockReturnValueOnce({ exp: futureExp, o: { o_id: 'org_456' } }); // Second call: parse org-scoped token
305
-
306
- vi.mocked(getOrgScopedToken).mockResolvedValue({
307
- jwt: 'org_scoped_jwt_after_refresh',
308
- });
309
-
310
- await loadAndRefreshToken();
311
-
312
- expect(global.fetch).toHaveBeenCalledWith(
313
- expect.stringContaining('/oauth/token'),
314
- expect.any(Object)
315
- );
316
- });
317
-
318
- it('continues without org token when OAuth refresh fails', async () => {
319
- const tokenNoSid = {
320
- idToken: 'token_no_sid',
321
- decoded: { exp: futureExp },
322
- selectedOrg: { id: 'org_456', name: 'Test Org' },
323
- refresh_token: 'rt_123',
324
- };
123
+ vi.mocked(getToken).mockReturnValue(validToken);
124
+ vi.mocked(isTokenExpired).mockReturnValue(false);
325
125
 
326
- vi.mocked(getToken).mockReturnValue(tokenNoSid);
327
-
328
- // Mock fetch to fail
329
- global.fetch = vi.fn().mockResolvedValue({
330
- ok: false,
331
- status: 400,
332
- });
333
-
334
- // Should not throw, just return token as-is
335
126
  const result = await loadAndRefreshToken();
336
127
 
337
- expect(result).toBeDefined();
338
- expect(result.idToken).toBe('token_no_sid');
128
+ expect(refreshAccessToken).not.toHaveBeenCalled();
129
+ expect(result).toEqual(validToken);
339
130
  });
340
131
 
341
- it('logs warning when no recovery path available', async () => {
342
- const fullyBrokenToken = {
343
- idToken: 'broken_token',
344
- decoded: { exp: futureExp },
345
- selectedOrg: { id: 'org_456', name: 'Test Org' },
346
- // No sid, no originalSid, no refresh_token
132
+ it('returns token with org context when present', async () => {
133
+ const orgToken = {
134
+ idToken: 'org_token',
135
+ decoded: { exp: futureExp, sub: 'user_123', org_id: 'org_456' },
136
+ selectedOrgId: 'org_456',
137
+ selectedOrgName: 'Test Org',
138
+ refresh_token: 'rt_123',
347
139
  };
348
140
 
349
- vi.mocked(getToken).mockReturnValue(fullyBrokenToken);
350
-
351
- // Spy on console.error
352
- const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
141
+ vi.mocked(getToken).mockReturnValue(orgToken);
142
+ vi.mocked(isTokenExpired).mockReturnValue(false);
353
143
 
354
144
  const result = await loadAndRefreshToken();
355
145
 
356
- // Should still return token (API calls may fail later)
357
- expect(result).toBeDefined();
358
-
359
- // Should have logged warning about missing session ID
360
- expect(consoleSpy).toHaveBeenCalledWith(
361
- expect.stringContaining('Could not obtain session ID')
362
- );
363
-
364
- consoleSpy.mockRestore();
146
+ expect(result.selectedOrgId).toBe('org_456');
147
+ expect(result.selectedOrgName).toBe('Test Org');
365
148
  });
366
149
  });
367
150
  });