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
156 changes: 156 additions & 0 deletions backend/__tests__/unit/services/agentMentionService.nativeCue.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/**
* A native turn is told only about tools it has (QA walk, 2026-10-16: a hosted
* agent claimed commonly_read_file and pointed a person at commonly_attach_file,
* and another offered "a coding specialist like @codex"). The kernel writes its
* mention frames for every runtime; the native driver rewrites the ones it
* cannot honour (nativeKernelCue). This suite runs REAL kernel-built content
* through that rewrite, so a new kernel clause naming a tool native lacks fails
* here rather than in front of a stranger.
*
* Contract under test:
* - the kernel's mention content splits into its tagged frames and the message
* - before the rewrite it names tools native lacks (the positive control)
* - after it, every commonly_* name is a native tool, and the message is intact
* - content that is not kernel-framed passes through unchanged
*/

jest.mock('../../../services/agentEventService', () => ({ enqueue: jest.fn() }));
jest.mock('../../../models/AgentRegistry', () => ({
AgentInstallation: { find: jest.fn(), findOne: jest.fn() },
}));
jest.mock('../../../models/AgentProfile', () => ({ find: jest.fn() }));
jest.mock('../../../models/Pod', () => ({ findById: jest.fn(), find: jest.fn() }));
jest.mock('../../../models/User', () => ({ find: jest.fn(), findById: jest.fn() }));
jest.mock('../../../services/chatSummarizerService', () => ({
constructor: { getLatestPodSummary: jest.fn() },
summarizePodMessages: jest.fn(),
}));
jest.mock('../../../models/AgentEvent', () => ({ countDocuments: jest.fn() }));
jest.mock('../../../services/welcomeWakeService', () => ({ maybeFireWelcomeWake: jest.fn() }));
jest.mock('../../../models/pg/Message', () => ({ findById: jest.fn(async () => null) }));
jest.mock('../../../models/pg/ThreadUserState', () => ({
followByParticipation: jest.fn().mockResolvedValue(true),
}));

const AgentMentionService = require('../../../services/agentMentionService');
const AgentEventService = require('../../../services/agentEventService');
const { AgentInstallation } = require('../../../models/AgentRegistry');
const AgentProfile = require('../../../models/AgentProfile');
const Pod = require('../../../models/Pod');
const User = require('../../../models/User');
const AgentEvent = require('../../../models/AgentEvent');

// Drive the REAL kernel. `installations` decides which frames it writes: one
// seat is the reference wake; two seats make a collaborative pod; a seat with
// wake-on-message on gets the wake frame for a message that names no one.
const kernelContent = async ({ installations, content, type }) => {
AgentInstallation.find.mockReturnValue({ lean: jest.fn().mockResolvedValue(installations) });
AgentProfile.find.mockReturnValue({ lean: jest.fn().mockResolvedValue([]) });
User.find.mockReturnValue({ select: jest.fn().mockReturnThis(), lean: jest.fn().mockResolvedValue([]) });
User.findById.mockImplementation(() => ({
select: jest.fn().mockReturnThis(),
lean: jest.fn().mockResolvedValue({ _id: 'user-1', isBot: false }),
}));
Pod.findById.mockReturnValue({
select: jest.fn().mockReturnValue({
lean: jest.fn().mockResolvedValue({ type: 'chat', members: ['user-1', 'bot-1', 'bot-2'] }),
}),
lean: jest.fn().mockResolvedValue({ _id: 'pod-1', type: 'chat', members: ['user-1', 'bot-1', 'bot-2'] }),
});
AgentEvent.countDocuments.mockResolvedValue(0);

await AgentMentionService.enqueueMentions({
podId: 'pod-1',
userId: 'user-1',
username: 'alice',
message: { id: 'm-1', content },
});
// The LAST matching call: one arm may drive several scenarios.
const call = AgentEventService.enqueue.mock.calls.filter(([a]) => a.type === type).pop();
if (!call) throw new Error(`the kernel enqueued no ${type}`);
return call[0].payload.content;
};

