fix(team): satisfy bootstrap redaction lint

This commit is contained in:
777genius 2026-05-06 18:39:17 +03:00
parent 2a41010610
commit 2080e86f44
12 changed files with 218 additions and 32 deletions

View file

@ -90,8 +90,8 @@ import {
} from '@shared/constants'; } from '@shared/constants';
import { shouldSuppressDesktopNotificationForInboxText } from '@shared/utils/idleNotificationSemantics'; import { shouldSuppressDesktopNotificationForInboxText } from '@shared/utils/idleNotificationSemantics';
import { parseInboxJson } from '@shared/utils/inboxNoise'; import { parseInboxJson } from '@shared/utils/inboxNoise';
import { isTeamInternalControlMessageText } from '@shared/utils/teamInternalControlMessages';
import { createLogger } from '@shared/utils/logger'; import { createLogger } from '@shared/utils/logger';
import { isTeamInternalControlMessageEnvelope } from '@shared/utils/teamInternalControlMessages';
import { app, BrowserWindow, ipcMain } from 'electron'; import { app, BrowserWindow, ipcMain } from 'electron';
import { existsSync } from 'fs'; import { existsSync } from 'fs';
import { join } from 'path'; import { join } from 'path';
@ -473,7 +473,7 @@ async function notifyNewInboxMessages(teamName: string, detail: string): Promise
if (msg.source && suppressedSources.has(msg.source)) continue; if (msg.source && suppressedSources.has(msg.source)) continue;
// Skip app-owned private bootstrap/control prompts. They are durable runtime proof inputs, // Skip app-owned private bootstrap/control prompts. They are durable runtime proof inputs,
// not user-visible conversation messages. // not user-visible conversation messages.
if (isTeamInternalControlMessageText(msg.text)) continue; if (isTeamInternalControlMessageEnvelope(msg)) continue;
// Skip internal coordination noise (idle_notification, shutdown_*, etc.) // Skip internal coordination noise (idle_notification, shutdown_*, etc.)
if (shouldSuppressDesktopNotificationForInboxText(msg.text)) continue; if (shouldSuppressDesktopNotificationForInboxText(msg.text)) continue;

View file

