gogcli-mcp-gmail 2.22.0 → 2.23.1

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,57 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { registerExtraGmailTools } from '../../src/tools/gmail-extra.js';
3
+ import * as lib from '../../../gogcli-mcp/src/lib.js';
4
+ import { createTestHarness, type TestHarness } from '@chrischall/mcp-utils/test';
5
+ import { rawTextResult, errorResult } from '@chrischall/mcp-utils';
6
+
7
+ vi.mock('../../../gogcli-mcp/src/lib.js', async (o) => ({ ...(await o<typeof lib>()), run: vi.fn(), runOrDiagnose: vi.fn(), diagnose: vi.fn() }));
8
+
9
+ let harness: TestHarness;
10
+ beforeEach(async () => {
11
+ vi.clearAllMocks();
12
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
13
+ vi.mocked(lib.diagnose).mockResolvedValue(errorResult('diagnosed'));
14
+ harness = await createTestHarness(registerExtraGmailTools);
15
+ });
16
+
17
+ const b64 = (t: string) => Buffer.from(t, 'utf8').toString('base64url');
18
+ const draft = (id: string, body: string) => JSON.stringify({
19
+ draft: {
20
+ id,
21
+ message: { id: `m${id}`, threadId: `t${id}`, payload: { mimeType: 'text/plain', headers: [{ name: 'Subject', value: 'S' }], body: { data: b64(body) } } },
22
+ },
23
+ });
24
+
25
+ /**
26
+ * #264: bodyLineCount counted RAW normalized lines while diffBodyLines compares
27
+ * Set members, so onlyInACount + sharedLineCount === bodyLineCount stopped
28
+ * holding the moment a body repeated a line. Dividers repeat constantly in real
29
+ * mail, so this is the ordinary case, not an edge one.
30
+ *
31
+ * Asserted through the TOOL, against the emitted payload, because
32
+ * describeDraftSide is module-private — and because the payload is what a
33
+ * caller actually does arithmetic on.
34
+ */
35
+ const A = ['Intro paragraph.', '---', 'Middle paragraph.', '---', 'Closing paragraph.'].join('\n');
36
+ const B = ['Intro paragraph.', '---', 'A different middle.', '---', 'Closing paragraph.'].join('\n');
37
+
38
+ describe('gog_gmail_drafts_diff count arithmetic', () => {
39
+ it('onlyInACount + sharedLineCount === bodyLineCount when a body repeats a line', async () => {
40
+ vi.mocked(lib.run).mockImplementation(async (args: readonly string[]) =>
41
+ draft(String(args[3]), String(args[3]) === 'a1' ? A : B));
42
+
43
+ const res = await harness.callTool('gog_gmail_drafts_diff', { draftIdA: 'a1', draftIdB: 'b1' });
44
+ const raw = res.content.map((c: any) => c.text).join('\n');
45
+ const payload = JSON.parse(raw);
46
+
47
+ const sideA = payload.drafts.a;
48
+ const diff = payload.bodyDiff;
49
+
50
+ expect(sideA.bodyLineCount).toBeDefined();
51
+ expect(diff.onlyInACount).toBeDefined();
52
+ expect(diff.sharedLineCount).toBeDefined();
53
+ // The body has 5 raw lines but only 4 distinct ones — that gap is the bug.
54
+ expect(sideA.bodyLineCount).toBe(4);
55
+ expect(diff.onlyInACount + diff.sharedLineCount).toBe(sideA.bodyLineCount);
56
+ });
57
+ });
@@ -0,0 +1,67 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { authoredBodyLines, measureBodyAgreement } from '../../src/tools/gmail-extra.js';
3
+
4
+ /**
5
+ * HAZARD A — the lineage judgement must not fire on two unrelated drafts.
6
+ *
7
+ * `boilerplateLineFlags` stripped the RFC 3676 `-- ` block, the client's own
8
+ * `Sent from my iPhone`, and the NAME under a sign-off — but the run stopped at
9
+ * the first line that was not name-shaped, so a USER-CONFIGURED signature
10
+ * survived as authored prose:
11
+ *
12
+ * Thanks, -> stripped
13
+ * Chris Hall -> stripped
14
+ * (704) 555-0142 -> run stopped; survived
15
+ * chris.c.hall@gmail.com -> survived
16
+ * https://example.com/chris -> survived
17
+ *
18
+ * Those lines are identical on every message the account composes. Measured
19
+ * before the fix: two unrelated drafts shared 3 authored lines / 61 chars and
20
+ * scored similarity 0.60 against a 0.60 threshold — meetsThreshold TRUE on a
21
+ * plumber note and a soccer note. In a mailbox whose drafts go to a parenting
22
+ * coordinator, a wrong pairing is the expensive direction.
23
+ */
24
+ const SIGNATURE = ['Thanks,', 'Chris Hall', '(704) 555-0142', 'chris.c.hall@gmail.com', 'https://example.com/chris'];
25
+ const withSig = (body: string[]) => ['Hi Sam,', '', ...body, '', ...SIGNATURE].join('\n');
26
+
27
+ describe('a shared signature is never lineage evidence', () => {
28
+ it('does not pair two unrelated drafts that share one signature', () => {
29
+ const m = measureBodyAgreement(withSig(['The plumber comes Tuesday at 9am.']), withSig(['Soccer registration closes Friday.']));
30
+ expect(m.sharedAuthoredLines).toBe(0);
31
+ expect(m.meetsThreshold).toBe(false);
32
+ });
33
+
34
+ it('strips the whole signature block, not just the name', () => {
35
+ expect(authoredBodyLines(withSig(['The plumber comes Tuesday at 9am.']))).toEqual(['The plumber comes Tuesday at 9am.']);
36
+ });
37
+ });
38
+
39
+ describe('and it still stops at authored content', () => {
40
+ it('keeps a postscript under the signature', () => {
41
+ expect(authoredBodyLines([...SIGNATURE, '', 'P.S. The invoice is attached.'].join('\n')))
42
+ .toEqual(['P.S. The invoice is attached.']);
43
+ });
44
+
45
+ it('keeps prose under the sign-off', () => {
46
+ expect(authoredBodyLines(['Thanks,', 'Chris', '', 'Actually the plumber moved to Wednesday.'].join('\n')))
47
+ .toEqual(['Actually the plumber moved to Wednesday.']);
48
+ });
49
+
50
+ it('keeps contact-shaped lines that are NOT under a sign-off', () => {
51
+ // The rule is positional. A bare phone number in the body is content.
52
+ expect(authoredBodyLines(['(704) 555-0142', 'chris@example.com'].join('\n')))
53
+ .toEqual(['(704) 555-0142', 'chris@example.com']);
54
+ });
55
+
56
+ it('keeps a phone number inside a sentence', () => {
57
+ expect(authoredBodyLines(['Call me at (704) 555-0142 about the roof.', '', 'Thanks,', 'Chris'].join('\n')))
58
+ .toEqual(['Call me at (704) 555-0142 about the roof.']);
59
+ });
60
+
61
+ it('still pairs two drafts that genuinely share authored prose', () => {
62
+ const shared = ['The plumber comes Tuesday at 9am.', 'He will need access to the crawlspace.', 'I left a key under the mat for him.'];
63
+ const m = measureBodyAgreement(withSig(shared), withSig([...shared, 'One more thing: the gate code is 4417.']));
64
+ expect(m.sharedAuthoredLines).toBeGreaterThanOrEqual(3);
65
+ expect(m.meetsThreshold).toBe(true);
66
+ });
67
+ });