const seat = (agentName, extra = {}) => ({ agentName, instanceId: 'default', displayName: agentName, ...extra });
const referenceWake = () => kernelContent({
installations: [seat('seat-a')], content: 'hi @seat-a', type: 'chat.mention',
});
const collaborativeMention = () => kernelContent({
installations: [seat('seat-a'), seat('seat-b')], content: 'hi @seat-a', type: 'chat.mention',
});
const wakeOnMessage = () => kernelContent({
installations: [seat('seat-a', { config: { wakeOnMessage: { enabled: true } } })],
content: 'just thinking out loud here',
type: 'message.posted',
});
beforeEach(() => { jest.clearAllMocks(); });

const { splitKernelFrames, frameTagOf, KERNEL_FRAME_TAGS } = require('../../../services/kernelFrames');
const { nativeCueFromKernel } = require('../../../services/nativeKernelCue');
const { NATIVE_TOOL_NAMES } = require('../../../services/nativeRuntimeService');

const toolNamesIn = (text) => [...new Set(text.match(/commonly_[a-z_]+/g) || [])];

describe('native cue from kernel-built mention content', () => {
test('the reference wake splits into its four tagged frames and the message', async () => {
const { frames, body } = splitKernelFrames(await referenceWake());

expect(frames.map(frameTagOf)).toEqual(['Pod context', 'Trigger', 'Collaboration', 'Reply mechanics']);
expect(body).toBe('hi @seat-a');
});

test('before the rewrite the kernel names tools native lacks; after it, only native tools', async () => {
const content = await referenceWake();
const foreign = toolNamesIn(content).filter((name) => !NATIVE_TOOL_NAMES.includes(name));
expect(foreign).toEqual(expect.arrayContaining(['commonly_attach_file', 'commonly_read_file', 'commonly_dm_agent']));

const cue = nativeCueFromKernel(content, 'pod-1', NATIVE_TOOL_NAMES);

expect(toolNamesIn(cue).filter((name) => !NATIVE_TOOL_NAMES.includes(name))).toEqual([]);
expect(cue).not.toMatch(/codex|token file|threadRootId/);
expect(cue).toMatch(/this conversation is in pod `pod-1`/);
expect(cue).toMatch(/You cannot open or attach files here/);
// The tool-agnostic part of the kernel's addressing rule survives.
expect(cue).toMatch(/a bare name reaches no one/);
expect(cue).not.toMatch(/commonly_request_decision/);
expect(cue).toContain('[Reply mechanics:');
expect(cue).toContain('[Trigger: this turn was raised by **alice**');
expect(cue.endsWith('\n\nhi @seat-a')).toBe(true);
});

// sprint-review on #2261: the property was only checked on a one-seat pod,
// and the collaborative-pod and wake frames carried DM and claim tools
// through. Every tag the kernel writes is driven here, from real content.
test('across every kernel frame tag, the native cue names only native tools', async () => {
const contents = [await referenceWake(), await collaborativeMention(), await wakeOnMessage()];
const seen = new Set(contents.flatMap((content) => splitKernelFrames(content).frames.map(frameTagOf)));
// The instrument covers every tag, or this arm proves nothing about the one it misses.
expect([...seen].sort()).toEqual(expect.arrayContaining(Object.values(KERNEL_FRAME_TAGS)));

for (const content of contents) {
const cue = nativeCueFromKernel(content, 'pod-1', NATIVE_TOOL_NAMES);
expect(toolNamesIn(cue).filter((name) => !NATIVE_TOOL_NAMES.includes(name))).toEqual([]);
}
});

test('the collaborative-pod and wake frames keep their guidance and lose only the tool sentences', async () => {
const collab = nativeCueFromKernel(await collaborativeMention(), 'pod-1', NATIVE_TOOL_NAMES);
expect(collab).toMatch(/\[Collaborative pod: .*EXECUTE it yourself.*race them by picking it up directly.*say so in the pod when you do\.\]/);
expect(collab).not.toMatch(/Delegate only when|commonly_dm_agent|commonly_open_dm/);

const wake = nativeCueFromKernel(await wakeOnMessage(), 'pod-1', NATIVE_TOOL_NAMES);
expect(wake).toMatch(/\[Wake-on-message: .*return NO_REPLY as your ENTIRE reply.*\]/);
expect(wake).not.toMatch(/commonly_claim_message|must be claimed first/);
});

test('a frame that still names a tool native lacks is dropped, not shown', () => {
const content = '[Trigger: raised by **alice**.]\n[Some new cue: call commonly_brand_new_tool.]\n\nhi';
const cue = nativeCueFromKernel(content, 'pod-1', NATIVE_TOOL_NAMES);
expect(cue).toBe('[Trigger: raised by **alice**.]\n\nhi');
});

test('content that is not kernel-framed passes through unchanged', () => {
expect(nativeCueFromKernel('A human just added you to this workspace.', 'pod-1', NATIVE_TOOL_NAMES))
.toBe('A human just added you to this workspace.');
});
});
53 changes: 53 additions & 0 deletions backend/__tests__/unit/services/kernelFrames.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* The kernel's mention frames have one owner (kernelFrames), so a driver that
* rewrites a frame for its own tools finds it by tag instead of parsing prose.
*
* Contract under test:
* - leading frame lines split from the message, and rejoin to the same text
* - an untagged frame is still a frame; its tag is null
* - content that does not open with a frame block comes back whole
*/

