/// // DEMO-ONLY setup tests, run on a test Google account under The AI Pipe's control. // They create two fictional workbooks and send letters ONLY to plus-addresses of the // account running them (refused otherwise). Not part of a real installation. import { EXAMPLE_LETTER } from "../core/letter-template"; import { HIRING_COLUMNS } from "../core/types"; import { ensureJournalHeader, prop, readGrid, tab, TABS } from "./common"; import { install, runPassNow, sheetJournal, sheetSource } from "./sender"; function self() { const email = Session.getEffectiveUser().getEmail(); const [local, domain] = email.split("@"); return { email, plus: (tag: string) => `${local}+${tag}@${domain}` }; } /** Refuses to send if any Welcome email is not a plus-address of the running account. */ function assertOnlyOwnInbox() { const { email } = self(); const [local, domain] = email.split("@"); const grid = readGrid(tab(prop("HIRING_BOOK_ID"), TABS.hiring)); const col = grid[0].indexOf(HIRING_COLUMNS.welcomeEmail); for (const r of grid.slice(1)) { const a = String(r[col] ?? "").trim(); if (a && !(a.startsWith(`${local}+`) && a.endsWith(`@${domain}`)) && a !== email) throw new Error(`Refusing to run: ${a} is not a test inbox of ${email}`); } } export function createDemoWorkbooks() { const { plus } = self(); const hiring = SpreadsheetApp.create("Welcome check demo: hiring sheet (fictional staff)"); const h = hiring.getSheets()[0].setName(TABS.hiring); h.appendRow(Object.values(HIRING_COLUMNS)); const rows: (string | Date)[][] = [ ["", "Past Therapist One", "Hired", "2025-03-03", plus("past1"), "KR"], ["", "Past Therapist Two", "Hired", "2025-06-16", plus("past2"), "KR"], ["", "Past Therapist Three", "hired ", "2026-02-02", plus("past3"), "KR"], ["", "Maya Okafor", "Hired", "2026-10-05", plus("maya"), "KR"], ["", "Daniel Reyes", "Hired", "2026-10-12", "", ""], ["", "Tom Beaulieu", "Offer accepted", "2026-10-19", plus("tom"), ""], ]; rows.forEach((r) => h.appendRow(r)); h.getRange(2, 4, rows.length, 1).setNumberFormat("yyyy-mm-dd"); hiring.insertSheet(TABS.decisions).appendRow(["Arrival ID", "Decision", "By"]); const l = hiring.insertSheet(TABS.letter); [ ["Subject", EXAMPLE_LETTER.subject], ["Body", EXAMPLE_LETTER.body], ["Sender name", "Welcome check demo (fictional)"], ["Reply-to", self().email], ["Sending", "On"], ].forEach((r) => l.appendRow(r)); const ops = SpreadsheetApp.create("Welcome check demo: journal"); const j = ops.getSheets()[0].setName(TABS.journal); ensureJournalHeader(j); const props = PropertiesService.getScriptProperties(); props.setProperties({ HIRING_BOOK_ID: hiring.getId(), OPS_BOOK_ID: ops.getId(), MODE: "dry-run", SCOPE_FROM_START_DATE: "2026-09-01", PASS_EVERY_MINUTES: "1", }); return { hiring: hiring.getUrl(), journal: ops.getUrl() }; } /** The documented setup tests, in order. Returns one line per test with what was observed. */ export function setupTests() { assertOnlyOwnInbox(); const props = PropertiesService.getScriptProperties(); const log: string[] = []; const say = (s: string) => { log.push(s); Logger.log(s); }; const sheet = () => tab(prop("HIRING_BOOK_ID"), TABS.hiring); const quota0 = MailApp.getRemainingDailyQuota(); // T1: first read in dry-run mode props.setProperty("MODE", "dry-run"); let r = runPassNow(); say(`T1 dry-run first read: outcome=${r.outcome} sent=${r.sent.length} baselined=${r.baselined.length} wouldSend=${r.wouldSend.map((x) => x.to).join(",")} ids=${r.idsAssigned.length}`); // T2: go live, one letter props.setProperty("MODE", "live"); r = runPassNow(); say(`T2 live pass: outcome=${r.outcome} sent=${r.sent.map((x) => x.to).join(",")} quotaUsed=${quota0 - MailApp.getRemainingDailyQuota()}`); // T3: copy Maya's row to the bottom, then sort the sheet by therapist name const s = sheet(); const header = s.getRange(1, 1, 1, s.getLastColumn()).getValues()[0].map(String); const nameCol = header.indexOf(HIRING_COLUMNS.therapist) + 1; const names = s.getRange(2, nameCol, s.getLastRow() - 1, 1).getValues().map((x) => String(x[0])); const mayaRow = names.indexOf("Maya Okafor") + 2; s.getRange(mayaRow, 1, 1, header.length).copyTo(s.getRange(s.getLastRow() + 1, 1)); s.getRange(2, 1, s.getLastRow() - 1, header.length).sort(nameCol); r = runPassNow(); say(`T3 copy + sort: outcome=${r.outcome} sent=${r.sent.length} mayaCopies=${r.views.find((v) => v.row.therapist === "Maya Okafor")?.copies}`); // T4: execution dies after the mail service accepted the letter s.appendRow(["", "Grace Lindqvist", "Hired", "2026-10-26", self().plus("grace"), "KR"]); const real = sheetJournal(); try { runPassNow({ journal: { read: real.read, append: (e) => { if (e.type === "sent") throw new Error("simulated stop after the send, before the journal write"); real.append(e); }, }, }); say("T4 interrupted send: NO EXCEPTION (unexpected)"); } catch (e) { say(`T4 interrupted send: stopped as simulated (${(e as Error).message})`); } // The lock was released by the finally block; next pass: r = runPassNow(); const grace = r.views.find((v) => v.row.therapist === "Grace Lindqvist"); say(`T4 next pass: sent=${r.sent.length} grace=${grace?.state}/${grace?.reason}`); // T5: a required column renamed const emailCol = header.indexOf(HIRING_COLUMNS.welcomeEmail) + 1; s.getRange(1, emailCol).setValue("Email (personal)"); r = runPassNow(); say(`T5 missing column: outcome=${r.outcome} reason=${r.reason} sent=${r.sent.length}`); s.getRange(1, emailCol).setValue(HIRING_COLUMNS.welcomeEmail); // T6: install twice const a = install(); const b = install(); say(`T6 install twice: first created=${a.created.join(",") || "none"}; second created=${b.created.join(",") || "none"}; triggers now=${b.now.join(",")}`); say(`Mail quota used by this run: ${quota0 - MailApp.getRemainingDailyQuota()}`); props.setProperty("SETUP_TEST_LOG", JSON.stringify({ at: new Date().toISOString(), log })); return log; } /** Deletes the sending trigger, as a person could by mistake. Used to test the check. */ export function deleteSendingTrigger() { const t = ScriptApp.getProjectTriggers().filter((x) => x.getHandlerFunction() === "runPass"); t.forEach((x) => ScriptApp.deleteTrigger(x)); return `deleted ${t.length} trigger(s) at ${new Date().toISOString()}`; } export { sheetSource };