Skip to content
Closed
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/**
* The wake frame has a size, and nobody was paying for it (TASK-074).
*
* Every clause added to `formatPodContextFrame` and its companions is prepended
* to `chat.mention.payload.content` for EVERY mention to EVERY agent, forever.
* That is the correct place for a kernel affordance — the inline cue is the one
* thing a model will not deprioritize — and it is also the reason the cost is
* invisible: each clause is a few hundred characters in a diff nobody measures,
* and the total is paid per wake, fleet-wide.
*
* Measured while gating #1216/#1244: the pod-context frame alone went 1,639 →
* 2,269 (+38%) → ~2,697 (+65%) across one two-PR stack. No review comment
* mentioned the size, because there was no number to compare against.
*
* This is not a cap. It is a budget: raising it is a one-line change, and that
* one line is the entire point — it turns an invisible spend into a deliberate
* one that a reviewer can see and argue with. A PR that legitimately needs more
* room should raise the ceiling and say why in its body.
*
* Two-sided on purpose. A ceiling alone is satisfied by deleting the frame, and
* the copy assertions elsewhere in this suite pin individual sentences rather
* than the whole. The floor catches a frame that silently lost a section.
*/

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');

// The reference wake: a plain chat pod, one installed seat, an explicit
// @mention, no thread, no wake-on-message opt-in. Deliberately the SMALLEST
// real frame — a collaborative pod and a wake-on-message seat both add more, so
// a budget measured here is a floor on what the fleet actually pays.
const BUDGET_MAX = 3000;
const BUDGET_MIN = 2600;

const referenceWake = async () => {
AgentInstallation.find.mockReturnValue({
lean: jest.fn().mockResolvedValue([
{ agentName: 'seat-a', instanceId: 'default', displayName: 'Seat A' },
]),
});
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'] }),
}),
lean: jest.fn().mockResolvedValue({ _id: 'pod-1', type: 'chat', members: ['user-1', 'bot-1'] }),
});
AgentEvent.countDocuments.mockResolvedValue(0);

await AgentMentionService.enqueueMentions({
podId: 'pod-1',
userId: 'user-1',
username: 'alice',
message: { id: 'm-1', content: 'hi @seat-a' },
});
const call = AgentEventService.enqueue.mock.calls.find(([a]) => a.type === 'chat.mention');
return call[0].payload.content;
};

beforeEach(() => { jest.clearAllMocks(); });

