Improve job status notifications

This commit is contained in:
2026-03-09 18:53:41 -04:00
parent 1a18ac825d
commit 12a9741eca
22 changed files with 2243 additions and 302 deletions

View File

@@ -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 {