const fs = require('fs');
const path = require('path');
const {
KERNEL_FRAME_TAGS, frameTagOf, joinKernelFrames, splitKernelFrames,
} = require('../../../services/kernelFrames');

const framed = [
`[${KERNEL_FRAME_TAGS.podContext}: this conversation is in pod \`p1\`.]`,
`[${KERNEL_FRAME_TAGS.trigger}: this turn was raised by **alice**.]`,
'[This message replies to YOUR earlier message.]',
].join('\n');

describe('kernelFrames', () => {
test('splits the frames from the message and rejoins to the same text', () => {
const content = `${framed}\n\nhi @seat-a\n\nsecond paragraph`;
const { frames, body } = splitKernelFrames(content);

expect(frames.map(frameTagOf)).toEqual(['Pod context', 'Trigger', null]);
expect(body).toBe('hi @seat-a\n\nsecond paragraph');
expect(joinKernelFrames(frames, body)).toBe(content);
});

test.each([
['no frames', 'hi @seat-a'],
['a bracketed message with no blank line', '[draft] hi @seat-a'],
['a block whose second line is prose', '[Pod context: p1.]\nnot a frame\n\nhi'],
['an empty string', ''],
])('%s comes back whole', (_label, content) => {
expect(splitKernelFrames(content)).toEqual({ frames: [], body: content });
expect(joinKernelFrames([], content)).toBe(content);
});

test.each(Object.entries(KERNEL_FRAME_TAGS))('the %s tag is the one agentMentionService writes', (_key, tag) => {
const source = fs.readFileSync(path.join(__dirname, '../../../services/agentMentionService.ts'), 'utf8');
expect(source).toContain(`[${tag}: `);
});

test('frameTagOf reads the tag before the first colon', () => {
expect(frameTagOf('[Reply mechanics: post it.]')).toBe('Reply mechanics');
expect(frameTagOf('[This message replies to YOUR earlier message — you are addressed.]')).toBeNull();
expect(frameTagOf('plain text')).toBeNull();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jest.mock('../../../models/AgentRun', () => ({ create: jest.fn(), countDocuments
let mockOwnerPro = false;
jest.mock('../../../models/User', () => ({
findOne: jest.fn(() => ({ select: () => ({ lean: () => Promise.resolve(null) }) })),
findById: jest.fn(() => ({ select: () => ({ lean: () => Promise.resolve({ entitlements: { pro: mockOwnerPro } }) }) })),
findById: jest.fn(() => ({ select: () => ({ lean: () => Promise.resolve({ entitlements: { pro: mockOwnerPro }, isBot: false }) }) })),
}));

jest.mock('../../../services/agentTypingService', () => ({
Expand Down Expand Up @@ -77,6 +77,21 @@ jest.mock('../../../services/turnSandboxService', () => ({
TurnSandbox: jest.fn().mockImplementation(() => ({ run: mockSandboxRun, close: mockSandboxClose })),
}));

// Reading a page: the guard is a spy here (outboundUrlGuard.fetch.test.js proves it).
const mockGetPage = jest.fn();
jest.mock('../../../services/outboundUrlGuard', () => ({
...jest.requireActual('../../../services/outboundUrlGuard'),
getFromPublicAddress: (...args) => mockGetPage(...args),
}));
const mockTaskRows = jest.fn();
const mockTaskCount = jest.fn();
jest.mock('../../../models/Task', () => ({
find: jest.fn(() => ({
sort: () => ({ limit: () => ({ select: () => ({ lean: () => mockTaskRows() }) }) }),
})),
countDocuments: (...args) => mockTaskCount(...args),
}));

jest.mock('../../../models/Message', () => {
const chain = {
sort: () => chain, limit: () => chain, select: () => chain, populate: () => chain, lean: () => Promise.resolve([]),
Expand All @@ -101,10 +116,16 @@ const hostedInstall = (hosted = { engine: 'native', provisionedAt: new Date(), a
config: { runtime: { runtimeType: 'hosted' }, hosted, ...extra },
});

const mention = { type: 'chat.mention', eventId: '507f1f77bcf86cd799439012', payload: { content: '@maya-agent what did we decide?' } };
// One pod message the owner (a known human) wrote.
const mention = {
type: 'chat.mention',
eventId: '507f1f77bcf86cd799439012',
payload: { content: '@maya-agent what did we decide?', userId: '507f1f77bcf86cd799439099', messageId: 'msg-1' },
};

const BASE_TOOLS = [
'commonly_create_task', 'commonly_post_message', 'commonly_read_context', 'commonly_read_memory', 'commonly_write_memory',
'commonly_create_task', 'commonly_fetch_url', 'commonly_list_tasks', 'commonly_post_message', 'commonly_read_context',
'commonly_read_memory', 'commonly_write_memory',
];
const call = (id, name, args) => ({ id, type: 'function', function: { name, arguments: JSON.stringify(args) } });
const toolCalls = (...calls) => ({
Expand Down Expand Up @@ -190,7 +211,7 @@ describe('nativeRuntimeService — hosted installs on the native engine', () =>
const prompt = systemPromptIn(0);
expect(prompt).toMatch(/Use commonly_web_search for anything current/);
expect(prompt).toMatch(/Use commonly_run_code to calculate/);
expect(prompt).toMatch(/for example, opening a link someone shares\)/);
expect(prompt).toMatch(/for example, opening an attached file\)/);
});

test('an instance with neither key offers neither, and the prompt does not claim them', async () => {
Expand All @@ -199,7 +220,7 @@ describe('nativeRuntimeService — hosted installs on the native engine', () =>
expect(offeredIn(0)).toEqual(BASE_TOOLS);
const prompt = systemPromptIn(0);
expect(prompt).not.toMatch(/commonly_web_search|commonly_run_code/);
expect(prompt).toMatch(/for example, running code, searching the web or opening a link someone shares\)/);
expect(prompt).toMatch(/for example, running code, searching the web or opening an attached file\)/);
});

test('with one key, only that tool is offered and named', async () => {
Expand All @@ -211,7 +232,7 @@ describe('nativeRuntimeService — hosted installs on the native engine', () =>
const prompt = systemPromptIn(0);
expect(prompt).toMatch(/Use commonly_web_search/);
expect(prompt).not.toMatch(/commonly_run_code/);
expect(prompt).toMatch(/for example, running code or opening a link someone shares\)/);
expect(prompt).toMatch(/for example, running code or opening an attached file\)/);
});

test('a hosted turn stops at three searches and records them for the spend alarm', async () => {
Expand Down Expand Up @@ -252,6 +273,75 @@ describe('nativeRuntimeService — hosted installs on the native engine', () =>
});
});

// The QA walk of 2026-10-16: a hosted agent could not open a shared link,
// said the board was empty when it held three tasks, and claimed file tools
// the kernel's frame had named but the turn never had.
describe('reading a link, reading the board, and a frame that names only its tools', () => {
const toolMessages = (callIndex) => axios.post.mock.calls[callIndex][1].messages.filter((m) => m.role === 'tool');
const userMessageIn = (callIndex) => axios.post.mock.calls[callIndex][1].messages.find((m) => m.role === 'user').content;

test('ask d: the README is read from the repo link the person gave; a composed README address is refused', async () => {
const repo = 'https://github.com/Team-Commonly/commonly';
mockGetPage.mockResolvedValue({
ok: true, status: 200, contentType: 'text/html', text: 'README What is Commonly? …', truncated: false, url: repo,
});
axios.post
.mockResolvedValueOnce(toolCalls(call('c1', 'commonly_fetch_url', {
url: 'https://raw.githubusercontent.com/Team-Commonly/commonly/main/README.md',
})))
.mockResolvedValueOnce(toolCalls(call('c2', 'commonly_fetch_url', { url: repo })));

await runAgent(hostedInstall(), {
...mention, payload: { ...mention.payload, content: '@maya-agent Read the README of github.com/Team-Commonly/commonly' },
});

expect(mockGetPage.mock.calls).toEqual([[repo]]);
expect(toolMessages(2)[0].content).toMatch(/does not appear in this conversation/);
expect(toolMessages(2)[1].content).toMatch(/What is Commonly\?/);
// The refused attempt sent nothing, so it does not spend one of the three.
expect(savedRun.webFetches).toBe(1);
});

test('the board is read, not guessed', async () => {
mockTaskRows.mockResolvedValue([{ taskId: 'TASK-1', title: 'Say hello to the pod', status: 'pending', assignee: null }]);
mockTaskCount.mockResolvedValue(3);
axios.post.mockResolvedValueOnce(toolCalls(call('c1', 'commonly_list_tasks', {})));

await runAgent(hostedInstall(), mention);

const Task = require('../../../models/Task');
expect(Task.find).toHaveBeenCalledWith({ podId: POD, status: { $in: ['pending', 'claimed', 'blocked'] } });
const listed = JSON.parse(toolMessages(1)[0].content);
expect(listed).toEqual({
status: 'open', total: 3, tasks: [{ taskId: 'TASK-1', title: 'Say hello to the pod', status: 'pending', assignee: null }],
});
expect(systemPromptIn(0)).toMatch(/check the board with commonly_list_tasks before saying what is or is not on it/);
// QA walk: a pod summary also searched the web and ran code (19 s).
expect(systemPromptIn(0)).toMatch(/Answer a question about this pod from the pod itself; reach for the tools below only when/);
// QA walk: an answer opening "@asker" filed every reply as "needs you".
expect(systemPromptIn(0)).toMatch(/Do not @-mention the person you are answering/);
expect(systemPromptIn(0)).toMatch(/@-mention someone only when you need them to do something/);
});

test('a kernel-framed mention reaches the model naming only tools the turn has', async () => {
const content = [
'[Pod context: this conversation is in pod `p`. When attaching files, call commonly_attach_file({ podId: "p" }). '
+ 'When reading files call commonly_read_file({ podId: "p", fileName }). Post with your token file.]',
'[Trigger: this turn was raised by **sam**. If it is not recent, call commonly_get_messages before you reply.]',
'[Collaboration: consult a coding specialist. Call commonly_dm_agent({ agentName: "codex" }).]',
'[Reply mechanics: post your reply by calling commonly_post_message({ podId: "p" }).]',
].join('\n');

await runAgent(hostedInstall(), { ...mention, payload: { ...mention.payload, content: `${content}\n\n@maya-agent what can you do?` } });

const sent = userMessageIn(0);
expect(sent).not.toMatch(/commonly_attach_file|commonly_read_file|commonly_dm_agent|commonly_get_messages|codex|token file/);
expect(sent).toMatch(/You cannot open or attach files here/);
expect(sent).toMatch(/call commonly_read_context before you reply/);
expect(sent).toMatch(/\n\n@maya-agent what can you do\?$/);
});
});

test('in a pod holding an outsider-writable grant, a hosted turn is narrowed like any native one', async () => {
const { podHasOutsiderWritableGrant } = require('../../../services/nativeOutsiderGrantPolicy');
podHasOutsiderWritableGrant.mockResolvedValue(true);
Expand Down
Loading
Loading