@ -1,7 +1,7 @@
import { classifyIdleNotificationText } from '@shared/utils/idleNotificationSemantics'; import { classifyIdleNotificationText } from '@shared/utils/idleNotificationSemantics';
import { createLogger } from '@shared/utils/logger'; import { createLogger } from '@shared/utils/logger';
import { buildStandaloneSlashCommandMeta } from '@shared/utils/slashCommands'; import { buildStandaloneSlashCommandMeta } from '@shared/utils/slashCommands';
import { isTeamInternalControlMessageText } from '@shared/utils/teamInternalControlMessages'; import { isTeamInternalControlMessageEnvelope } from '@shared/utils/teamInternalControlMessages';
import { createHash } from 'crypto'; import { createHash } from 'crypto';
import { getEffectiveInboxMessageId } from './inboxMessageIdentity'; import { getEffectiveInboxMessageId } from './inboxMessageIdentity';
@ -140,7 +140,7 @@ function buildSyntheticOpenCodeBootstrapMessages(config: TeamConfig): InboxMessa
} }
function isVisibleTeamMessage(message: InboxMessage): boolean { function isVisibleTeamMessage(message: InboxMessage): boolean {
return !isTeamInternalControlMessageText(message.text); return !isTeamInternalControlMessageEnvelope(message);
} }
function annotateSlashCommandResponses(messages: InboxMessage[]): void { function annotateSlashCommandResponses(messages: InboxMessage[]): void {

View file

@ -84,11 +84,14 @@ import { createLogger } from '@shared/utils/logger';
import { migrateProviderBackendId } from '@shared/utils/providerBackend'; import { migrateProviderBackendId } from '@shared/utils/providerBackend';
import { isDefaultProviderModelSelection } from '@shared/utils/providerModelSelection'; import { isDefaultProviderModelSelection } from '@shared/utils/providerModelSelection';
import { formatTaskDisplayLabel } from '@shared/utils/taskIdentity'; import { formatTaskDisplayLabel } from '@shared/utils/taskIdentity';
import {
isTeamInternalControlMessageText,
stripExactInternalControlEchoPrefix,
} from '@shared/utils/teamInternalControlMessages';
import { import {
parseAllTeammateMessages, parseAllTeammateMessages,
type ParsedTeammateContent, type ParsedTeammateContent,
} from '@shared/utils/teammateMessageParser'; } from '@shared/utils/teammateMessageParser';
import { isTeamInternalControlMessageText } from '@shared/utils/teamInternalControlMessages';
import { buildTeamMemberColorMap } from '@shared/utils/teamMemberColors'; import { buildTeamMemberColorMap } from '@shared/utils/teamMemberColors';
import { createCliAutoSuffixNameGuard, parseNumericSuffixName } from '@shared/utils/teamMemberName'; import { createCliAutoSuffixNameGuard, parseNumericSuffixName } from '@shared/utils/teamMemberName';
import { import {
@ -143,6 +146,14 @@ import {
type TeamRuntimeSettingsJson, type TeamRuntimeSettingsJson,
} from '../runtime/teamRuntimeSettingsBundle'; } from '../runtime/teamRuntimeSettingsBundle';
import {
parseBootstrapRuntimeProofDetail,
validateBootstrapRuntimeProofEnvelope,
} from './bootstrap/BootstrapProofValidation';
import {
buildNativeAppManagedBootstrapSpecs,
type NativeAppManagedBootstrapSpec,
} from './bootstrap/NativeAppManagedBootstrapContextBuilder';
import { import {
createOpenCodePromptDeliveryLedgerStore, createOpenCodePromptDeliveryLedgerStore,
hashOpenCodePromptDeliveryPayload, hashOpenCodePromptDeliveryPayload,
@ -256,14 +267,6 @@ import {
import { TeamSentMessagesStore } from './TeamSentMessagesStore'; import { TeamSentMessagesStore } from './TeamSentMessagesStore';
import { TeamTaskReader } from './TeamTaskReader'; import { TeamTaskReader } from './TeamTaskReader';
import { TeamTranscriptProjectResolver } from './TeamTranscriptProjectResolver'; import { TeamTranscriptProjectResolver } from './TeamTranscriptProjectResolver';
import {
buildNativeAppManagedBootstrapSpecs,
type NativeAppManagedBootstrapSpec,
} from './bootstrap/NativeAppManagedBootstrapContextBuilder';
import {
parseBootstrapRuntimeProofDetail,
validateBootstrapRuntimeProofEnvelope,
} from './bootstrap/BootstrapProofValidation';
import type { import type {
OpenCodeCommittedBootstrapSessionRecord, OpenCodeCommittedBootstrapSessionRecord,
@ -19399,7 +19402,12 @@ export class TeamProvisioningService {
// Strip agent-only blocks — lead may respond with pure coordination content // Strip agent-only blocks — lead may respond with pure coordination content
// that is not meant for the human user. // that is not meant for the human user.
const cleanReply = replyText ? stripAgentBlocks(replyText) : null; const cleanReply = replyText
? stripExactInternalControlEchoPrefix(
stripAgentBlocks(replyText),
stripAgentBlocks(message)
)
: null;
if (cleanReply) { if (cleanReply) {
if (isTeamInternalControlMessageText(cleanReply)) { if (isTeamInternalControlMessageText(cleanReply)) {
logger.debug(`[${teamName}] Suppressed internal lead relay echo`); logger.debug(`[${teamName}] Suppressed internal lead relay echo`);

View file

@ -1,9 +1,9 @@
import { getClaudeBasePath } from '@main/utils/pathDecoder';
import { normalizeOptionalTeamProviderId } from '@shared/utils/teamProvider';
import * as agentTeamsControllerModule from 'agent-teams-controller'; import * as agentTeamsControllerModule from 'agent-teams-controller';
import { createHash } from 'crypto'; import { createHash } from 'crypto';
import { getClaudeBasePath } from '@main/utils/pathDecoder';
import type { TeamCreateRequest, TeamProviderId } from '@shared/types'; import type { TeamCreateRequest, TeamProviderId } from '@shared/types';
import { normalizeOptionalTeamProviderId } from '@shared/utils/teamProvider';
const { createController } = agentTeamsControllerModule; const { createController } = agentTeamsControllerModule;
@ -41,7 +41,7 @@ function redactNativeBootstrapContextText(input: string): string {
.replace(/sk-ant-[A-Za-z0-9_-]+/g, '[REDACTED_ANTHROPIC_API_KEY]') .replace(/sk-ant-[A-Za-z0-9_-]+/g, '[REDACTED_ANTHROPIC_API_KEY]')
.replace(/sk-[A-Za-z0-9_-]{20,}/g, '[REDACTED_API_KEY]') .replace(/sk-[A-Za-z0-9_-]{20,}/g, '[REDACTED_API_KEY]')
.replace(/(ANTHROPIC_API_KEY|OPENAI_API_KEY|CODEX_API_KEY)=\S+/g, '$1=[REDACTED]') .replace(/(ANTHROPIC_API_KEY|OPENAI_API_KEY|CODEX_API_KEY)=\S+/g, '$1=[REDACTED]')
.replace(/Bearer\s+[A-Za-z0-9._-]+/gi, 'Bearer [REDACTED]'); .replace(/Bearer\s+[A-Z0-9._-]+/gi, 'Bearer [REDACTED]');
} }
function boundText(input: string, maxChars: number): string { function boundText(input: string, maxChars: number): string {

View file

@ -6,7 +6,7 @@ import {
} from '@renderer/utils/teamModelCatalog'; } from '@renderer/utils/teamModelCatalog';
import { import {
isNativeAppManagedBootstrapCheckText, isNativeAppManagedBootstrapCheckText,
isTeamInternalControlMessageText, isTeamInternalControlMessageEnvelope,
} from '@shared/utils/teamInternalControlMessages'; } from '@shared/utils/teamInternalControlMessages';
import type { InboxMessage, TeamProviderId } from '@shared/types'; import type { InboxMessage, TeamProviderId } from '@shared/types';
@ -135,17 +135,17 @@ export interface InternalControlMessageDisplay {
} }
export function getInternalControlMessageDisplay( export function getInternalControlMessageDisplay(
message: Pick<InboxMessage, 'text'> message: Pick<InboxMessage, 'text'> & Partial<Pick<InboxMessage, 'source'>>
): InternalControlMessageDisplay | null { ): InternalControlMessageDisplay | null {
if (!isTeamInternalControlMessageEnvelope(message)) {
return null;
}
if (isNativeAppManagedBootstrapCheckText(message.text)) { if (isNativeAppManagedBootstrapCheckText(message.text)) {
return { return {
summary: 'Internal bootstrap check', summary: 'Internal bootstrap check',
body: 'Internal bootstrap check hidden in the UI.', body: 'Internal bootstrap check hidden in the UI.',
}; };
} }
if (!isTeamInternalControlMessageText(message.text)) {
return null;
}
return { return {
summary: 'Internal control message', summary: 'Internal control message',
body: 'Internal control message hidden in the UI.', body: 'Internal control message hidden in the UI.',
@ -236,7 +236,9 @@ export function getBootstrapAcknowledgementDisplay(
}; };
} }
export function getSanitizedInboxMessageText(message: Pick<InboxMessage, 'text' | 'to'>): string { export function getSanitizedInboxMessageText(
message: Pick<InboxMessage, 'text' | 'to'> & Partial<Pick<InboxMessage, 'source'>>
): string {
return ( return (
getInternalControlMessageDisplay(message)?.body ?? getInternalControlMessageDisplay(message)?.body ??
getBootstrapPromptDisplay(message)?.body ?? getBootstrapPromptDisplay(message)?.body ??
@ -247,7 +249,8 @@ export function getSanitizedInboxMessageText(message: Pick<InboxMessage, 'text'
} }
export function getSanitizedInboxMessageSummary( export function getSanitizedInboxMessageSummary(
message: Pick<InboxMessage, 'text' | 'to' | 'from' | 'summary'> message: Pick<InboxMessage, 'text' | 'to' | 'from' | 'summary'> &
Partial<Pick<InboxMessage, 'source'>>
): string { ): string {
return ( return (
getInternalControlMessageDisplay(message)?.summary ?? getInternalControlMessageDisplay(message)?.summary ??

View file

@ -4,7 +4,7 @@ import {
} from '@renderer/utils/bootstrapPromptSanitizer'; } from '@renderer/utils/bootstrapPromptSanitizer';
import { shouldKeepIdleMessageInActivityWhenNoiseHidden } from '@renderer/utils/idleNotificationSemantics'; import { shouldKeepIdleMessageInActivityWhenNoiseHidden } from '@renderer/utils/idleNotificationSemantics';
import { isInboxNoiseMessage } from '@shared/utils/inboxNoise'; import { isInboxNoiseMessage } from '@shared/utils/inboxNoise';
import { isTeamInternalControlMessageText } from '@shared/utils/teamInternalControlMessages'; import { isTeamInternalControlMessageEnvelope } from '@shared/utils/teamInternalControlMessages';
import type { InboxMessage } from '@shared/types'; import type { InboxMessage } from '@shared/types';
@ -127,8 +127,7 @@ export function filterTeamMessages(
const leadNames = normalizeLeadNames(rawLeadNames); const leadNames = normalizeLeadNames(rawLeadNames);
let list = messages.filter( let list = messages.filter(
(m) => (m) => m.messageKind !== 'task_comment_notification' && !isTeamInternalControlMessageEnvelope(m)
m.messageKind !== 'task_comment_notification' && !isTeamInternalControlMessageText(m.text)
); );
if (timeWindow) { if (timeWindow) {
list = list.filter((m) => { list = list.filter((m) => {

View file

@ -2,7 +2,14 @@ const NATIVE_APP_MANAGED_BOOTSTRAP_CHECK_OPEN = '<agent_teams_native_app_managed
const LEAD_INBOX_RELAY_PROMPT_OPEN = 'You have new inbox messages addressed to you (team lead '; const LEAD_INBOX_RELAY_PROMPT_OPEN = 'You have new inbox messages addressed to you (team lead ';
const TEAMMATE_MESSAGE_OPEN_RE = /^<teammate-message\s/i; const TEAMMATE_MESSAGE_OPEN_RE = /^<teammate-message\s/i;
function stripTranscriptSpeakerPrefix(value: string): string { const INTERNAL_CONTROL_MESSAGE_SOURCES = new Set([
'lead_process',
'lead_session',
'runtime_delivery',
'system_notification',
]);
export function stripTranscriptSpeakerPrefix(value: string): string {
let normalized = value.trim(); let normalized = value.trim();
for (let i = 0; i < 3; i += 1) { for (let i = 0; i < 3; i += 1) {
const next = normalized.replace(/^(?:Human|User):\s*/i, '').trimStart(); const next = normalized.replace(/^(?:Human|User):\s*/i, '').trimStart();
@ -45,3 +52,25 @@ export function isTeamInternalControlMessageText(value: unknown): boolean {
isTeammateProtocolControlText(value) isTeammateProtocolControlText(value)
); );
} }
export function isTeamInternalControlMessageEnvelope(message: {
text?: unknown;
source?: unknown;
}): boolean {
if (!isTeamInternalControlMessageText(message.text)) {
return false;
}
return typeof message.source === 'string' && INTERNAL_CONTROL_MESSAGE_SOURCES.has(message.source);
}
export function stripExactInternalControlEchoPrefix(
value: string,
expectedControlText: string
): string {
const text = stripTranscriptSpeakerPrefix(value);
const expected = stripTranscriptSpeakerPrefix(expectedControlText);
if (!expected || !text.startsWith(expected)) {
return value.trim();
}
return text.slice(expected.length).trim();
}

View file

@ -97,6 +97,32 @@ describe('TeamMessageFeedService', () => {
expect(feed.messages.map((message) => message.messageId)).toEqual(['visible-user-message']); expect(feed.messages.map((message) => message.messageId)).toEqual(['visible-user-message']);
}); });
it('does not hide user-authored text just because it resembles an internal prompt', async () => {
const service = new TeamMessageFeedService({
getConfig: vi.fn(async () => config),
getInboxMessages: vi.fn(async () => [
makeMessage({
messageId: 'quoted-control-prompt',
source: 'user_sent',
text: `Human: You have new inbox messages addressed to you (team lead "team-lead").
Process them in order (oldest first).
Messages:
1) From: tom
Timestamp: 2026-05-06T15:02:54.853Z
Text:
#f8d7235a done.`,
}),
]),
getLeadSessionMessages: vi.fn(async () => []),
getSentMessages: vi.fn(async () => []),
});
const feed = await service.getFeed('signal-ops-4');
expect(feed.messages.map((message) => message.messageId)).toEqual(['quoted-control-prompt']);
});
it('refreshes the durable feed after cache expiry even when the dirty signal was missed', async () => { it('refreshes the durable feed after cache expiry even when the dirty signal was missed', async () => {
let inboxMessages: InboxMessage[] = [makeMessage()]; let inboxMessages: InboxMessage[] = [makeMessage()];
const getInboxMessages = vi.fn(async () => inboxMessages); const getInboxMessages = vi.fn(async () => inboxMessages);

View file

@ -342,6 +342,47 @@ describe('TeamProvisioningService relayLeadInboxMessages', () => {
expect(hoisted.files.get(`/mock/teams/${teamName}/sentMessages.json`)).toBeUndefined(); expect(hoisted.files.get(`/mock/teams/${teamName}/sentMessages.json`)).toBeUndefined();
}); });
it('preserves visible summary text after stripping an echoed lead relay prompt', async () => {
const service = new TeamProvisioningService();
const teamName = 'my-team';
seedConfig(teamName);
seedLeadInbox(teamName, [
{
from: 'tom',
text: '#f8d7235a done.',
timestamp: '2026-02-23T10:00:00.000Z',
read: false,
summary: '#f8d7235a done',
messageId: 'm-1',
},
]);
const { writeSpy } = attachAliveRun(service, teamName);
const relayPromise = service.relayLeadInboxMessages(teamName);
const run = await waitForCapture(service);
const payload = JSON.parse(String(writeSpy.mock.calls[0]?.[0] ?? '{}')) as {
message?: { content?: Array<{ text?: string }> };
};
const relayedPrompt = payload.message?.content?.[0]?.text ?? '';
(service as any).handleStreamJsonMessage(run, {
type: 'assistant',
content: [{ type: 'text', text: `Human: ${relayedPrompt}\n\nDelegated to bob.` }],
});
(service as any).handleStreamJsonMessage(run, { type: 'result', subtype: 'success' });
await expect(relayPromise).resolves.toBe(1);
expect(service.getLiveLeadProcessMessages(teamName).map((message) => message.text)).toEqual([
'Delegated to bob.',
]);
const sentRows = JSON.parse(
hoisted.files.get(`/mock/teams/${teamName}/sentMessages.json`) ?? '[]'
) as Array<{
text?: string;
}>;
expect(sentRows.map((message) => message.text)).toEqual(['Delegated to bob.']);
});
it('treats member work sync nudges as actionable in lead relay prompt', async () => { it('treats member work sync nudges as actionable in lead relay prompt', async () => {
const service = new TeamProvisioningService(); const service = new TeamProvisioningService();
const teamName = 'my-team'; const teamName = 'my-team';

View file

@ -67,16 +67,20 @@ Do NOT send acknowledgement-only messages such as "ready" or "online".`);
}); });
it('sanitizes native app-managed bootstrap private control prompts defensively', () => { it('sanitizes native app-managed bootstrap private control prompts defensively', () => {
const message = makeMessage(`<agent_teams_native_app_managed_bootstrap_check> const message = makeMessage(
`<agent_teams_native_app_managed_bootstrap_check>
Your Agent Teams startup context was already loaded by the app. Your Agent Teams startup context was already loaded by the app.
</agent_teams_native_app_managed_bootstrap_check>`); </agent_teams_native_app_managed_bootstrap_check>`,
{ source: 'system_notification' }
);
expect(getInternalControlMessageDisplay(message)?.summary).toBe('Internal bootstrap check'); expect(getInternalControlMessageDisplay(message)?.summary).toBe('Internal bootstrap check');
expect(getSanitizedInboxMessageText(message)).toBe('Internal bootstrap check hidden in the UI.'); expect(getSanitizedInboxMessageText(message)).toBe('Internal bootstrap check hidden in the UI.');
}); });
it('sanitizes leaked lead inbox relay prompts defensively', () => { it('sanitizes leaked lead inbox relay prompts defensively', () => {
const message = makeMessage(`Human: You have new inbox messages addressed to you (team lead "team-lead"). const message = makeMessage(
`Human: You have new inbox messages addressed to you (team lead "team-lead").
Process them in order (oldest first). Process them in order (oldest first).
If action is required, delegate via task creation or SendMessage, and keep responses minimal. If action is required, delegate via task creation or SendMessage, and keep responses minimal.
@ -84,9 +88,26 @@ Messages:
1) From: tom 1) From: tom
Timestamp: 2026-05-06T15:02:54.853Z Timestamp: 2026-05-06T15:02:54.853Z
Text: Text:
#f8d7235a done.`); #f8d7235a done.`,
{ source: 'lead_process' }
);
expect(getInternalControlMessageDisplay(message)?.summary).toBe('Internal control message'); expect(getInternalControlMessageDisplay(message)?.summary).toBe('Internal control message');
expect(getSanitizedInboxMessageText(message)).toBe('Internal control message hidden in the UI.'); expect(getSanitizedInboxMessageText(message)).toBe('Internal control message hidden in the UI.');
}); });
it('does not sanitize user-authored text that quotes an internal prompt', () => {
const text = `Human: You have new inbox messages addressed to you (team lead "team-lead").
Process them in order (oldest first).
Messages:
1) From: tom
Timestamp: 2026-05-06T15:02:54.853Z
Text:
#f8d7235a done.`;
const message = makeMessage(text, { source: 'user_sent' });
expect(getInternalControlMessageDisplay(message)).toBeNull();
expect(getSanitizedInboxMessageText(message)).toBe(text);
});
}); });

View file

@ -90,6 +90,31 @@ Messages:
expect(result.map((message) => message.messageId)).toEqual(['visible-message']); expect(result.map((message) => message.messageId)).toEqual(['visible-message']);
}); });
it('does not hide user-authored text that quotes an internal prompt', () => {
const messages = [
makeMessage({
messageId: 'quoted-control-prompt',
source: 'user_sent',
text: `Human: You have new inbox messages addressed to you (team lead "team-lead").
Process them in order (oldest first).
Messages:
1) From: tom
Timestamp: 2026-05-06T15:02:54.853Z
Text:
#f8d7235a done.`,
}),
];
const result = filterTeamMessages(messages, {
timeWindow: null,
filter: { from: new Set(), to: new Set(), showNoise: true },
searchQuery: '',
});
expect(result.map((message) => message.messageId)).toEqual(['quoted-control-prompt']);
});
it('hides Human-prefixed teammate protocol echoes', () => { it('hides Human-prefixed teammate protocol echoes', () => {
const messages = [ const messages = [
makeMessage({ makeMessage({

View file

@ -1,9 +1,11 @@
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { import {
isTeamInternalControlMessageEnvelope,
isLeadInboxRelayControlPromptText, isLeadInboxRelayControlPromptText,
isTeamInternalControlMessageText, isTeamInternalControlMessageText,
isTeammateProtocolControlText, isTeammateProtocolControlText,
stripExactInternalControlEchoPrefix,
} from '@shared/utils/teamInternalControlMessages'; } from '@shared/utils/teamInternalControlMessages';
const leadRelayPrompt = `You have new inbox messages addressed to you (team lead "team-lead"). const leadRelayPrompt = `You have new inbox messages addressed to you (team lead "team-lead").
@ -39,4 +41,36 @@ describe('teamInternalControlMessages', () => {
expect(isTeammateProtocolControlText(text)).toBe(true); expect(isTeammateProtocolControlText(text)).toBe(true);
expect(isTeamInternalControlMessageText(text)).toBe(true); expect(isTeamInternalControlMessageText(text)).toBe(true);
}); });
it('only treats internal-looking text as hidden for internal message sources', () => {
expect(
isTeamInternalControlMessageEnvelope({
source: 'lead_process',
text: `Human: ${leadRelayPrompt}`,
})
).toBe(true);
expect(
isTeamInternalControlMessageEnvelope({
source: 'user_sent',
text: `Human: ${leadRelayPrompt}`,
})
).toBe(false);
expect(
isTeamInternalControlMessageEnvelope({
text: `Human: ${leadRelayPrompt}`,
})
).toBe(false);
});
it('strips an exact echoed control prefix while preserving visible trailing text', () => {
expect(stripExactInternalControlEchoPrefix(`Human: ${leadRelayPrompt}`, leadRelayPrompt)).toBe(
''
);
expect(
stripExactInternalControlEchoPrefix(
`Human: ${leadRelayPrompt}\n\nDelegated to bob.`,
leadRelayPrompt
)
).toBe('Delegated to bob.');
});
}); });