feat(mcp-server): add team provisioning follow-up hooks
This commit is contained in:
parent
8976caa966
commit
e996962b64
@ -22,6 +22,9 @@ vi.mock('../../lib/extraction-client.js', () => ({
|
||||
vi.mock('../../lib/jarvis-client.js', () => ({
|
||||
jarvisMarketplaceGetListing: vi.fn(),
|
||||
jarvisMarketplaceCertify: vi.fn(),
|
||||
jarvisTeamsListMembers: vi.fn(),
|
||||
jarvisAgentsList: vi.fn(),
|
||||
jarvisMemoryCreate: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../lib/lysnrai-client.js', () => ({
|
||||
@ -178,4 +181,43 @@ describe('A2A governance integration', () => {
|
||||
expect.objectContaining({ productId: 'lysnrai' })
|
||||
);
|
||||
});
|
||||
|
||||
it('creates a support case when team provisioning is only partially successful', async () => {
|
||||
const { runTeamProvisioningPipeline } = await import('./team-provisioning-pipeline.js');
|
||||
const { jarvisTeamsListMembers, jarvisAgentsList, jarvisMemoryCreate } =
|
||||
await import('../../lib/jarvis-client.js');
|
||||
|
||||
vi.mocked(jarvisTeamsListMembers).mockResolvedValue({
|
||||
members: [
|
||||
{
|
||||
userId: 'u1',
|
||||
teamId: 'team_1',
|
||||
role: 'member',
|
||||
status: 'invited',
|
||||
joinedAt: new Date().toISOString(),
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
} as never);
|
||||
vi.mocked(jarvisAgentsList).mockResolvedValue({
|
||||
agents: [
|
||||
{
|
||||
id: 'agt_1',
|
||||
role: 'productivity_coach',
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
} as never);
|
||||
vi.mocked(jarvisMemoryCreate).mockRejectedValue(new Error('memory down'));
|
||||
|
||||
const report = await runTeamProvisioningPipeline('team_1', 48, buildReq('req_team'));
|
||||
|
||||
expect(report.failed).toBe(1);
|
||||
expect(supportCasesCreate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
title: 'Team provisioning follow-up for team_1',
|
||||
}),
|
||||
expect.objectContaining({ productId: 'jarvisjr' })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -21,6 +21,15 @@ import {
|
||||
type JarvisTeamMemberDoc,
|
||||
type JarvisAgentDoc,
|
||||
} from '../../lib/jarvis-client.js';
|
||||
import {
|
||||
trackRunCompleted,
|
||||
trackRunFailed,
|
||||
trackRunStarted,
|
||||
trackStepCompleted,
|
||||
trackStepFailed,
|
||||
trackStepStarted,
|
||||
} from './run-tracker.js';
|
||||
import { createSupportCaseForRun } from './governance.js';
|
||||
|
||||
// ── Types ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
@ -180,7 +189,7 @@ function assembleProvisioningReport(
|
||||
|
||||
// ── Pipeline runner ────────────────────────────────────────────────────────────
|
||||
|
||||
async function runTeamProvisioningPipeline(
|
||||
export async function runTeamProvisioningPipeline(
|
||||
teamId: string,
|
||||
sinceHours: number,
|
||||
req: McpToolRequest
|
||||
@ -188,37 +197,194 @@ async function runTeamProvisioningPipeline(
|
||||
const runId = randomUUID();
|
||||
const opts = { token: req.headers.authorization?.slice(7), requestId: req.id };
|
||||
const sinceMs = sinceHours * 60 * 60 * 1000;
|
||||
let currentStep: { stepName: 'detect' | 'onboard' | 'report'; order: number } | undefined;
|
||||
|
||||
req.log.info({ runId, stepId: 'detect', teamId, sinceHours }, 'NewMemberDetectorAgent start');
|
||||
const { all, newMembers } = await detectNewMembers(teamId, sinceMs, opts);
|
||||
req.log.info(
|
||||
{ runId, stepId: 'detect', total: all.length, newCount: newMembers.length },
|
||||
'NewMemberDetectorAgent done'
|
||||
await safeTrack(() =>
|
||||
trackRunStarted({
|
||||
runId,
|
||||
productId: 'jarvisjr',
|
||||
name: 'team-provisioning',
|
||||
requestId: req.id,
|
||||
token: opts.token,
|
||||
input: { teamId, sinceHours },
|
||||
})
|
||||
);
|
||||
|
||||
req.log.info({ runId, stepId: 'onboard', newCount: newMembers.length }, 'OnboardingAgent start');
|
||||
const agentsResult = await jarvisAgentsList({ limit: 20 }, opts).catch(() => ({
|
||||
agents: [] as JarvisAgentDoc[],
|
||||
total: 0,
|
||||
}));
|
||||
const results: MemberOnboardingResult[] = [];
|
||||
for (const member of newMembers) {
|
||||
const result = await onboardMember(member, agentsResult.agents, opts);
|
||||
results.push(result);
|
||||
try {
|
||||
currentStep = { stepName: 'detect', order: 1 };
|
||||
const detectStep = currentStep;
|
||||
await safeTrack(() =>
|
||||
trackStepStarted({
|
||||
runId,
|
||||
productId: 'jarvisjr',
|
||||
stepName: detectStep.stepName,
|
||||
order: detectStep.order,
|
||||
token: opts.token,
|
||||
requestId: req.id,
|
||||
input: { teamId, sinceHours },
|
||||
})
|
||||
);
|
||||
req.log.info({ runId, stepId: 'detect', teamId, sinceHours }, 'NewMemberDetectorAgent start');
|
||||
const { all, newMembers } = await detectNewMembers(teamId, sinceMs, opts);
|
||||
await safeTrack(() =>
|
||||
trackStepCompleted({
|
||||
runId,
|
||||
productId: 'jarvisjr',
|
||||
stepName: detectStep.stepName,
|
||||
order: detectStep.order,
|
||||
token: opts.token,
|
||||
requestId: req.id,
|
||||
output: { totalMembers: all.length, newMembers: newMembers.length },
|
||||
})
|
||||
);
|
||||
req.log.info(
|
||||
{ runId, stepId: 'detect', total: all.length, newCount: newMembers.length },
|
||||
'NewMemberDetectorAgent done'
|
||||
);
|
||||
|
||||
currentStep = { stepName: 'onboard', order: 2 };
|
||||
const onboardStep = currentStep;
|
||||
await safeTrack(() =>
|
||||
trackStepStarted({
|
||||
runId,
|
||||
productId: 'jarvisjr',
|
||||
stepName: onboardStep.stepName,
|
||||
order: onboardStep.order,
|
||||
token: opts.token,
|
||||
requestId: req.id,
|
||||
input: { newMembers: newMembers.length },
|
||||
})
|
||||
);
|
||||
req.log.info(
|
||||
{ runId, stepId: 'onboard', newCount: newMembers.length },
|
||||
'OnboardingAgent start'
|
||||
);
|
||||
const agentsResult = await jarvisAgentsList({ limit: 20 }, opts).catch(() => ({
|
||||
agents: [] as JarvisAgentDoc[],
|
||||
total: 0,
|
||||
}));
|
||||
const results: MemberOnboardingResult[] = [];
|
||||
for (const member of newMembers) {
|
||||
const result = await onboardMember(member, agentsResult.agents, opts);
|
||||
results.push(result);
|
||||
}
|
||||
await safeTrack(() =>
|
||||
trackStepCompleted({
|
||||
runId,
|
||||
productId: 'jarvisjr',
|
||||
stepName: onboardStep.stepName,
|
||||
order: onboardStep.order,
|
||||
token: opts.token,
|
||||
requestId: req.id,
|
||||
output: {
|
||||
seeded: results.filter(r => r.memorySeeded).length,
|
||||
failed: results.filter(r => r.errors.length > 0).length,
|
||||
},
|
||||
})
|
||||
);
|
||||
req.log.info(
|
||||
{ runId, stepId: 'onboard', seeded: results.filter(r => r.memorySeeded).length },
|
||||
'OnboardingAgent done'
|
||||
);
|
||||
|
||||
currentStep = { stepName: 'report', order: 3 };
|
||||
const reportStep = currentStep;
|
||||
await safeTrack(() =>
|
||||
trackStepStarted({
|
||||
runId,
|
||||
productId: 'jarvisjr',
|
||||
stepName: reportStep.stepName,
|
||||
order: reportStep.order,
|
||||
token: opts.token,
|
||||
requestId: req.id,
|
||||
})
|
||||
);
|
||||
req.log.info({ runId, stepId: 'report' }, 'ProvisioningReportAgent start');
|
||||
const report = assembleProvisioningReport(runId, teamId, all.length, results);
|
||||
await safeTrack(() =>
|
||||
trackStepCompleted({
|
||||
runId,
|
||||
productId: 'jarvisjr',
|
||||
stepName: reportStep.stepName,
|
||||
order: reportStep.order,
|
||||
token: opts.token,
|
||||
requestId: req.id,
|
||||
output: {
|
||||
fullyOnboarded: report.fullyOnboarded,
|
||||
partiallyOnboarded: report.partiallyOnboarded,
|
||||
failed: report.failed,
|
||||
},
|
||||
})
|
||||
);
|
||||
await safeTrack(() =>
|
||||
trackRunCompleted({
|
||||
runId,
|
||||
productId: 'jarvisjr',
|
||||
name: 'team-provisioning',
|
||||
requestId: req.id,
|
||||
token: opts.token,
|
||||
output: {
|
||||
fullyOnboarded: report.fullyOnboarded,
|
||||
partiallyOnboarded: report.partiallyOnboarded,
|
||||
failed: report.failed,
|
||||
},
|
||||
})
|
||||
);
|
||||
if (report.failed > 0 || report.partiallyOnboarded > 0) {
|
||||
await createSupportCaseForRun(
|
||||
{
|
||||
productId: 'jarvisjr',
|
||||
runId,
|
||||
title: `Team provisioning follow-up for ${teamId}`,
|
||||
description: report.summary,
|
||||
priority: report.failed > 0 ? 'high' : 'medium',
|
||||
tags: ['a2a', 'team-provisioning', 'onboarding'],
|
||||
},
|
||||
opts
|
||||
);
|
||||
}
|
||||
req.log.info(
|
||||
{ runId, stepId: 'report', summary: report.summary },
|
||||
'ProvisioningReportAgent done'
|
||||
);
|
||||
|
||||
return report;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
if (currentStep) {
|
||||
const failedStep = currentStep;
|
||||
await safeTrack(() =>
|
||||
trackStepFailed({
|
||||
runId,
|
||||
productId: 'jarvisjr',
|
||||
stepName: failedStep.stepName,
|
||||
order: failedStep.order,
|
||||
token: opts.token,
|
||||
requestId: req.id,
|
||||
error: message,
|
||||
})
|
||||
);
|
||||
}
|
||||
await safeTrack(() =>
|
||||
trackRunFailed({
|
||||
runId,
|
||||
productId: 'jarvisjr',
|
||||
name: 'team-provisioning',
|
||||
requestId: req.id,
|
||||
token: opts.token,
|
||||
error: message,
|
||||
})
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
req.log.info(
|
||||
{ runId, stepId: 'onboard', seeded: results.filter(r => r.memorySeeded).length },
|
||||
'OnboardingAgent done'
|
||||
);
|
||||
}
|
||||
|
||||
req.log.info({ runId, stepId: 'report' }, 'ProvisioningReportAgent start');
|
||||
const report = assembleProvisioningReport(runId, teamId, all.length, results);
|
||||
req.log.info(
|
||||
{ runId, stepId: 'report', summary: report.summary },
|
||||
'ProvisioningReportAgent done'
|
||||
);
|
||||
|
||||
return report;
|
||||
async function safeTrack(fn: () => Promise<void>): Promise<void> {
|
||||
try {
|
||||
await fn();
|
||||
} catch {
|
||||
// Tracking must never fail the pipeline itself.
|
||||
}
|
||||
}
|
||||
|
||||
// ── MCP tool registration ─────────────────────────────────────────────────────
|
||||
|
||||
Loading…
Reference in New Issue
Block a user