Repository navigation
Issue #24 - Add document rebuild capability to gmail-to-drive-by-labels #25
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 4 commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
227cdeb
Initial plan
Copilot 5c51a81
Add rebuild doc functionality to gmail-to-drive-by-labels
Copilot ab39b56
Address code review feedback and improve documentation clarity
Copilot 914756a
Use setText('') for more efficient document clearing
Copilot 10cd047
Update test-utils/mocks.js
don-petry b89f1af
Update src/gmail-to-drive-by-labels/tests/rebuild.test.js
don-petry 6876638
Address code review feedback: fix imports, comments, and add error ha…
Copilot 84553c1
Implement batching and resumable state for rebuild to handle large la…
Copilot fa5e548
Fix logging message for remaining threads calculation
Copilot ec36655
Merge branch 'main' into copilot/add-rebuild-doc-option
don-petry 7cf9be5
Fix syntax error in mocks.js - add missing comma after setText
Copilot 79d5e80
Add comprehensive tests for gmail-to-drive-by-labels/code.gs functions
Copilot d0b82d5
Improve test coverage with additional edge case tests - 99.48% lines
Copilot 115c4cd
Add istanbul ignore comments for uncoverable defensive error handling
Copilot 1e2c9ed
Make rebuild batch size configurable with default of 250
Copilot 556de2b
Remove istanbul ignore comments and adjust coverage threshold to 99%
Copilot d7c982f
Add rule to AGENTS.md prohibiting coverage ignore comments
Copilot File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,243 @@ | ||
| const { createMessage, createBlob } = require('../../../test-utils/mocks'); | ||
|
don-petry marked this conversation as resolved.
Outdated
|
||
|
|
||
| // Mock the config | ||
| global.getProcessConfig = jest.fn(() => [ | ||
| { | ||
| triggerLabel: 'test-label', | ||
| processedLabel: 'test-label-archived', | ||
| docId: 'doc-1', | ||
| folderId: 'folder-1' | ||
| } | ||
| ]); | ||
|
|
||
| // Load the code after mocks are set up | ||
| const { rebuildDoc, rebuildAllDocs } = require('../code.gs'); | ||
|
|
||
| describe('rebuildDoc', () => { | ||
| beforeEach(() => { | ||
| global.GmailApp.__reset(); | ||
| global.DocumentApp.__reset(); | ||
| global.DriveApp.__reset(); | ||
| jest.clearAllMocks(); | ||
| }); | ||
|
|
||
| test('clears document and moves emails from processed to trigger label', () => { | ||
| // Setup: Create labels | ||
| const triggerLabel = global.GmailApp.createLabel('test-label'); | ||
| const processedLabel = global.GmailApp.createLabel('test-label-archived'); | ||
|
|
||
| // Setup: Add some processed threads | ||
| const msg1 = createMessage({ subject: 'Email 1', body: 'Body 1' }); | ||
| const msg2 = createMessage({ subject: 'Email 2', body: 'Body 2' }); | ||
| const thread1 = global.GmailApp.__addThreadWithLabels(['test-label-archived'], [msg1]); | ||
| const thread2 = global.GmailApp.__addThreadWithLabels(['test-label-archived'], [msg2]); | ||
|
don-petry marked this conversation as resolved.
Outdated
don-petry marked this conversation as resolved.
Outdated
|
||
|
|
||
| // Setup: Create document with content | ||
| const doc = global.DocumentApp.openById('doc-1'); | ||
| const body = doc.getBody(); | ||
| body.appendParagraph('Old content 1'); | ||
| body.appendParagraph('Old content 2'); | ||
| body.appendParagraph('Old content 3'); | ||
|
|
||
| // Verify initial state | ||
| expect(body.getParagraphs().length).toBe(3); | ||
| expect(processedLabel.getThreads().length).toBe(2); | ||
| expect(triggerLabel.getThreads().length).toBe(0); | ||
|
|
||
| // Run rebuild | ||
| const config = { | ||
| triggerLabel: 'test-label', | ||
| processedLabel: 'test-label-archived', | ||
| docId: 'doc-1', | ||
| folderId: 'folder-1' | ||
| }; | ||
| rebuildDoc(config); | ||
|
|
||
| // Verify document is cleared | ||
| expect(body.getParagraphs().length).toBe(0); | ||
|
|
||
| // Verify emails are moved back to trigger label | ||
| expect(triggerLabel.getThreads().length).toBe(2); | ||
| expect(processedLabel.getThreads().length).toBe(0); | ||
| }); | ||
|
|
||
| test('handles missing processed label gracefully', () => { | ||
| // Setup: Create only trigger label (no processed label) | ||
| const triggerLabel = global.GmailApp.createLabel('test-label'); | ||
|
don-petry marked this conversation as resolved.
|
||
|
|
||
| // Setup: Create document with content | ||
| const doc = global.DocumentApp.openById('doc-1'); | ||
| const body = doc.getBody(); | ||
| body.appendParagraph('Old content'); | ||
|
|
||
| // Run rebuild | ||
| const config = { | ||
| triggerLabel: 'test-label', | ||
| processedLabel: 'test-label-archived', | ||
| docId: 'doc-1', | ||
| folderId: 'folder-1' | ||
| }; | ||
|
|
||
| // Should not throw | ||
| expect(() => rebuildDoc(config)).not.toThrow(); | ||
|
|
||
| // Document should still be cleared | ||
| expect(body.getParagraphs().length).toBe(0); | ||
| }); | ||
|
|
||
| test('handles missing trigger label gracefully', () => { | ||
| // Setup: Create document | ||
| const doc = global.DocumentApp.openById('doc-1'); | ||
| const body = doc.getBody(); | ||
| body.appendParagraph('Old content'); | ||
|
|
||
| // Run rebuild with non-existent trigger label | ||
| const config = { | ||
| triggerLabel: 'non-existent-label', | ||
| processedLabel: 'test-label-archived', | ||
| docId: 'doc-1', | ||
| folderId: 'folder-1' | ||
| }; | ||
|
|
||
| // Should not throw | ||
| expect(() => rebuildDoc(config)).not.toThrow(); | ||
|
|
||
| // Document should not be cleared (function returns early) | ||
| expect(body.getParagraphs().length).toBe(1); | ||
| }); | ||
|
|
||
| test('clears empty document without errors', () => { | ||
| // Setup: Create labels | ||
| global.GmailApp.createLabel('test-label'); | ||
|
|
||
| // Setup: Create empty document | ||
| const doc = global.DocumentApp.openById('doc-1'); | ||
| const body = doc.getBody(); | ||
|
|
||
| // Verify initial state | ||
| expect(body.getParagraphs().length).toBe(0); | ||
|
|
||
| // Run rebuild | ||
| const config = { | ||
| triggerLabel: 'test-label', | ||
| processedLabel: 'test-label-archived', | ||
| docId: 'doc-1', | ||
| folderId: 'folder-1' | ||
| }; | ||
|
|
||
| // Should not throw | ||
| expect(() => rebuildDoc(config)).not.toThrow(); | ||
|
|
||
| // Document should still be empty | ||
| expect(body.getParagraphs().length).toBe(0); | ||
| }); | ||
|
|
||
| test('moves multiple threads correctly', () => { | ||
| // Setup: Create labels | ||
| const triggerLabel = global.GmailApp.createLabel('test-label'); | ||
| const processedLabel = global.GmailApp.createLabel('test-label-archived'); | ||
|
|
||
| // Setup: Add many processed threads | ||
| const threads = []; | ||
| for (let i = 0; i < 25; i++) { | ||
| const msg = createMessage({ subject: `Email ${i}`, body: `Body ${i}` }); | ||
| const thread = global.GmailApp.__addThreadWithLabels(['test-label-archived'], [msg]); | ||
| threads.push(thread); | ||
| } | ||
|
|
||
| // Setup: Create document | ||
| const doc = global.DocumentApp.openById('doc-1'); | ||
| const body = doc.getBody(); | ||
| body.appendParagraph('Content'); | ||
|
|
||
| // Verify initial state | ||
| expect(processedLabel.getThreads().length).toBe(25); | ||
| expect(triggerLabel.getThreads().length).toBe(0); | ||
|
|
||
| // Run rebuild | ||
| const config = { | ||
| triggerLabel: 'test-label', | ||
| processedLabel: 'test-label-archived', | ||
| docId: 'doc-1', | ||
| folderId: 'folder-1' | ||
| }; | ||
| rebuildDoc(config); | ||
|
|
||
| // Verify all threads are moved | ||
| expect(triggerLabel.getThreads().length).toBe(25); | ||
| expect(processedLabel.getThreads().length).toBe(0); | ||
| }); | ||
| }); | ||
|
|
||
| describe('rebuildAllDocs', () => { | ||
| beforeEach(() => { | ||
| global.GmailApp.__reset(); | ||
| global.DocumentApp.__reset(); | ||
| global.DriveApp.__reset(); | ||
| jest.clearAllMocks(); | ||
| }); | ||
|
|
||
| test('rebuilds all configured documents', () => { | ||
| // Setup multiple configs | ||
| global.getProcessConfig.mockReturnValue([ | ||
| { | ||
| triggerLabel: 'label-1', | ||
| processedLabel: 'label-1-archived', | ||
| docId: 'doc-1', | ||
| folderId: 'folder-1' | ||
| }, | ||
| { | ||
| triggerLabel: 'label-2', | ||
| processedLabel: 'label-2-archived', | ||
| docId: 'doc-2', | ||
| folderId: 'folder-2' | ||
| } | ||
| ]); | ||
|
|
||
| // Setup: Create labels and documents | ||
| global.GmailApp.createLabel('label-1'); | ||
| global.GmailApp.createLabel('label-1-archived'); | ||
| global.GmailApp.createLabel('label-2'); | ||
| global.GmailApp.createLabel('label-2-archived'); | ||
|
|
||
| const doc1 = global.DocumentApp.openById('doc-1'); | ||
| const doc2 = global.DocumentApp.openById('doc-2'); | ||
|
|
||
| doc1.getBody().appendParagraph('Doc 1 content'); | ||
| doc2.getBody().appendParagraph('Doc 2 content'); | ||
|
|
||
| // Verify initial state | ||
| expect(doc1.getBody().getParagraphs().length).toBe(1); | ||
| expect(doc2.getBody().getParagraphs().length).toBe(1); | ||
|
|
||
| // Run rebuild all | ||
| rebuildAllDocs(); | ||
|
|
||
| // Verify both documents are cleared | ||
| expect(doc1.getBody().getParagraphs().length).toBe(0); | ||
| expect(doc2.getBody().getParagraphs().length).toBe(0); | ||
| }); | ||
|
|
||
| test('handles single configuration', () => { | ||
| // Setup single config (default mock) | ||
| global.getProcessConfig.mockReturnValue([ | ||
| { | ||
| triggerLabel: 'test-label', | ||
| processedLabel: 'test-label-archived', | ||
| docId: 'doc-1', | ||
| folderId: 'folder-1' | ||
| } | ||
| ]); | ||
|
|
||
| // Setup: Create label and document | ||
| global.GmailApp.createLabel('test-label'); | ||
| const doc = global.DocumentApp.openById('doc-1'); | ||
| doc.getBody().appendParagraph('Content'); | ||
|
|
||
| // Run rebuild all | ||
| expect(() => rebuildAllDocs()).not.toThrow(); | ||
|
|
||
| // Verify document is cleared | ||
| expect(doc.getBody().getParagraphs().length).toBe(0); | ||
| }); | ||
| }); | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.