inboxd 1.0.13 → 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.
- package/.claude/skills/inbox-assistant/SKILL.md +17 -6
- package/CLAUDE.md +19 -4
- package/package.json +1 -1
- package/src/archive-log.js +104 -0
- package/src/cli.js +473 -17
- package/src/deletion-log.js +101 -0
- package/src/gmail-monitor.js +29 -0
- package/src/sent-log.js +35 -0
- package/tests/archive-log.test.js +196 -0
- package/tests/cleanup-suggest.test.js +239 -0
- package/tests/install-service.test.js +210 -0
- package/tests/interactive-confirm.test.js +175 -0
- package/tests/json-output.test.js +189 -0
- package/tests/stats.test.js +218 -0
- package/tests/unarchive.test.js +228 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
|
|
5
|
+
// Test install-service logic for both macOS and Linux
|
|
6
|
+
// Tests the service file generation without actually installing
|
|
7
|
+
|
|
8
|
+
describe('Install Service Command', () => {
|
|
9
|
+
const homeDir = os.homedir();
|
|
10
|
+
|
|
11
|
+
describe('Platform detection', () => {
|
|
12
|
+
it('should recognize darwin as macOS', () => {
|
|
13
|
+
const platform = 'darwin';
|
|
14
|
+
expect(platform === 'darwin').toBe(true);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should recognize linux as Linux', () => {
|
|
18
|
+
const platform = 'linux';
|
|
19
|
+
expect(platform === 'linux').toBe(true);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should reject unsupported platforms', () => {
|
|
23
|
+
const platform = 'win32';
|
|
24
|
+
const isSupported = platform === 'darwin' || platform === 'linux';
|
|
25
|
+
expect(isSupported).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('macOS launchd configuration', () => {
|
|
30
|
+
const launchAgentsDir = path.join(homeDir, 'Library/LaunchAgents');
|
|
31
|
+
const plistName = 'com.danielparedes.inboxd.plist';
|
|
32
|
+
const plistPath = path.join(launchAgentsDir, plistName);
|
|
33
|
+
|
|
34
|
+
it('should use correct plist path', () => {
|
|
35
|
+
expect(plistPath).toContain('Library/LaunchAgents');
|
|
36
|
+
expect(plistPath).toContain('com.danielparedes.inboxd.plist');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should generate valid plist structure', () => {
|
|
40
|
+
const interval = 5;
|
|
41
|
+
const seconds = interval * 60;
|
|
42
|
+
const nodePath = '/usr/local/bin/node';
|
|
43
|
+
const scriptPath = '/path/to/cli.js';
|
|
44
|
+
const workingDir = '/path/to';
|
|
45
|
+
|
|
46
|
+
const plistContent = `<?xml version="1.0" encoding="UTF-8"?>
|
|
47
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
48
|
+
<plist version="1.0">
|
|
49
|
+
<dict>
|
|
50
|
+
<key>Label</key>
|
|
51
|
+
<string>com.danielparedes.inboxd</string>
|
|
52
|
+
<key>ProgramArguments</key>
|
|
53
|
+
<array>
|
|
54
|
+
<string>${nodePath}</string>
|
|
55
|
+
<string>${scriptPath}</string>
|
|
56
|
+
<string>check</string>
|
|
57
|
+
<string>--quiet</string>
|
|
58
|
+
</array>
|
|
59
|
+
<key>StartInterval</key>
|
|
60
|
+
<integer>${seconds}</integer>
|
|
61
|
+
</dict>
|
|
62
|
+
</plist>`;
|
|
63
|
+
|
|
64
|
+
expect(plistContent).toContain('com.danielparedes.inboxd');
|
|
65
|
+
expect(plistContent).toContain('<integer>300</integer>'); // 5 * 60
|
|
66
|
+
expect(plistContent).toContain('check');
|
|
67
|
+
expect(plistContent).toContain('--quiet');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should convert minutes to seconds', () => {
|
|
71
|
+
const intervalMinutes = 10;
|
|
72
|
+
const seconds = intervalMinutes * 60;
|
|
73
|
+
expect(seconds).toBe(600);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('Linux systemd configuration', () => {
|
|
78
|
+
const systemdUserDir = path.join(homeDir, '.config/systemd/user');
|
|
79
|
+
const servicePath = path.join(systemdUserDir, 'inboxd.service');
|
|
80
|
+
const timerPath = path.join(systemdUserDir, 'inboxd.timer');
|
|
81
|
+
|
|
82
|
+
it('should use correct systemd paths', () => {
|
|
83
|
+
expect(systemdUserDir).toContain('.config/systemd/user');
|
|
84
|
+
expect(servicePath).toContain('inboxd.service');
|
|
85
|
+
expect(timerPath).toContain('inboxd.timer');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should generate valid service unit', () => {
|
|
89
|
+
const nodePath = '/usr/bin/node';
|
|
90
|
+
const scriptPath = '/path/to/cli.js';
|
|
91
|
+
const workingDir = '/path/to';
|
|
92
|
+
|
|
93
|
+
const serviceContent = `[Unit]
|
|
94
|
+
Description=inboxd - Gmail monitoring and notifications
|
|
95
|
+
After=network-online.target
|
|
96
|
+
Wants=network-online.target
|
|
97
|
+
|
|
98
|
+
[Service]
|
|
99
|
+
Type=oneshot
|
|
100
|
+
ExecStart=${nodePath} ${scriptPath} check --quiet
|
|
101
|
+
WorkingDirectory=${workingDir}
|
|
102
|
+
Environment=PATH=/usr/local/bin:/usr/bin:/bin
|
|
103
|
+
|
|
104
|
+
[Install]
|
|
105
|
+
WantedBy=default.target
|
|
106
|
+
`;
|
|
107
|
+
|
|
108
|
+
expect(serviceContent).toContain('[Unit]');
|
|
109
|
+
expect(serviceContent).toContain('[Service]');
|
|
110
|
+
expect(serviceContent).toContain('[Install]');
|
|
111
|
+
expect(serviceContent).toContain('Type=oneshot');
|
|
112
|
+
expect(serviceContent).toContain('check --quiet');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should generate valid timer unit', () => {
|
|
116
|
+
const interval = 5;
|
|
117
|
+
|
|
118
|
+
const timerContent = `[Unit]
|
|
119
|
+
Description=Run inboxd every ${interval} minutes
|
|
120
|
+
|
|
121
|
+
[Timer]
|
|
122
|
+
OnBootSec=1min
|
|
123
|
+
OnUnitActiveSec=${interval}min
|
|
124
|
+
Persistent=true
|
|
125
|
+
|
|
126
|
+
[Install]
|
|
127
|
+
WantedBy=timers.target
|
|
128
|
+
`;
|
|
129
|
+
|
|
130
|
+
expect(timerContent).toContain('[Unit]');
|
|
131
|
+
expect(timerContent).toContain('[Timer]');
|
|
132
|
+
expect(timerContent).toContain('[Install]');
|
|
133
|
+
expect(timerContent).toContain('OnUnitActiveSec=5min');
|
|
134
|
+
expect(timerContent).toContain('Persistent=true');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should use correct timer interval format', () => {
|
|
138
|
+
const intervals = [1, 5, 10, 15, 30];
|
|
139
|
+
|
|
140
|
+
intervals.forEach(interval => {
|
|
141
|
+
const timerInterval = `${interval}min`;
|
|
142
|
+
expect(timerInterval).toMatch(/^\d+min$/);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe('Interval parsing', () => {
|
|
148
|
+
it('should parse interval option as integer', () => {
|
|
149
|
+
const optionValue = '10';
|
|
150
|
+
const interval = parseInt(optionValue, 10);
|
|
151
|
+
expect(interval).toBe(10);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should use default interval of 5 minutes', () => {
|
|
155
|
+
const defaultInterval = '5';
|
|
156
|
+
const interval = parseInt(defaultInterval, 10);
|
|
157
|
+
expect(interval).toBe(5);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe('Uninstall logic', () => {
|
|
162
|
+
it('should identify service files for removal (macOS)', () => {
|
|
163
|
+
const plistPath = path.join(homeDir, 'Library/LaunchAgents/com.danielparedes.inboxd.plist');
|
|
164
|
+
expect(plistPath).toContain('com.danielparedes.inboxd.plist');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should identify service files for removal (Linux)', () => {
|
|
168
|
+
const servicePath = path.join(homeDir, '.config/systemd/user/inboxd.service');
|
|
169
|
+
const timerPath = path.join(homeDir, '.config/systemd/user/inboxd.timer');
|
|
170
|
+
|
|
171
|
+
expect(servicePath).toContain('inboxd.service');
|
|
172
|
+
expect(timerPath).toContain('inboxd.timer');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('should track if files were removed', () => {
|
|
176
|
+
let removed = false;
|
|
177
|
+
|
|
178
|
+
// Simulate file removal
|
|
179
|
+
const files = [
|
|
180
|
+
{ path: '/path/to/service', exists: true },
|
|
181
|
+
{ path: '/path/to/timer', exists: true },
|
|
182
|
+
];
|
|
183
|
+
|
|
184
|
+
files.forEach(file => {
|
|
185
|
+
if (file.exists) {
|
|
186
|
+
// fs.unlinkSync(file.path)
|
|
187
|
+
removed = true;
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
expect(removed).toBe(true);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe('Path generation', () => {
|
|
196
|
+
it('should resolve script path correctly', () => {
|
|
197
|
+
// Simulates path.resolve(__dirname, 'cli.js')
|
|
198
|
+
const mockDirname = '/Users/test/inboxd/src';
|
|
199
|
+
const scriptPath = path.resolve(mockDirname, 'cli.js');
|
|
200
|
+
expect(scriptPath).toContain('cli.js');
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('should resolve working directory correctly', () => {
|
|
204
|
+
// Simulates path.resolve(__dirname, '..')
|
|
205
|
+
const mockDirname = '/Users/test/inboxd/src';
|
|
206
|
+
const workingDir = path.resolve(mockDirname, '..');
|
|
207
|
+
expect(workingDir).not.toContain('/src');
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
});
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// Test interactive confirmation logic for send/reply commands
|
|
4
|
+
// Tests the prompt handling without actual readline interaction
|
|
5
|
+
|
|
6
|
+
describe('Interactive Confirm', () => {
|
|
7
|
+
describe('Prompt function', () => {
|
|
8
|
+
// Mirrors the prompt function from cli.js
|
|
9
|
+
function prompt(rl, question) {
|
|
10
|
+
return new Promise((resolve) => {
|
|
11
|
+
rl.question(question, (answer) => {
|
|
12
|
+
resolve(answer.trim());
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
it('should resolve with user answer', async () => {
|
|
18
|
+
const mockRl = {
|
|
19
|
+
question: vi.fn((q, callback) => callback('yes')),
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const answer = await prompt(mockRl, 'Confirm? ');
|
|
23
|
+
expect(answer).toBe('yes');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should trim whitespace from answer', async () => {
|
|
27
|
+
const mockRl = {
|
|
28
|
+
question: vi.fn((q, callback) => callback(' yes ')),
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const answer = await prompt(mockRl, 'Confirm? ');
|
|
32
|
+
expect(answer).toBe('yes');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should call question with provided prompt', async () => {
|
|
36
|
+
const mockRl = {
|
|
37
|
+
question: vi.fn((q, callback) => callback('y')),
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
await prompt(mockRl, 'Send this email? (y/N): ');
|
|
41
|
+
expect(mockRl.question).toHaveBeenCalledWith(
|
|
42
|
+
'Send this email? (y/N): ',
|
|
43
|
+
expect.any(Function)
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('Answer validation for send', () => {
|
|
49
|
+
function isConfirmed(answer) {
|
|
50
|
+
return answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
it('should accept "y" as confirmation', () => {
|
|
54
|
+
expect(isConfirmed('y')).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should accept "Y" as confirmation (case-insensitive)', () => {
|
|
58
|
+
expect(isConfirmed('Y')).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should accept "yes" as confirmation', () => {
|
|
62
|
+
expect(isConfirmed('yes')).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should accept "YES" as confirmation (case-insensitive)', () => {
|
|
66
|
+
expect(isConfirmed('YES')).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should reject "n" as not confirmed', () => {
|
|
70
|
+
expect(isConfirmed('n')).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should reject "no" as not confirmed', () => {
|
|
74
|
+
expect(isConfirmed('no')).toBe(false);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should reject empty string as not confirmed', () => {
|
|
78
|
+
expect(isConfirmed('')).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should reject random input as not confirmed', () => {
|
|
82
|
+
expect(isConfirmed('maybe')).toBe(false);
|
|
83
|
+
expect(isConfirmed('okay')).toBe(false);
|
|
84
|
+
expect(isConfirmed('sure')).toBe(false);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('readline interface lifecycle', () => {
|
|
89
|
+
it('should close interface after getting answer', () => {
|
|
90
|
+
const mockRl = {
|
|
91
|
+
question: vi.fn((q, callback) => callback('y')),
|
|
92
|
+
close: vi.fn(),
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// Simulate the pattern in cli.js
|
|
96
|
+
mockRl.question('Confirm?', () => {});
|
|
97
|
+
mockRl.close();
|
|
98
|
+
|
|
99
|
+
expect(mockRl.close).toHaveBeenCalled();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('--confirm flag behavior', () => {
|
|
104
|
+
it('should skip prompt when --confirm is provided', () => {
|
|
105
|
+
const options = { confirm: true };
|
|
106
|
+
|
|
107
|
+
if (options.confirm) {
|
|
108
|
+
// Skip prompt, proceed directly
|
|
109
|
+
expect(true).toBe(true);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should require prompt when --confirm is not provided', () => {
|
|
114
|
+
const options = { confirm: false };
|
|
115
|
+
const needsPrompt = !options.confirm;
|
|
116
|
+
|
|
117
|
+
expect(needsPrompt).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should require prompt when --confirm is undefined', () => {
|
|
121
|
+
const options = {};
|
|
122
|
+
const needsPrompt = !options.confirm;
|
|
123
|
+
|
|
124
|
+
expect(needsPrompt).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe('Cancel behavior', () => {
|
|
129
|
+
it('should cancel when user answers "n"', () => {
|
|
130
|
+
const answer = 'n';
|
|
131
|
+
const shouldProceed = answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes';
|
|
132
|
+
|
|
133
|
+
expect(shouldProceed).toBe(false);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should cancel when user presses enter (empty)', () => {
|
|
137
|
+
const answer = '';
|
|
138
|
+
const shouldProceed = answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes';
|
|
139
|
+
|
|
140
|
+
expect(shouldProceed).toBe(false);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe('Send command confirmation prompt', () => {
|
|
145
|
+
it('should use correct prompt message for send', () => {
|
|
146
|
+
const promptMessage = 'Send this email? (y/N): ';
|
|
147
|
+
expect(promptMessage).toContain('Send');
|
|
148
|
+
expect(promptMessage).toContain('y/N');
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe('Reply command confirmation prompt', () => {
|
|
153
|
+
it('should use correct prompt message for reply', () => {
|
|
154
|
+
const promptMessage = 'Send this reply? (y/N): ';
|
|
155
|
+
expect(promptMessage).toContain('reply');
|
|
156
|
+
expect(promptMessage).toContain('y/N');
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe('Integration with --dry-run', () => {
|
|
161
|
+
it('should not prompt when --dry-run is provided', () => {
|
|
162
|
+
const options = { dryRun: true, confirm: false };
|
|
163
|
+
|
|
164
|
+
// --dry-run takes precedence, no send happens
|
|
165
|
+
if (options.dryRun) {
|
|
166
|
+
// Preview only, skip confirmation
|
|
167
|
+
expect(true).toBe(true);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// This line should not be reached
|
|
172
|
+
expect(false).toBe(true);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
});
|
|
@@ -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
|
+
});
|