Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion src/e2e/ai.e2e.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from "bun:test";
import { runTool } from "./helpers";
import { runTool } from "@app/utils/e2e/helpers";

describe("tools ai", () => {
describe("help", () => {
Expand Down
11 changes: 4 additions & 7 deletions src/e2e/benchmark.e2e.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { afterAll, describe, expect, it } from "bun:test";
import { runTool } from "./helpers";
import { getOutput, runTool } from "@app/utils/e2e/helpers";

describe("tools benchmark", () => {
afterAll(async () => {
Expand Down Expand Up @@ -41,17 +41,15 @@ describe("tools benchmark", () => {
it("add, list, remove suite", async () => {
const add = await runTool(["benchmark", "add", "e2e-suite", "a:echo hi", "b:echo bye"]);
expect(add.exitCode).toBe(0);
const addOut = add.stdout + add.stderr;
expect(addOut.toLowerCase()).toMatch(/saved|added|created/i);
expect(getOutput(add).toLowerCase()).toMatch(/saved|added|created/i);

const list = await runTool(["benchmark", "list"]);
expect(list.exitCode).toBe(0);
expect(list.stdout).toContain("e2e-suite");

const remove = await runTool(["benchmark", "remove", "e2e-suite"]);
expect(remove.exitCode).toBe(0);
const removeOut = remove.stdout + remove.stderr;
expect(removeOut.toLowerCase()).toMatch(/removed|deleted/i);
expect(getOutput(remove).toLowerCase()).toMatch(/removed|deleted/i);

const listAfter = await runTool(["benchmark", "list"]);
expect(listAfter.exitCode).toBe(0);
Expand All @@ -62,8 +60,7 @@ describe("tools benchmark", () => {
describe("remove non-existent", () => {
it("remove nonexistent shows error", async () => {
const r = await runTool(["benchmark", "remove", "nonexistent-xyz"]);
const output = r.stdout + r.stderr;
expect(output.toLowerCase()).toMatch(/not found|no suite|error|doesn't exist/i);
expect(getOutput(r).toLowerCase()).toMatch(/not found|no suite|error|doesn't exist/i);
});
});
});
34 changes: 0 additions & 34 deletions src/e2e/helpers.ts

This file was deleted.

77 changes: 77 additions & 0 deletions src/e2e/macos-mail.e2e.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { describe, expect, it } from "bun:test";
import { ALL_COLUMN_KEYS } from "@app/macos/lib/mail/columns";
import { extractJson, getOutput, runTool, stripAnsi } from "@app/utils/e2e/helpers";

describe("tools macos mail", () => {
describe("help", () => {
it("--help exits 0", async () => {
const r = await runTool(["macos", "mail", "--help"]);
expect(r.exitCode).toBe(0);
});

it("list --help exits 0 and mentions --columns, --format, --limit", async () => {
const r = await runTool(["macos", "mail", "list", "--help"]);
expect(r.exitCode).toBe(0);
const out = stripAnsi(r.stdout);

expect(out).toContain("--columns");
expect(out).toContain("--format");
expect(out).toContain("--limit");
});

it("search --help exits 0 and mentions --columns, --format", async () => {
const r = await runTool(["macos", "mail", "search", "--help"]);
expect(r.exitCode).toBe(0);
const out = stripAnsi(r.stdout);

expect(out).toContain("--columns");
expect(out).toContain("--format");
});
});

describe("list --help column names", () => {
it("list --help output contains all column names", async () => {
const r = await runTool(["macos", "mail", "list", "--help"]);
const out = stripAnsi(r.stdout);

for (const col of ALL_COLUMN_KEYS) {
expect(out).toContain(col);
}
});
});

describe("invalid column", () => {
it("list --columns invalid_col should warn about unknown column", async () => {
const r = await runTool(["macos", "mail", "list", "--columns", "invalid_col"]);
expect(getOutput(r)).toContain("Unknown column");
});
});

describe("monitor", () => {
it("monitor --help exits 0 and mentions --limit, --notify-telegram, --dry-run", async () => {
const r = await runTool(["macos", "mail", "monitor", "--help"]);
expect(r.exitCode).toBe(0);
const out = stripAnsi(r.stdout);

expect(out).toContain("--limit");
expect(out).toContain("--notify-telegram");
expect(out).toContain("--dry-run");
});
});

describe("format flag", () => {
it("list --format json --limit 1 accepts the format flag", async () => {
const r = await runTool(["macos", "mail", "list", "--format", "json", "--limit", "1"], 30_000);

// If mail DB is accessible, we get valid JSON output.
// If not (e.g. no Full Disk Access), we still verify the flags are accepted
// by checking it doesn't fail with "unknown option".
expect(getOutput(r)).not.toContain("unknown option");

if (r.exitCode === 0 && r.stdout.trim().length > 0) {
const parsed = extractJson<unknown[]>(r.stdout);
expect(Array.isArray(parsed)).toBe(true);
}
}, 30_000);
});
});
2 changes: 1 addition & 1 deletion src/e2e/notify.e2e.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from "bun:test";
import { runTool } from "./helpers";
import { runTool } from "@app/utils/e2e/helpers";

describe("tools notify", () => {
describe("help & no-args", () => {
Expand Down
8 changes: 3 additions & 5 deletions src/e2e/say.e2e.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { afterAll, describe, expect, it } from "bun:test";
import { runTool } from "./helpers";
import { getOutput, runTool } from "@app/utils/e2e/helpers";

describe("tools say", () => {
afterAll(async () => {
Expand Down Expand Up @@ -32,8 +32,7 @@ describe("tools say", () => {
await runTool(["say", "--mute"]);
const r = await runTool(["say", "should be muted"]);
expect(r.exitCode).toBe(0);
const output = r.stdout + r.stderr;
expect(output.toLowerCase()).toContain("muted");
expect(getOutput(r).toLowerCase()).toContain("muted");
});

it("--unmute exits 0", async () => {
Expand All @@ -52,8 +51,7 @@ describe("tools say", () => {
await runTool(["say", "--mute", "--app", "e2e-test"]);
const r = await runTool(["say", "test", "--app", "e2e-test"]);
expect(r.exitCode).toBe(0);
const output = r.stdout + r.stderr;
expect(output.toLowerCase()).toContain("muted");
expect(getOutput(r).toLowerCase()).toContain("muted");
});

it("--unmute --app e2e-test exits 0", async () => {
Expand Down
7 changes: 3 additions & 4 deletions src/e2e/timer.e2e.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { afterAll, describe, expect, it } from "bun:test";
import { runTool } from "./helpers";
import { getOutput, runTool } from "@app/utils/e2e/helpers";

describe("tools timer", () => {
afterAll(async () => {
Expand Down Expand Up @@ -49,12 +49,11 @@ describe("tools timer", () => {
it("starts bg timer, lists it, waits for expiry", async () => {
const start = await runTool(["timer", "3s", "e2e-bg", "--bg"]);
expect(start.exitCode).toBe(0);
const startOutput = start.stdout + start.stderr;
expect(startOutput.toLowerCase()).toMatch(/timer|started|background/i);
expect(getOutput(start)).toMatch(/timer|started|background/i);

const list = await runTool(["timer", "list"]);
expect(list.exitCode).toBe(0);
expect(list.stdout + list.stderr).toContain("e2e-bg");
expect(getOutput(list)).toContain("e2e-bg");

await Bun.sleep(4000);

Expand Down
8 changes: 3 additions & 5 deletions src/e2e/transcribe.e2e.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { afterAll, describe, expect, it } from "bun:test";
import { existsSync, unlinkSync, writeFileSync } from "node:fs";
import { runTool } from "./helpers";
import { getOutput, runTool } from "@app/utils/e2e/helpers";

const ZERO_BYTE_MP3 = "/tmp/e2e-empty.mp3";

Expand Down Expand Up @@ -30,17 +30,15 @@ describe("tools transcribe", () => {
it("nonexistent file exits 1", async () => {
const r = await runTool(["transcribe", "/nonexistent/path.mp3"]);
expect(r.exitCode).toBe(1);
const output = r.stdout + r.stderr;
expect(output.toLowerCase()).toContain("not found");
expect(getOutput(r).toLowerCase()).toContain("not found");
});

it("unsupported extension exits 1", async () => {
writeFileSync("/tmp/e2e-test.xyz", "fake");
try {
const r = await runTool(["transcribe", "/tmp/e2e-test.xyz"]);
expect(r.exitCode).toBe(1);
const output = r.stdout + r.stderr;
expect(output.toLowerCase()).toContain("unsupported");
expect(getOutput(r).toLowerCase()).toContain("unsupported");
} finally {
unlinkSync("/tmp/e2e-test.xyz");
}
Expand Down
5 changes: 2 additions & 3 deletions src/e2e/voice-memos.e2e.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { afterAll, describe, expect, it } from "bun:test";
import { existsSync, readdirSync } from "node:fs";
import { rm } from "node:fs/promises";
import { runTool } from "./helpers";
import { getOutput, runTool } from "@app/utils/e2e/helpers";

const EXPORT_DIR = "/tmp/vm-e2e-export";

Expand Down Expand Up @@ -83,8 +83,7 @@ describe("tools macos voice-memos", () => {
it("transcribe invalid ID exits 1", async () => {
const r = await runTool(["macos", "voice-memos", "transcribe", "999999"]);
expect(r.exitCode).toBe(1);
const output = r.stdout + r.stderr;
expect(output.toLowerCase()).toMatch(/no memo|not found/i);
expect(getOutput(r).toLowerCase()).toMatch(/no memo|not found/i);
});

it("transcribe --all exits 0", async () => {
Expand Down
8 changes: 2 additions & 6 deletions src/e2e/youtube.e2e.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { afterAll, describe, expect, it } from "bun:test";
import { existsSync, readFileSync, unlinkSync } from "node:fs";
import { SafeJSON } from "@app/utils/json";
import { runTool, stripAnsi } from "./helpers";
import { extractJson, runTool } from "@app/utils/e2e/helpers";

const OUTPUT_FILE = "/tmp/yt-e2e-test.txt";

Expand Down Expand Up @@ -53,10 +52,7 @@ describe("tools youtube", () => {
it("fetches captions as JSON", async () => {
const r = await runTool(["youtube", "transcribe", "dQw4w9WgXcQ", "--format", "json"]);
expect(r.exitCode).toBe(0);
const clean = stripAnsi(r.stdout);
const jsonStart = clean.indexOf("{");
expect(jsonStart).toBeGreaterThanOrEqual(0);
const parsed = SafeJSON.parse(clean.slice(jsonStart));
const parsed = extractJson<Record<string, unknown>>(r.stdout);
expect(parsed).toHaveProperty("text");
expect(parsed).toHaveProperty("segments");
}, 30_000);
Expand Down
28 changes: 4 additions & 24 deletions src/macos/commands/mail/download.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,13 @@
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { basename, join, resolve } from "node:path";
import logger from "@app/logger";
import { generateEmailMarkdown, generateIndexMarkdown, generateSlug } from "@app/macos/lib/mail/format";
import { getMessageBody, saveAttachment } from "@app/macos/lib/mail/jxa";
import { MailStorage } from "@app/macos/lib/mail/mail-storage";
import { cleanup, getRecipients } from "@app/macos/lib/mail/sqlite";
import type { MailMessage } from "@app/macos/lib/mail/types";
import { SafeJSON } from "@app/utils/json";
import * as p from "@clack/prompts";
import type { Command } from "commander";

/** Load the last search results from temp file */
function loadLastSearchResults(): MailMessage[] | null {
const path = join(tmpdir(), "macos-mail-last-search.json");
if (!existsSync(path)) {
return null;
}

try {
const raw = readFileSync(path, "utf-8");
const parsed = SafeJSON.parse(raw) as Array<Record<string, unknown>>;
return parsed.map((m) => ({
...m,
dateSent: new Date(m.dateSent as string),
dateReceived: new Date(m.dateReceived as string),
})) as MailMessage[];
} catch {
return null;
}
}

export function registerDownloadCommand(program: Command): void {
program
.command("download <output-dir>")
Expand All @@ -53,7 +31,9 @@ export function registerDownloadCommand(program: Command): void {
const isTTY = process.stdout.isTTY;

// Load last search results
const messages = loadLastSearchResults();
const mailStorage = new MailStorage();
const messages = mailStorage.loadSearchResults();

if (!messages || messages.length === 0) {
p.log.error("No search results found. Run 'tools macos mail search <query>' first.");
process.exit(1);
Expand Down
2 changes: 2 additions & 0 deletions src/macos/commands/mail/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { Command } from "commander";
import { registerDownloadCommand } from "./download";
import { registerListCommand } from "./list";
import { registerMonitorCommand } from "./monitor";
import { registerSearchCommand } from "./search";

/**
Expand All @@ -16,6 +17,7 @@ export function registerMailCommand(program: Command): void {
registerSearchCommand(mail);
registerListCommand(mail);
registerDownloadCommand(mail);
registerMonitorCommand(mail);

program.addCommand(mail);
}
Loading