Files
nsrbandCursor f270f531ea feat(watcher): add agent Gmail CLI with audit and defaults
Provide list/read/summarize plus reason-gated archive, label, draft, and forward-as-draft, with SQLite audit logging and default-mailbox resolution.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-22 10:09:25 +07:00

209 lines
6.1 KiB
JavaScript

import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { resolveUser } from "../src/cli/resolve-user.js";
import { parseArgv, parseIds, requireReason } from "../src/cli/parse-args.js";
import {
archiveModifyBody,
buildDraftRaw,
buildForwardDraftRaw,
labelModifyBody,
rawToGmailMessage,
} from "../src/gmail/draft.js";
function usersFixture() {
return [
{ id: 1, email: "a@example.com", is_default: 0 },
{ id: 2, email: "b@example.com", is_default: 1 },
];
}
describe("resolveUser", () => {
it("uses --user email", () => {
const users = usersFixture();
const result = resolveUser("a@example.com", {
listUsers: () => users,
getByEmail: (e) => users.find((u) => u.email === e) || null,
getById: (id) => users.find((u) => u.id === Number(id)) || null,
getDefault: () => users.find((u) => u.is_default) || null,
envDefault: "",
});
assert.equal(result.ok, true);
assert.equal(result.user.email, "a@example.com");
});
it("uses --user id", () => {
const users = usersFixture();
const result = resolveUser("2", {
listUsers: () => users,
getByEmail: () => null,
getById: (id) => users.find((u) => u.id === Number(id)) || null,
getDefault: () => null,
envDefault: "",
});
assert.equal(result.ok, true);
assert.equal(result.user.id, 2);
});
it("uses GMAIL_DEFAULT_USER over is_default", () => {
const users = usersFixture();
const result = resolveUser(null, {
listUsers: () => users,
getByEmail: (e) => users.find((u) => u.email === e) || null,
getById: () => null,
getDefault: () => users.find((u) => u.is_default) || null,
envDefault: "a@example.com",
});
assert.equal(result.ok, true);
assert.equal(result.user.email, "a@example.com");
});
it("uses is_default when no flag/env", () => {
const users = usersFixture();
const result = resolveUser(null, {
listUsers: () => users,
getByEmail: () => null,
getById: () => null,
getDefault: () => users.find((u) => u.is_default) || null,
envDefault: "",
});
assert.equal(result.ok, true);
assert.equal(result.user.email, "b@example.com");
});
it("uses sole connected user", () => {
const users = [{ id: 9, email: "only@example.com", is_default: 0 }];
const result = resolveUser(null, {
listUsers: () => users,
getByEmail: () => null,
getById: () => null,
getDefault: () => null,
envDefault: "",
});
assert.equal(result.ok, true);
assert.equal(result.user.email, "only@example.com");
});
it("fails when multiple and no default", () => {
const users = [
{ id: 1, email: "a@example.com", is_default: 0 },
{ id: 2, email: "b@example.com", is_default: 0 },
];
const result = resolveUser(null, {
listUsers: () => users,
getByEmail: () => null,
getById: () => null,
getDefault: () => null,
envDefault: "",
});
assert.equal(result.ok, false);
assert.match(result.error, /Multiple accounts/);
});
it("fails when no users", () => {
const result = resolveUser(null, {
listUsers: () => [],
getByEmail: () => null,
getById: () => null,
getDefault: () => null,
envDefault: "",
});
assert.equal(result.ok, false);
assert.match(result.error, /No signed-in users/);
});
});
describe("requireReason", () => {
it("rejects missing reason", () => {
const r = requireReason({ reason: " " }, "archive");
assert.equal(r.ok, false);
assert.match(r.error, /--reason is required/);
});
it("accepts non-empty reason", () => {
const r = requireReason({ reason: "noise" }, "archive");
assert.equal(r.ok, true);
assert.equal(r.reason, "noise");
});
});
describe("parseArgv / parseIds", () => {
it("parses dry-run and multi ids", () => {
const args = parseArgv(
["abc", "--dry-run", "--ids", "x,y", "--reason", "why"],
{
booleans: ["dryRun"],
strings: ["reason"],
multi: ["ids"],
},
);
assert.equal(args.dryRun, true);
assert.deepEqual(args.ids, ["x", "y"]);
assert.equal(args.reason, "why");
assert.deepEqual(parseIds(args), ["x", "y", "abc"]);
});
});
describe("draft MIME helpers", () => {
it("buildDraftRaw includes headers and body", () => {
const raw = buildDraftRaw({
to: "a@b.com",
subject: "Hello",
body: "Line1\nLine2",
cc: "c@d.com",
});
assert.match(raw, /^To: a@b.com\r\n/);
assert.match(raw, /Cc: c@d.com/);
assert.match(raw, /Subject: Hello/);
assert.match(raw, /Line1\nLine2$/);
});
it("buildForwardDraftRaw prefixes Fwd and quotes original", () => {
const raw = buildForwardDraftRaw({
to: "acct@example.com",
note: "Please book.",
original: {
from: "grab@example.com",
to: "me@example.com",
subject: "Your Grab E-Receipt",
date: "Mon, 1 Jan 2026",
body: "Total Rp10.000",
},
});
assert.match(raw, /Subject: Fwd: Your Grab E-Receipt/);
assert.match(raw, /Please book\./);
assert.match(raw, /---------- Forwarded message ---------/);
assert.match(raw, /From: grab@example.com/);
assert.match(raw, /Total Rp10\.000/);
});
it("does not double Fwd: prefix", () => {
const raw = buildForwardDraftRaw({
to: "a@b.com",
original: { subject: "Fwd: Already", body: "x" },
});
assert.match(raw, /Subject: Fwd: Already/);
assert.doesNotMatch(raw, /Subject: Fwd: Fwd:/);
});
it("rawToGmailMessage base64url-encodes", () => {
const { raw } = rawToGmailMessage("hi+/=?");
assert.equal(raw.includes("+"), false);
assert.equal(raw.includes("/"), false);
assert.ok(raw.length > 0);
});
it("archiveModifyBody removes INBOX only", () => {
assert.deepEqual(archiveModifyBody(), { removeLabelIds: ["INBOX"] });
});
it("labelModifyBody adds and removes", () => {
assert.deepEqual(labelModifyBody({ add: ["Label_1"], remove: ["INBOX"] }), {
addLabelIds: ["Label_1"],
removeLabelIds: ["INBOX"],
});
assert.deepEqual(labelModifyBody({ add: ["A"] }), {
addLabelIds: ["A"],
});
});
});