describe('wake frame size budget', () => {
test('the reference wake stays inside its character budget', async () => {
const content = await referenceWake();

// If this fails you added a clause. That is allowed — raise BUDGET_MAX in
// the same diff and say in the PR body what the fleet is buying, so the
// trade is on the record instead of inside a paragraph.
expect(content.length).toBeLessThanOrEqual(BUDGET_MAX);
});

test('and has not silently lost a section', async () => {
const content = await referenceWake();

// The other half of a budget. A ceiling on its own is satisfied by an
// empty frame, and the copy assertions in this suite pin sentences one at
// a time — none of them notices a whole section going missing.
expect(content.length).toBeGreaterThanOrEqual(BUDGET_MIN);
});

test('the sections that make up the cost are all present', async () => {
// Named so a budget failure is diagnosable: the number alone says the
// frame grew, not where. These are the four bracketed blocks a reference
// wake carries.
const content = await referenceWake();

expect(content).toContain('[Pod context:');
expect(content).toContain('[Trigger:');
expect(content).toContain('[Collaboration:');
expect(content).toContain('[Reply mechanics:');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/**
* A human @handle routes nothing (TASK-070a / TASK-074, follow-up to #1244).
*
* #1244 added a paragraph to the pod-context frame telling every agent that a
* human's handle "is necessary and not sufficient — it flags the message in a
* mentions filter the human pulls; nothing pushes". That is an assertion about
* the kernel, made to a reader who cannot check it: an agent acts on the cue
* and has no view of `enqueueMentions`.
*
* #1244's three tests pin the SENTENCE (`toContain('nothing pushes')`). They go
* red if someone rewords the cue and stay green if someone makes it false. This
* file pins the other half: the behaviour the sentence describes.
*
* Two mechanisms, stated separately because they can break separately:
*
* 1. A handle that resolves to a human enqueues no `AgentEvent` of any type.
* `enqueueMentions` has no human delivery branch at all — the handle is
* filtered into `humanMentionHandles` and never reaches an enqueue. The
* realistic future edit is TASK-070b (should a bare name route?): an
* implementer who answers "yes, and push it" makes the cue a lie taught on
* every wake, fleet-wide, and nothing in the suite objects.
*
* 2. Even the thread-follow half is narrower than the cue's readers assume:
* `resolveHumanMentionUserIds` is called only inside `if (threadRootId)`,
* so a plain channel post materialises no state for the mentioned human.
* Hoisting that call out of the guard is the consistency fix that looks
* correct and quietly widens what a handle does.
*
* Every negative here is paired with a control, per the house rule: an
* assertion that nothing was enqueued is worthless from a harness that cannot
* enqueue anything.
*/

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');
const ThreadUserState = require('../../../models/pg/ThreadUserState');

const SEAT = { agentName: 'seat-a', instanceId: 'default', displayName: 'Seat A' };
const HUMAN = { _id: 'user-sam', username: 'sam' };

const mockInstallations = (installations) => {
AgentInstallation.find.mockReturnValue({ lean: jest.fn().mockResolvedValue(installations) });
AgentProfile.find.mockReturnValue({ lean: jest.fn().mockResolvedValue([]) });
};

// The human-handle resolver. MOCKED DELIBERATELY and load-bearing: `sam` is a
// real non-bot row inside the pod, so anything that hands this handle to a
// delivery path WILL find a user to deliver to. A harness where the lookup
// returns nothing would pass every negative below for the wrong reason.
const mockUserLookup = () => {
User.find.mockReturnValue({
select: jest.fn().mockReturnThis(),
lean: jest.fn().mockResolvedValue([HUMAN]),
});
User.findById.mockImplementation(() => ({
select: jest.fn().mockReturnThis(),
lean: jest.fn().mockResolvedValue({ _id: 'user-1', isBot: false }),
}));
};

const enqueued = () => AgentEventService.enqueue.mock.calls.map(([a]) => a);
const mentions = () => enqueued().filter((a) => a.type === 'chat.mention');

const send = (message, extra = {}) => AgentMentionService.enqueueMentions({
podId: 'pod-1', userId: 'user-1', username: 'alice', message, ...extra,
});

beforeEach(() => {
jest.clearAllMocks();
mockUserLookup();
Pod.findById.mockReturnValue({
select: jest.fn().mockReturnValue({
lean: jest.fn().mockResolvedValue({ type: 'chat', members: ['user-1', 'user-sam', 'bot-1'] }),
}),
lean: jest.fn().mockResolvedValue({
_id: 'pod-1', type: 'chat', members: ['user-1', 'user-sam', 'bot-1'],
}),
});
AgentEvent.countDocuments.mockResolvedValue(0);
mockInstallations([SEAT]);
});

describe('a human handle is not a delivery target', () => {
test('@handle for a human enqueues no event of any kind', async () => {
await send({ id: 'm-1', content: 'can you decide this @sam' });

// Not "no chat.mention" — no event at all. A future human-push branch is
// as likely to invent a type as to reuse this one.
expect(enqueued()).toHaveLength(0);
});

test('CONTROL: the same sentence addressed to an installed agent DOES enqueue', async () => {
await send({ id: 'm-2', content: 'can you decide this @seat-a' });

const got = mentions();
expect(got).toHaveLength(1);
expect(got[0]).toMatchObject({ agentName: 'seat-a', type: 'chat.mention' });
});

test('one message naming both routes to the agent only', async () => {
// The discriminating case. A human-push branch added beside the agent one
// leaves every single-target fixture above intact — it only shows up when
// both appear at once and the count stops being 1.
await send({ id: 'm-3', content: '@seat-a please answer, @sam to press' });

expect(enqueued()).toHaveLength(1);
expect(enqueued()[0]).toMatchObject({ agentName: 'seat-a' });
});
});

describe('the thread-follow half is guarded by threadRootId', () => {
test('a plain channel post materialises no thread state for the mentioned human', async () => {
await send({ id: 'm-4', content: 'over to you @sam' });

expect(ThreadUserState.followByParticipation).not.toHaveBeenCalled();
// And it declined to make the lookup at all, rather than making it and
// finding nobody — the guard is on the call, not on the result.
expect(User.find).not.toHaveBeenCalled();
});

test('CONTROL: the same message inside a thread DOES follow that human', async () => {
await send({
id: 'm-5', content: 'over to you @sam', thread_root_id: 101, threadRootId: 101,
});

expect(ThreadUserState.followByParticipation).toHaveBeenCalledWith(101, 'user-sam', 'pod-1');
});

test('a follow is not a wake — the threaded case still enqueues no event', async () => {
// Both halves of the cue's ceiling in one assertion: the handle bought a
// pull-surface row, and nothing was pushed.
await send({
id: 'm-6', content: 'over to you @sam', thread_root_id: 101, threadRootId: 101,
});

expect(ThreadUserState.followByParticipation).toHaveBeenCalled();
expect(enqueued()).toHaveLength(0);
});
});
Loading