From 577ef6bbdfb45e6a727db94c6dae47c806bbc073 Mon Sep 17 00:00:00 2001 From: mldangelo-oai <269034524+mldangelo-oai@users.noreply.github.com> Date: Mon, 19 Oct 2026 22:49:29 +0000 Subject: [PATCH] fix(findings): order occurrence history by scan start time --- .../scripts/workbench_scan_history.py | 3 +- .../tests/test_workbench_scan_history.py | 31 +++++++++++ .../tests-ts/workbench-scan-history.test.ts | 52 ++++++++++++++----- 3 files changed, 71 insertions(+), 15 deletions(-) diff --git a/plugins/codex-security/scripts/workbench_scan_history.py b/plugins/codex-security/scripts/workbench_scan_history.py index 8bb095921f..e0654d6a77 100644 --- a/plugins/codex-security/scripts/workbench_scan_history.py +++ b/plugins/codex-security/scripts/workbench_scan_history.py @@ -990,7 +990,8 @@ def finding_matches( ) ) known_scans = sorted( - {(started_at, scan_id)} | {(row["started_at"], row["scan_id"]) for row in linked_rows} + {(started_at, scan_id)} | {(row["started_at"], row["scan_id"]) for row in linked_rows}, + key=lambda scan: (timestamp_key(scan[0]), scan[1]), ) included = {occurrence_id, *(row["occurrence_id"] for row in rows)} rows.extend( diff --git a/plugins/codex-security/tests/test_workbench_scan_history.py b/plugins/codex-security/tests/test_workbench_scan_history.py index 67e56ba291..c3b6ed26b1 100644 --- a/plugins/codex-security/tests/test_workbench_scan_history.py +++ b/plugins/codex-security/tests/test_workbench_scan_history.py @@ -847,6 +847,37 @@ def test_cli_scan_comparison_tracks_stable_findings_without_copying_triage(tmp_p assert connection.execute("SELECT COUNT(*) FROM finding_occurrences").fetchone() == (2,) +@pytest.mark.parametrize( + ("timestamps", "expected_order"), + [ + (("2026-08-01T00:00:00Z", "2026-08-01T01:00:00+02:00"), (1, 0)), + (("2026-08-01T00:00:00.000200Z", "2026-08-01T01:00:00.000100+01:00"), (1, 0)), + (("2026-08-01T01:00:00+01:00", "2026-08-01T00:00:00Z"), (0, 1)), + ], + ids=["mixed-offsets", "microseconds", "equal-instants"], +) +def test_finding_occurrence_history_uses_start_instants( + tmp_path: Path, timestamps: tuple[str, str], expected_order: tuple[int, int] +) -> None: + state_dir = tmp_path / "state" + repository = tmp_path / "repository" + repository.mkdir() + scans = sorted( + (create_cli_scan(state_dir, tmp_path / "results", repository) for _ in range(2)), + key=lambda scan: scan["scanId"], + ) + with sqlite3.connect(state_dir / "workbench.sqlite3") as connection: + connection.executemany( + "UPDATE scans SET started_at = ? WHERE id = ?", + [(timestamp, scan["scanId"]) for scan, timestamp in zip(scans, timestamps)], + ) + + for scan in scans: + finding = get_scan(state_dir, scan["scanId"])["scan"]["findings"][0] + assert finding["knownSince"] == timestamps[expected_order[0]] + assert finding["knownScanIds"] == [scans[index]["scanId"] for index in expected_order] + + def test_scan_comparison_requires_saved_matches_and_remains_read_only(tmp_path: Path) -> None: state_dir = tmp_path / "state" repository = tmp_path / "repository" diff --git a/sdk/typescript/tests-ts/workbench-scan-history.test.ts b/sdk/typescript/tests-ts/workbench-scan-history.test.ts index 0610271e19..57a4a335f7 100644 --- a/sdk/typescript/tests-ts/workbench-scan-history.test.ts +++ b/sdk/typescript/tests-ts/workbench-scan-history.test.ts @@ -662,7 +662,7 @@ CREATE TABLE scan_comparison_matches ( ''') scans = [('a', 'a'), ('b', 'b'), ('c', 'c'), ('a-repeat', 'a'), ('c-repeat', 'c'), ('unlinked', 'unlinked')] for index, (scan, finding) in enumerate(scans): - connection.execute('INSERT INTO scans VALUES (?, ?)', (scan, str(index))) + connection.execute('INSERT INTO scans VALUES (?, ?)', (scan, f'2026-08-01T00:00:0{index}Z')) connection.execute('INSERT INTO finding_occurrences VALUES (?, ?, ?, ?)', (scan, finding, scan, scan)) connection.executemany('INSERT INTO scan_comparison_matches VALUES (?, ?, ?, ?, ?)', [ ('a', 'b', 'a', 'b', 'First confirmed link.'), @@ -671,7 +671,7 @@ connection.executemany('INSERT INTO scan_comparison_matches VALUES (?, ?, ?, ?, def collect_history(): result = {} for index, (scan, finding) in enumerate(scans): - matches, first, bounds = finding_matches(connection, scan, scan, str(index)) + matches, first, bounds = finding_matches(connection, scan, scan, f'2026-08-01T00:00:0{index}Z') result[scan] = {'linked': [match['occurrenceId'] for match in matches], 'first': first, 'bounds': bounds} for match in matches: assert match['reason'] @@ -686,38 +686,62 @@ print(json.dumps({'withLinks': with_links, 'withoutLinks': collect_history()})) withLinks: { a: { linked: ["a-repeat", "b", "c", "c-repeat"], - first: "0", + first: "2026-08-01T00:00:00Z", bounds: ["a", "c-repeat"], }, b: { linked: ["a", "a-repeat", "c", "c-repeat"], - first: "0", + first: "2026-08-01T00:00:00Z", bounds: ["a", "c-repeat"], }, c: { linked: ["a", "a-repeat", "b", "c-repeat"], - first: "0", + first: "2026-08-01T00:00:00Z", bounds: ["a", "c-repeat"], }, "a-repeat": { linked: ["a", "b", "c", "c-repeat"], - first: "0", + first: "2026-08-01T00:00:00Z", bounds: ["a", "c-repeat"], }, "c-repeat": { linked: ["a", "a-repeat", "b", "c"], - first: "0", + first: "2026-08-01T00:00:00Z", bounds: ["a", "c-repeat"], }, - unlinked: { linked: [], first: "5", bounds: ["unlinked"] }, + unlinked: { + linked: [], + first: "2026-08-01T00:00:05Z", + bounds: ["unlinked"], + }, }, withoutLinks: { - a: { linked: ["a-repeat"], first: "0", bounds: ["a", "a-repeat"] }, - "a-repeat": { linked: ["a"], first: "0", bounds: ["a", "a-repeat"] }, - b: { linked: [], first: "1", bounds: ["b"] }, - c: { linked: ["c-repeat"], first: "2", bounds: ["c", "c-repeat"] }, - "c-repeat": { linked: ["c"], first: "2", bounds: ["c", "c-repeat"] }, - unlinked: { linked: [], first: "5", bounds: ["unlinked"] }, + a: { + linked: ["a-repeat"], + first: "2026-08-01T00:00:00Z", + bounds: ["a", "a-repeat"], + }, + "a-repeat": { + linked: ["a"], + first: "2026-08-01T00:00:00Z", + bounds: ["a", "a-repeat"], + }, + b: { linked: [], first: "2026-08-01T00:00:01Z", bounds: ["b"] }, + c: { + linked: ["c-repeat"], + first: "2026-08-01T00:00:02Z", + bounds: ["c", "c-repeat"], + }, + "c-repeat": { + linked: ["c"], + first: "2026-08-01T00:00:02Z", + bounds: ["c", "c-repeat"], + }, + unlinked: { + linked: [], + first: "2026-08-01T00:00:05Z", + bounds: ["unlinked"], + }, }, }); });