fix: read project attribution from the conversation's gizmo_id
A conversation in a project absent from CHATGPT_PROJECT_IDS exported into no-project/ even though its own payload names the project. Found while investigating the media 403s: bpi-f3-case-options sits in g-p-6a4edf5160848191927b05da20f49151, which is not among the 13 configured ids, so it filed under no-project.2026. Both existing sources are bounded by what the user configured. The detail response is not: it carries gizmo_id. Use it as a third fallback, after the listing annotation and the project map, and cache the result into the map. Attribution now stays correct with no list to maintain, and moving a chat into a new project stops silently misfiling it. Only g-p- ids are treated as projects — a custom GPT is not a project and must not become a folder. CHATGPT_PROJECT_IDS still matters for the listing pass: conversations that live only inside a project never appear in the default listing, so an unconfigured project's chats can be missed entirely. Each one is now reported once per run with the id to add, which turns "some chats are missing" into a line to paste. 8 tests cover precedence, the g-p- guard, map caching and the once-per-run report. 318 pass.
This commit is contained in:
@@ -1299,3 +1299,83 @@ class TestDeletedAssetReporting:
|
||||
),
|
||||
)
|
||||
assert _classify_failure(err) == "expired-or-missing"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Project attribution: a conversation names its own project via gizmo_id, so
|
||||
# it should not depend on the user having listed that project in config.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestProjectAttribution:
|
||||
def _provider(self, *, project_ids=None, gizmo_name="My Project"):
|
||||
from src.providers.chatgpt import ChatGPTProvider
|
||||
|
||||
p = ChatGPTProvider.__new__(ChatGPTProvider)
|
||||
p._project_map = {}
|
||||
p._project_name_cache = {}
|
||||
p._project_ids = project_ids or []
|
||||
p._hidden_content = "placeholder"
|
||||
p._fetch_project_name = lambda gid: gizmo_name
|
||||
return p
|
||||
|
||||
def _raw(self, **extra):
|
||||
raw = {
|
||||
"conversation_id": "conv-1",
|
||||
"title": "T",
|
||||
"create_time": 1_700_000_000,
|
||||
"update_time": 1_700_000_000,
|
||||
"mapping": {},
|
||||
}
|
||||
raw.update(extra)
|
||||
return raw
|
||||
|
||||
def test_gizmo_id_supplies_project_when_unconfigured(self):
|
||||
p = self._provider(gizmo_name="Tech Questions")
|
||||
out = p.normalize_conversation(self._raw(gizmo_id="g-p-abc123"))
|
||||
assert out["project"] == "Tech Questions"
|
||||
|
||||
def test_explicit_annotation_still_wins(self):
|
||||
p = self._provider(gizmo_name="From Gizmo")
|
||||
out = p.normalize_conversation(
|
||||
self._raw(gizmo_id="g-p-abc123", _project_name="From Listing")
|
||||
)
|
||||
assert out["project"] == "From Listing"
|
||||
|
||||
def test_project_map_beats_gizmo_lookup(self):
|
||||
p = self._provider(gizmo_name="From Gizmo")
|
||||
p._project_map["conv-1"] = "From Map"
|
||||
out = p.normalize_conversation(self._raw(gizmo_id="g-p-abc123"))
|
||||
assert out["project"] == "From Map"
|
||||
|
||||
def test_custom_gpt_is_not_treated_as_a_project(self):
|
||||
"""Only g-p- ids are projects; a custom GPT must not become a folder."""
|
||||
p = self._provider()
|
||||
out = p.normalize_conversation(self._raw(gizmo_id="g-xyz789"))
|
||||
assert out["project"] is None
|
||||
|
||||
def test_no_gizmo_id_stays_unprojected(self):
|
||||
p = self._provider()
|
||||
assert p.normalize_conversation(self._raw())["project"] is None
|
||||
|
||||
def test_resolved_project_is_cached_into_the_map(self):
|
||||
p = self._provider(gizmo_name="Cached")
|
||||
p.normalize_conversation(self._raw(gizmo_id="g-p-abc123"))
|
||||
assert p._project_map["conv-1"] == "Cached"
|
||||
|
||||
def test_unconfigured_project_is_reported_once(self, caplog):
|
||||
p = self._provider(project_ids=["g-p-known"], gizmo_name="Surprise")
|
||||
with caplog.at_level(logging.INFO):
|
||||
p.normalize_conversation(self._raw(gizmo_id="g-p-surprise"))
|
||||
p.normalize_conversation(
|
||||
self._raw(conversation_id="conv-2", gizmo_id="g-p-surprise")
|
||||
)
|
||||
hits = [r for r in caplog.records if "not in CHATGPT_PROJECT_IDS" in r.message]
|
||||
assert len(hits) == 1
|
||||
assert "Surprise" in hits[0].message
|
||||
|
||||
def test_configured_project_is_not_reported(self, caplog):
|
||||
p = self._provider(project_ids=["g-p-known"], gizmo_name="Known")
|
||||
with caplog.at_level(logging.INFO):
|
||||
p.normalize_conversation(self._raw(gizmo_id="g-p-known"))
|
||||
assert not [r for r in caplog.records if "not in CHATGPT_PROJECT_IDS" in r.message]
|
||||
|
||||
Reference in New Issue
Block a user