fix(team): respond to teammate permission requests
This commit is contained in:
parent
e185f1f686
commit
b1cccf6e3c
2 changed files with 302 additions and 19 deletions
|
|
@ -31483,7 +31483,9 @@ export class TeamProvisioningService {
|
||||||
perm.requestId,
|
perm.requestId,
|
||||||
true,
|
true,
|
||||||
undefined,
|
undefined,
|
||||||
perm.permissionSuggestions
|
perm.permissionSuggestions,
|
||||||
|
perm.toolName,
|
||||||
|
perm.input
|
||||||
);
|
);
|
||||||
this.emitToolApprovalEvent({
|
this.emitToolApprovalEvent({
|
||||||
autoResolved: true,
|
autoResolved: true,
|
||||||
|
|
@ -31703,8 +31705,10 @@ export class TeamProvisioningService {
|
||||||
approval.source,
|
approval.source,
|
||||||
requestId,
|
requestId,
|
||||||
allow,
|
allow,
|
||||||
allow ? undefined : 'Timed out — auto-denied by settings',
|
allow ? undefined : 'Timed out - auto-denied by settings',
|
||||||
approval.permissionSuggestions
|
approval.permissionSuggestions,
|
||||||
|
approval.toolName,
|
||||||
|
approval.toolInput
|
||||||
).finally(() => {
|
).finally(() => {
|
||||||
run.pendingApprovals.delete(requestId);
|
run.pendingApprovals.delete(requestId);
|
||||||
this.inFlightResponses.delete(requestId);
|
this.inFlightResponses.delete(requestId);
|
||||||
|
|
@ -31789,7 +31793,9 @@ export class TeamProvisioningService {
|
||||||
requestId,
|
requestId,
|
||||||
true,
|
true,
|
||||||
undefined,
|
undefined,
|
||||||
approval.permissionSuggestions
|
approval.permissionSuggestions,
|
||||||
|
approval.toolName,
|
||||||
|
approval.toolInput
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
this.autoAllowControlRequest(run, requestId);
|
this.autoAllowControlRequest(run, requestId);
|
||||||
|
|
@ -31865,7 +31871,9 @@ export class TeamProvisioningService {
|
||||||
requestId,
|
requestId,
|
||||||
allow,
|
allow,
|
||||||
message,
|
message,
|
||||||
approval.permissionSuggestions
|
approval.permissionSuggestions,
|
||||||
|
approval.toolName,
|
||||||
|
approval.toolInput
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
run.pendingApprovals.delete(requestId);
|
run.pendingApprovals.delete(requestId);
|
||||||
|
|
@ -31951,32 +31959,52 @@ export class TeamProvisioningService {
|
||||||
/**
|
/**
|
||||||
* Respond to a teammate's permission_request by applying permission_suggestions.
|
* Respond to a teammate's permission_request by applying permission_suggestions.
|
||||||
*
|
*
|
||||||
* FACT: Claude Code teammate runtime sends permission_request via SendMessage (inbox protocol).
|
* FACT: Claude Code teammate runtime sends permission_request via the inbox protocol.
|
||||||
* FACT: Writing permission_response to teammate inbox does NOT work - runtime ignores it.
|
* FACT: Teammates wait for permission_response in their own inbox.
|
||||||
* FACT: control_response via stdin does NOT work for teammate requests - request_id doesn't match.
|
* FACT: control_response via the lead stdin does not reliably reach teammate request ids.
|
||||||
* FACT: permission_suggestions.destination "localSettings" refers to {cwd}/.claude/settings.local.json.
|
* FACT: permission_suggestions.destination "localSettings" refers to {cwd}/.claude/settings.local.json.
|
||||||
* FACT: Claude Code CLI reads this file via --setting-sources user,project,local.
|
* FACT: Claude Code CLI reads this file via --setting-sources user,project,local.
|
||||||
*
|
*
|
||||||
* When allow=true: applies permission_suggestions (adds tool rules to project settings).
|
* When allow=true: applies permission_suggestions, then replies to the teammate.
|
||||||
* When allow=false: no action needed - tool stays blocked by default.
|
* When allow=false: replies with an error so the teammate does not hang.
|
||||||
*/
|
*/
|
||||||
private async respondToTeammatePermission(
|
private async respondToTeammatePermission(
|
||||||
run: ProvisioningRun,
|
run: ProvisioningRun,
|
||||||
agentId: string,
|
agentId: string,
|
||||||
requestId: string,
|
requestId: string,
|
||||||
allow: boolean,
|
allow: boolean,
|
||||||
_message?: string,
|
message?: string,
|
||||||
permissionSuggestions?: import('@shared/utils/inboxNoise').PermissionSuggestion[]
|
permissionSuggestions?: import('@shared/utils/inboxNoise').PermissionSuggestion[],
|
||||||
|
toolName?: string,
|
||||||
|
toolInput?: Record<string, unknown>
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!allow) {
|
if (!allow) {
|
||||||
logger.info(`[${run.teamName}] Denied teammate ${agentId} permission ${requestId}`);
|
logger.info(`[${run.teamName}] Denied teammate ${agentId} permission ${requestId}`);
|
||||||
|
this.sendTeammatePermissionResponse(run, agentId, requestId, {
|
||||||
|
allow: false,
|
||||||
|
message,
|
||||||
|
toolName,
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply permission_suggestions: add tool rules to project settings file
|
|
||||||
const suggestions = permissionSuggestions ?? [];
|
const suggestions = permissionSuggestions ?? [];
|
||||||
|
const sendSuccessResponse = (): void => {
|
||||||
|
this.sendTeammatePermissionResponse(run, agentId, requestId, {
|
||||||
|
allow: true,
|
||||||
|
message,
|
||||||
|
permissionUpdates: suggestions,
|
||||||
|
toolName,
|
||||||
|
toolInput,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Apply permission_suggestions: add tool rules to project settings file
|
||||||
if (suggestions.length === 0) {
|
if (suggestions.length === 0) {
|
||||||
logger.warn(`[${run.teamName}] No permission_suggestions for ${requestId} — cannot add rule`);
|
sendSuccessResponse();
|
||||||
|
logger.info(
|
||||||
|
`[${run.teamName}] No permission_suggestions for ${requestId}; inbox response sent`
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -31989,7 +32017,10 @@ export class TeamProvisioningService {
|
||||||
// best-effort
|
// best-effort
|
||||||
}
|
}
|
||||||
if (!projectCwd) {
|
if (!projectCwd) {
|
||||||
logger.warn(`[${run.teamName}] Cannot resolve project cwd for permission rule — skipping`);
|
logger.warn(
|
||||||
|
`[${run.teamName}] Cannot resolve project cwd for permission rule; sending inbox response only`
|
||||||
|
);
|
||||||
|
sendSuccessResponse();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -32004,7 +32035,7 @@ export class TeamProvisioningService {
|
||||||
if (mode === 'acceptEdits') {
|
if (mode === 'acceptEdits') {
|
||||||
toolNames = ['Edit', 'Write', 'NotebookEdit'];
|
toolNames = ['Edit', 'Write', 'NotebookEdit'];
|
||||||
} else if (mode === 'bypassPermissions') {
|
} else if (mode === 'bypassPermissions') {
|
||||||
// Broad approval — add common tools
|
// Broad approval - add common tools
|
||||||
toolNames = ['Edit', 'Write', 'NotebookEdit', 'Bash', 'Read', 'Grep', 'Glob'];
|
toolNames = ['Edit', 'Write', 'NotebookEdit', 'Bash', 'Read', 'Grep', 'Glob'];
|
||||||
}
|
}
|
||||||
if (toolNames.length > 0) {
|
if (toolNames.length > 0) {
|
||||||
|
|
@ -32057,7 +32088,7 @@ export class TeamProvisioningService {
|
||||||
try {
|
try {
|
||||||
await this.addPermissionRulesToSettings(settingsPath, toolNames, behavior);
|
await this.addPermissionRulesToSettings(settingsPath, toolNames, behavior);
|
||||||
logger.info(
|
logger.info(
|
||||||
`[${run.teamName}] Added permission rules for ${agentId}: ${toolNames.join(', ')} → ${behavior} in ${settingsPath}`
|
`[${run.teamName}] Added permission rules for ${agentId}: ${toolNames.join(', ')} -> ${behavior} in ${settingsPath}`
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(
|
logger.error(
|
||||||
|
|
@ -32068,17 +32099,21 @@ export class TeamProvisioningService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also attempt control_response via stdin — the lead runtime MAY forward it
|
sendSuccessResponse();
|
||||||
|
|
||||||
|
// Also attempt control_response via stdin - the lead runtime MAY forward it
|
||||||
// to the teammate subprocess. This was broken before (missing updatedInput: {})
|
// to the teammate subprocess. This was broken before (missing updatedInput: {})
|
||||||
// but is now fixed. Belt-and-suspenders: settings handle future calls,
|
// but is now fixed. Belt-and-suspenders: settings handle future calls,
|
||||||
// control_response may unblock the CURRENT waiting prompt.
|
// control_response may unblock the CURRENT waiting prompt.
|
||||||
if (allow && run.child?.stdin?.writable) {
|
if (allow && run.child?.stdin?.writable) {
|
||||||
|
const updatedInput =
|
||||||
|
this.buildTeammatePermissionUpdatedInput(toolName, toolInput, message) ?? {};
|
||||||
const controlResponse = {
|
const controlResponse = {
|
||||||
type: 'control_response',
|
type: 'control_response',
|
||||||
response: {
|
response: {
|
||||||
subtype: 'success',
|
subtype: 'success',
|
||||||
request_id: requestId,
|
request_id: requestId,
|
||||||
response: { behavior: 'allow', updatedInput: {} },
|
response: { behavior: 'allow', updatedInput },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
run.child.stdin.write(JSON.stringify(controlResponse) + '\n', (err) => {
|
run.child.stdin.write(JSON.stringify(controlResponse) + '\n', (err) => {
|
||||||
|
|
@ -32091,6 +32126,96 @@ export class TeamProvisioningService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sendTeammatePermissionResponse(
|
||||||
|
run: ProvisioningRun,
|
||||||
|
agentId: string,
|
||||||
|
requestId: string,
|
||||||
|
params: {
|
||||||
|
allow: boolean;
|
||||||
|
message?: string;
|
||||||
|
permissionUpdates?: unknown[];
|
||||||
|
toolName?: string;
|
||||||
|
toolInput?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
): void {
|
||||||
|
const payload = params.allow
|
||||||
|
? {
|
||||||
|
type: 'permission_response',
|
||||||
|
request_id: requestId,
|
||||||
|
subtype: 'success',
|
||||||
|
response: {
|
||||||
|
updated_input: this.buildTeammatePermissionUpdatedInput(
|
||||||
|
params.toolName,
|
||||||
|
params.toolInput,
|
||||||
|
params.message
|
||||||
|
),
|
||||||
|
permission_updates: params.permissionUpdates ?? [],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
type: 'permission_response',
|
||||||
|
request_id: requestId,
|
||||||
|
subtype: 'error',
|
||||||
|
error: params.message ?? 'Permission denied',
|
||||||
|
};
|
||||||
|
|
||||||
|
this.persistInboxMessage(run.teamName, agentId, {
|
||||||
|
from:
|
||||||
|
run.request?.members.find((member) => member.role?.toLowerCase().includes('lead'))?.name ??
|
||||||
|
'team-lead',
|
||||||
|
to: agentId,
|
||||||
|
text: JSON.stringify(payload),
|
||||||
|
timestamp: nowIso(),
|
||||||
|
read: false,
|
||||||
|
summary: params.allow
|
||||||
|
? `Approved ${params.toolName ?? 'tool'} request`
|
||||||
|
: `Denied ${params.toolName ?? 'tool'} request`,
|
||||||
|
messageId: `permission-response-${run.runId}-${requestId}-${Date.now()}`,
|
||||||
|
source: 'lead_process',
|
||||||
|
});
|
||||||
|
this.teamChangeEmitter?.({
|
||||||
|
type: 'inbox',
|
||||||
|
teamName: run.teamName,
|
||||||
|
detail: `inboxes/${agentId}.json`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildTeammatePermissionUpdatedInput(
|
||||||
|
toolName: string | undefined,
|
||||||
|
toolInput: Record<string, unknown> | undefined,
|
||||||
|
message: string | undefined
|
||||||
|
): Record<string, unknown> | undefined {
|
||||||
|
if (!toolInput) return undefined;
|
||||||
|
if (toolName !== 'AskUserQuestion' || !message) return toolInput;
|
||||||
|
|
||||||
|
const answers = this.parseAskUserQuestionAnswers(message, toolInput);
|
||||||
|
return Object.keys(answers).length > 0 ? { ...toolInput, answers } : toolInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseAskUserQuestionAnswers(
|
||||||
|
message: string,
|
||||||
|
toolInput: Record<string, unknown>
|
||||||
|
): Record<string, string> {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(message) as unknown;
|
||||||
|
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(parsed as Record<string, unknown>).filter(
|
||||||
|
(entry): entry is [string, string] => typeof entry[1] === 'string'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Fall back to using the raw message as the first answer.
|
||||||
|
}
|
||||||
|
|
||||||
|
const questions = Array.isArray(toolInput.questions)
|
||||||
|
? (toolInput.questions as { question?: unknown }[])
|
||||||
|
: [];
|
||||||
|
const firstQuestion = questions.find((question) => typeof question.question === 'string');
|
||||||
|
return typeof firstQuestion?.question === 'string' ? { [firstQuestion.question]: message } : {};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Safely add tool names to the permissions.allow (or deny) array in a Claude settings file.
|
* Safely add tool names to the permissions.allow (or deny) array in a Claude settings file.
|
||||||
* Creates the file and parent directories if they don't exist.
|
* Creates the file and parent directories if they don't exist.
|
||||||
|
|
|
||||||
|
|
@ -14350,6 +14350,8 @@ describe('TeamProvisioningService', () => {
|
||||||
getConfig,
|
getConfig,
|
||||||
getConfigSnapshot,
|
getConfigSnapshot,
|
||||||
} as any);
|
} as any);
|
||||||
|
const persistInboxMessage = vi.fn();
|
||||||
|
(svc as any).persistInboxMessage = persistInboxMessage;
|
||||||
|
|
||||||
await (svc as any).respondToTeammatePermission(
|
await (svc as any).respondToTeammatePermission(
|
||||||
{ teamName: 'ops-team' },
|
{ teamName: 'ops-team' },
|
||||||
|
|
@ -14378,6 +14380,13 @@ describe('TeamProvisioningService', () => {
|
||||||
expect(settings.permissions?.allow).not.toContain('mcp__agent-teams__kanban_clear');
|
expect(settings.permissions?.allow).not.toContain('mcp__agent-teams__kanban_clear');
|
||||||
expect(getConfig).toHaveBeenCalledWith('ops-team');
|
expect(getConfig).toHaveBeenCalledWith('ops-team');
|
||||||
expect(getConfigSnapshot).not.toHaveBeenCalled();
|
expect(getConfigSnapshot).not.toHaveBeenCalled();
|
||||||
|
expect(persistInboxMessage).toHaveBeenCalledWith(
|
||||||
|
'ops-team',
|
||||||
|
'alice',
|
||||||
|
expect.objectContaining({
|
||||||
|
text: expect.stringContaining('"type":"permission_response"'),
|
||||||
|
})
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not broaden admin/runtime teammate permission suggestions', async () => {
|
it('does not broaden admin/runtime teammate permission suggestions', async () => {
|
||||||
|
|
@ -14388,6 +14397,7 @@ describe('TeamProvisioningService', () => {
|
||||||
members: [{ cwd: tempClaudeRoot }],
|
members: [{ cwd: tempClaudeRoot }],
|
||||||
})),
|
})),
|
||||||
} as any);
|
} as any);
|
||||||
|
(svc as any).persistInboxMessage = vi.fn();
|
||||||
|
|
||||||
await (svc as any).respondToTeammatePermission(
|
await (svc as any).respondToTeammatePermission(
|
||||||
{ teamName: 'ops-team' },
|
{ teamName: 'ops-team' },
|
||||||
|
|
@ -14412,6 +14422,154 @@ describe('TeamProvisioningService', () => {
|
||||||
expect(settings.permissions?.allow).toEqual(['mcp__agent-teams__team_stop']);
|
expect(settings.permissions?.allow).toEqual(['mcp__agent-teams__team_stop']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('builds teammate AskUserQuestion permission responses with answers', () => {
|
||||||
|
const svc = new TeamProvisioningService();
|
||||||
|
const toolInput = {
|
||||||
|
questions: [
|
||||||
|
{
|
||||||
|
question: 'What type of calculator app would you like?',
|
||||||
|
header: 'App type',
|
||||||
|
options: [
|
||||||
|
{ label: 'Web UI (Recommended)', description: 'Browser app' },
|
||||||
|
{ label: 'CLI', description: 'Terminal app' },
|
||||||
|
],
|
||||||
|
multiSelect: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(
|
||||||
|
(svc as any).buildTeammatePermissionUpdatedInput(
|
||||||
|
'AskUserQuestion',
|
||||||
|
toolInput,
|
||||||
|
JSON.stringify({
|
||||||
|
'What type of calculator app would you like?': 'Web UI (Recommended)',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
).toEqual({
|
||||||
|
...toolInput,
|
||||||
|
answers: {
|
||||||
|
'What type of calculator app would you like?': 'Web UI (Recommended)',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sends teammate AskUserQuestion permission responses to the teammate inbox', async () => {
|
||||||
|
const svc = new TeamProvisioningService();
|
||||||
|
const persistInboxMessage = vi.fn();
|
||||||
|
(svc as any).persistInboxMessage = persistInboxMessage;
|
||||||
|
const toolInput = {
|
||||||
|
questions: [
|
||||||
|
{
|
||||||
|
question: 'What type of calculator app would you like?',
|
||||||
|
options: [{ label: 'Web UI (Recommended)', description: 'Browser app' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
await (svc as any).respondToTeammatePermission(
|
||||||
|
{ teamName: 'ops-team', runId: 'run-1' },
|
||||||
|
'bob',
|
||||||
|
'perm-1',
|
||||||
|
true,
|
||||||
|
JSON.stringify({
|
||||||
|
'What type of calculator app would you like?': 'Web UI (Recommended)',
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
'AskUserQuestion',
|
||||||
|
toolInput
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(persistInboxMessage).toHaveBeenCalledTimes(1);
|
||||||
|
const [, recipient, message] = persistInboxMessage.mock.calls[0];
|
||||||
|
expect(recipient).toBe('bob');
|
||||||
|
expect(JSON.parse(message.text)).toEqual({
|
||||||
|
type: 'permission_response',
|
||||||
|
request_id: 'perm-1',
|
||||||
|
subtype: 'success',
|
||||||
|
response: {
|
||||||
|
updated_input: {
|
||||||
|
...toolInput,
|
||||||
|
answers: {
|
||||||
|
'What type of calculator app would you like?': 'Web UI (Recommended)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
permission_updates: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sends teammate denial responses to the teammate inbox', async () => {
|
||||||
|
const svc = new TeamProvisioningService();
|
||||||
|
const persistInboxMessage = vi.fn();
|
||||||
|
(svc as any).persistInboxMessage = persistInboxMessage;
|
||||||
|
|
||||||
|
await (svc as any).respondToTeammatePermission(
|
||||||
|
{ teamName: 'ops-team', runId: 'run-1' },
|
||||||
|
'bob',
|
||||||
|
'perm-deny',
|
||||||
|
false,
|
||||||
|
'Denied by test',
|
||||||
|
[],
|
||||||
|
'Bash',
|
||||||
|
{ command: 'echo blocked' }
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(persistInboxMessage).toHaveBeenCalledTimes(1);
|
||||||
|
const [, recipient, message] = persistInboxMessage.mock.calls[0];
|
||||||
|
expect(recipient).toBe('bob');
|
||||||
|
expect(JSON.parse(message.text)).toEqual({
|
||||||
|
type: 'permission_response',
|
||||||
|
request_id: 'perm-deny',
|
||||||
|
subtype: 'error',
|
||||||
|
error: 'Denied by test',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps AskUserQuestion answers in teammate fallback control responses', async () => {
|
||||||
|
const write = vi.fn((_line: string, cb?: (error?: Error | null) => void) => {
|
||||||
|
cb?.();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
const svc = new TeamProvisioningService({
|
||||||
|
getConfig: vi.fn(async () => ({
|
||||||
|
projectPath: tempClaudeRoot,
|
||||||
|
members: [{ cwd: tempClaudeRoot }],
|
||||||
|
})),
|
||||||
|
} as any);
|
||||||
|
(svc as any).persistInboxMessage = vi.fn();
|
||||||
|
const toolInput = {
|
||||||
|
questions: [
|
||||||
|
{
|
||||||
|
question: 'What features do you need?',
|
||||||
|
options: [{ label: 'Basic', description: 'Arithmetic' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
await (svc as any).respondToTeammatePermission(
|
||||||
|
{
|
||||||
|
teamName: 'ops-team',
|
||||||
|
runId: 'run-1',
|
||||||
|
child: { stdin: { writable: true, write } },
|
||||||
|
},
|
||||||
|
'bob',
|
||||||
|
'perm-2',
|
||||||
|
true,
|
||||||
|
JSON.stringify({ 'What features do you need?': 'Basic' }),
|
||||||
|
[{ type: 'setMode', mode: 'acceptEdits', destination: 'session' }],
|
||||||
|
'AskUserQuestion',
|
||||||
|
toolInput
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(write).toHaveBeenCalledTimes(1);
|
||||||
|
const payload = JSON.parse(write.mock.calls[0][0]);
|
||||||
|
expect(payload.response.response.updatedInput).toEqual({
|
||||||
|
...toolInput,
|
||||||
|
answers: { 'What features do you need?': 'Basic' },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('uses a non-alarming model delay message before 2 minutes of silence', () => {
|
it('uses a non-alarming model delay message before 2 minutes of silence', () => {
|
||||||
const svc = new TeamProvisioningService();
|
const svc = new TeamProvisioningService();
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue