gogcli-mcp 2.23.0 → 2.23.2

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.
@@ -40,6 +40,152 @@ describe('gog_gmail_search', () => {
40
40
  });
41
41
  });
42
42
 
43
+ describe('gog_gmail_search — pagination and result finalization', () => {
44
+ // The cursor is appended LAST because it rides per page rather than being
45
+ // baked into the shared args, so the multi-page walk can advance it. gog
46
+ // parses flags position-independently, so the order carries no meaning.
47
+ it('appends --page and --all when provided', async () => {
48
+ vi.mocked(runner.run).mockResolvedValue('{"threads":[]}');
49
+ const harness = await setupHandlers();
50
+ await harness.callTool('gog_gmail_search', { query: 'x', pageToken: 'tok', all: true });
51
+ expect(runner.run).toHaveBeenCalledWith(['gmail', 'search', 'x', '--all', '--page=tok'], { account: undefined });
52
+ });
53
+
54
+ it('omits --all when false', async () => {
55
+ vi.mocked(runner.run).mockResolvedValue('{"threads":[]}');
56
+ const harness = await setupHandlers();
57
+ await harness.callTool('gog_gmail_search', { query: 'x', all: false });
58
+ expect(runner.run).toHaveBeenCalledWith(['gmail', 'search', 'x'], { account: undefined });
59
+ });
60
+
61
+ it('sorts results newest-first', async () => {
62
+ vi.mocked(runner.run).mockResolvedValue(JSON.stringify({
63
+ threads: [
64
+ { id: 'old', internalDateIso: '2026-08-01T09:00:00-04:00' },
65
+ { id: 'new', internalDateIso: '2026-08-12T12:36:00-04:00' },
66
+ ],
67
+ nextPageToken: '',
68
+ }));
69
+ const harness = await setupHandlers();
70
+ const result = await harness.callTool('gog_gmail_search', { query: 'x' });
71
+ const out = JSON.parse(result.content[0].text as string);
72
+ expect(out.threads.map((t: { id: string }) => t.id)).toEqual(['new', 'old']);
73
+ });
74
+
75
+ it('marks a capped result set truncated and counts the real total', async () => {
76
+ vi.mocked(runner.run)
77
+ .mockResolvedValueOnce(JSON.stringify({ threads: [{ id: 'a' }], nextPageToken: 'tok' }))
78
+ .mockResolvedValueOnce(JSON.stringify({ threads: [{ id: 'a' }, { id: 'b' }, { id: 'c' }] }));
79
+ const harness = await setupHandlers();
80
+ const result = await harness.callTool('gog_gmail_search', { query: 'invoice', max: 1 });
81
+ const out = JSON.parse(result.content[0].text as string);
82
+ expect(out.truncated).toBe(true);
83
+ expect(out.returned).toBe(1);
84
+ expect(out.totalMatches).toBe(3);
85
+ expect(out.warning).toContain('INCOMPLETE RESULT SET: returned 1 of 3 matches');
86
+ expect(runner.run).toHaveBeenCalledWith(
87
+ ['api', 'call', 'gmail', 'v1', 'users.threads.list',
88
+ '--params={"userId":"me","q":"invoice","maxResults":500,"fields":"threads/id,nextPageToken"}'],
89
+ { account: undefined },
90
+ );
91
+ });
92
+
93
+ it('does not spend a count probe when --from-contact rewrote the query', async () => {
94
+ vi.mocked(runner.run).mockResolvedValue(JSON.stringify({ threads: [{ id: 'a' }], nextPageToken: 'tok' }));
95
+ const harness = await setupHandlers();
96
+ const result = await harness.callTool('gog_gmail_search', { query: 'x', fromContact: 'Alice' });
97
+ const out = JSON.parse(result.content[0].text as string);
98
+ expect(out.truncated).toBe(true);
99
+ expect(out).not.toHaveProperty('totalMatches');
100
+ expect(runner.run).toHaveBeenCalledTimes(1);
101
+ });
102
+ });
103
+
104
+ describe('gog_gmail_search — the page cursor reaches the API', () => {
105
+ // THE REGRESSION. `page` was the declared name while the response field was
106
+ // `nextPageToken`; a caller passing pageToken had it silently stripped by zod
107
+ // and got page 1 forever — same threads, same token, no error. These fail if
108
+ // a caller-supplied cursor is dropped before the gog call.
109
+ it('threads a pageToken through to the gog invocation', async () => {
110
+ vi.mocked(runner.run).mockResolvedValue('{"threads":[]}');
111
+ const harness = await setupHandlers();
112
+ await harness.callTool('gog_gmail_search', { query: 'x', pageToken: 'CURSOR' });
113
+ const args = vi.mocked(runner.run).mock.calls[0][0] as string[];
114
+ expect(args).toContain('--page=CURSOR');
115
+ });
116
+
117
+ it('still accepts the deprecated page alias', async () => {
118
+ vi.mocked(runner.run).mockResolvedValue('{"threads":[]}');
119
+ const harness = await setupHandlers();
120
+ await harness.callTool('gog_gmail_search', { query: 'x', page: 'CURSOR' });
121
+ expect(vi.mocked(runner.run).mock.calls[0][0]).toContain('--page=CURSOR');
122
+ });
123
+
124
+ it('prefers pageToken when both are supplied', async () => {
125
+ vi.mocked(runner.run).mockResolvedValue('{"threads":[]}');
126
+ const harness = await setupHandlers();
127
+ await harness.callTool('gog_gmail_search', { query: 'x', pageToken: 'NEW', page: 'OLD' });
128
+ expect(vi.mocked(runner.run).mock.calls[0][0]).toContain('--page=NEW');
129
+ });
130
+
131
+ it('returns a genuinely different page for page 2, and no token on the last', async () => {
132
+ vi.mocked(runner.run)
133
+ .mockResolvedValueOnce(JSON.stringify({ threads: [{ id: 'a' }, { id: 'b' }], nextPageToken: 'TOK1' }))
134
+ .mockResolvedValueOnce('{"threads":[{"id":"x"}]}') // count probe for page 1
135
+ .mockResolvedValueOnce(JSON.stringify({ threads: [{ id: 'c' }, { id: 'd' }], nextPageToken: '' }));
136
+ const harness = await setupHandlers();
137
+
138
+ const p1 = JSON.parse((await harness.callTool('gog_gmail_search', { query: 'q', max: 2 })).content[0].text as string);
139
+ expect(p1.threads.map((t: { id: string }) => t.id)).toEqual(['a', 'b']);
140
+ expect(p1.nextPageToken).toBe('TOK1');
141
+ expect(p1.truncated).toBe(true);
142
+
143
+ const p2 = JSON.parse((await harness.callTool('gog_gmail_search',
144
+ { query: 'q', max: 2, pageToken: p1.nextPageToken })).content[0].text as string);
145
+ expect(p2.threads.map((t: { id: string }) => t.id)).toEqual(['c', 'd']);
146
+ // Exhausted cursor stripped, so its absence means "last page" unambiguously.
147
+ expect(p2).not.toHaveProperty('nextPageToken');
148
+ expect(p2).not.toHaveProperty('truncated');
149
+ });
150
+ });
151
+
152
+ describe('gog_gmail_search — maxPages', () => {
153
+ it('walks pages and merges them, stopping at the last page', async () => {
154
+ vi.mocked(runner.run)
155
+ .mockResolvedValueOnce(JSON.stringify({ threads: [{ id: 'a' }], nextPageToken: 'T1' }))
156
+ .mockResolvedValueOnce(JSON.stringify({ threads: [{ id: 'b' }], nextPageToken: 'T2' }))
157
+ .mockResolvedValueOnce(JSON.stringify({ threads: [{ id: 'c' }], nextPageToken: '' }));
158
+ const harness = await setupHandlers();
159
+ const out = JSON.parse((await harness.callTool('gog_gmail_search',
160
+ { query: 'q', maxPages: 5 })).content[0].text as string);
161
+ expect(out.threads.map((t: { id: string }) => t.id)).toEqual(['a', 'b', 'c']);
162
+ expect(out).not.toHaveProperty('truncated');
163
+ expect(vi.mocked(runner.run).mock.calls[1][0]).toContain('--page=T1');
164
+ expect(vi.mocked(runner.run).mock.calls[2][0]).toContain('--page=T2');
165
+ });
166
+
167
+ it('stays truncated when the page cap is hit before the end', async () => {
168
+ vi.mocked(runner.run)
169
+ .mockResolvedValueOnce(JSON.stringify({ threads: [{ id: 'a' }], nextPageToken: 'T1' }))
170
+ .mockResolvedValueOnce(JSON.stringify({ threads: [{ id: 'b' }], nextPageToken: 'T2' }))
171
+ .mockResolvedValueOnce('{"threads":[{"id":"1"},{"id":"2"},{"id":"3"}]}'); // count probe
172
+ const harness = await setupHandlers();
173
+ const out = JSON.parse((await harness.callTool('gog_gmail_search',
174
+ { query: 'q', maxPages: 2 })).content[0].text as string);
175
+ expect(out.threads.map((t: { id: string }) => t.id)).toEqual(['a', 'b']);
176
+ expect(out.truncated).toBe(true);
177
+ expect(out.nextPageToken).toBe('T2');
178
+ expect(out.totalMatches).toBe(3);
179
+ });
180
+
181
+ it('starts the walk from a caller-supplied cursor', async () => {
182
+ vi.mocked(runner.run).mockResolvedValue('{"threads":[]}');
183
+ const harness = await setupHandlers();
184
+ await harness.callTool('gog_gmail_search', { query: 'q', maxPages: 3, pageToken: 'START' });
185
+ expect(vi.mocked(runner.run).mock.calls[0][0]).toContain('--page=START');
186
+ });
187
+ });
188
+
43
189
  describe('gog_gmail_get', () => {
44
190
  it('calls run with message ID', async () => {
45
191
  vi.mocked(runner.run).mockResolvedValue('{"id":"msg1"}');