insight-decision.test.ts 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572
  1. import assert from 'node:assert/strict';
  2. import type { AddressInfo } from 'node:net';
  3. import test from 'node:test';
  4. import express, { type ErrorRequestHandler } from 'express';
  5. import { ZodError } from 'zod';
  6. import { ApiError } from '../src/http/api-error.js';
  7. import { LocalSyncJobStore } from '../src/modules/domestic-voc/local/local-sync-job.store.js';
  8. import {
  9. createAuthenticationMiddleware,
  10. type RequestAuthenticator,
  11. WorkspaceAccessService,
  12. } from '../src/modules/saas-platform/auth.js';
  13. import type {
  14. ActionItemCreateInput,
  15. AnalysisRun,
  16. InsightDecisionCreateInput,
  17. } from '../src/modules/saas-platform/domain.js';
  18. import { assertValidActionDecisionSource } from '../src/modules/saas-platform/domain.js';
  19. import { LocalPlatformRepository } from '../src/modules/saas-platform/local-platform.repository.js';
  20. import { createSaasPlatformRouter } from '../src/modules/saas-platform/routes.js';
  21. import type { DomesticDataset } from '../src/types/domestic-dataset.js';
  22. const dataset: DomesticDataset = {
  23. schemaVersion: 1,
  24. generatedAt: '2026-08-07T00:00:00.000Z',
  25. caseName: 'Insight decision test',
  26. platform: 'jd',
  27. source: {
  28. sourceFile: 'insight-decision.json',
  29. sourceHash: 'test',
  30. sheets: [],
  31. dateRange: { start: '2026-08-01', end: '2026-08-07' },
  32. },
  33. summary: {
  34. metricRows: 0,
  35. metricProducts: 0,
  36. mappingRows: 0,
  37. relations: 0,
  38. uniqueCompetitorProducts: 0,
  39. category2Count: 0,
  40. category3Count: 0,
  41. reviewCount: 0,
  42. },
  43. dailyTotals: [],
  44. products: [],
  45. mappingGroups: [],
  46. relations: [],
  47. reviews: [],
  48. quality: { orphanMappings: [], mappingsWithoutCompetitor: [], brandWithoutProductId: [] },
  49. };
  50. function createRepository(): LocalPlatformRepository {
  51. let tick = 0;
  52. return new LocalPlatformRepository(
  53. dataset,
  54. new LocalSyncJobStore(dataset),
  55. { userId: 'local-admin', email: 'local-admin@localhost', displayName: 'Local Admin' },
  56. 'demashi',
  57. () => new Date(Date.parse('2026-08-07T00:00:00.000Z') + tick++ * 1_000),
  58. );
  59. }
  60. async function createAnalysis(
  61. repository: LocalPlatformRepository,
  62. input: {
  63. id: string;
  64. analysisType?: AnalysisRun['analysisType'];
  65. status?: 'pending' | 'completed' | 'partial';
  66. result?: Record<string, unknown>;
  67. },
  68. ): Promise<AnalysisRun> {
  69. const analysis = await repository.createAnalysis({
  70. id: input.id,
  71. workspaceId: 'demashi',
  72. analysisType: input.analysisType ?? 'voc_insight',
  73. targetKind: 'workspace',
  74. targetKey: '',
  75. input: {},
  76. requestedBy: 'local-admin',
  77. });
  78. if (!input.status || input.status === 'pending') return analysis;
  79. await repository.updateAnalysis('demashi', input.id, { status: 'processing' });
  80. return (await repository.updateAnalysis('demashi', input.id, {
  81. status: input.status,
  82. result: input.result ?? {
  83. mode: 'ai',
  84. insights: [{ id: 'insight-1', evidenceIds: ['evidence-1', 'evidence-2'] }],
  85. },
  86. evidenceCount: 2,
  87. }))!;
  88. }
  89. function decisionInput(overrides: Partial<InsightDecisionCreateInput> = {}): InsightDecisionCreateInput {
  90. return {
  91. id: 'decision-1',
  92. workspaceId: 'demashi',
  93. sourceAnalysisId: 'analysis-ai',
  94. sourceInsightId: 'insight-1',
  95. decision: 'confirmed',
  96. reviewedEvidenceIds: ['evidence-1', 'evidence-2'],
  97. comment: '',
  98. decidedBy: 'local-admin',
  99. ...overrides,
  100. };
  101. }
  102. function rejectsWith(code: string): (error: unknown) => boolean {
  103. return (error) => error instanceof ApiError && error.status === 400 && error.code === code;
  104. }
  105. function rejectsWithGate(...codes: string[]): (error: unknown) => boolean {
  106. return (error) => error instanceof ApiError
  107. && (error.status === 400 || error.status === 409)
  108. && codes.includes(error.code);
  109. }
  110. test('local insight decisions validate sources and retain an append-only current version chain', async () => {
  111. const repository = createRepository();
  112. await createAnalysis(repository, { id: 'analysis-pending' });
  113. await assert.rejects(
  114. repository.createInsightDecision(decisionInput({ sourceAnalysisId: 'analysis-pending' })),
  115. rejectsWith('insight_decision_analysis_not_ready'),
  116. );
  117. await createAnalysis(repository, { id: 'analysis-voice', analysisType: 'voice', status: 'completed' });
  118. await assert.rejects(
  119. repository.createInsightDecision(decisionInput({ sourceAnalysisId: 'analysis-voice' })),
  120. rejectsWith('insight_decision_analysis_not_voc_insight'),
  121. );
  122. await createAnalysis(repository, { id: 'analysis-ai', status: 'completed' });
  123. await assert.rejects(
  124. repository.createInsightDecision(decisionInput({ sourceInsightId: 'missing-insight' })),
  125. rejectsWith('insight_decision_insight_not_found'),
  126. );
  127. await assert.rejects(
  128. repository.createInsightDecision(decisionInput({ reviewedEvidenceIds: [] })),
  129. rejectsWith('insight_decision_evidence_required'),
  130. );
  131. await assert.rejects(
  132. repository.createInsightDecision(decisionInput({ reviewedEvidenceIds: ['evidence-outside-insight'] })),
  133. rejectsWith('insight_decision_evidence_not_in_insight'),
  134. );
  135. await assert.rejects(
  136. repository.createInsightDecision(decisionInput({ reviewedEvidenceIds: ['evidence-1'] })),
  137. rejectsWith('insight_decision_evidence_incomplete'),
  138. );
  139. await assert.rejects(
  140. repository.createInsightDecision(decisionInput({ decision: 'rejected', comment: ' ' })),
  141. rejectsWith('insight_decision_comment_required'),
  142. );
  143. const first = await repository.createInsightDecision(decisionInput({
  144. reviewedEvidenceIds: ['evidence-1', 'evidence-1', 'evidence-2'],
  145. comment: ' confirmed by reviewer ',
  146. }));
  147. assert.equal(first.version, 1);
  148. assert.equal(first.supersedesId, null);
  149. assert.equal(first.isCurrent, true);
  150. assert.deepEqual(first.reviewedEvidenceIds, ['evidence-1', 'evidence-2']);
  151. assert.equal(first.comment, 'confirmed by reviewer');
  152. const second = await repository.createInsightDecision(decisionInput({
  153. id: 'decision-2',
  154. decision: 'needs_more_evidence',
  155. reviewedEvidenceIds: ['evidence-1', 'evidence-2'],
  156. comment: 'Collect another review cycle.',
  157. }));
  158. assert.equal(second.version, 2);
  159. assert.equal(second.supersedesId, first.id);
  160. assert.equal(second.isCurrent, true);
  161. const storedFirst = await repository.getInsightDecision('demashi', first.id);
  162. assert.equal(storedFirst?.isCurrent, false);
  163. assert.equal(storedFirst?.decision, 'confirmed');
  164. assert.deepEqual(storedFirst?.reviewedEvidenceIds, ['evidence-1', 'evidence-2']);
  165. assert.equal(storedFirst?.createdAt, first.createdAt);
  166. const current = await repository.listInsightDecisions({
  167. workspaceId: 'demashi', limit: 10, cursor: null,
  168. sourceAnalysisId: 'analysis-ai', sourceInsightId: 'insight-1', currentOnly: true,
  169. });
  170. assert.deepEqual(current.items.map((item) => item.id), [second.id]);
  171. const history = await repository.listInsightDecisions({
  172. workspaceId: 'demashi', limit: 10, cursor: null,
  173. sourceAnalysisId: 'analysis-ai', sourceInsightId: '', currentOnly: false,
  174. });
  175. assert.deepEqual(history.items.map((item) => item.version), [2, 1]);
  176. await createAnalysis(repository, {
  177. id: 'analysis-deterministic',
  178. status: 'partial',
  179. result: {
  180. mode: 'deterministic',
  181. insights: [{ id: 'insight-rule', evidenceIds: ['evidence-rule'] }],
  182. },
  183. });
  184. await assert.rejects(
  185. repository.createInsightDecision(decisionInput({
  186. id: 'decision-rule-confirmed',
  187. sourceAnalysisId: 'analysis-deterministic',
  188. sourceInsightId: 'insight-rule',
  189. reviewedEvidenceIds: ['evidence-rule'],
  190. })),
  191. rejectsWith('insight_decision_deterministic_requires_more_evidence'),
  192. );
  193. const deterministic = await repository.createInsightDecision(decisionInput({
  194. id: 'decision-rule-more-evidence',
  195. sourceAnalysisId: 'analysis-deterministic',
  196. sourceInsightId: 'insight-rule',
  197. decision: 'needs_more_evidence',
  198. reviewedEvidenceIds: ['evidence-rule'],
  199. comment: 'Rule output needs human evidence.',
  200. }));
  201. assert.equal(deterministic.decision, 'needs_more_evidence');
  202. });
  203. test('actions require the current matching decision and use its reviewed evidence', async () => {
  204. const repository = createRepository();
  205. await createAnalysis(repository, { id: 'analysis-ai', status: 'completed' });
  206. const confirmed = await repository.createInsightDecision(decisionInput({
  207. reviewedEvidenceIds: ['evidence-1', 'evidence-2'],
  208. comment: 'All evidence supports a formal action.',
  209. }));
  210. const actionInput: ActionItemCreateInput = {
  211. id: 'action-1',
  212. workspaceId: 'demashi',
  213. sourceAnalysisId: 'analysis-ai',
  214. sourceInsightId: 'insight-1',
  215. sourceDecisionId: confirmed.id,
  216. sourceKind: 'insight',
  217. creationKey: 'client-value-is-normalized',
  218. evidenceIds: ['evidence-1'],
  219. validationMetric: 'Issue rate decreases within 30 days.',
  220. actionType: 'experience',
  221. title: 'Address the confirmed issue',
  222. description: '',
  223. priority: 'high',
  224. status: 'open',
  225. productKey: null,
  226. assigneeUserId: null,
  227. dueAt: null,
  228. createdBy: 'local-admin',
  229. };
  230. const created = await repository.createAction(actionInput);
  231. assert.equal(created.creationKey, 'demashi|analysis-ai|insight-1|decision-1');
  232. const duplicate = await repository.createAction({ ...actionInput, id: 'action-duplicate' });
  233. assert.equal(duplicate.id, created.id);
  234. const rejected = await repository.createInsightDecision(decisionInput({
  235. id: 'decision-rejected',
  236. decision: 'rejected',
  237. reviewedEvidenceIds: ['evidence-1', 'evidence-2'],
  238. comment: 'The issue is not supported.',
  239. }));
  240. assert.throws(
  241. () => assertValidActionDecisionSource({
  242. workspaceId: 'demashi',
  243. sourceAnalysisId: 'analysis-ai',
  244. sourceInsightId: 'insight-1',
  245. sourceDecisionId: confirmed.id,
  246. sourceKind: 'insight',
  247. creationKey: created.creationKey,
  248. evidenceIds: ['evidence-1'],
  249. actionType: 'experience',
  250. validationMetric: actionInput.validationMetric,
  251. }, { ...confirmed, isCurrent: false }),
  252. (error) => error instanceof ApiError && error.code === 'source_decision_superseded',
  253. );
  254. await assert.rejects(
  255. repository.createAction({ ...actionInput, id: 'action-stale-decision-replay' }),
  256. (error) => error instanceof ApiError
  257. && error.status === 409
  258. && error.code === 'source_decision_superseded',
  259. );
  260. await assert.rejects(
  261. repository.createAction({ ...actionInput, id: 'action-rejected', sourceDecisionId: rejected.id }),
  262. (error) => error instanceof ApiError && error.code === 'source_decision_rejected',
  263. );
  264. await createAnalysis(repository, {
  265. id: 'analysis-rule',
  266. status: 'partial',
  267. result: {
  268. mode: 'deterministic',
  269. insights: [{ id: 'insight-rule', evidenceIds: ['evidence-rule'] }],
  270. },
  271. });
  272. const moreEvidence = await repository.createInsightDecision(decisionInput({
  273. id: 'decision-more-evidence',
  274. sourceAnalysisId: 'analysis-rule',
  275. sourceInsightId: 'insight-rule',
  276. decision: 'needs_more_evidence',
  277. reviewedEvidenceIds: ['evidence-rule'],
  278. comment: 'Collect another review cycle.',
  279. }));
  280. const dataQualityInput: ActionItemCreateInput = {
  281. ...actionInput,
  282. id: 'action-data-quality',
  283. sourceAnalysisId: 'analysis-rule',
  284. sourceInsightId: 'insight-rule',
  285. sourceDecisionId: moreEvidence.id,
  286. evidenceIds: ['evidence-rule'],
  287. actionType: 'data_quality',
  288. };
  289. await assert.rejects(
  290. repository.createAction({ ...dataQualityInput, id: 'action-formal', actionType: 'general' }),
  291. (error) => error instanceof ApiError && error.code === 'source_decision_requires_data_quality_action',
  292. );
  293. const dataQualityAction = await repository.createAction(dataQualityInput);
  294. assert.equal(dataQualityAction.actionType, 'data_quality');
  295. });
  296. test('insight-backed actions cannot disguise their source kind to bypass a decision', async () => {
  297. for (const sourceKind of ['raw_feedback', 'rule_action'] as const) {
  298. const repository = createRepository();
  299. await createAnalysis(repository, { id: 'analysis-ai', status: 'completed' });
  300. await assert.rejects(
  301. repository.createAction({
  302. id: `action-disguised-${sourceKind}`,
  303. workspaceId: 'demashi',
  304. sourceAnalysisId: 'analysis-ai',
  305. sourceInsightId: 'insight-1',
  306. sourceDecisionId: null,
  307. sourceKind,
  308. creationKey: `disguised-${sourceKind}`,
  309. evidenceIds: ['evidence-1', 'evidence-2'],
  310. validationMetric: 'Issue rate decreases within 30 days.',
  311. actionType: 'experience',
  312. title: `Disguised ${sourceKind} action`,
  313. description: '',
  314. priority: 'high',
  315. status: 'open',
  316. productKey: null,
  317. assigneeUserId: null,
  318. dueAt: null,
  319. createdBy: 'local-admin',
  320. }),
  321. rejectsWithGate('insight_source_requires_insight_kind'),
  322. );
  323. }
  324. });
  325. test('idempotent action replay rejects changes to its decision-bound payload', async () => {
  326. const repository = createRepository();
  327. await createAnalysis(repository, {
  328. id: 'analysis-rule',
  329. status: 'partial',
  330. result: {
  331. mode: 'deterministic',
  332. insights: [{ id: 'insight-rule', evidenceIds: ['evidence-rule-1', 'evidence-rule-2'] }],
  333. },
  334. });
  335. const decision = await repository.createInsightDecision(decisionInput({
  336. id: 'decision-rule',
  337. sourceAnalysisId: 'analysis-rule',
  338. sourceInsightId: 'insight-rule',
  339. decision: 'needs_more_evidence',
  340. reviewedEvidenceIds: ['evidence-rule-1', 'evidence-rule-2'],
  341. comment: 'Collect enough evidence to verify the rule output.',
  342. }));
  343. const actionInput: ActionItemCreateInput = {
  344. id: 'action-rule',
  345. workspaceId: 'demashi',
  346. sourceAnalysisId: 'analysis-rule',
  347. sourceInsightId: 'insight-rule',
  348. sourceDecisionId: decision.id,
  349. sourceKind: 'insight',
  350. creationKey: 'same-client-creation-key',
  351. evidenceIds: ['evidence-rule-1', 'evidence-rule-2'],
  352. validationMetric: 'Collect 20 additional verified reviews.',
  353. actionType: 'data_quality',
  354. title: 'Collect additional evidence',
  355. description: '',
  356. priority: 'high',
  357. status: 'open',
  358. productKey: null,
  359. assigneeUserId: null,
  360. dueAt: null,
  361. createdBy: 'local-admin',
  362. };
  363. await repository.createAction(actionInput);
  364. await assert.rejects(
  365. repository.createAction({
  366. ...actionInput,
  367. id: 'action-rule-evidence-tampered',
  368. evidenceIds: ['evidence-rule-1'],
  369. }),
  370. rejectsWithGate('action_creation_key_conflict'),
  371. );
  372. await assert.rejects(
  373. repository.createAction({
  374. ...actionInput,
  375. id: 'action-rule-type-tampered',
  376. actionType: 'experience',
  377. }),
  378. rejectsWithGate(
  379. 'action_creation_key_conflict',
  380. 'source_decision_requires_data_quality_action',
  381. ),
  382. );
  383. await assert.rejects(
  384. repository.createAction({
  385. ...actionInput,
  386. id: 'action-rule-metric-tampered',
  387. validationMetric: 'Collect only 3 additional verified reviews.',
  388. }),
  389. rejectsWithGate('action_creation_key_conflict'),
  390. );
  391. });
  392. async function listen(app: ReturnType<typeof express>) {
  393. const server = await new Promise<ReturnType<typeof app.listen>>((resolve) => {
  394. const listening = app.listen(0, '127.0.0.1', () => resolve(listening));
  395. });
  396. const address = server.address() as AddressInfo;
  397. return {
  398. baseUrl: `http://127.0.0.1:${address.port}`,
  399. close: () => new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve())),
  400. };
  401. }
  402. async function json(response: Response): Promise<Record<string, any>> {
  403. return await response.json() as Record<string, any>;
  404. }
  405. test('insight decision routes create versions, filter current records, return details, audit, and enforce permissions', async () => {
  406. const repository = createRepository();
  407. await repository.upsertMember({
  408. workspaceId: 'demashi',
  409. userId: 'viewer-user',
  410. email: 'viewer@example.test',
  411. displayName: 'Viewer',
  412. role: 'viewer',
  413. status: 'active',
  414. });
  415. await createAnalysis(repository, { id: 'analysis-http', status: 'completed' });
  416. const authenticator: RequestAuthenticator = {
  417. async authenticate(request) {
  418. const userId = request.header('X-Test-User') || 'local-admin';
  419. return { userId, email: `${userId}@example.test`, displayName: userId, authMode: 'disabled' };
  420. },
  421. };
  422. const app = express();
  423. app.use(express.json());
  424. app.use('/api', createAuthenticationMiddleware(authenticator));
  425. app.use('/api/saas', createSaasPlatformRouter({
  426. repository,
  427. access: new WorkspaceAccessService(repository),
  428. }));
  429. const errorHandler: ErrorRequestHandler = (error, _request, response, _next) => {
  430. if (error instanceof ApiError) {
  431. response.status(error.status).json({ error: error.code });
  432. return;
  433. }
  434. if (error instanceof ZodError) {
  435. response.status(400).json({ error: 'invalid_request' });
  436. return;
  437. }
  438. response.status(500).json({ error: 'internal_error' });
  439. };
  440. app.use(errorHandler);
  441. const server = await listen(app);
  442. const basePath = `${server.baseUrl}/api/saas/workspaces/demashi/insight-decisions`;
  443. const ownerHeaders = { 'Content-Type': 'application/json', 'X-Test-User': 'local-admin' };
  444. const viewerHeaders = { 'Content-Type': 'application/json', 'X-Test-User': 'viewer-user' };
  445. try {
  446. const firstResponse = await fetch(basePath, {
  447. method: 'POST',
  448. headers: ownerHeaders,
  449. body: JSON.stringify({
  450. sourceAnalysisId: 'analysis-http',
  451. sourceInsightId: 'insight-1',
  452. decision: 'confirmed',
  453. reviewedEvidenceIds: ['evidence-1', 'evidence-1', 'evidence-2'],
  454. }),
  455. });
  456. assert.equal(firstResponse.status, 201);
  457. const first = (await json(firstResponse)).decision;
  458. assert.equal(first.version, 1);
  459. assert.equal(first.decidedBy, 'local-admin');
  460. assert.deepEqual(first.reviewedEvidenceIds, ['evidence-1', 'evidence-2']);
  461. const missingCommentResponse = await fetch(basePath, {
  462. method: 'POST',
  463. headers: ownerHeaders,
  464. body: JSON.stringify({
  465. sourceAnalysisId: 'analysis-http',
  466. sourceInsightId: 'insight-1',
  467. decision: 'rejected',
  468. reviewedEvidenceIds: ['evidence-1', 'evidence-2'],
  469. }),
  470. });
  471. assert.equal(missingCommentResponse.status, 400);
  472. assert.equal((await json(missingCommentResponse)).error, 'insight_decision_comment_required');
  473. const secondResponse = await fetch(basePath, {
  474. method: 'POST',
  475. headers: ownerHeaders,
  476. body: JSON.stringify({
  477. sourceAnalysisId: 'analysis-http',
  478. sourceInsightId: 'insight-1',
  479. decision: 'rejected',
  480. reviewedEvidenceIds: ['evidence-1', 'evidence-2'],
  481. comment: 'The evidence does not support the proposed opportunity.',
  482. }),
  483. });
  484. assert.equal(secondResponse.status, 201);
  485. const second = (await json(secondResponse)).decision;
  486. assert.equal(second.version, 2);
  487. assert.equal(second.supersedesId, first.id);
  488. const currentResponse = await fetch(
  489. `${basePath}?analysisId=analysis-http&insightId=insight-1&currentOnly=true`,
  490. { headers: viewerHeaders },
  491. );
  492. assert.equal(currentResponse.status, 200);
  493. const current = await json(currentResponse);
  494. assert.equal(current.items.length, 1);
  495. assert.equal(current.items[0].id, second.id);
  496. const historyResponse = await fetch(`${basePath}?analysisId=analysis-http`, { headers: ownerHeaders });
  497. assert.equal(historyResponse.status, 200);
  498. const history = await json(historyResponse);
  499. assert.equal(history.items.length, 2);
  500. const detailResponse = await fetch(`${basePath}/${first.id}`, { headers: viewerHeaders });
  501. assert.equal(detailResponse.status, 200);
  502. const detail = (await json(detailResponse)).decision;
  503. assert.equal(detail.id, first.id);
  504. assert.equal(detail.isCurrent, false);
  505. const viewerWrite = await fetch(basePath, {
  506. method: 'POST',
  507. headers: viewerHeaders,
  508. body: JSON.stringify({
  509. sourceAnalysisId: 'analysis-http',
  510. sourceInsightId: 'insight-1',
  511. decision: 'confirmed',
  512. reviewedEvidenceIds: ['evidence-1', 'evidence-2'],
  513. }),
  514. });
  515. assert.equal(viewerWrite.status, 403);
  516. assert.equal((await json(viewerWrite)).error, 'workspace_permission_denied');
  517. const missingDetail = await fetch(`${basePath}/00000000-0000-4000-8000-000000000000`, { headers: ownerHeaders });
  518. assert.equal(missingDetail.status, 404);
  519. assert.equal((await json(missingDetail)).error, 'insight_decision_not_found');
  520. const auditResponse = await fetch(
  521. `${server.baseUrl}/api/saas/workspaces/demashi/audit?limit=20`,
  522. { headers: ownerHeaders },
  523. );
  524. assert.equal(auditResponse.status, 200);
  525. const audit = await json(auditResponse);
  526. const decisionEntries = audit.items.filter((item: { action: string }) => item.action === 'decision.created');
  527. assert.equal(decisionEntries.length, 2);
  528. const secondEntry = decisionEntries.find((item: { entityId: string }) => item.entityId === second.id);
  529. assert.equal(secondEntry.entityType, 'insight_decision');
  530. assert.equal(secondEntry.metadata.sourceAnalysisId, 'analysis-http');
  531. assert.deepEqual(secondEntry.metadata.reviewedEvidenceIds, ['evidence-1', 'evidence-2']);
  532. assert.equal(secondEntry.metadata.version, 2);
  533. assert.equal(secondEntry.metadata.supersedesId, first.id);
  534. } finally {
  535. await server.close();
  536. }
  537. });