From 0a1b1321a51baa063f02150d31d1ca36227031b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tao=20Bojl=C3=A9n?= Date: Mon, 17 Aug 2026 10:01:25 +0100 Subject: [PATCH] Fix investigation rule result selection Co-authored-by: Amp Amp-Thread-ID: https://ampcode.com/threads/T-019fec69-19ab-77e8-b6f0-85af1ed67eed --- .../investigation/ItemInvestigation.tsx | 1 + .../ItemInvestigationRuleResults.test.tsx | 110 ++++++++++++++++++ .../ItemInvestigationRuleResults.tsx | 53 +++++---- 3 files changed, 140 insertions(+), 24 deletions(-) create mode 100644 client/src/webpages/dashboard/investigation/ItemInvestigationRuleResults.test.tsx diff --git a/client/src/webpages/dashboard/investigation/ItemInvestigation.tsx b/client/src/webpages/dashboard/investigation/ItemInvestigation.tsx index c0ae2937..e3176c54 100644 --- a/client/src/webpages/dashboard/investigation/ItemInvestigation.tsx +++ b/client/src/webpages/dashboard/investigation/ItemInvestigation.tsx @@ -436,6 +436,7 @@ export default function ItemInvestigation(props: { /> diff --git a/client/src/webpages/dashboard/investigation/ItemInvestigationRuleResults.test.tsx b/client/src/webpages/dashboard/investigation/ItemInvestigationRuleResults.test.tsx new file mode 100644 index 00000000..69373d75 --- /dev/null +++ b/client/src/webpages/dashboard/investigation/ItemInvestigationRuleResults.test.tsx @@ -0,0 +1,110 @@ +import { render, screen, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { MemoryRouter } from 'react-router-dom'; +import { describe, expect, it, vi } from 'vitest'; + +import { useGQLInvestigationItemsQuery } from '../../../graphql/generated'; +import ItemInvestigationRuleResults from './ItemInvestigationRuleResults'; + +vi.mock('../../../graphql/generated', async (importOriginal) => ({ + ...(await importOriginal()), + useGQLInvestigationItemsQuery: vi.fn(), + useGQLMatchingBankNamesQuery: vi.fn(() => ({ + loading: false, + error: undefined, + data: undefined, + })), +})); + +const execution = ( + outcome: 'PASSED' | 'FAILED', + timestamp: string, + conditionDetail: string, +) => ({ + __typename: 'RuleExecutionResult', + date: timestamp, + ts: timestamp, + contentId: 'content', + itemTypeName: 'Post', + itemTypeId: 'post', + content: '{}', + environment: 'LIVE', + passed: outcome === 'PASSED', + ruleId: 'repeated-rule', + ruleName: 'Repeated Rule', + policies: [], + tags: [], + result: { + __typename: 'ConditionSetWithResult', + conjunction: 'AND', + conditions: [ + { + __typename: 'LeafConditionWithResult', + input: { + __typename: 'ConditionInputField', + type: 'CONTENT_FIELD', + name: conditionDetail, + }, + comparator: 'EQUALS', + result: { __typename: 'ConditionResult', outcome }, + }, + ], + result: { __typename: 'ConditionResult', outcome }, + }, +}); + +describe('ItemInvestigationRuleResults', () => { + it('shows the stored result for the selected execution', () => { + vi.mocked(useGQLInvestigationItemsQuery).mockReturnValue({ + loading: false, + error: undefined, + data: { + __typename: 'Query', + itemWithHistory: { + __typename: 'ItemHistoryResult', + item: { + __typename: 'ContentItem', + id: 'item', + submissionId: 'submission', + type: { __typename: 'ContentItemType', id: 'post' }, + }, + executions: [ + execution( + 'FAILED', + '2026-01-01T20:11:00.000Z', + 'Older execution field', + ), + execution( + 'PASSED', + '2026-01-01T20:13:00.000Z', + 'Newer execution field', + ), + ], + }, + }, + } as unknown as ReturnType); + + render( + + + , + ); + + const failedRow = screen + .getAllByRole('row') + .find((row) => within(row).queryByText('Did Not Match'))!; + userEvent.click(failedRow); + + const dialog = screen.getByRole('dialog'); + expect(within(dialog).getByText('Rule Result: Repeated Rule')).toBeTruthy(); + const outcome = within(dialog).getByText('Outcome:').parentElement; + expect(outcome?.textContent).toContain('Did Not Match'); + expect(outcome?.textContent).not.toContain('Matched'); + expect(within(dialog).getByText('Older execution field')).toBeTruthy(); + expect(within(dialog).queryByText('Newer execution field')).toBeNull(); + }); +}); diff --git a/client/src/webpages/dashboard/investigation/ItemInvestigationRuleResults.tsx b/client/src/webpages/dashboard/investigation/ItemInvestigationRuleResults.tsx index 9a6cdb91..8304c285 100644 --- a/client/src/webpages/dashboard/investigation/ItemInvestigationRuleResults.tsx +++ b/client/src/webpages/dashboard/investigation/ItemInvestigationRuleResults.tsx @@ -24,41 +24,48 @@ import Table from '../components/table/Table'; import { GQLConditionOutcome, + GQLInvestigationItemsQuery, + GQLItemType, useGQLInvestigationItemsQuery, } from '../../../graphql/generated'; import { ReadonlyDeep } from '../../../utils/typescript-types'; -import { LookbackVersion } from '../rules/info/insights/RuleInsightsSamplesTable'; -import RuleInsightsSampleDetailResults, { +import { getDisplayName, outcomeIcon, + RuleInsightsSampleDetailResultsImpl, } from '../rules/info/insights/sample_details/RuleInsightsSampleDetailResults'; +import type { ConditionSetWithResult } from '../rules/types'; import InvestigationTag from './InvestigationTag'; +type InvestigationExecution = Extract< + GQLInvestigationItemsQuery['itemWithHistory'], + { readonly __typename: 'ItemHistoryResult' } +>['executions'][number]; +type InvestigationExecutionResult = InvestigationExecution['result']; + export default function ItemInvestigationRuleResults(props: { itemIdentifier: ItemIdentifier; + itemTypes: readonly GQLItemType[]; submissionTime?: string; rules: Readonly[]>; }) { - const { rules, itemIdentifier, submissionTime } = props; + const { rules, itemIdentifier, submissionTime, itemTypes } = props; const navigate = useNavigate(); const [modalInfo, setModalInfo] = useState< | { visible: false; title: undefined; - ruleId: undefined; - contentId: undefined; + result: undefined; } | { visible: true; title: string; - ruleId: string; - contentId: string; + result: InvestigationExecutionResult; } >({ visible: false, title: undefined, - ruleId: undefined, - contentId: undefined, + result: undefined, }); const { @@ -250,8 +257,7 @@ export default function ItemInvestigationRuleResults(props: { setModalInfo({ visible: false, title: undefined, - ruleId: undefined, - contentId: undefined, + result: undefined, }); const modal = ( @@ -262,28 +268,27 @@ export default function ItemInvestigationRuleResults(props: { > {modalInfo.visible && (
- + {modalInfo.result ? ( + + ) : ( +
Rule result is unavailable
+ )}
)} ); const onSelectRow = (rowData: Row) => { - const executionResult = ruleExecutionsHistory[rowData.index]; - if (executionResult == null) { - return; - } - setModalInfo({ visible: true, title: `Rule Result: ${rowData.original.rule}`, - ruleId: executionResult.ruleId, - contentId: executionResult.contentId, + result: rowData.original.ruleExecutionResult, }); };