Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 21 additions & 21 deletions deploy/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -651,7 +651,6 @@ <h2><span class="step-badge">3</span>Configure your scripts</h2>
} catch {
// best-effort; ignore storage errors
}
const safeId = escapeHtml(scriptId)
const section = document.getElementById('setup-section-' + scriptId)
if (section) {
section.innerHTML = `<div style="font-size:12px;color:#137333;">⏱ Trigger previously activated.</div>`
Expand All @@ -667,7 +666,7 @@ <h2><span class="step-badge">3</span>Configure your scripts</h2>
* to activate the hourly trigger.
*/
function buildSetupSource(triggerFn) {
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(triggerFn)) {
if (!/^[a-zA-Z_]\w*$/.test(triggerFn)) {
throw new Error(
'Invalid trigger function name: ' + JSON.stringify(triggerFn)
)
Expand Down Expand Up @@ -1189,14 +1188,11 @@ <h2><span class="step-badge">3</span>Configure your scripts</h2>
'<label>At approximately</label>' +
'<select class="cfg-field" data-key="triggerHour" id="cfg-triggerHour">' +
Array.from({ length: 24 }, (_, h) => {
const label =
h === 0
? '12 AM'
: h < 12
? h + ' AM'
: h === 12
? '12 PM'
: h - 12 + ' PM'
let label
if (h === 0) label = '12 AM'
else if (h < 12) label = h + ' AM'
else if (h === 12) label = '12 PM'
else label = h - 12 + ' PM'
return (
'<option value="' +
h +
Expand Down Expand Up @@ -1565,11 +1561,12 @@ <h2><span class="step-badge">3</span>Configure your scripts</h2>
fields.email ? [fields.email] : []
)
const subject = JSON.stringify(fields.subject || 'Weekly Briefing')
const lookahead = parseInt(fields.lookaheadDays, 10) || 7
const lookahead = Number.parseInt(fields.lookaheadDays, 10) || 7
const freq = JSON.stringify(fields.frequency || 'weekly')
const day = JSON.stringify(fields.triggerDay || 'MONDAY')
const hour = parseInt(fields.triggerHour, 10) || 7
const interval = parseInt(fields.triggerInterval, 10) || 1
const hourParsed = Number.parseInt(fields.triggerHour, 10)
const hour = Number.isNaN(hourParsed) ? 7 : hourParsed
const interval = Number.parseInt(fields.triggerInterval, 10) || 1
let scheduleLines =
' scheduleFrequency: ' +
freq +
Expand Down Expand Up @@ -1676,11 +1673,11 @@ <h2><span class="step-badge">3</span>Configure your scripts</h2>
// Tested in src/deploy/tests/index.test.js (escapeHtml suite)
function escapeHtml(str) {
return String(str == null ? '' : str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#x27;')
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;')
}

/**
Expand Down Expand Up @@ -2118,7 +2115,7 @@ <h2><span class="step-badge">3</span>Configure your scripts</h2>
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
...(options.headers || {}),
...options.headers,
},
})
if (!res.ok) {
Expand All @@ -2133,8 +2130,11 @@ <h2><span class="step-badge">3</span>Configure your scripts</h2>
? `${json.error.message} — ${JSON.stringify(json.error.details)}`
: json.error.message
}
} catch (_) {
/* ignore parse errors */
} catch (parseError) {
console.debug(
'apiFetch: response body was not valid JSON',
parseError
)
}
const err = new Error(message)
err.httpStatus = res.status
Expand Down
8 changes: 6 additions & 2 deletions deploy/tests/ui.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -272,9 +272,13 @@ test.describe('deploy index.html', () => {
test('Step 3 stays hidden after sign-in when no projects in Drive', async ({
page,
}) => {
// Default mock returns empty files array; wait for the Drive lookup to
// actually complete rather than sleeping for a fixed interval.
const driveLookup = page.waitForResponse((resp) =>
resp.url().includes('drive/v3/files')
)
await signIn(page)
// Default mock returns empty files array
await page.waitForTimeout(500)
await driveLookup
Comment thread
coderabbitai[bot] marked this conversation as resolved.
await expect(page.locator('#step3-card')).toBeHidden()
})

Expand Down
8 changes: 4 additions & 4 deletions gas-installer/Index.html
Original file line number Diff line number Diff line change
Expand Up @@ -371,10 +371,10 @@ <h2><span class="step-badge">2</span>Name your project</h2>

function escapeHtml(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
}
</script>
</body>
Expand Down
7 changes: 3 additions & 4 deletions gas-installer/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ const APPS_SCRIPT_API_BASE = 'https://script.googleapis.com/v1'
*/
function getFileType(filename) {
if (!filename || typeof filename !== 'string') return null
if (/\.gs$/.test(filename)) return 'SERVER_JS'
if (/\.html$/.test(filename)) return 'HTML'
if (filename.endsWith('.gs')) return 'SERVER_JS'
if (filename.endsWith('.html')) return 'HTML'
return null
}

Expand All @@ -42,8 +42,7 @@ function filterGithubItems(items) {
return items
.filter(
(item) =>
item &&
item.type === 'file' &&
item?.type === 'file' &&
typeof item.name === 'string' &&
item.download_url
)
Expand Down
8 changes: 4 additions & 4 deletions scripts/check-coverage.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
* its minimum threshold, or 0 if all pass.
*/

const fs = require('fs')
const path = require('path')
const fs = require('node:fs')
const path = require('node:path')

const coveragePath = path.join(process.cwd(), 'coverage', 'coverage-final.json')

Expand Down Expand Up @@ -44,9 +44,9 @@ for (const filePath in coverage) {
if (fileCoverage.b) {
for (const key in fileCoverage.b) {
const branches = fileCoverage.b[key]
for (let i = 0; i < branches.length; i++) {
for (const hitCount of branches) {
totals.branches.total++
if (branches[i] > 0) totals.branches.covered++
if (hitCount > 0) totals.branches.covered++
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions src/calendar-to-briefing-doc/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ function detectConflicts(eventTuples) {
* @returns {string}
*/
function formatConflictWarning(conflicts, formatTime) {
if (!conflicts || !conflicts.length) return ''
if (!conflicts?.length) return ''
return conflicts
.map((c) => {
const aLabel = c.a.calendarName
Expand Down Expand Up @@ -249,7 +249,7 @@ function formatBriefing(title, groupedEvents, formatTime) {
* @param {string} body - The full briefing text
*/
function emailBriefing(gmailApp, recipients, subject, body) {
if (!recipients || !recipients.length) return
if (!recipients?.length) return
for (const recipient of recipients) {
gmailApp.sendEmail(recipient, subject, body)
}
Expand Down Expand Up @@ -312,7 +312,7 @@ function generateBriefingForConfig(
getDateKey,
formatTime
) {
if (!config.emailRecipients || !config.emailRecipients.length) {
if (!config.emailRecipients?.length) {
throw new Error('emailRecipients is required')
}

Expand Down
37 changes: 11 additions & 26 deletions src/calendar-to-briefing-doc/tests/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -408,37 +408,22 @@ describe('emailBriefing', () => {
// ── shouldRunNow ─────────────────────────────────────────────────────────────

describe('shouldRunNow', () => {
it('returns true on the configured weekly day and hour', () => {
it.each([
[
'configured weekly day and hour (Monday 7 AM)',
'2025-01-13T07:00:00',
true,
],
['wrong day (Tuesday 7 AM)', '2025-01-14T07:00:00', false],
['wrong hour (Monday 8 AM)', '2025-01-13T08:00:00', false],
])('returns %s -> %s', (_desc, nowIso, expected) => {
const config = {
scheduleFrequency: 'weekly',
scheduleDay: 'MONDAY',
scheduleHour: 7,
}
// Monday at 7 AM
const now = new Date('2025-01-13T07:00:00')
expect(shouldRunNow(config, now, null)).toBe(true)
})

it('returns false on wrong day', () => {
const config = {
scheduleFrequency: 'weekly',
scheduleDay: 'MONDAY',
scheduleHour: 7,
}
// Tuesday at 7 AM
const now = new Date('2025-01-14T07:00:00')
expect(shouldRunNow(config, now, null)).toBe(false)
})

it('returns false on wrong hour', () => {
const config = {
scheduleFrequency: 'weekly',
scheduleDay: 'MONDAY',
scheduleHour: 7,
}
// Monday at 8 AM
const now = new Date('2025-01-13T08:00:00')
expect(shouldRunNow(config, now, null)).toBe(false)
const now = new Date(nowIso)
expect(shouldRunNow(config, now, null)).toBe(expected)
})

it('defaults to weekly Monday 7 AM when schedule fields are missing', () => {
Expand Down
10 changes: 7 additions & 3 deletions src/calendar-to-sheets/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ function rowsToMap(rows) {
const m = new Map()
for (let i = 0; i < rows.length; i++) {
const r = rows[i]
if (!r || !r[0]) continue
if (!r?.[0]) continue
m.set(r[0], { rowIndex: i + 2, values: r }) // assume header at row 1
}
return m
Expand All @@ -51,7 +51,8 @@ function rowsEqual(a, b) {
// Handle Date comparison (a is ISO string, b is Date object from sheet)
if (typeof valA === 'string' && valB instanceof Date) {
const dateA = new Date(valA)
if (!isNaN(dateA) && dateA.getTime() === valB.getTime()) continue
if (!Number.isNaN(dateA.getTime()) && dateA.getTime() === valB.getTime())
continue
}

// Handle sanitized formula comparison (a has leading ', b does not)
Expand Down Expand Up @@ -260,7 +261,10 @@ async function syncCalendarToSheet(
const toDelete = computeDeletableRows(existingMap, desiredMap, start, end)
// delete from bottom to top
console.log('[syncCalendarToSheet] Deleting rows:', toDelete.length)
toDelete.sort((a, b) => b - a).forEach((r) => sheet.deleteRow(r))
toDelete
.slice()
.sort((a, b) => b - a)
.forEach((r) => sheet.deleteRow(r))
console.log('[syncCalendarToSheet] Sync complete')
}

Expand Down
2 changes: 1 addition & 1 deletion src/deploy/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ function buildProjectContent(files) {
throw new Error('Each file must have a non-empty string name')
}
if (typeof f.source !== 'string') {
throw new Error('Each file must have a string source')
throw new TypeError('Each file must have a string source')
}
}
return {
Expand Down
9 changes: 3 additions & 6 deletions src/gas-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ function getCleanBody(text) {
/^\s*On\s.+\swrote:/m,
/^\s*From:\s.+\sSent:\s+/m,
/^\s*_{10,}/m,
/^\s*From:\s+.+<.+@.+>/m,
/confidentiality notice/im,
]

Expand All @@ -19,10 +18,8 @@ function getCleanBody(text) {
const match = text.match(pattern)
if (match) {
// Prefer splitting at the start of the line containing the match
const lineStart =
text.lastIndexOf('\n', match.index) === -1
? 0
: text.lastIndexOf('\n', match.index) + 1
const nlIndex = text.lastIndexOf('\n', match.index)
const lineStart = nlIndex === -1 ? 0 : nlIndex + 1
if (splitIndex === -1 || lineStart < splitIndex) {
splitIndex = lineStart
}
Expand Down Expand Up @@ -53,7 +50,7 @@ function getFileHash(blob) {
bytes = blob
} else if (blob && typeof blob.getBytes === 'function') {
bytes = Buffer.from(blob.getBytes())
} else if (blob && blob.bytes) {
} else if (blob?.bytes) {
bytes = Buffer.from(blob.bytes)
} else {
throw new TypeError('Unsupported blob type')
Expand Down
6 changes: 3 additions & 3 deletions src/gmail-to-drive-by-labels/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -231,14 +231,14 @@
let currentIndex = 0

// Insert subject
const subjectText = 'Subject: ' + (subject ? subject : '(No Subject)')
const subjectText = 'Subject: ' + (subject || '(No Subject)')
const headingPara = body.insertParagraph(currentIndex++, subjectText)

// Try to set heading style (GAS only)
if (DocumentApp) {
try {
headingPara.setHeading(DocumentApp.ParagraphHeading.HEADING_3)
} catch (e) {

Check warning on line 241 in src/gmail-to-drive-by-labels/src/index.js

View workflow job for this annotation

GitHub Actions / autofix

'e' is defined but never used

Check warning on line 241 in src/gmail-to-drive-by-labels/src/index.js

View workflow job for this annotation

GitHub Actions / autofix

'e' is defined but never used
const style = {}
style[DocumentApp.Attribute.BOLD] = true
headingPara.setAttributes(style)
Expand Down Expand Up @@ -535,13 +535,13 @@
// This ensures the most recent emails appear first.
let currentIndex = 0

const subjectText = 'Subject: ' + (subject ? subject : '(No Subject)')
const subjectText = 'Subject: ' + (subject || '(No Subject)')
const headingPara = body.insertParagraph(currentIndex++, subjectText)

// Try to set heading, fallback to bold if Doc is busy
try {
headingPara.setHeading(DocumentApp.ParagraphHeading.HEADING_3)
} catch (e) {

Check warning on line 544 in src/gmail-to-drive-by-labels/src/index.js

View workflow job for this annotation

GitHub Actions / autofix

'e' is defined but never used

Check warning on line 544 in src/gmail-to-drive-by-labels/src/index.js

View workflow job for this annotation

GitHub Actions / autofix

'e' is defined but never used
const style = {}
style[DocumentApp.Attribute.BOLD] = true
headingPara.setAttributes(style)
Expand Down Expand Up @@ -634,7 +634,7 @@
'_HHmmss'
)
// Insert timestamp before the file extension
let newName = fileName.replace(/(\.[\w\d_-]+)$/i, timeTag + '$1')
let newName = fileName.replace(/(\.[\w-]+)$/i, timeTag + '$1')
// Fallback if regex fails (files without extension)
if (newName === fileName) newName += timeTag

Expand Down
14 changes: 14 additions & 0 deletions src/gmail-to-drive-by-labels/tests/gas-utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,20 @@ describe('getCleanBody', () => {
expect(getCleanBody(input)).toBe('Line1')
})

test('cuts off at Outlook-style From:/Sent: reply header', () => {
const input =
'My reply\nFrom: Jane Doe\nSent: Monday, Jan 1, 2020 9:00 AM\nQuoted'
expect(getCleanBody(input)).toBe('My reply')
})

test('does not truncate at standalone From: line (forwarded content should be preserved)', () => {
const input =
'Please see the forwarded email below:\nFrom: Jane Doe <jane@example.com>\nHere is the content'
expect(getCleanBody(input)).toBe(
'Please see the forwarded email below:\nFrom: Jane Doe <jane@example.com>\nHere is the content'
)
})

test('cuts off at confidentiality notice', () => {
const input = 'Message body\nThis is a confidentiality notice: do not share'
expect(getCleanBody(input)).toBe('Message body')
Expand Down
6 changes: 3 additions & 3 deletions test-utils/mocks.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ function createDriveFolder(id = 'root') {
}

function createFile(name, blob) {
let _name = name || (blob && blob.getName && blob.getName()) || 'file'
let _name = name || blob?.getName?.() || 'file'
const bytes =
blob && typeof blob.getBytes === 'function'
? blob.getBytes()
Expand Down Expand Up @@ -244,7 +244,7 @@ function createCalendarEvent({
}
}

function createCalendar(id = 'primary', name) {
function createCalendar(id = 'primary', name = id) {
const calName = name || id
const events = []
return {
Expand Down Expand Up @@ -458,7 +458,7 @@ function resetAll(globals) {
globals.__mocks.calendar.__reset()
globals.__mocks.spreadsheet.__reset()
globals.__mocks.properties.__reset()
if (globals.CalendarApp && globals.CalendarApp.__resetCalendars) {
if (globals.CalendarApp?.__resetCalendars) {
globals.CalendarApp.__resetCalendars()
}
}
Expand Down
2 changes: 1 addition & 1 deletion test-utils/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ global.Utilities = {
computeDigest: (algorithm, bytes) => {
// Simple deterministic hash for testing
// Convert bytes to a string and create a fake hash
const crypto = require('crypto')
const crypto = require('node:crypto')
const hash = crypto.createHash('md5').update(Buffer.from(bytes)).digest()
// Return as array of numbers (like GAS does)
return Array.from(hash)
Expand Down
Loading