claude-session-share 1.0.0

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.
@@ -0,0 +1,341 @@
1
+ /**
2
+ * Tests for GitHub Gist client authentication and initialization
3
+ */
4
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
5
+ import { GistClient, GistAuthError, GistApiError } from '../gist/client.js';
6
+ describe('GistClient', () => {
7
+ let originalToken;
8
+ beforeEach(() => {
9
+ // Save original token
10
+ originalToken = process.env.GITHUB_TOKEN;
11
+ });
12
+ afterEach(() => {
13
+ // Restore original token
14
+ if (originalToken) {
15
+ process.env.GITHUB_TOKEN = originalToken;
16
+ }
17
+ else {
18
+ delete process.env.GITHUB_TOKEN;
19
+ }
20
+ });
21
+ describe('initialization', () => {
22
+ it('should initialize successfully with valid GITHUB_TOKEN', () => {
23
+ process.env.GITHUB_TOKEN = 'ghp_test_token_1234567890';
24
+ expect(() => new GistClient()).not.toThrow();
25
+ });
26
+ it('should throw GistAuthError when GITHUB_TOKEN is missing', () => {
27
+ delete process.env.GITHUB_TOKEN;
28
+ expect(() => new GistClient()).toThrow(GistAuthError);
29
+ expect(() => new GistClient()).toThrow(/GITHUB_TOKEN environment variable is required/);
30
+ });
31
+ it('should provide helpful error message with token setup instructions', () => {
32
+ delete process.env.GITHUB_TOKEN;
33
+ try {
34
+ new GistClient();
35
+ expect.fail('Should have thrown GistAuthError');
36
+ }
37
+ catch (error) {
38
+ expect(error).toBeInstanceOf(GistAuthError);
39
+ expect(error.message).toContain('https://github.com/settings/tokens');
40
+ expect(error.message).toContain('gist');
41
+ }
42
+ });
43
+ it('should initialize Octokit with authentication', () => {
44
+ process.env.GITHUB_TOKEN = 'ghp_test_token_1234567890';
45
+ const client = new GistClient();
46
+ const octokit = client.getOctokit();
47
+ expect(octokit).toBeDefined();
48
+ });
49
+ });
50
+ describe('throttling configuration', () => {
51
+ it('should configure rate limit handling', () => {
52
+ process.env.GITHUB_TOKEN = 'ghp_test_token_1234567890';
53
+ // Just verify initialization doesn't throw
54
+ // Actual throttling behavior is tested via Octokit's own tests
55
+ expect(() => new GistClient()).not.toThrow();
56
+ });
57
+ });
58
+ describe('fetchGist', () => {
59
+ beforeEach(() => {
60
+ process.env.GITHUB_TOKEN = 'ghp_test_token_1234567890';
61
+ });
62
+ it('should fetch a gist by ID successfully', async () => {
63
+ const client = new GistClient();
64
+ const octokit = client.getOctokit();
65
+ const mockGet = vi.fn().mockResolvedValue({
66
+ data: {
67
+ id: 'abc123',
68
+ url: 'https://api.github.com/gists/abc123',
69
+ html_url: 'https://gist.github.com/user/abc123',
70
+ files: {
71
+ 'session.jsonl': {
72
+ filename: 'session.jsonl',
73
+ type: 'application/jsonl',
74
+ language: 'JSON',
75
+ raw_url: 'https://gist.githubusercontent.com/raw/abc123/session.jsonl',
76
+ size: 1024,
77
+ content: '{"type":"user","uuid":"123"}\n{"type":"assistant","uuid":"456"}',
78
+ },
79
+ },
80
+ public: false,
81
+ created_at: '2024-01-01T00:00:00Z',
82
+ updated_at: '2024-01-01T00:00:00Z',
83
+ description: 'Session export',
84
+ },
85
+ });
86
+ octokit.rest.gists.get = mockGet;
87
+ const result = await client.fetchGist('abc123');
88
+ expect(result.id).toBe('abc123');
89
+ expect(result.html_url).toBe('https://gist.github.com/user/abc123');
90
+ expect(result.public).toBe(false);
91
+ expect(result.description).toBe('Session export');
92
+ expect(result.files['session.jsonl']).toBeDefined();
93
+ expect(result.files['session.jsonl'].content).toContain('type');
94
+ expect(mockGet).toHaveBeenCalledWith({
95
+ gist_id: 'abc123',
96
+ });
97
+ });
98
+ it('should extract gist ID from full URL', async () => {
99
+ const client = new GistClient();
100
+ const octokit = client.getOctokit();
101
+ const mockGet = vi.fn().mockResolvedValue({
102
+ data: {
103
+ id: 'abc123def456',
104
+ url: 'https://api.github.com/gists/abc123def456',
105
+ html_url: 'https://gist.github.com/user/abc123def456',
106
+ files: {},
107
+ public: false,
108
+ created_at: '2024-01-01T00:00:00Z',
109
+ updated_at: '2024-01-01T00:00:00Z',
110
+ description: '',
111
+ },
112
+ });
113
+ octokit.rest.gists.get = mockGet;
114
+ await client.fetchGist('https://gist.github.com/username/abc123def456');
115
+ expect(mockGet).toHaveBeenCalledWith({
116
+ gist_id: 'abc123def456',
117
+ });
118
+ });
119
+ it('should handle gist URL with trailing slash', async () => {
120
+ const client = new GistClient();
121
+ const octokit = client.getOctokit();
122
+ const mockGet = vi.fn().mockResolvedValue({
123
+ data: {
124
+ id: 'xyz789',
125
+ url: 'https://api.github.com/gists/xyz789',
126
+ html_url: 'https://gist.github.com/user/xyz789',
127
+ files: {},
128
+ public: false,
129
+ created_at: '2024-01-01T00:00:00Z',
130
+ updated_at: '2024-01-01T00:00:00Z',
131
+ description: '',
132
+ },
133
+ });
134
+ octokit.rest.gists.get = mockGet;
135
+ await client.fetchGist('https://gist.github.com/user/xyz789/');
136
+ // Should still extract xyz789 even with trailing slash
137
+ expect(mockGet).toHaveBeenCalledWith({
138
+ gist_id: 'xyz789',
139
+ });
140
+ });
141
+ it('should throw GistApiError on 404 (not found)', async () => {
142
+ const client = new GistClient();
143
+ const octokit = client.getOctokit();
144
+ octokit.rest.gists.get = vi.fn().mockRejectedValue({
145
+ status: 404,
146
+ message: 'Not Found',
147
+ });
148
+ await expect(client.fetchGist('nonexistent123')).rejects.toThrow(GistApiError);
149
+ await expect(client.fetchGist('nonexistent123')).rejects.toThrow(/Gist not found: nonexistent123/);
150
+ });
151
+ it('should throw GistApiError on 403 (private/deleted gist)', async () => {
152
+ const client = new GistClient();
153
+ const octokit = client.getOctokit();
154
+ octokit.rest.gists.get = vi.fn().mockRejectedValue({
155
+ status: 403,
156
+ message: 'Forbidden',
157
+ });
158
+ await expect(client.fetchGist('private123')).rejects.toThrow(GistApiError);
159
+ await expect(client.fetchGist('private123')).rejects.toThrow(/Access denied to gist private123/);
160
+ });
161
+ it('should throw GistApiError on 403 rate limit', async () => {
162
+ const client = new GistClient();
163
+ const octokit = client.getOctokit();
164
+ octokit.rest.gists.get = vi.fn().mockRejectedValue({
165
+ status: 403,
166
+ message: 'API rate limit exceeded',
167
+ });
168
+ await expect(client.fetchGist('abc123')).rejects.toThrow(GistApiError);
169
+ await expect(client.fetchGist('abc123')).rejects.toThrow(/rate limit exceeded/);
170
+ });
171
+ it('should throw GistAuthError on 401 (invalid token)', async () => {
172
+ const client = new GistClient();
173
+ const octokit = client.getOctokit();
174
+ octokit.rest.gists.get = vi.fn().mockRejectedValue({
175
+ status: 401,
176
+ message: 'Bad credentials',
177
+ });
178
+ await expect(client.fetchGist('abc123')).rejects.toThrow(GistAuthError);
179
+ await expect(client.fetchGist('abc123')).rejects.toThrow(/Invalid GITHUB_TOKEN/);
180
+ });
181
+ it('should extract content from files object', async () => {
182
+ const client = new GistClient();
183
+ const octokit = client.getOctokit();
184
+ const sessionContent = '{"type":"user","uuid":"123"}\n{"type":"assistant","uuid":"456"}';
185
+ const mockGet = vi.fn().mockResolvedValue({
186
+ data: {
187
+ id: 'abc123',
188
+ url: 'https://api.github.com/gists/abc123',
189
+ html_url: 'https://gist.github.com/abc123',
190
+ files: {
191
+ 'session.jsonl': {
192
+ filename: 'session.jsonl',
193
+ type: 'application/jsonl',
194
+ language: 'JSON',
195
+ raw_url: 'https://gist.githubusercontent.com/raw/abc123/session.jsonl',
196
+ size: sessionContent.length,
197
+ content: sessionContent,
198
+ },
199
+ },
200
+ public: false,
201
+ created_at: '2024-01-01T00:00:00Z',
202
+ updated_at: '2024-01-01T00:00:00Z',
203
+ description: 'Test',
204
+ },
205
+ });
206
+ octokit.rest.gists.get = mockGet;
207
+ const result = await client.fetchGist('abc123');
208
+ expect(result.files['session.jsonl'].content).toBe(sessionContent);
209
+ });
210
+ });
211
+ describe('createGist', () => {
212
+ beforeEach(() => {
213
+ process.env.GITHUB_TOKEN = 'ghp_test_token_1234567890';
214
+ });
215
+ it('should create a secret gist successfully', async () => {
216
+ const client = new GistClient();
217
+ const octokit = client.getOctokit();
218
+ // Mock the gists.create method
219
+ const mockCreate = vi.fn().mockResolvedValue({
220
+ data: {
221
+ id: 'abc123',
222
+ url: 'https://api.github.com/gists/abc123',
223
+ html_url: 'https://gist.github.com/abc123',
224
+ files: {
225
+ 'test.txt': {
226
+ filename: 'test.txt',
227
+ type: 'text/plain',
228
+ language: 'Text',
229
+ raw_url: 'https://gist.githubusercontent.com/raw/abc123/test.txt',
230
+ size: 11,
231
+ content: 'Hello World',
232
+ },
233
+ },
234
+ public: false,
235
+ created_at: '2024-01-01T00:00:00Z',
236
+ updated_at: '2024-01-01T00:00:00Z',
237
+ description: 'Test gist',
238
+ },
239
+ });
240
+ octokit.rest.gists.create = mockCreate;
241
+ const result = await client.createGist('Test gist', {
242
+ 'test.txt': 'Hello World',
243
+ });
244
+ expect(result.id).toBe('abc123');
245
+ expect(result.html_url).toBe('https://gist.github.com/abc123');
246
+ expect(result.public).toBe(false);
247
+ expect(result.description).toBe('Test gist');
248
+ expect(result.files['test.txt']).toBeDefined();
249
+ // Verify it was called with public: false (secret gist)
250
+ expect(mockCreate).toHaveBeenCalledWith({
251
+ description: 'Test gist',
252
+ public: false,
253
+ files: {
254
+ 'test.txt': { content: 'Hello World' },
255
+ },
256
+ });
257
+ });
258
+ it('should handle multiple files', async () => {
259
+ const client = new GistClient();
260
+ const octokit = client.getOctokit();
261
+ const mockCreate = vi.fn().mockResolvedValue({
262
+ data: {
263
+ id: 'abc123',
264
+ url: 'https://api.github.com/gists/abc123',
265
+ html_url: 'https://gist.github.com/abc123',
266
+ files: {
267
+ 'file1.txt': { content: 'Content 1' },
268
+ 'file2.txt': { content: 'Content 2' },
269
+ },
270
+ public: false,
271
+ created_at: '2024-01-01T00:00:00Z',
272
+ updated_at: '2024-01-01T00:00:00Z',
273
+ description: 'Multi-file gist',
274
+ },
275
+ });
276
+ octokit.rest.gists.create = mockCreate;
277
+ await client.createGist('Multi-file gist', {
278
+ 'file1.txt': 'Content 1',
279
+ 'file2.txt': 'Content 2',
280
+ });
281
+ expect(mockCreate).toHaveBeenCalledWith({
282
+ description: 'Multi-file gist',
283
+ public: false,
284
+ files: {
285
+ 'file1.txt': { content: 'Content 1' },
286
+ 'file2.txt': { content: 'Content 2' },
287
+ },
288
+ });
289
+ });
290
+ it('should throw GistAuthError on 401 (invalid token)', async () => {
291
+ const client = new GistClient();
292
+ const octokit = client.getOctokit();
293
+ octokit.rest.gists.create = vi.fn().mockRejectedValue({
294
+ status: 401,
295
+ message: 'Bad credentials',
296
+ });
297
+ await expect(client.createGist('Test', { 'test.txt': 'content' })).rejects.toThrow(GistAuthError);
298
+ await expect(client.createGist('Test', { 'test.txt': 'content' })).rejects.toThrow(/Invalid GITHUB_TOKEN/);
299
+ });
300
+ it('should throw GistApiError on 403 (permission denied)', async () => {
301
+ const client = new GistClient();
302
+ const octokit = client.getOctokit();
303
+ octokit.rest.gists.create = vi.fn().mockRejectedValue({
304
+ status: 403,
305
+ message: 'Forbidden',
306
+ });
307
+ await expect(client.createGist('Test', { 'test.txt': 'content' })).rejects.toThrow(GistApiError);
308
+ await expect(client.createGist('Test', { 'test.txt': 'content' })).rejects.toThrow(/Permission denied/);
309
+ });
310
+ it('should throw GistApiError on 403 rate limit', async () => {
311
+ const client = new GistClient();
312
+ const octokit = client.getOctokit();
313
+ octokit.rest.gists.create = vi.fn().mockRejectedValue({
314
+ status: 403,
315
+ message: 'API rate limit exceeded',
316
+ });
317
+ await expect(client.createGist('Test', { 'test.txt': 'content' })).rejects.toThrow(GistApiError);
318
+ await expect(client.createGist('Test', { 'test.txt': 'content' })).rejects.toThrow(/rate limit exceeded/);
319
+ });
320
+ it('should throw GistApiError on 422 (validation error)', async () => {
321
+ const client = new GistClient();
322
+ const octokit = client.getOctokit();
323
+ octokit.rest.gists.create = vi.fn().mockRejectedValue({
324
+ status: 422,
325
+ message: 'Validation Failed',
326
+ });
327
+ await expect(client.createGist('Test', { 'test.txt': 'content' })).rejects.toThrow(GistApiError);
328
+ await expect(client.createGist('Test', { 'test.txt': 'content' })).rejects.toThrow(/Invalid gist data/);
329
+ });
330
+ it('should throw GistApiError on other errors', async () => {
331
+ const client = new GistClient();
332
+ const octokit = client.getOctokit();
333
+ octokit.rest.gists.create = vi.fn().mockRejectedValue({
334
+ status: 500,
335
+ message: 'Internal Server Error',
336
+ });
337
+ await expect(client.createGist('Test', { 'test.txt': 'content' })).rejects.toThrow(GistApiError);
338
+ await expect(client.createGist('Test', { 'test.txt': 'content' })).rejects.toThrow(/Failed to create gist/);
339
+ });
340
+ });
341
+ });
@@ -0,0 +1,16 @@
1
+ import { describe, it, expect } from "vitest";
2
+ /**
3
+ * Smoke test for MCP server module
4
+ *
5
+ * This test verifies that the server module can be imported successfully
6
+ * without runtime errors. Full MCP protocol testing requires a client setup
7
+ * and will be added in later phases as we implement actual tools.
8
+ */
9
+ describe("MCP Server Module", () => {
10
+ it("should import without errors", async () => {
11
+ // This test verifies that the module structure is valid
12
+ // and all dependencies can be resolved
13
+ const moduleImport = import("../index.js");
14
+ await expect(moduleImport).resolves.toBeDefined();
15
+ });
16
+ });