the picker above an email composer listed conversations from the owner's home — a chat started somewhere else entirely, offered as if it belonged to the mailbox. the address had two resolvers. the chat socket knew `context: 'email'` and ran the turn in the account's directory; `GET /chat/sessions` did not, and fell through to `who.home`. so a message went into the mailbox and could not be found in the list directly above where it was typed. two answers to "where does this chat live" is one too many, and the one that was missing failed quietly. `resolveEmailAccountDir` in data-path.ts is now the only answer, and both call it. it verifies a client-named mailbox against disk rather than sanitising it into shape, which matters because the socket's caller ends in mkdirSync — an unrecognised value would not fail, it would CREATE a directory named after whatever arrived and run turns in it. `all`, the list's own sentinel for the unified view, is exactly the value most likely to arrive by accident. pure: no mkdir, no writes. the caller that needs the directory says so itself. tested, because a missing branch is what this was: the named mailbox wins, `all` never becomes a directory, a mailbox with no emails.db is not a mailbox, and no account at all resolves to the accounts root rather than to something invented. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
56 lines
2.4 KiB
TypeScript
56 lines
2.4 KiB
TypeScript
import { describe, expect, test, beforeAll, afterAll } from 'bun:test';
|
|
import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { getEmailAccountsDir, resolveEmailAccountDir } from './data-path';
|
|
|
|
/**
|
|
* Where an email chat runs.
|
|
*
|
|
* This is regression cover rather than API cover. The address had two resolvers — the chat socket's, for
|
|
* running the turn, and `GET /chat/sessions`'s, for listing the conversations — and only the socket knew
|
|
* about the `email` context. The list fell through to the owner's home, so the picker above an email
|
|
* composer offered conversations from somewhere else and a message sent into the mailbox could not be
|
|
* found in the list directly above it. One resolver now, and these are the three answers it owes.
|
|
*/
|
|
|
|
const OWNER = 'resolve-email-dir-test@officer.dev';
|
|
const ROOT = getEmailAccountsDir(OWNER);
|
|
|
|
const account = (name: string, withDb = true) => {
|
|
const dir = join(ROOT, name);
|
|
mkdirSync(dir, { recursive: true });
|
|
if (withDb) writeFileSync(join(dir, 'emails.db'), '');
|
|
return dir;
|
|
};
|
|
|
|
describe('resolveEmailAccountDir', () => {
|
|
beforeAll(() => {
|
|
rmSync(ROOT, { recursive: true, force: true });
|
|
mkdirSync(ROOT, { recursive: true });
|
|
});
|
|
|
|
afterAll(() => rmSync(ROOT, { recursive: true, force: true }));
|
|
|
|
test('a named mailbox that exists is the answer', () => {
|
|
const dir = account('someone@gmail.com');
|
|
expect(resolveEmailAccountDir(OWNER, 'someone@gmail.com')).toBe(dir);
|
|
});
|
|
|
|
test("`all` is the list's sentinel, not an address — it never becomes a directory", () => {
|
|
// The caller ends in mkdirSync, so accepting this would CREATE a mailbox named `all` and run turns in
|
|
// it. The panel drops the sentinel; this is the half that does not depend on the panel remembering to.
|
|
expect(resolveEmailAccountDir(OWNER, 'all')).not.toBe(join(ROOT, 'all'));
|
|
expect(resolveEmailAccountDir(OWNER, 'all')).toBe(join(ROOT, 'someone@gmail.com'));
|
|
});
|
|
|
|
test('a mailbox with no database is not a mailbox', () => {
|
|
account('half-configured@gmail.com', false);
|
|
expect(resolveEmailAccountDir(OWNER, 'half-configured@gmail.com')).toBe(join(ROOT, 'someone@gmail.com'));
|
|
});
|
|
|
|
test('no account at all resolves to the accounts root, which exists', () => {
|
|
const empty = 'no-accounts-test@officer.dev';
|
|
expect(resolveEmailAccountDir(empty)).toBe(getEmailAccountsDir(empty));
|
|
});
|
|
});
|