from fastapi.testclient import TestClient from memrelay.projects import normalize_git_remote from tests.conftest import csrf_headers from tests.fakes import FakeBasicMemory def test_git_remote_normalization() -> None: assert normalize_git_remote("git@GitHub.com:Owner/Repo.git") == "git://github.com/owner/repo" assert ( normalize_git_remote("https://user@example.com/Team/Repo/") == "git://example.com/team/repo" ) def test_create_list_resolve_and_archive_project(initialized_client: TestClient) -> None: client = initialized_client created = client.post( "/api/v1/projects", json={ "name": "MemRelay", "git_remote": "git@github.com:nixevol/MemRelay.git", "aliases": ["E:\\NIXProject\\MemRelay"], }, headers=csrf_headers(client), ) assert created.status_code == 201 project = created.json() assert project["slug"] == "memrelay" assert project["git_remote"] == "git@github.com:nixevol/MemRelay.git" assert project["git_remote_normalized"] == "git://github.com/nixevol/memrelay" assert project["workspace_type"] == "development" assert len(client.get("/api/v1/projects").json()) == 1 by_remote = client.post( "/api/v1/projects/resolve", json={"git_remote": "https://github.com/NIXEVOL/MemRelay.git"}, ) assert by_remote.json()["id"] == project["id"] by_path = client.post("/api/v1/projects/resolve", json={"directory": "e:/nixproject/memrelay/"}) assert by_path.json()["id"] == project["id"] def test_resolve_remembers_a_new_device_directory(initialized_client: TestClient) -> None: client = initialized_client project = client.post( "/api/v1/projects", json={ "name": "Cross-device project", "git_remote": "https://example.test/team/cross-device.git", }, headers=csrf_headers(client), ).json() first = client.post( "/api/v1/projects/resolve", json={ "git_remote": "git@example.test:team/cross-device.git", "directory": "D:\\Work\\CrossDevice", }, ) assert first.status_code == 200 assert first.json()["id"] == project["id"] assert "d:/work/crossdevice" in first.json()["aliases"] second = client.post( "/api/v1/projects/resolve", json={"directory": "d:/WORK/crossdevice/"}, ) assert second.status_code == 200 assert second.json()["id"] == project["id"] renamed = client.patch( f"/api/v1/projects/{project['id']}", json={"name": "MemRelay Renamed"}, headers=csrf_headers(client), ) assert renamed.status_code == 200 assert renamed.json()["name"] == "MemRelay Renamed" assert renamed.json()["slug"] == "memrelay-renamed" assert ( client.delete(f"/api/v1/projects/{project['id']}", headers=csrf_headers(client)).status_code == 409 ) archived = client.post( f"/api/v1/projects/{project['id']}/archive", headers=csrf_headers(client) ) assert archived.json()["archived"] is True deleted = client.delete(f"/api/v1/projects/{project['id']}", headers=csrf_headers(client)) assert deleted.status_code == 204 assert client.get("/api/v1/projects").json() == [] def test_duplicate_project_is_rejected(initialized_client: TestClient) -> None: client = initialized_client payload = {"name": "Same Project", "git_remote": "git@example.com:team/repo.git"} assert ( client.post("/api/v1/projects", json=payload, headers=csrf_headers(client)).status_code == 201 ) assert ( client.post("/api/v1/projects", json=payload, headers=csrf_headers(client)).status_code == 409 ) def test_project_without_remote_defaults_to_general_workspace( initialized_client: TestClient, ) -> None: response = initialized_client.post( "/api/v1/projects", json={"name": "General notes"}, headers=csrf_headers(initialized_client), ) assert response.status_code == 201 assert response.json()["workspace_type"] == "general" def test_projects_without_git_remote_do_not_conflict_and_source_settings_round_trip( initialized_client: TestClient, ) -> None: client = initialized_client first = client.post( "/api/v1/projects", json={"name": "Notes A", "curation_enabled": False, "source_strategy": "mcp"}, headers=csrf_headers(client), ) second = client.post( "/api/v1/projects", json={"name": "Notes B"}, headers=csrf_headers(client), ) assert first.status_code == 201, first.text assert second.status_code == 201, second.text assert first.json()["git_remote"] is None assert first.json()["git_remote_normalized"] is None assert first.json()["curation_enabled"] is False assert first.json()["source_strategy"] == "mcp" updated = client.patch( f"/api/v1/projects/{first.json()['id']}", json={ "git_remote": "https://example.test/Owner/Notes.git", "curation_enabled": True, "source_strategy": "git", "source_branch": "release/docs", "source_credential_item_id": "vault-item-1", }, headers=csrf_headers(client), ) assert updated.status_code == 200, updated.text project = updated.json() assert project["git_remote"] == "https://example.test/Owner/Notes.git" assert project["git_remote_normalized"] == "git://example.test/owner/notes" assert project["source_strategy"] == "git" assert project["source_branch"] == "release/docs" assert project["source_credential_item_id"] == "vault-item-1" assert project["source_last_commit"] is None assert project["last_agent_sync_at"] is None def test_project_custom_curation_template_can_be_updated_and_cleared( initialized_client: TestClient, ) -> None: client = initialized_client created = client.post( "/api/v1/projects", json={ "name": "Custom Curation", "custom_curation_template": "Always preserve experiment identifiers.", }, headers=csrf_headers(client), ) assert created.status_code == 201 project = created.json() assert project["custom_curation_template"] == "Always preserve experiment identifiers." prompt = client.app.state.curation._document_prompt( "Evidence", ["overview"], project["name"], project["workspace_type"], project["custom_curation_template"], ) assert "Always preserve experiment identifiers." in prompt updated = client.patch( f"/api/v1/projects/{project['id']}", json={"custom_curation_template": "Prefer a dated laboratory timeline."}, headers=csrf_headers(client), ) assert updated.status_code == 200 assert updated.json()["custom_curation_template"] == "Prefer a dated laboratory timeline." cleared = client.patch( f"/api/v1/projects/{project['id']}", json={"custom_curation_template": None}, headers=csrf_headers(client), ) assert cleared.status_code == 200 assert cleared.json()["custom_curation_template"] is None def test_deleting_archived_project_removes_its_memories(initialized_client: TestClient) -> None: client = initialized_client fake = FakeBasicMemory() client.app.state.basic_memory = fake project = client.post( "/api/v1/projects", json={"name": "Disposable Project"}, headers=csrf_headers(client), ).json() memory = client.post( "/api/v1/memories", json={ "request_id": "project-delete-memory", "scope": "project", "project_id": project["id"], "memory_type": "fact", "title": "Disposable memory", "content": "Removed with its project.", }, headers=csrf_headers(client), ) assert memory.status_code == 201 assert fake.notes assert ( client.post( f"/api/v1/projects/{project['id']}/archive", headers=csrf_headers(client) ).status_code == 200 ) assert ( client.delete(f"/api/v1/projects/{project['id']}", headers=csrf_headers(client)).status_code == 204 ) assert fake.notes == {} assert client.get("/api/v1/memories", params={"project_id": project["id"]}).json() == []