agent-paths.test.ts5.3 KBView on GitHub /**
* The client's copy of the agent path convention.
*
* These two functions are a deliberate re-implementation of
* `agentNamespacePath`/`agentMemoryDirPath` in apps/server — the server modules that own
* them drag in the document service and the DB layer, so the browser mints the strings
* itself. A duplicated convention is only safe while something pins the exact shape, and
* this is that something: if the server's minting ever changes, these assertions are what
* fail rather than a silently empty file tree.
*
* The scope parameter is what lets the agent workspace render at a DEAL rather than at
* the user (apps/mail/docs/meeting-tab.md phase 1). Its default is the whole compatibility
* story, so it is asserted first and separately.
*/
import {
agentConfigDirPath,
agentDefaultFilePath,
agentMemoryDirPath,
agentNamespacePath,
} from '@/modules/agents/utils/agent-paths';
const AGENT = 'b41c0000-0000-0000-0000-000000000001';
const CONVERSATION = 'e8c4ead2-ed2e-471a-a03c-b73bfd2987e6';
describe('agentNamespacePath', () => {
it('defaults to the user scope, so /agents/:agentId is unchanged', () => {
expect(agentNamespacePath(AGENT)).toBe(`user/agent-${AGENT}`);
});
it('mints the same string when the user scope is passed explicitly', () => {
expect(agentNamespacePath(AGENT, { type: 'user' })).toBe(agentNamespacePath(AGENT));
});
it('roots the namespace in the deal under a conversation scope', () => {
expect(agentNamespacePath(AGENT, { type: 'conversation', conversationId: CONVERSATION })).toBe(
`conversation/${CONVERSATION}/agent-${AGENT}`,
);
});
it('matches the paths the meeting-prep agent already writes', () => {
// Not a restatement of the line above: this is the shape `automations.ts` instructs
// the agent to archive under, and the shape `resolveOpenDoc`'s `__meeting_prep__`
// branch resolves. The tree has to root at exactly this prefix or it lists nothing.
const namespace = agentNamespacePath(AGENT, {
type: 'conversation',
conversationId: CONVERSATION,
});
expect(`${namespace}/overview`).toBe(
`conversation/${CONVERSATION}/agent-${AGENT}/overview`,
);
expect(`${namespace}/archives/2026-08-13`).toBe(
`conversation/${CONVERSATION}/agent-${AGENT}/archives/2026-08-13`,
);
});
});
describe('agentMemoryDirPath', () => {
it('defaults to the user scope', () => {
expect(agentMemoryDirPath(AGENT)).toBe(`user/agent-${AGENT}/memory`);
});
it('follows the scope it is given, rather than pinning memory to the user', () => {
expect(agentMemoryDirPath(AGENT, { type: 'conversation', conversationId: CONVERSATION })).toBe(
`conversation/${CONVERSATION}/agent-${AGENT}/memory`,
);
});
it('is always the namespace plus /memory, in every scope', () => {
// The Output tab excludes exactly this path from the tree, by string equality. If the
// two helpers ever disagreed about a scope, memory would show up as output.
for (const scope of [
{ type: 'user' } as const,
{ type: 'conversation', conversationId: CONVERSATION } as const,
]) {
expect(agentMemoryDirPath(AGENT, scope)).toBe(`${agentNamespacePath(AGENT, scope)}/memory`);
}
});
});
/**
* The ORG scope — an agent whose document declares `namespace: org` keeps ONE folder for the
* whole team instead of a private copy per teammate.
*
* Asserted here as well as on the server because these two implementations are deliberately
* separate (the server modules pull in the DB layer, which the browser must not), and a mirror
* that drifts renders the tab against a path that holds none of the agent's files — an empty
* Output tab for an agent that has written plenty, with no error anywhere.
*/
describe('the org scope', () => {
it('mints one folder for the team, under organisation/', () => {
expect(agentNamespacePath(AGENT, { type: 'org' })).toBe(`organisation/agent-${AGENT}`);
});
it('is a different folder from the same agent’s user namespace', () => {
expect(agentNamespacePath(AGENT, { type: 'org' })).not.toBe(agentNamespacePath(AGENT));
});
it('carries the memory folder with it', () => {
expect(agentMemoryDirPath(AGENT, { type: 'org' })).toBe(`organisation/agent-${AGENT}/memory`);
});
it('roots the config folder there too', () => {
expect(agentConfigDirPath(AGENT, { type: 'org' })).toBe(`organisation/agent-${AGENT}/config`);
});
});
describe('agentDefaultFilePath', () => {
it('joins the agent’s declared file onto its folder, per scope', () => {
expect(agentDefaultFilePath(AGENT, 'overview')).toBe(`user/agent-${AGENT}/overview`);
expect(agentDefaultFilePath(AGENT, 'outputs/weekly', { type: 'org' })).toBe(
`organisation/agent-${AGENT}/outputs/weekly`,
);
expect(
agentDefaultFilePath(AGENT, 'overview', { type: 'conversation', conversationId: 'c1' }),
).toBe(`conversation/c1/agent-${AGENT}/overview`);
});
it('opens nothing for `none` (null) and nothing while the agent is still loading', () => {
// `null` is the agent's choice; `undefined` is "not answered yet". The browser opens
// `autoOpenPath` ONCE per mount, so a guess before the answer cannot be withdrawn.
expect(agentDefaultFilePath(AGENT, null)).toBeUndefined();
expect(agentDefaultFilePath(AGENT, undefined)).toBeUndefined();
});
});