inboxd 1.0.12 → 1.1.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,189 @@
1
+ import { describe, it, expect } from 'vitest';
2
+
3
+ // Test JSON output structures for commands that gained --json support
4
+ // Validates the output format for AI agent consumption
5
+
6
+ describe('JSON Output Formats', () => {
7
+ describe('inbox accounts --json', () => {
8
+ it('should output account list structure', () => {
9
+ const jsonOutput = {
10
+ accounts: [
11
+ { name: 'personal', email: 'user@gmail.com' },
12
+ { name: 'work', email: 'user@company.com' },
13
+ ],
14
+ };
15
+
16
+ expect(jsonOutput).toHaveProperty('accounts');
17
+ expect(Array.isArray(jsonOutput.accounts)).toBe(true);
18
+ expect(jsonOutput.accounts[0]).toHaveProperty('name');
19
+ expect(jsonOutput.accounts[0]).toHaveProperty('email');
20
+ });
21
+
22
+ it('should handle empty accounts list', () => {
23
+ const jsonOutput = {
24
+ accounts: [],
25
+ };
26
+
27
+ expect(jsonOutput.accounts).toEqual([]);
28
+ });
29
+ });
30
+
31
+ describe('inbox deletion-log --json', () => {
32
+ it('should output deletion log structure', () => {
33
+ const jsonOutput = {
34
+ days: 30,
35
+ count: 15,
36
+ logPath: '/Users/test/.config/inboxd/deletion-log.json',
37
+ deletions: [
38
+ {
39
+ deletedAt: '2026-01-03T15:45:00.000Z',
40
+ account: 'personal',
41
+ id: '19b84376ff5f5ed2',
42
+ threadId: '19b84376ff5f5ed2',
43
+ from: 'sender@example.com',
44
+ subject: 'Test Subject',
45
+ snippet: 'Email preview...',
46
+ },
47
+ ],
48
+ };
49
+
50
+ expect(jsonOutput).toHaveProperty('days');
51
+ expect(jsonOutput).toHaveProperty('count');
52
+ expect(jsonOutput).toHaveProperty('logPath');
53
+ expect(jsonOutput).toHaveProperty('deletions');
54
+ expect(Array.isArray(jsonOutput.deletions)).toBe(true);
55
+ });
56
+
57
+ it('should include all deletion entry fields', () => {
58
+ const deletion = {
59
+ deletedAt: '2026-01-03T15:45:00.000Z',
60
+ account: 'personal',
61
+ id: '19b84376ff5f5ed2',
62
+ threadId: '19b84376ff5f5ed2',
63
+ from: 'sender@example.com',
64
+ subject: 'Test Subject',
65
+ snippet: 'Email preview...',
66
+ };
67
+
68
+ expect(deletion).toHaveProperty('deletedAt');
69
+ expect(deletion).toHaveProperty('account');
70
+ expect(deletion).toHaveProperty('id');
71
+ expect(deletion).toHaveProperty('threadId');
72
+ expect(deletion).toHaveProperty('from');
73
+ expect(deletion).toHaveProperty('subject');
74
+ expect(deletion).toHaveProperty('snippet');
75
+ });
76
+ });
77
+
78
+ describe('inbox delete --dry-run --json', () => {
79
+ it('should output preview structure', () => {
80
+ const jsonOutput = {
81
+ dryRun: true,
82
+ count: 5,
83
+ emails: [
84
+ {
85
+ id: 'msg1',
86
+ account: 'personal',
87
+ from: 'sender@example.com',
88
+ subject: 'Test Subject',
89
+ date: 'Fri, 03 Jan 2026 10:30:00 -0800',
90
+ },
91
+ ],
92
+ };
93
+
94
+ expect(jsonOutput).toHaveProperty('dryRun');
95
+ expect(jsonOutput.dryRun).toBe(true);
96
+ expect(jsonOutput).toHaveProperty('count');
97
+ expect(jsonOutput).toHaveProperty('emails');
98
+ expect(Array.isArray(jsonOutput.emails)).toBe(true);
99
+ });
100
+
101
+ it('should include email details for each item', () => {
102
+ const email = {
103
+ id: 'msg1',
104
+ account: 'personal',
105
+ from: 'sender@example.com',
106
+ subject: 'Test Subject',
107
+ date: 'Fri, 03 Jan 2026 10:30:00 -0800',
108
+ };
109
+
110
+ expect(email).toHaveProperty('id');
111
+ expect(email).toHaveProperty('account');
112
+ expect(email).toHaveProperty('from');
113
+ expect(email).toHaveProperty('subject');
114
+ expect(email).toHaveProperty('date');
115
+ });
116
+
117
+ it('should handle empty preview', () => {
118
+ const jsonOutput = {
119
+ dryRun: true,
120
+ count: 0,
121
+ emails: [],
122
+ };
123
+
124
+ expect(jsonOutput.count).toBe(0);
125
+ expect(jsonOutput.emails).toEqual([]);
126
+ });
127
+ });
128
+
129
+ describe('inbox restore --json', () => {
130
+ it('should output restore results structure', () => {
131
+ const jsonOutput = {
132
+ restored: 3,
133
+ failed: 1,
134
+ results: [
135
+ { id: 'msg1', account: 'personal', from: 'a@b.com', subject: 'Test', success: true },
136
+ { id: 'msg2', account: 'work', from: 'c@d.com', subject: 'Test 2', success: false },
137
+ ],
138
+ };
139
+
140
+ expect(jsonOutput).toHaveProperty('restored');
141
+ expect(jsonOutput).toHaveProperty('failed');
142
+ expect(jsonOutput).toHaveProperty('results');
143
+ expect(Array.isArray(jsonOutput.results)).toBe(true);
144
+ });
145
+
146
+ it('should include success status for each result', () => {
147
+ const result = {
148
+ id: 'msg1',
149
+ account: 'personal',
150
+ from: 'a@b.com',
151
+ subject: 'Test',
152
+ success: true,
153
+ };
154
+
155
+ expect(result).toHaveProperty('success');
156
+ expect(typeof result.success).toBe('boolean');
157
+ });
158
+
159
+ it('should output error structure on failure', () => {
160
+ const jsonOutput = {
161
+ error: 'Must specify either --ids or --last',
162
+ };
163
+
164
+ expect(jsonOutput).toHaveProperty('error');
165
+ expect(typeof jsonOutput.error).toBe('string');
166
+ });
167
+ });
168
+
169
+ describe('JSON formatting', () => {
170
+ it('should produce valid JSON', () => {
171
+ const data = {
172
+ accounts: [{ name: 'test', email: 'test@example.com' }],
173
+ };
174
+
175
+ const jsonString = JSON.stringify(data, null, 2);
176
+ const parsed = JSON.parse(jsonString);
177
+
178
+ expect(parsed).toEqual(data);
179
+ });
180
+
181
+ it('should use 2-space indentation', () => {
182
+ const data = { key: 'value' };
183
+ const jsonString = JSON.stringify(data, null, 2);
184
+
185
+ expect(jsonString).toContain('\n');
186
+ expect(jsonString).toContain(' '); // 2-space indent
187
+ });
188
+ });
189
+ });
@@ -0,0 +1,218 @@
1
+ import { describe, it, expect } from 'vitest';
2
+
3
+ // Test the stats logic without importing real modules
4
+ // This follows the pattern used in deletion-log.test.js
5
+
6
+ describe('Stats Command', () => {
7
+ describe('extractDomain helper', () => {
8
+ // Mirrors the extractDomain function in deletion-log.js
9
+ function extractDomain(from) {
10
+ const match = from.match(/@([a-zA-Z0-9.-]+)/);
11
+ return match ? match[1].toLowerCase() : 'unknown';
12
+ }
13
+
14
+ it('should extract domain from email in angle brackets', () => {
15
+ expect(extractDomain('John Doe <john@example.com>')).toBe('example.com');
16
+ });
17
+
18
+ it('should extract domain from bare email', () => {
19
+ expect(extractDomain('john@example.com')).toBe('example.com');
20
+ });
21
+
22
+ it('should handle subdomain', () => {
23
+ expect(extractDomain('noreply@mail.linkedin.com')).toBe('mail.linkedin.com');
24
+ });
25
+
26
+ it('should return unknown for invalid format', () => {
27
+ expect(extractDomain('No Email Here')).toBe('unknown');
28
+ expect(extractDomain('')).toBe('unknown');
29
+ });
30
+
31
+ it('should lowercase the domain', () => {
32
+ expect(extractDomain('user@EXAMPLE.COM')).toBe('example.com');
33
+ });
34
+ });
35
+
36
+ describe('Deletion Stats', () => {
37
+ // Mirrors getStats() logic from deletion-log.js
38
+ function getStats(deletions) {
39
+ const byAccount = {};
40
+ deletions.forEach(d => {
41
+ const account = d.account || 'default';
42
+ byAccount[account] = (byAccount[account] || 0) + 1;
43
+ });
44
+
45
+ const bySender = {};
46
+ deletions.forEach(d => {
47
+ const match = (d.from || '').match(/@([a-zA-Z0-9.-]+)/);
48
+ const domain = match ? match[1].toLowerCase() : 'unknown';
49
+ bySender[domain] = (bySender[domain] || 0) + 1;
50
+ });
51
+
52
+ const topSenders = Object.entries(bySender)
53
+ .sort((a, b) => b[1] - a[1])
54
+ .slice(0, 10)
55
+ .map(([domain, count]) => ({ domain, count }));
56
+
57
+ return {
58
+ total: deletions.length,
59
+ byAccount,
60
+ topSenders,
61
+ };
62
+ }
63
+
64
+ it('should count total deletions', () => {
65
+ const deletions = [
66
+ { id: '1', account: 'personal', from: 'a@example.com' },
67
+ { id: '2', account: 'work', from: 'b@test.com' },
68
+ { id: '3', account: 'personal', from: 'c@example.com' },
69
+ ];
70
+ const stats = getStats(deletions);
71
+ expect(stats.total).toBe(3);
72
+ });
73
+
74
+ it('should group by account', () => {
75
+ const deletions = [
76
+ { id: '1', account: 'personal', from: 'a@example.com' },
77
+ { id: '2', account: 'work', from: 'b@test.com' },
78
+ { id: '3', account: 'personal', from: 'c@example.com' },
79
+ ];
80
+ const stats = getStats(deletions);
81
+ expect(stats.byAccount).toEqual({ personal: 2, work: 1 });
82
+ });
83
+
84
+ it('should use default for missing account', () => {
85
+ const deletions = [
86
+ { id: '1', from: 'a@example.com' },
87
+ ];
88
+ const stats = getStats(deletions);
89
+ expect(stats.byAccount).toEqual({ default: 1 });
90
+ });
91
+
92
+ it('should rank top senders by count', () => {
93
+ const deletions = [
94
+ { id: '1', from: 'a@linkedin.com', account: 'test' },
95
+ { id: '2', from: 'b@linkedin.com', account: 'test' },
96
+ { id: '3', from: 'c@linkedin.com', account: 'test' },
97
+ { id: '4', from: 'd@amazon.com', account: 'test' },
98
+ { id: '5', from: 'e@amazon.com', account: 'test' },
99
+ { id: '6', from: 'f@github.com', account: 'test' },
100
+ ];
101
+ const stats = getStats(deletions);
102
+ expect(stats.topSenders[0]).toEqual({ domain: 'linkedin.com', count: 3 });
103
+ expect(stats.topSenders[1]).toEqual({ domain: 'amazon.com', count: 2 });
104
+ expect(stats.topSenders[2]).toEqual({ domain: 'github.com', count: 1 });
105
+ });
106
+
107
+ it('should limit top senders to 10', () => {
108
+ const deletions = [];
109
+ for (let i = 0; i < 15; i++) {
110
+ deletions.push({ id: `${i}`, from: `user@domain${i}.com`, account: 'test' });
111
+ }
112
+ const stats = getStats(deletions);
113
+ expect(stats.topSenders.length).toBe(10);
114
+ });
115
+
116
+ it('should handle empty deletions', () => {
117
+ const stats = getStats([]);
118
+ expect(stats.total).toBe(0);
119
+ expect(stats.byAccount).toEqual({});
120
+ expect(stats.topSenders).toEqual([]);
121
+ });
122
+ });
123
+
124
+ describe('Sent Stats', () => {
125
+ // Mirrors getSentStats() logic from sent-log.js
126
+ function getSentStats(sent) {
127
+ let replies = 0;
128
+ let newEmails = 0;
129
+ sent.forEach(s => {
130
+ if (s.replyToId) {
131
+ replies++;
132
+ } else {
133
+ newEmails++;
134
+ }
135
+ });
136
+
137
+ const byAccount = {};
138
+ sent.forEach(s => {
139
+ const account = s.account || 'default';
140
+ byAccount[account] = (byAccount[account] || 0) + 1;
141
+ });
142
+
143
+ return {
144
+ total: sent.length,
145
+ replies,
146
+ newEmails,
147
+ byAccount,
148
+ };
149
+ }
150
+
151
+ it('should count total sent emails', () => {
152
+ const sent = [
153
+ { id: '1', account: 'personal' },
154
+ { id: '2', account: 'work' },
155
+ ];
156
+ const stats = getSentStats(sent);
157
+ expect(stats.total).toBe(2);
158
+ });
159
+
160
+ it('should distinguish replies from new emails', () => {
161
+ const sent = [
162
+ { id: '1', account: 'test', replyToId: 'orig1' },
163
+ { id: '2', account: 'test', replyToId: 'orig2' },
164
+ { id: '3', account: 'test', replyToId: null },
165
+ { id: '4', account: 'test' },
166
+ ];
167
+ const stats = getSentStats(sent);
168
+ expect(stats.replies).toBe(2);
169
+ expect(stats.newEmails).toBe(2);
170
+ });
171
+
172
+ it('should group by account', () => {
173
+ const sent = [
174
+ { id: '1', account: 'personal' },
175
+ { id: '2', account: 'work' },
176
+ { id: '3', account: 'personal' },
177
+ ];
178
+ const stats = getSentStats(sent);
179
+ expect(stats.byAccount).toEqual({ personal: 2, work: 1 });
180
+ });
181
+
182
+ it('should handle empty sent list', () => {
183
+ const stats = getSentStats([]);
184
+ expect(stats.total).toBe(0);
185
+ expect(stats.replies).toBe(0);
186
+ expect(stats.newEmails).toBe(0);
187
+ expect(stats.byAccount).toEqual({});
188
+ });
189
+ });
190
+
191
+ describe('Stats JSON output structure', () => {
192
+ it('should have correct structure for JSON output', () => {
193
+ const jsonOutput = {
194
+ period: 30,
195
+ deleted: {
196
+ total: 10,
197
+ byAccount: { personal: 5, work: 5 },
198
+ topSenders: [{ domain: 'example.com', count: 3 }],
199
+ },
200
+ sent: {
201
+ total: 3,
202
+ replies: 2,
203
+ newEmails: 1,
204
+ byAccount: { personal: 3 },
205
+ },
206
+ };
207
+
208
+ expect(jsonOutput).toHaveProperty('period');
209
+ expect(jsonOutput).toHaveProperty('deleted');
210
+ expect(jsonOutput).toHaveProperty('sent');
211
+ expect(jsonOutput.deleted).toHaveProperty('total');
212
+ expect(jsonOutput.deleted).toHaveProperty('byAccount');
213
+ expect(jsonOutput.deleted).toHaveProperty('topSenders');
214
+ expect(jsonOutput.sent).toHaveProperty('replies');
215
+ expect(jsonOutput.sent).toHaveProperty('newEmails');
216
+ });
217
+ });
218
+ });
@@ -0,0 +1,228 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+
3
+ // Test unarchive command logic
4
+ // Tests the gmail-monitor unarchiveEmails and CLI command logic
5
+
6
+ describe('Unarchive Command', () => {
7
+ describe('unarchiveEmails API logic', () => {
8
+ // Simulates the unarchiveEmails function in gmail-monitor.js
9
+ async function unarchiveEmails(mockGmail, messageIds) {
10
+ const results = [];
11
+
12
+ for (const id of messageIds) {
13
+ try {
14
+ await mockGmail.users.messages.modify({
15
+ userId: 'me',
16
+ id: id,
17
+ requestBody: {
18
+ addLabelIds: ['INBOX'],
19
+ },
20
+ });
21
+ results.push({ id, success: true });
22
+ } catch (err) {
23
+ results.push({ id, success: false, error: err.message });
24
+ }
25
+ }
26
+
27
+ return results;
28
+ }
29
+
30
+ it('should add INBOX label to unarchive emails', async () => {
31
+ const modifyCalls = [];
32
+ const mockGmail = {
33
+ users: {
34
+ messages: {
35
+ modify: vi.fn().mockImplementation((params) => {
36
+ modifyCalls.push(params);
37
+ return Promise.resolve({ data: { id: params.id } });
38
+ }),
39
+ },
40
+ },
41
+ };
42
+
43
+ await unarchiveEmails(mockGmail, ['msg1', 'msg2']);
44
+
45
+ expect(modifyCalls).toHaveLength(2);
46
+ expect(modifyCalls[0].requestBody.addLabelIds).toContain('INBOX');
47
+ expect(modifyCalls[1].requestBody.addLabelIds).toContain('INBOX');
48
+ });
49
+
50
+ it('should return success for each email', async () => {
51
+ const mockGmail = {
52
+ users: {
53
+ messages: {
54
+ modify: vi.fn().mockResolvedValue({ data: {} }),
55
+ },
56
+ },
57
+ };
58
+
59
+ const results = await unarchiveEmails(mockGmail, ['msg1', 'msg2']);
60
+
61
+ expect(results).toHaveLength(2);
62
+ expect(results[0]).toEqual({ id: 'msg1', success: true });
63
+ expect(results[1]).toEqual({ id: 'msg2', success: true });
64
+ });
65
+
66
+ it('should handle API errors gracefully', async () => {
67
+ const mockGmail = {
68
+ users: {
69
+ messages: {
70
+ modify: vi.fn()
71
+ .mockResolvedValueOnce({ data: {} })
72
+ .mockRejectedValueOnce(new Error('Not found'))
73
+ .mockResolvedValueOnce({ data: {} }),
74
+ },
75
+ },
76
+ };
77
+
78
+ const results = await unarchiveEmails(mockGmail, ['msg1', 'msg2', 'msg3']);
79
+
80
+ expect(results).toHaveLength(3);
81
+ expect(results[0].success).toBe(true);
82
+ expect(results[1].success).toBe(false);
83
+ expect(results[1].error).toBe('Not found');
84
+ expect(results[2].success).toBe(true);
85
+ });
86
+
87
+ it('should handle empty message list', async () => {
88
+ const mockGmail = {
89
+ users: {
90
+ messages: {
91
+ modify: vi.fn(),
92
+ },
93
+ },
94
+ };
95
+
96
+ const results = await unarchiveEmails(mockGmail, []);
97
+
98
+ expect(results).toHaveLength(0);
99
+ expect(mockGmail.users.messages.modify).not.toHaveBeenCalled();
100
+ });
101
+ });
102
+
103
+ describe('Command option parsing', () => {
104
+ it('should parse --ids option into array', () => {
105
+ const idsString = 'id1,id2,id3';
106
+ const ids = idsString.split(',').map(id => id.trim()).filter(Boolean);
107
+
108
+ expect(ids).toEqual(['id1', 'id2', 'id3']);
109
+ });
110
+
111
+ it('should handle whitespace in --ids', () => {
112
+ const idsString = 'id1, id2 , id3';
113
+ const ids = idsString.split(',').map(id => id.trim()).filter(Boolean);
114
+
115
+ expect(ids).toEqual(['id1', 'id2', 'id3']);
116
+ });
117
+
118
+ it('should filter empty strings from --ids', () => {
119
+ const idsString = 'id1,,id2,';
120
+ const ids = idsString.split(',').map(id => id.trim()).filter(Boolean);
121
+
122
+ expect(ids).toEqual(['id1', 'id2']);
123
+ });
124
+
125
+ it('should parse --last option as integer', () => {
126
+ const lastValue = '5';
127
+ const count = parseInt(lastValue, 10);
128
+
129
+ expect(count).toBe(5);
130
+ expect(typeof count).toBe('number');
131
+ });
132
+ });
133
+
134
+ describe('Archive log lookup', () => {
135
+ it('should find email in archive log by id', () => {
136
+ const archiveLog = [
137
+ { id: 'msg1', account: 'personal', from: 'a@b.com', subject: 'Test 1' },
138
+ { id: 'msg2', account: 'work', from: 'c@d.com', subject: 'Test 2' },
139
+ { id: 'msg3', account: 'personal', from: 'e@f.com', subject: 'Test 3' },
140
+ ];
141
+
142
+ const idsToFind = ['msg1', 'msg3'];
143
+ const found = idsToFind
144
+ .map(id => archiveLog.find(e => e.id === id))
145
+ .filter(Boolean);
146
+
147
+ expect(found).toHaveLength(2);
148
+ expect(found[0].id).toBe('msg1');
149
+ expect(found[1].id).toBe('msg3');
150
+ });
151
+
152
+ it('should return undefined for missing ids', () => {
153
+ const archiveLog = [
154
+ { id: 'msg1', account: 'personal' },
155
+ ];
156
+
157
+ const entry = archiveLog.find(e => e.id === 'nonexistent');
158
+ expect(entry).toBeUndefined();
159
+ });
160
+ });
161
+
162
+ describe('Grouping by account', () => {
163
+ it('should group emails by account for batch operations', () => {
164
+ const emailsToUnarchive = [
165
+ { id: 'msg1', account: 'personal' },
166
+ { id: 'msg2', account: 'work' },
167
+ { id: 'msg3', account: 'personal' },
168
+ { id: 'msg4', account: 'work' },
169
+ ];
170
+
171
+ const byAccount = {};
172
+ for (const email of emailsToUnarchive) {
173
+ if (!byAccount[email.account]) {
174
+ byAccount[email.account] = [];
175
+ }
176
+ byAccount[email.account].push(email);
177
+ }
178
+
179
+ expect(Object.keys(byAccount)).toEqual(['personal', 'work']);
180
+ expect(byAccount.personal).toHaveLength(2);
181
+ expect(byAccount.work).toHaveLength(2);
182
+ });
183
+ });
184
+
185
+ describe('JSON output structure', () => {
186
+ it('should have correct structure for success', () => {
187
+ const jsonOutput = {
188
+ unarchived: 3,
189
+ failed: 1,
190
+ results: [
191
+ { id: 'msg1', account: 'personal', from: 'a@b.com', subject: 'Test', success: true },
192
+ { id: 'msg2', account: 'work', from: 'c@d.com', subject: 'Test 2', success: false },
193
+ ],
194
+ };
195
+
196
+ expect(jsonOutput).toHaveProperty('unarchived');
197
+ expect(jsonOutput).toHaveProperty('failed');
198
+ expect(jsonOutput).toHaveProperty('results');
199
+ expect(Array.isArray(jsonOutput.results)).toBe(true);
200
+ expect(jsonOutput.results[0]).toHaveProperty('success');
201
+ });
202
+
203
+ it('should have correct structure for error', () => {
204
+ const jsonOutput = {
205
+ error: 'Must specify either --ids or --last',
206
+ };
207
+
208
+ expect(jsonOutput).toHaveProperty('error');
209
+ });
210
+ });
211
+
212
+ describe('Result counting', () => {
213
+ it('should count successful and failed operations', () => {
214
+ const results = [
215
+ { id: 'msg1', success: true },
216
+ { id: 'msg2', success: true },
217
+ { id: 'msg3', success: false },
218
+ { id: 'msg4', success: true },
219
+ ];
220
+
221
+ const successfulIds = results.filter(r => r.success).map(r => r.id);
222
+ const failedCount = results.filter(r => !r.success).length;
223
+
224
+ expect(successfulIds).toEqual(['msg1', 'msg2', 'msg4']);
225
+ expect(failedCount).toBe(1);
226
+ });
227
+ });
228
+ });