"""Tests for TCOC Jira ticket ingest, dossiers, workspace links, and merge resolution.""" from __future__ import annotations import json import uuid from datetime import date import pytest from investibook.core.jira_ticket_import import ( backfill_workspace_jira_links_from_db, build_jira_import_jql, ingest_jira_issue_dict, load_jira_http_config, repair_comma_concatenated_workspace_sources, run_admin_jira_import_once, run_scheduled_tcoc_import, ensure_workspace_tcoc_links, split_jira_issue_keys, ) from investibook.db import queries as dbq def _epoch_utc(iso_s: str) -> int: dt = dbq._parse_utc_datetime(iso_s) assert dt is not None return int(dt.timestamp()) def _issue(key: str = "TCOC-42") -> dict: return { "key": key, "fields": { "summary": "CPU spike", "status": {"name": "Open"}, "issuetype": {"name": "Incident"}, "created": "2026-05-09T10:00:00.000+0000", "updated": "2026-05-09T11:00:00.000+0000", "customfield_10149": "c-1", "customfield_10157": "t-1", "customfield_10160": None, }, } @pytest.fixture def db(monkeypatch, tmp_path): p = tmp_path / "investibook.db" monkeypatch.setattr(dbq, "DB_PATH", p) dbq._db_init() return p def test_load_jira_config_requires_all(monkeypatch): monkeypatch.delenv("INVESTIBOOK_JIRA_BASE_URL", raising=False) assert load_jira_http_config() is None monkeypatch.setenv("INVESTIBOOK_JIRA_BASE_URL", "https://x.test") monkeypatch.setenv("INVESTIBOOK_JIRA_USERNAME", "u") monkeypatch.setenv("INVESTIBOOK_JIRA_API_TOKEN", "t") cfg = load_jira_http_config() assert cfg == ("https://x.test", "u", "t") def test_build_jira_import_jql_rolling_window(): jql = build_jira_import_jql("TCOC", None, None) assert "project = TCOC" in jql assert "updated >= -25h" in jql assert jql.endswith("ORDER BY updated ASC") def test_build_jira_import_jql_date_range(): jql = build_jira_import_jql("tcoc", date(2026, 5, 1), date(2026, 5, 13)) assert 'updated >= "2026-05-01"' in jql assert 'updated < "2026-05-14"' in jql def test_build_jira_import_jql_invalid_project(): with pytest.raises(ValueError, match="project_key"): build_jira_import_jql("BAD-KEY", None, None) def test_ingest_creates_ticket_and_dossier(db): k = ingest_jira_issue_dict(_issue(), "scheduled_import") assert k == "TCOC-42" row = dbq._db_get_jira_ticket("TCOC-42") assert row is not None assert row["summary"] == "CPU spike" assert row["cluster_id"] == "c-1" assert row["tenant_id"] == "t-1" assert row["jira_incident_flag"] == "yes" assert "scheduled_import" in row["provenance_json"] dossier_id = dbq._db_get_canonical_dossier_id_for_issue("TCOC-42") assert dossier_id dos_row = dbq._db_get_incident_dossier_row(dossier_id) assert dos_row is not None exp_created = _epoch_utc("2026-05-09T10:00:00.000+0000") assert dos_row["created_at"] == exp_created def test_ingest_non_incident_does_not_auto_create_dossier(db): i = _issue("TCOC-NO-DOS") i["fields"]["issuetype"] = {"name": "Task"} ingest_jira_issue_dict(i, "scheduled_import") assert dbq._db_get_jira_ticket("TCOC-NO-DOS") is not None assert dbq._db_get_canonical_dossier_id_for_issue("TCOC-NO-DOS") is None def test_jira_incident_flag_follows_issuetype(db): i_cq = _issue("TCOC-F1") i_cq["fields"]["issuetype"] = {"name": "Customer Question"} ingest_jira_issue_dict(i_cq, "scheduled_import") assert dbq._db_get_jira_ticket("TCOC-F1")["jira_incident_flag"] == "no" assert dbq._db_get_canonical_dossier_id_for_issue("TCOC-F1") is None i_inc = _issue("TCOC-F1") i_inc["fields"]["issuetype"] = {"name": "Incident"} ingest_jira_issue_dict(i_inc, "b") assert dbq._db_get_jira_ticket("TCOC-F1")["jira_incident_flag"] == "yes" assert dbq._db_get_canonical_dossier_id_for_issue("TCOC-F1") is not None i_task = _issue("TCOC-F1") i_task["fields"]["issuetype"] = {"name": "Task"} ingest_jira_issue_dict(i_task, "c") assert dbq._db_get_jira_ticket("TCOC-F1")["jira_incident_flag"] == "no" def test_jira_incident_flag_preserved_when_issuetype_omitted(db): ingest_jira_issue_dict(_issue("TCOC-F2"), "a") assert dbq._db_get_jira_ticket("TCOC-F2")["jira_incident_flag"] == "yes" i2 = _issue("TCOC-F2") del i2["fields"]["issuetype"] ingest_jira_issue_dict(i2, "b") assert dbq._db_get_jira_ticket("TCOC-F2")["jira_incident_flag"] == "yes" def test_stub_does_not_clear_jira_incident_flag(db): ingest_jira_issue_dict(_issue("TCOC-F3"), "a") assert dbq._db_get_jira_ticket("TCOC-F3")["jira_incident_flag"] == "yes" dbq._db_upsert_jira_ticket_stub("TCOC-F3", "stub") assert dbq._db_get_jira_ticket("TCOC-F3")["jira_incident_flag"] == "yes" @pytest.mark.asyncio async def test_ensure_workspace_links(db, monkeypatch): monkeypatch.setenv("INVESTIBOOK_JIRA_BASE_URL", "https://jira.test") monkeypatch.setenv("INVESTIBOOK_JIRA_USERNAME", "u") monkeypatch.setenv("INVESTIBOOK_JIRA_API_TOKEN", "t") async def fake_fetch(client, base, auth, issue_key): return _issue(issue_key) monkeypatch.setattr( "investibook.core.jira_ticket_import.fetch_jira_issue", fake_fetch, ) ws_id = "ws-test-1" with dbq._db_lock: conn = dbq._db_connect() conn.execute( """INSERT INTO workspaces (id, user_id, name, description, is_example, is_shared, created_at, updated_at) VALUES (?, ?, 'n', '', 0, 0, ?, ?)""", (ws_id, "user-1", dbq._utc_now(), dbq._utc_now()), ) conn.commit() conn.close() linked = await ensure_workspace_tcoc_links( ws_id, [ { "type": "jira_issue", "issueId": "TCOC-99", "id": "src-1", } ], "user-1", ) assert linked == ["TCOC-99"] row = dbq._db_get_jira_ticket("TCOC-99") assert row is not None @pytest.mark.asyncio async def test_ensure_workspace_links_falls_back_to_name_and_meta(db, monkeypatch): """Sources that only store TCOC key in ``name`` or ``meta`` must still link.""" monkeypatch.setenv("INVESTIBOOK_JIRA_BASE_URL", "https://jira.test") monkeypatch.setenv("INVESTIBOOK_JIRA_USERNAME", "u") monkeypatch.setenv("INVESTIBOOK_JIRA_API_TOKEN", "t") async def fake_fetch(client, base, auth, issue_key): return _issue(issue_key) monkeypatch.setattr( "investibook.core.jira_ticket_import.fetch_jira_issue", fake_fetch, ) ws_id = "ws-test-fallback" with dbq._db_lock: conn = dbq._db_connect() conn.execute( """INSERT INTO workspaces (id, user_id, name, description, is_example, is_shared, created_at, updated_at) VALUES (?, ?, 'n', '', 0, 0, ?, ?)""", (ws_id, "user-1", dbq._utc_now(), dbq._utc_now()), ) conn.commit() conn.close() linked = await ensure_workspace_tcoc_links( ws_id, [ { "type": "jira_issue", "name": "TCOC-200", "id": "src-a", }, { "type": "jira_issue", "name": "Human title", "id": "src-b", "meta": {"alert_id": "TCOC-201"}, }, ], "user-1", ) assert set(linked) == {"TCOC-200", "TCOC-201"} assert dbq._db_get_jira_ticket("TCOC-200") is not None assert dbq._db_get_jira_ticket("TCOC-201") is not None @pytest.mark.asyncio async def test_ensure_workspace_links_o11y_and_legacy_alert_type(db, monkeypatch): monkeypatch.setenv("INVESTIBOOK_JIRA_BASE_URL", "https://jira.test") monkeypatch.setenv("INVESTIBOOK_JIRA_USERNAME", "u") monkeypatch.setenv("INVESTIBOOK_JIRA_API_TOKEN", "t") async def fake_fetch(client, base, auth, issue_key): return _issue(issue_key) monkeypatch.setattr( "investibook.core.jira_ticket_import.fetch_jira_issue", fake_fetch, ) ws_id = "ws-o11y" with dbq._db_lock: conn = dbq._db_connect() conn.execute( """INSERT INTO workspaces (id, user_id, name, description, is_example, is_shared, created_at, updated_at) VALUES (?, ?, 'n', '', 0, 0, ?, ?)""", (ws_id, "user-1", dbq._utc_now(), dbq._utc_now()), ) conn.commit() conn.close() linked = await ensure_workspace_tcoc_links( ws_id, [ {"type": "jira_issue", "issueId": "O11Y-5001", "id": "s-o11y"}, {"type": "alert", "issueId": "TCID-7000", "id": "s-legacy"}, ], "user-1", ) assert set(linked) == {"O11Y-5001", "TCID-7000"} assert dbq._db_get_jira_ticket("O11Y-5001") is not None assert dbq._db_get_jira_ticket("TCID-7000") is not None @pytest.mark.asyncio async def test_ensure_workspace_links_skips_unknown_project(db, monkeypatch): monkeypatch.setenv("INVESTIBOOK_JIRA_BASE_URL", "https://jira.test") monkeypatch.setenv("INVESTIBOOK_JIRA_USERNAME", "u") monkeypatch.setenv("INVESTIBOOK_JIRA_API_TOKEN", "t") ws_id = "ws-skip" with dbq._db_lock: conn = dbq._db_connect() conn.execute( """INSERT INTO workspaces (id, user_id, name, description, is_example, is_shared, created_at, updated_at) VALUES (?, ?, 'n', '', 0, 0, ?, ?)""", (ws_id, "user-1", dbq._utc_now(), dbq._utc_now()), ) conn.commit() conn.close() diag: dict[str, int] = {} linked = await ensure_workspace_tcoc_links( ws_id, [{"type": "jira_issue", "issueId": "FOO-1", "id": "x"}], "user-1", diag=diag, ) assert linked == [] assert diag.get("skipped_prefix", 0) >= 1 @pytest.mark.asyncio async def test_backfill_scoped_single_workspace_only(db, monkeypatch): """Regression: local workspace shape (ST-*) must link when backfill is scoped.""" monkeypatch.delenv("INVESTIBOOK_JIRA_BASE_URL", raising=False) monkeypatch.delenv("JIRA_BASE_URL", raising=False) ws_only = "ws-scope-1" ws_other = "ws-scope-2" now = dbq._utc_now() st_blob = [ { "type": "jira_issue", "name": "ST-1523", "issueId": "ST-1523", "status": "ready", "id": "src-1778226900610-65f9d1a82", "createdAt": "2026-05-08T07:55:00.610Z", "issueDir": "issues/ST-1523", } ] other_blob = [ {"type": "jira_issue", "issueId": "ST-8888", "id": "s2"}, ] with dbq._db_lock: conn = dbq._db_connect() for wid in (ws_only, ws_other): conn.execute( """INSERT INTO workspaces (id, user_id, name, description, is_example, is_shared, created_at, updated_at) VALUES (?, ?, 'n', '', 0, 0, ?, ?)""", (wid, "user-1", now, now), ) conn.execute( "INSERT INTO workspace_sources (workspace_id, data) VALUES (?, ?)", (ws_only, json.dumps(st_blob)), ) conn.execute( "INSERT INTO workspace_sources (workspace_id, data) VALUES (?, ?)", (ws_other, json.dumps(other_blob)), ) conn.commit() conn.close() stats = await backfill_workspace_jira_links_from_db(workspace_id=ws_only) assert stats["scoped_workspace_id"] == ws_only assert stats.get("links", 0) >= 1 assert dbq._db_get_jira_ticket("ST-1523") is not None assert dbq._db_get_jira_ticket("ST-8888") is None @pytest.mark.asyncio async def test_backfill_unknown_workspace_raises(db): with pytest.raises(ValueError, match="Workspace not found"): await backfill_workspace_jira_links_from_db(workspace_id="ws-does-not-exist-xyz") @pytest.mark.asyncio async def test_run_scheduled_import_mocked(db, monkeypatch): monkeypatch.setenv("INVESTIBOOK_JIRA_BASE_URL", "https://jira.test") monkeypatch.setenv("INVESTIBOOK_JIRA_USERNAME", "u") monkeypatch.setenv("INVESTIBOOK_JIRA_API_TOKEN", "t") async def fake_page(client, base, auth, **kwargs): return {"issues": [_issue("TCOC-77")], "isLast": True} monkeypatch.setattr( "investibook.core.jira_ticket_import._search_tcoc_page", fake_page, ) stats = await run_scheduled_tcoc_import() assert stats.get("skipped") is False assert stats.get("issues") == 1 assert dbq._db_get_jira_ticket("TCOC-77") is not None assert ( dbq._db_get_jira_import_state("tcoc_max_jira_updated") == "2026-05-09T11:00:00.000+0000" ) @pytest.mark.asyncio async def test_run_admin_import_does_not_advance_tcoc_cursor(db, monkeypatch): monkeypatch.setenv("INVESTIBOOK_JIRA_BASE_URL", "https://jira.test") monkeypatch.setenv("INVESTIBOOK_JIRA_USERNAME", "u") monkeypatch.setenv("INVESTIBOOK_JIRA_API_TOKEN", "t") dbq._db_set_jira_import_state("tcoc_max_jira_updated", "2020-01-01T00:00:00.000+0000") async def fake_page(client, base, auth, **kwargs): return {"issues": [_issue("O11Y-77")], "isLast": True} monkeypatch.setattr( "investibook.core.jira_ticket_import._search_tcoc_page", fake_page, ) await run_admin_jira_import_once( project_key="O11Y", updated_from=date(2026, 5, 1), updated_to=date(2026, 5, 2), ) assert dbq._db_get_jira_import_state("tcoc_max_jira_updated") == ( "2020-01-01T00:00:00.000+0000" ) def test_resolve_canonical_dossier_after_merge(db): ingest_jira_issue_dict(_issue("TCOC-A1"), "scheduled_import") ingest_jira_issue_dict(_issue("TCOC-A2"), "scheduled_import") d1 = dbq._db_get_canonical_dossier_id_for_issue("TCOC-A1") d2 = dbq._db_get_canonical_dossier_id_for_issue("TCOC-A2") assert d1 != d2 with dbq._db_lock: conn = dbq._db_connect() conn.execute( "UPDATE incident_dossiers SET merged_into_dossier_id = ?, status = 'merged' WHERE id = ?", (d2, d1), ) conn.commit() conn.close() assert dbq._db_resolve_canonical_dossier_id(d1) == d2 assert dbq._db_get_canonical_dossier_id_for_issue("TCOC-A1") == d2 def test_clear_jira_incident_import_data(db): ingest_jira_issue_dict(_issue("TCOC-Z1"), "scheduled_import") assert dbq._db_get_jira_ticket("TCOC-Z1") is not None stats = dbq._db_clear_jira_incident_import_data() assert stats.get("jira_tickets", 0) >= 1 assert stats.get("incident_dossiers", 0) >= 1 assert dbq._db_get_jira_ticket("TCOC-Z1") is None assert dbq._db_get_canonical_dossier_id_for_issue("TCOC-Z1") is None def test_jira_upsert_normalizes_jira_created_offset(db): ingest_jira_issue_dict(_issue("TCOC-NORM1"), "scheduled_import") row = dbq._db_get_jira_ticket("TCOC-NORM1") assert row is not None jc = str(row["jira_created_at"]) assert "+0000" not in jc assert "T" in jc def test_split_jira_issue_keys(): assert split_jira_issue_keys("O11Y-1") == ["O11Y-1"] assert split_jira_issue_keys( "O11Y-2335030,O11Y-2335028,O11Y-2335027" ) == ["O11Y-2335030", "O11Y-2335028", "O11Y-2335027"] @pytest.mark.asyncio async def test_ensure_workspace_links_splits_comma_separated_keys(db, monkeypatch): monkeypatch.delenv("INVESTIBOOK_JIRA_BASE_URL", raising=False) ws_id = "ws-comma-split" with dbq._db_lock: conn = dbq._db_connect() conn.execute( """INSERT INTO workspaces (id, user_id, name, description, is_example, created_at, updated_at) VALUES (?, ?, 'n', '', 0, ?, ?)""", (ws_id, "user-1", dbq._utc_now(), dbq._utc_now()), ) conn.commit() conn.close() combined = "O11Y-2335030,O11Y-2335028,O11Y-2335027" linked = await ensure_workspace_tcoc_links( ws_id, [{"type": "jira_issue", "issueId": combined, "id": "s-multi"}], "user-1", ) assert set(linked) == { "O11Y-2335030", "O11Y-2335028", "O11Y-2335027", } assert dbq._db_get_jira_ticket(combined) is None def test_repair_comma_concatenated_workspace_sources(db): ws_id = "ws-repair-comma" combined = "O11Y-2335030,O11Y-2335028,O11Y-2335027" blob = [ { "type": "jira_issue", "id": "src-combined", "name": combined, "issueId": combined, "issueDir": f"issues/{combined}", "status": "ready", } ] with dbq._db_lock: conn = dbq._db_connect() conn.execute( """INSERT INTO workspaces (id, user_id, name, description, is_example, created_at, updated_at) VALUES (?, ?, 'n', '', 0, ?, ?)""", (ws_id, "user-1", dbq._utc_now(), dbq._utc_now()), ) conn.execute( "INSERT INTO workspace_sources (workspace_id, data) VALUES (?, ?)", (ws_id, json.dumps(blob)), ) conn.execute( """INSERT INTO jira_tickets ( issue_key, project_key, summary, status, issue_type, cluster_id, jira_created_at, jira_updated_at, raw_payload_json, first_seen_at, last_jira_sync_at ) VALUES (?, 'O11Y', '', '', '', '', NULL, NULL, '{}', ?, ?)""", (combined, dbq._utc_epoch_now(), dbq._utc_epoch_now()), ) conn.execute( """INSERT INTO workspace_jira_links ( id, workspace_id, issue_key, source_id, user_id, linked_at ) VALUES (?, ?, ?, 'src-combined', 'user-1', ?)""", (str(uuid.uuid4()), ws_id, combined, dbq._utc_now()), ) conn.commit() conn.close() stats = repair_comma_concatenated_workspace_sources(workspace_id=ws_id) assert stats["sources_repaired"] == 1 assert stats["keys_split"] == 3 assert stats["bad_links_removed"] == 1 assert stats["bad_tickets_removed"] == 1 sources = dbq._db_get_blob("workspace_sources", ws_id) or [] jira_sources = [s for s in sources if s.get("type") == "jira_issue"] assert len(jira_sources) == 3 assert {s["issueId"] for s in jira_sources} == { "O11Y-2335030", "O11Y-2335028", "O11Y-2335027", } assert dbq._db_get_jira_ticket(combined) is None conn = dbq._db_connect() bad = conn.execute( "SELECT 1 FROM workspace_jira_links WHERE issue_key LIKE '%,%'" ).fetchone() conn.close() assert bad is None