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 apps/hub/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"@corbits/inference-catalog": "workspace:*",
"@corbits/insights": "workspace:*",
"@corbits/last-30-days-research-workflow": "workspace:*",
"@corbits/mailbox": "github:corbitsdev/corbits-mailbox#118a2cd3b0b2d83abcd57b4bed1698e95ee41b6d",
"@corbits/mailbox": "github:corbitsdev/corbits-mailbox#fea6211e42fddfc87e8e1755c219c437e45a7d62",
"@corbits/mcp-tools": "workspace:*",
"@corbits/memory": "github:corbitsdev/corbits-memory#9e6f213fa2c002b531d3f6af1aa0abd737b8afe3",
"@corbits/memory-hub": "workspace:*",
Expand Down
20 changes: 6 additions & 14 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/chat/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
"@corbits/commands": "workspace:*",
"@corbits/error-sink": "workspace:*",
"@corbits/hub-api-client": "workspace:*",
"@corbits/mailbox": "github:corbitsdev/corbits-mailbox#118a2cd3b0b2d83abcd57b4bed1698e95ee41b6d",
"@corbits/mailbox": "github:corbitsdev/corbits-mailbox#fea6211e42fddfc87e8e1755c219c437e45a7d62",
"@corbits/memory": "github:corbitsdev/corbits-memory#9e6f213fa2c002b531d3f6af1aa0abd737b8afe3",
"@corbits/turn-artifacts": "workspace:*",
"@corbits/workflows": "workspace:*",
Expand Down
2 changes: 1 addition & 1 deletion packages/inbox/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
},
"dependencies": {
"@corbits/error-sink": "workspace:*",
"@corbits/mailbox": "github:corbitsdev/corbits-mailbox#118a2cd3b0b2d83abcd57b4bed1698e95ee41b6d",
"@corbits/mailbox": "github:corbitsdev/corbits-mailbox#fea6211e42fddfc87e8e1755c219c437e45a7d62",
"@corbits/notify": "workspace:*",
"@intx/hub-api": "workspace:*",
"@intx/log": "0.3.0",
Expand Down
30 changes: 20 additions & 10 deletions packages/inbox/src/client.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
// Browser-safe Inbox domain surface: the wire schemas/types a UI parses
// responses with, group classification, and the pure bulk-op eligibility
// rules. Re-exported from their owning modules rather than redeclared —
// `./project`, `./group`, and `./bulk` already import nothing but arktype
// and (type-only) `@corbits/mailbox`, so this subpath adds no code of its
// own, only a browser-safe cut of the package root. The root export (".")
// also carries `./routes` (hono, `@intx/hub-api`), `./delivery`
// (`@corbits/notify/mailbox`), and `./migrations` (`@corbits/mailbox`'s
// Postgres migration runner) — none of which belong in a browser bundle —
// which is why a UI imports this subpath instead of the root. Enforced
// by `bun run check:browser-safe-subpaths`
// responses with, group classification, the pure bulk-op eligibility rules,
// and the mailbox thread-read fetch client. Re-exported from their owning
// modules rather than redeclared — `./project`, `./group`, and `./bulk`
// import nothing but arktype and (type-only) `@corbits/mailbox`; `./thread-
// client` imports arktype and `@corbits/mailbox`'s own thread schemas
// (a value import, but `@corbits/mailbox` is an external git dependency the
// browser-safe-subpaths check treats as an opaque leaf, not something it
// walks into) — so this subpath adds only `fetch` composition, no server-
// only dependency. The root export (".") also carries `./routes` (hono,
// `@intx/hub-api`), `./delivery` (`@corbits/notify/mailbox`), and
// `./migrations` (`@corbits/mailbox`'s Postgres migration runner) — none of
// which belong in a browser bundle — which is why a UI imports this subpath
// instead of the root. Enforced by `bun run check:browser-safe-subpaths`
// (scripts/checks/browser-safe-subpaths.ts), not just by convention.

export { itemsEligibleForClearDone, itemsEligibleForMarkAllRead } from "./bulk";
Expand All @@ -29,3 +32,10 @@ export {
type InboxItem,
type InboxItemDetail,
} from "./project";
export {
MailboxThreadFetchError,
listMailboxThreadsClient,
readMailboxThreadClient,
type MailboxThreadListResult,
type MailboxThreadResult,
} from "./thread-client";
99 changes: 99 additions & 0 deletions packages/inbox/src/thread-client.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { describe, expect, test } from "bun:test";

import {
MailboxThreadFetchError,
listMailboxThreadsClient,
readMailboxThreadClient,
} from "./thread-client";

function fetchStub(body: unknown, status = 200): typeof fetch {
return (async () => {
return {
ok: status >= 200 && status < 300,
status,
json: async () => body,
} as Response;
}) as unknown as typeof fetch;
}

describe("listMailboxThreadsClient", () => {
test("parses a page of thread summaries", async () => {
const page = await listMailboxThreadsClient(
"/api/tenants/t1/mailbox",
{ refs: [{ kind: "workbench", id: "w1" }], limit: 10 },
fetchStub({
threads: [
{
rootId: "m1",
rootMessageId: "m1@mailbox",
messageCount: 2,
unreadCount: 1,
lastMessageId: "m2@mailbox",
lastFromAddress: "routine:x",
lastCreatedAt: "2026-01-01T00:00:00.000000Z",
},
],
nextCursor: "cur1",
}),
);
expect(page.threads).toHaveLength(1);
expect(page.nextCursor).toBe("cur1");
});

test("throws MailboxThreadFetchError on a malformed body", async () => {
await expect(
listMailboxThreadsClient(
"/api/tenants/t1/mailbox",
{},
fetchStub({ threads: "not-an-array" }),
),
).rejects.toBeInstanceOf(MailboxThreadFetchError);
});

test("throws MailboxThreadFetchError on a non-ok response", async () => {
await expect(
listMailboxThreadsClient(
"/api/tenants/t1/mailbox",
{},
fetchStub({ error: "bad refs" }, 400),
),
).rejects.toBeInstanceOf(MailboxThreadFetchError);
});
});

describe("readMailboxThreadClient", () => {
test("parses a thread's messages oldest-first", async () => {
const page = await readMailboxThreadClient(
"/api/tenants/t1/mailbox",
"root@mailbox",
{},
fetchStub({
messages: [
{
id: "m1",
messageId: "m1@mailbox",
references: [],
fromAddress: "routine:x",
createdAt: "2026-01-01T00:00:00.000000Z",
read: true,
archived: false,
parentId: null,
},
],
}),
);
expect(page.messages).toHaveLength(1);
expect(page.nextCursor).toBeUndefined();
});

test("404 surfaces as MailboxThreadFetchError", async () => {
await expect(
readMailboxThreadClient(
"/api/tenants/t1/mailbox",
"unknown@mailbox",
{},
fetchStub({ error: "Thread not found" }, 404),
),
).rejects.toBeInstanceOf(MailboxThreadFetchError);
});
});
Loading
Loading