Files
wehub-resource-sync c889a57b6b
Test Suites / Build CI Environment (push) Has been cancelled
Test Suites / Basic Tests (push) Has been cancelled
Test Suites / End-to-End Tests (push) Has been cancelled
Test Suites / CLI Tests (push) Has been cancelled
Test Suites / Slow End-to-End Tests (push) Has been cancelled
Test Suites / Graph Database Tests (push) Has been cancelled
Test Suites / Vector DB Tests (push) Has been cancelled
Test Suites / Temporal Graph Test (push) Has been cancelled
Test Suites / Search Test on Different DBs (push) Has been cancelled
Test Suites / Example Tests (push) Has been cancelled
Test Suites / Notebook Tests (push) Has been cancelled
Test Suites / OS and Python Tests Ubuntu (push) Has been cancelled
Test Suites / OS and Python Tests Extended (push) Has been cancelled
Test Suites / LLM Test Suite (push) Has been cancelled
Test Suites / S3 File Storage Test (push) Has been cancelled
Test Suites / Run Integration Tests (push) Has been cancelled
Test Suites / MCP Tests (push) Has been cancelled
Test Suites / Docker Compose Test (push) Has been cancelled
Test Suites / Docker CI test (push) Has been cancelled
Test Suites / Relational DB Migration Tests (push) Has been cancelled
Test Suites / Distributed Cognee Test (push) Has been cancelled
Test Suites / DB Examples Tests (push) Has been cancelled
Test Suites / Test Completion Status (push) Has been cancelled
Test Suites / Claude Code Review (push) Has been cancelled
Test Suites / basic checks (push) Has been cancelled
build | Build and Push Cognee MCP Docker Image to dockerhub / docker-build-and-push (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
build | Build and Push Docker Image to dockerhub / docker-build-and-push (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges Core Functionality (3.11) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges Core Functionality (3.12) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges with Different Graph Databases (kuzu, kuzu) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges with Different Graph Databases (neo4j, neo4j) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges Examples (push) Has been cancelled
Weighted Edges Tests / Code Quality for Weighted Edges (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:02:24 +08:00

441 lines
16 KiB
Python

"""Unit tests for cognee.push() and the COGX archive transport helpers.
All tests are pure: no databases, no LLM calls, no network.
"""
import asyncio
import importlib
import io
import tarfile
import pytest
from cognee.modules.migration.archive import (
ARCHIVE_SUFFIX,
find_archive_root,
pack_archive,
unpack_archive,
)
from cognee.modules.migration.cogx import (
COGXArchiveWriter,
COGXEntity,
COGXFact,
read_archive,
read_manifest,
)
def _make_tarball(files):
"""Build an in-memory ``.tar.gz`` from ``{name: payload_bytes}``."""
buffer = io.BytesIO()
with tarfile.open(fileobj=buffer, mode="w:gz") as tar:
for name, payload in files.items():
info = tarfile.TarInfo(name=name)
info.size = len(payload)
tar.addfile(info, io.BytesIO(payload))
buffer.seek(0)
return buffer
def _write_sample_archive(directory):
with COGXArchiveWriter(directory, source_system="cognee") as writer:
writer.write(COGXEntity(external_system="cognee", external_id="e1", name="Alice"))
writer.write(COGXEntity(external_system="cognee", external_id="e2", name="Bob"))
writer.write(
COGXFact(
external_system="cognee",
external_id="e1:knows:e2",
subject_ref="e1",
predicate="knows",
object_ref="e2",
)
)
class TestArchiveTransport:
def test_pack_unpack_round_trip(self, tmp_path):
archive_dir = tmp_path / "cogx"
_write_sample_archive(archive_dir)
tar_path = tmp_path / f"sample{ARCHIVE_SUFFIX}"
pack_archive(archive_dir, tar_path)
assert tar_path.exists()
extracted = tmp_path / "extracted"
with open(tar_path, "rb") as archive_file:
root = unpack_archive(archive_file, extracted)
manifest = read_manifest(root)
assert manifest is not None
assert manifest.source_system == "cognee"
records = list(read_archive(root))
assert {record.kind for record in records} == {"entity", "fact"}
assert len(records) == 3
def test_unpack_rejects_path_traversal(self, tmp_path):
malicious = io.BytesIO()
with tarfile.open(fileobj=malicious, mode="w:gz") as tar:
payload = b"evil"
info = tarfile.TarInfo(name="../escape.txt")
info.size = len(payload)
tar.addfile(info, io.BytesIO(payload))
malicious.seek(0)
with pytest.raises(ValueError, match="Unsafe path"):
unpack_archive(malicious, tmp_path / "out")
def test_unpack_skips_symlinks(self, tmp_path):
buffer = io.BytesIO()
with tarfile.open(fileobj=buffer, mode="w:gz") as tar:
link = tarfile.TarInfo(name="link")
link.type = tarfile.SYMTYPE
link.linkname = "/etc/passwd"
tar.addfile(link)
payload = b"{}"
info = tarfile.TarInfo(name="manifest.json")
info.size = len(payload)
tar.addfile(info, io.BytesIO(payload))
buffer.seek(0)
root = unpack_archive(buffer, tmp_path / "out")
assert not (root / "link").exists()
assert (root / "manifest.json").exists()
def test_find_archive_root_nested_one_level(self, tmp_path):
nested = tmp_path / "wrapper" / "inner"
_write_sample_archive(nested)
assert find_archive_root(tmp_path / "wrapper") == nested
def test_find_archive_root_missing_manifest(self, tmp_path):
(tmp_path / "empty").mkdir()
with pytest.raises(ValueError, match="manifest.json"):
find_archive_root(tmp_path / "empty")
class TestArchiveExtractionLimits:
"""Decompression-bomb caps on ``unpack_archive`` (H4)."""
def test_rejects_too_many_members(self, tmp_path):
tarball = _make_tarball({f"file{i}.jsonl": b"{}" for i in range(4)})
with pytest.raises(ValueError, match="more than 3 members"):
unpack_archive(tarball, tmp_path / "out", max_members=3)
def test_rejects_oversized_member(self, tmp_path):
tarball = _make_tarball({"big.jsonl": b"x" * 100})
with pytest.raises(ValueError, match="per-member limit"):
unpack_archive(tarball, tmp_path / "out", max_member_bytes=10)
def test_rejects_oversized_total(self, tmp_path):
# Each member fits the per-member cap; together they exceed the total.
tarball = _make_tarball({"a.jsonl": b"x" * 60, "b.jsonl": b"y" * 60})
with pytest.raises(ValueError, match="in total"):
unpack_archive(tarball, tmp_path / "out", max_member_bytes=80, max_total_bytes=100)
def test_aborted_extraction_cleans_up(self, tmp_path):
tarball = _make_tarball({"a.jsonl": b"x" * 60, "b.jsonl": b"y" * 60})
destination = tmp_path / "out"
with pytest.raises(ValueError):
unpack_archive(tarball, destination, max_member_bytes=80, max_total_bytes=100)
# The first member was extracted before the abort; it must be removed.
assert list(destination.iterdir()) == []
def test_limits_within_bounds_extracts_normally(self, tmp_path):
archive_dir = tmp_path / "cogx"
_write_sample_archive(archive_dir)
tar_path = tmp_path / f"sample{ARCHIVE_SUFFIX}"
pack_archive(archive_dir, tar_path)
with open(tar_path, "rb") as archive_file:
root = unpack_archive(
archive_file,
tmp_path / "out",
max_members=10,
max_member_bytes=1024 * 1024,
max_total_bytes=1024 * 1024,
)
assert len(list(read_archive(root))) == 3
class TestResolveClient:
def _clear_sources(self, monkeypatch):
from cognee.api.v1.serve import credentials as credentials_module
from cognee.api.v1.serve import state
monkeypatch.setattr(state, "_remote_client", None)
monkeypatch.setattr(credentials_module, "load_credentials", lambda: None)
monkeypatch.delenv("COGNEE_SERVICE_URL", raising=False)
monkeypatch.delenv("COGNEE_API_KEY", raising=False)
def test_explicit_url_wins(self, monkeypatch):
from cognee.api.v1.push.push import _resolve_client
self._clear_sources(monkeypatch)
client, created = _resolve_client("https://explicit.example", "key1")
assert created is True
assert client.service_url == "https://explicit.example"
assert client.api_key == "key1"
def test_live_serve_connection_reused(self, monkeypatch):
from cognee.api.v1.push.push import _resolve_client
from cognee.api.v1.serve import state
from cognee.api.v1.serve.cloud_client import CloudClient
self._clear_sources(monkeypatch)
live = CloudClient("https://live.example", "live-key")
monkeypatch.setattr(state, "_remote_client", live)
client, created = _resolve_client(None, None)
assert created is False
assert client is live
def test_saved_credentials_used(self, monkeypatch):
from cognee.api.v1.push.push import _resolve_client
from cognee.api.v1.serve.credentials import CloudCredentials
self._clear_sources(monkeypatch)
from cognee.api.v1.serve import credentials as credentials_module
creds = CloudCredentials(
access_token="t", service_url="https://saved.example", api_key="saved-key"
)
monkeypatch.setattr(credentials_module, "load_credentials", lambda: creds)
client, created = _resolve_client(None, None)
assert created is True
assert client.service_url == "https://saved.example"
assert client.api_key == "saved-key"
def test_env_fallback(self, monkeypatch):
from cognee.api.v1.push.push import _resolve_client
self._clear_sources(monkeypatch)
monkeypatch.setenv("COGNEE_SERVICE_URL", "https://env.example")
monkeypatch.setenv("COGNEE_API_KEY", "env-key")
client, created = _resolve_client(None, None)
assert created is True
assert client.service_url == "https://env.example"
assert client.api_key == "env-key"
def test_env_beats_saved_credentials(self, monkeypatch):
"""Precedence matches serve(): COGNEE_SERVICE_URL wins over saved creds."""
from cognee.api.v1.push.push import _resolve_client
from cognee.api.v1.serve import credentials as credentials_module
from cognee.api.v1.serve.credentials import CloudCredentials
self._clear_sources(monkeypatch)
creds = CloudCredentials(
access_token="t", service_url="https://saved.example", api_key="saved-key"
)
monkeypatch.setattr(credentials_module, "load_credentials", lambda: creds)
monkeypatch.setenv("COGNEE_SERVICE_URL", "https://env.example")
monkeypatch.setenv("COGNEE_API_KEY", "env-key")
client, created = _resolve_client(None, None)
assert created is True
assert client.service_url == "https://env.example"
assert client.api_key == "env-key"
def test_no_connection_raises(self, monkeypatch):
from cognee.api.v1.push.push import _resolve_client
self._clear_sources(monkeypatch)
with pytest.raises(RuntimeError, match="cognee-cli serve"):
_resolve_client(None, None)
# A remote response whose items prove the server ran the migration import path.
_MIGRATION_RESPONSE = {
"status": "completed",
"items": [{"kind": "migration_import", "entities": 2, "facts": 1}],
}
def _fake_export(dataset_name="main_dataset", num_nodes=2, num_edges=1):
from cognee.modules.migration.export import ExportResult
async def fake_export(dataset, format, destination, user):
_write_sample_archive(destination)
return ExportResult(
format=format,
destination=str(destination),
dataset_name=dataset_name,
dataset_id="d-1",
num_nodes=num_nodes,
num_edges=num_edges,
)
return fake_export
class TestPush:
def _patch(self, monkeypatch, fake_export, fake_client):
push_module = importlib.import_module("cognee.api.v1.push.push")
# The package re-exports the function, shadowing the submodule for
# plain attribute access — resolve the real module via importlib.
export_module = importlib.import_module("cognee.api.v1.export.export")
monkeypatch.setattr(export_module, "export", fake_export)
monkeypatch.setattr(
push_module, "_resolve_client", lambda url, api_key: (fake_client, True)
)
return push_module
def test_push_uploads_cogx_archive(self, monkeypatch, tmp_path):
captured = {}
class FakeClient:
service_url = "https://fake.example"
closed = False
async def remember(self, data, dataset_name, **kwargs):
captured["dataset_name"] = dataset_name
captured["kwargs"] = kwargs
captured["payload"] = data.read()
return dict(_MIGRATION_RESPONSE)
async def close(self):
FakeClient.closed = True
push_module = self._patch(monkeypatch, _fake_export(), FakeClient())
result = asyncio.run(push_module.push("main_dataset", mode="hybrid"))
assert result.status == "completed"
assert result.dataset_name == "main_dataset"
assert result.target_dataset == "main_dataset"
assert result.num_nodes == 2
assert result.num_edges == 1
assert result.remote_response["status"] == "completed"
assert captured["dataset_name"] == "main_dataset"
assert captured["kwargs"]["content_type"] == "cogx-archive"
assert captured["kwargs"]["import_mode"] == "hybrid"
assert FakeClient.closed is True
# The uploaded payload is a valid, re-importable archive.
root = unpack_archive(io.BytesIO(captured["payload"]), tmp_path / "out")
assert len(list(read_archive(root))) == 3
def test_push_target_dataset_override(self, monkeypatch):
captured = {}
class FakeClient:
service_url = "https://fake.example"
async def remember(self, data, dataset_name, **kwargs):
captured["dataset_name"] = dataset_name
return dict(_MIGRATION_RESPONSE)
async def close(self):
pass
push_module = self._patch(
monkeypatch, _fake_export(dataset_name="local_name"), FakeClient()
)
result = asyncio.run(push_module.push("local_name", target_dataset="remote_name"))
assert captured["dataset_name"] == "remote_name"
assert result.target_dataset == "remote_name"
def test_push_empty_dataset_raises_before_upload(self, monkeypatch):
class FakeClient:
service_url = "https://fake.example"
remember_called = False
async def remember(self, data, dataset_name, **kwargs):
FakeClient.remember_called = True
return dict(_MIGRATION_RESPONSE)
async def close(self):
pass
push_module = self._patch(monkeypatch, _fake_export(num_nodes=0, num_edges=0), FakeClient())
with pytest.raises(ValueError, match="exported 0 nodes"):
asyncio.run(push_module.push("main_dataset"))
assert FakeClient.remember_called is False
def test_push_rejects_non_migration_response(self, monkeypatch):
"""A pre-migration server ingests the tarball as a plain file; push must fail loudly."""
class FakeClient:
service_url = "https://fake.example"
async def remember(self, data, dataset_name, **kwargs):
return {"status": "completed", "items": [{"name": "main_dataset.cogx.tar.gz"}]}
async def close(self):
pass
push_module = self._patch(monkeypatch, _fake_export(), FakeClient())
with pytest.raises(RuntimeError, match="did not perform a COGX archive import"):
asyncio.run(push_module.push("main_dataset"))
def test_push_invalid_mode(self):
from cognee.api.v1.push.push import push
with pytest.raises(ValueError, match="Unknown push mode"):
asyncio.run(push("main_dataset", mode="bogus"))
def test_push_result_is_typed_dataclass(self, monkeypatch):
"""push() returns a PushResult dataclass, not a mutated raw dict."""
import dataclasses
from cognee.api.v1.push.push import PushResult
class FakeClient:
service_url = "https://fake.example"
async def remember(self, data, dataset_name, **kwargs):
return dict(_MIGRATION_RESPONSE)
async def close(self):
pass
push_module = self._patch(monkeypatch, _fake_export(), FakeClient())
result = asyncio.run(push_module.push("main_dataset"))
assert isinstance(result, PushResult)
assert dataclasses.is_dataclass(result)
assert {field.name for field in dataclasses.fields(result)} == {
"status",
"dataset_name",
"target_dataset",
"num_nodes",
"num_edges",
"remote_response",
"pipeline_run_id",
}
assert isinstance(result.status, str)
assert isinstance(result.num_nodes, int)
assert isinstance(result.num_edges, int)
assert isinstance(result.remote_response, dict)
class TestCloudClientTimeout:
"""The push transport must survive uploads + imports longer than 5 minutes (H6)."""
def test_session_default_keeps_total_timeout(self):
"""Ordinary API calls keep a bounded total; only uploads are unbounded."""
import aiohttp
from cognee.api.v1.serve.cloud_client import CloudClient
async def get_timeout():
client = CloudClient("https://fake.example", "key")
session = await client._get_session()
try:
return session.timeout
finally:
await client.close()
timeout = asyncio.run(get_timeout())
assert isinstance(timeout, aiohttp.ClientTimeout)
assert timeout.total == 300
assert timeout.sock_connect == 30
def test_upload_timeout_is_unbounded_and_scoped_to_archives(self):
"""The per-request archive-upload timeout drops the total deadline."""
from cognee.api.v1.serve.cloud_client import CloudClient
assert CloudClient.UPLOAD_TIMEOUT.total is None
assert CloudClient.UPLOAD_TIMEOUT.sock_connect == 30
assert CloudClient.UPLOAD_TIMEOUT.sock_read == 300
assert CloudClient.DEFAULT_TIMEOUT.total == 300