Improve job status notifications
This commit is contained in:
@@ -89,7 +89,10 @@ function applySqlMigrations(client: { exec: (query: string) => void }) {
|
||||
'0003_task_stage_event_timeline.sql',
|
||||
'0004_watchlist_company_taxonomy.sql',
|
||||
'0005_financial_taxonomy_v3.sql',
|
||||
'0006_coverage_journal_tracking.sql'
|
||||
'0006_coverage_journal_tracking.sql',
|
||||
'0007_company_financial_bundles.sql',
|
||||
'0008_research_workspace.sql',
|
||||
'0009_task_notification_context.sql'
|
||||
];
|
||||
|
||||
for (const file of migrationFiles) {
|
||||
@@ -592,6 +595,159 @@ if (process.env.RUN_TASK_WORKFLOW_E2E === '1') {
|
||||
expect(resetTask.notification_silenced_at).toBeNull();
|
||||
});
|
||||
|
||||
it('returns enriched stage context and notification payloads for tasks and timelines', async () => {
|
||||
if (!sqliteClient) {
|
||||
throw new Error('sqlite client not initialized');
|
||||
}
|
||||
|
||||
const created = await jsonRequest('POST', '/api/filings/0000000000-26-000010/analyze');
|
||||
const taskId = (created.json as { task: { id: string } }).task.id;
|
||||
const now = new Date().toISOString();
|
||||
const stageContext = JSON.stringify({
|
||||
progress: {
|
||||
current: 2,
|
||||
total: 5,
|
||||
unit: 'steps'
|
||||
},
|
||||
subject: {
|
||||
accessionNumber: '0000000000-26-000010'
|
||||
}
|
||||
});
|
||||
|
||||
sqliteClient.query(`
|
||||
UPDATE task_run
|
||||
SET status = ?, stage = ?, stage_detail = ?, stage_context = ?, workflow_run_id = NULL, updated_at = ?
|
||||
WHERE id = ?;
|
||||
`).run(
|
||||
'running',
|
||||
'analyze.extract',
|
||||
'Generating extraction context from filing text',
|
||||
stageContext,
|
||||
now,
|
||||
taskId
|
||||
);
|
||||
sqliteClient.query(`
|
||||
INSERT INTO task_stage_event (task_id, user_id, stage, stage_detail, stage_context, status, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?);
|
||||
`).run(
|
||||
taskId,
|
||||
TEST_USER_ID,
|
||||
'analyze.extract',
|
||||
'Generating extraction context from filing text',
|
||||
stageContext,
|
||||
'running',
|
||||
now
|
||||
);
|
||||
|
||||
const tasksResponse = await jsonRequest('GET', '/api/tasks?limit=5');
|
||||
expect(tasksResponse.response.status).toBe(200);
|
||||
const apiTask = (tasksResponse.json as {
|
||||
tasks: Array<{
|
||||
id: string;
|
||||
stage_context: { progress?: { current: number } | null } | null;
|
||||
notification: { title: string; actions: Array<{ id: string }> };
|
||||
}>;
|
||||
}).tasks.find((entry) => entry.id === taskId);
|
||||
|
||||
expect(apiTask?.stage_context?.progress?.current).toBe(2);
|
||||
expect(apiTask?.notification.title).toBe('Filing analysis');
|
||||
expect(apiTask?.notification.actions.some((action) => action.id === 'open_filings')).toBe(true);
|
||||
|
||||
const timeline = await jsonRequest('GET', `/api/tasks/${taskId}/timeline`);
|
||||
expect(timeline.response.status).toBe(200);
|
||||
const event = (timeline.json as {
|
||||
events: Array<{
|
||||
stage: string;
|
||||
stage_context: { progress?: { total: number } | null } | null;
|
||||
}>;
|
||||
}).events.find((entry) => entry.stage === 'analyze.extract');
|
||||
|
||||
expect(event?.stage_context?.progress?.total).toBe(5);
|
||||
});
|
||||
|
||||
it('returns task-specific notification actions for completed and failed analyze tasks', async () => {
|
||||
if (!sqliteClient) {
|
||||
throw new Error('sqlite client not initialized');
|
||||
}
|
||||
|
||||
const completedCreate = await jsonRequest('POST', '/api/filings/0000000000-26-000020/analyze');
|
||||
const completedTaskId = (completedCreate.json as { task: { id: string } }).task.id;
|
||||
sqliteClient.query(`
|
||||
UPDATE task_run
|
||||
SET status = ?, stage = ?, stage_detail = ?, stage_context = ?, result = ?, workflow_run_id = NULL, updated_at = ?, finished_at = ?
|
||||
WHERE id = ?;
|
||||
`).run(
|
||||
'completed',
|
||||
'completed',
|
||||
'Analysis report generated for AAPL 10-Q 0000000000-26-000020.',
|
||||
JSON.stringify({
|
||||
subject: {
|
||||
ticker: 'AAPL',
|
||||
accessionNumber: '0000000000-26-000020',
|
||||
label: '10-Q'
|
||||
}
|
||||
}),
|
||||
JSON.stringify({
|
||||
ticker: 'AAPL',
|
||||
accessionNumber: '0000000000-26-000020',
|
||||
filingType: '10-Q',
|
||||
provider: 'test',
|
||||
model: 'fixture',
|
||||
extractionProvider: 'test',
|
||||
extractionModel: 'fixture',
|
||||
searchTaskId: null
|
||||
}),
|
||||
'2026-03-09T15:00:00.000Z',
|
||||
'2026-03-09T15:00:00.000Z',
|
||||
completedTaskId
|
||||
);
|
||||
|
||||
const completed = await jsonRequest('GET', `/api/tasks/${completedTaskId}`);
|
||||
expect(completed.response.status).toBe(200);
|
||||
const completedActions = (completed.json as {
|
||||
task: {
|
||||
notification: { actions: Array<{ id: string; href: string | null }> };
|
||||
};
|
||||
}).task.notification.actions;
|
||||
expect(completedActions[0]?.id).toBe('open_analysis_report');
|
||||
expect(completedActions[0]?.href).toContain('/analysis/reports/AAPL/0000000000-26-000020');
|
||||
|
||||
const failedCreate = await jsonRequest('POST', '/api/filings/0000000000-26-000021/analyze');
|
||||
const failedTaskId = (failedCreate.json as { task: { id: string } }).task.id;
|
||||
sqliteClient.query(`
|
||||
UPDATE task_run
|
||||
SET status = ?, stage = ?, stage_detail = ?, stage_context = ?, error = ?, workflow_run_id = NULL, updated_at = ?, finished_at = ?
|
||||
WHERE id = ?;
|
||||
`).run(
|
||||
'failed',
|
||||
'failed',
|
||||
'Primary filing document fetch failed.',
|
||||
JSON.stringify({
|
||||
subject: {
|
||||
ticker: 'AAPL',
|
||||
accessionNumber: '0000000000-26-000021'
|
||||
}
|
||||
}),
|
||||
'Primary filing document fetch failed.',
|
||||
'2026-03-09T15:01:00.000Z',
|
||||
'2026-03-09T15:01:00.000Z',
|
||||
failedTaskId
|
||||
);
|
||||
|
||||
const failed = await jsonRequest('GET', `/api/tasks/${failedTaskId}`);
|
||||
expect(failed.response.status).toBe(200);
|
||||
const failedTask = (failed.json as {
|
||||
task: {
|
||||
notification: {
|
||||
detailLine: string | null;
|
||||
actions: Array<{ id: string; href: string | null }>;
|
||||
};
|
||||
};
|
||||
}).task;
|
||||
expect(failedTask.notification.detailLine).toBe('Primary filing document fetch failed.');
|
||||
expect(failedTask.notification.actions.some((action) => action.id === 'open_filings')).toBe(true);
|
||||
});
|
||||
|
||||
it('reconciles workflow run status into projection state and degrades health when workflow backend is down', async () => {
|
||||
const created = await jsonRequest('POST', '/api/filings/0000000000-26-000100/analyze');
|
||||
const task = (created.json as {
|
||||
|
||||
Reference in New Issue
Block a user