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', () => ({
|
vi.mock('../../lib/jarvis-client.js', () => ({
|
||||||
jarvisMarketplaceGetListing: vi.fn(),
|
jarvisMarketplaceGetListing: vi.fn(),
|
||||||
jarvisMarketplaceCertify: vi.fn(),
|
jarvisMarketplaceCertify: vi.fn(),
|
||||||
|
jarvisTeamsListMembers: vi.fn(),
|
||||||
|
jarvisAgentsList: vi.fn(),
|
||||||
|
jarvisMemoryCreate: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../../lib/lysnrai-client.js', () => ({
|
vi.mock('../../lib/lysnrai-client.js', () => ({
|
||||||
@ -178,4 +181,43 @@ describe('A2A governance integration', () => {
|
|||||||
expect.objectContaining({ productId: 'lysnrai' })
|
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 JarvisTeamMemberDoc,
|
||||||
type JarvisAgentDoc,
|
type JarvisAgentDoc,
|
||||||
} from '../../lib/jarvis-client.js';
|
} from '../../lib/jarvis-client.js';
|
||||||
|
import {
|
||||||
|
trackRunCompleted,
|
||||||
|
trackRunFailed,
|
||||||
|
trackRunStarted,
|
||||||
|
trackStepCompleted,
|
||||||
|
trackStepFailed,
|
||||||
|
trackStepStarted,
|
||||||
|
} from './run-tracker.js';
|
||||||
|
import { createSupportCaseForRun } from './governance.js';
|
||||||
|
|
||||||
// ── Types ──────────────────────────────────────────────────────────────────────
|
// ── Types ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@ -180,7 +189,7 @@ function assembleProvisioningReport(
|
|||||||
|
|
||||||
// ── Pipeline runner ────────────────────────────────────────────────────────────
|
// ── Pipeline runner ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
async function runTeamProvisioningPipeline(
|
export async function runTeamProvisioningPipeline(
|
||||||
teamId: string,
|
teamId: string,
|
||||||
sinceHours: number,
|
sinceHours: number,
|
||||||
req: McpToolRequest
|
req: McpToolRequest
|
||||||
@ -188,37 +197,194 @@ async function runTeamProvisioningPipeline(
|
|||||||
const runId = randomUUID();
|
const runId = randomUUID();
|
||||||
const opts = { token: req.headers.authorization?.slice(7), requestId: req.id };
|
const opts = { token: req.headers.authorization?.slice(7), requestId: req.id };
|
||||||
const sinceMs = sinceHours * 60 * 60 * 1000;
|
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');
|
await safeTrack(() =>
|
||||||
const { all, newMembers } = await detectNewMembers(teamId, sinceMs, opts);
|
trackRunStarted({
|
||||||
req.log.info(
|
runId,
|
||||||
{ runId, stepId: 'detect', total: all.length, newCount: newMembers.length },
|
productId: 'jarvisjr',
|
||||||
'NewMemberDetectorAgent done'
|
name: 'team-provisioning',
|
||||||
|
requestId: req.id,
|
||||||
|
token: opts.token,
|
||||||
|
input: { teamId, sinceHours },
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
req.log.info({ runId, stepId: 'onboard', newCount: newMembers.length }, 'OnboardingAgent start');
|
try {
|
||||||
const agentsResult = await jarvisAgentsList({ limit: 20 }, opts).catch(() => ({
|
currentStep = { stepName: 'detect', order: 1 };
|
||||||
agents: [] as JarvisAgentDoc[],
|
const detectStep = currentStep;
|
||||||
total: 0,
|
await safeTrack(() =>
|
||||||
}));
|
trackStepStarted({
|
||||||
const results: MemberOnboardingResult[] = [];
|
runId,
|
||||||
for (const member of newMembers) {
|
productId: 'jarvisjr',
|
||||||
const result = await onboardMember(member, agentsResult.agents, opts);
|
stepName: detectStep.stepName,
|
||||||
results.push(result);
|
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');
|
async function safeTrack(fn: () => Promise<void>): Promise<void> {
|
||||||
const report = assembleProvisioningReport(runId, teamId, all.length, results);
|
try {
|
||||||
req.log.info(
|
await fn();
|
||||||
{ runId, stepId: 'report', summary: report.summary },
|
} catch {
|
||||||
'ProvisioningReportAgent done'
|
// Tracking must never fail the pipeline itself.
|
||||||
);
|
}
|
||||||
|
|
||||||
return report;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── MCP tool registration ─────────────────────────────────────────────────────
|
// ── MCP tool registration ─────────────────────────────────────────────────────
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user