eigent/backend/tests/app/controller/test_file_controller.py

489 lines
15 KiB
Python

# ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
import os
import time
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import MagicMock
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.testclient import TestClient
from app import api as brain_api
from app.auth.local_control import LOCAL_CONTROL_CAPABILITY_HEADER
from app.controller import file_controller
def test_brain_cors_exposes_file_preview_metadata_headers():
cors = next(
middleware
for middleware in brain_api.user_middleware
if middleware.cls is CORSMiddleware
)
exposed = {header.lower() for header in cors.kwargs["expose_headers"]}
assert {
"accept-ranges",
"content-disposition",
"content-length",
"content-range",
"last-modified",
} <= exposed
def test_resolve_project_root_prefers_user_id_root(
monkeypatch, tmp_path, caplog
):
eigent_root = tmp_path / "eigent"
user_project = eigent_root / "user_20" / "project_p1"
user_project.mkdir(parents=True)
(eigent_root / "other_user" / "project_p1").mkdir(parents=True)
monkeypatch.setattr(
file_controller, "_get_eigent_root", lambda: eigent_root
)
resolved = file_controller._resolve_project_root(
"yueming.lai@example.com", "p1", "20"
)
assert resolved == user_project
assert "Resolved project root via fallback lookup" not in caplog.text
def test_resolve_project_root_falls_back_to_legacy_email_root(
monkeypatch, tmp_path
):
eigent_root = tmp_path / "eigent"
legacy_project = eigent_root / "yueming.lai" / "project_p1"
legacy_project.mkdir(parents=True)
monkeypatch.setattr(
file_controller, "_get_eigent_root", lambda: eigent_root
)
resolved = file_controller._resolve_project_root(
"yueming.lai@example.com", "p1", "20"
)
assert resolved == legacy_project
def test_resolve_project_root_does_not_fallback_to_other_user_root(
monkeypatch, tmp_path
):
eigent_root = tmp_path / "eigent"
(eigent_root / "user_20" / "project_p1").mkdir(parents=True)
expected = eigent_root / "user_42" / "project_p1"
monkeypatch.setattr(
file_controller, "_get_eigent_root", lambda: eigent_root
)
resolved = file_controller._resolve_project_root(
"yueming.lai@example.com", "p1", "42"
)
assert resolved == expected
def test_resolve_project_root_without_user_id_stays_email_scoped(
monkeypatch, tmp_path
):
eigent_root = tmp_path / "eigent"
(eigent_root / "user_20" / "project_p1").mkdir(parents=True)
expected = eigent_root / "yueming.lai" / "project_p1"
monkeypatch.setattr(
file_controller, "_get_eigent_root", lambda: eigent_root
)
resolved = file_controller._resolve_project_root(
"yueming.lai@example.com", "p1"
)
assert resolved == expected
def test_task_changes_include_all_outputs_but_only_recent_workspace_edits(
tmp_path,
):
output_root = tmp_path / "outputs"
working_root = tmp_path / "workspace"
output_root.mkdir()
working_root.mkdir()
started_at = time.time()
copied_output = output_root / "copied-report.csv"
copied_output.write_text("output", encoding="utf-8")
old_workspace_file = working_root / "existing.md"
old_workspace_file.write_text("old", encoding="utf-8")
old_time = started_at - 60
os.utime(copied_output, (old_time, old_time))
os.utime(old_workspace_file, (old_time, old_time))
# Old files must be filtered before the 500-item result bound. Otherwise
# a large selected folder can hide a recent artifact later in the walk.
for index in range(510):
old_file = working_root / f"old-{index:03d}.txt"
old_file.write_text("old", encoding="utf-8")
os.utime(old_file, (old_time, old_time))
edited_workspace_file = working_root / "reports" / "final.md"
edited_workspace_file.parent.mkdir()
edited_workspace_file.write_text("new", encoding="utf-8")
files = file_controller._list_task_changed_files(
SimpleNamespace(
task_output_root=str(output_root),
working_directory=str(working_root),
task_start_time=started_at,
)
)
assert {item["path"] for item in files} == {
str(copied_output.resolve()),
str(edited_workspace_file.resolve()),
}
assert {item["relativePath"] for item in files} == {
"copied-report.csv",
"reports/final.md",
}
assert {item["path"]: item["changeType"] for item in files} == {
str(copied_output.resolve()): "generated",
str(edited_workspace_file.resolve()): "changed",
}
assert {item["path"]: item["size"] for item in files} == {
str(copied_output.resolve()): 6,
str(edited_workspace_file.resolve()): 3,
}
assert all(item["modifiedAt"] > 0 for item in files)
def test_task_changes_exclude_files_written_after_run_attempt(tmp_path):
output_root = tmp_path / "outputs"
working_root = tmp_path / "shared-space"
output_root.mkdir()
working_root.mkdir()
started_at = time.time() - 120
ended_at = started_at + 30
current_run_file = working_root / "bank-transfer.csv"
current_run_file.write_text("current", encoding="utf-8")
os.utime(
current_run_file,
(started_at + 10, started_at + 10),
)
future_run_file = working_root / "long-horizon" / "report.md"
future_run_file.parent.mkdir()
future_run_file.write_text("future", encoding="utf-8")
os.utime(
future_run_file,
(ended_at + 60, ended_at + 60),
)
files = file_controller._list_task_changed_files(
SimpleNamespace(
task_output_root=str(output_root),
working_directory=str(working_root),
task_start_time=started_at,
),
modification_windows=((started_at - 1, ended_at),),
)
assert [item["relativePath"] for item in files] == ["bank-transfer.csv"]
def test_task_changes_skip_file_deleted_between_walk_and_stat(
monkeypatch, tmp_path
):
output_root = tmp_path / "outputs"
output_root.mkdir()
disappearing = output_root / "ephemeral.txt"
disappearing.write_text("temporary", encoding="utf-8")
original_stat = Path.stat
target_stat_calls = 0
monkeypatch.setattr(
file_controller,
"list_files",
lambda *_args, **_kwargs: [str(disappearing)],
)
def disappearing_stat(path, *args, **kwargs):
nonlocal target_stat_calls
if path == disappearing:
target_stat_calls += 1
if target_stat_calls >= 2:
raise FileNotFoundError(path)
return original_stat(path, *args, **kwargs)
monkeypatch.setattr(Path, "stat", disappearing_stat)
files = file_controller._list_task_changed_files(
SimpleNamespace(
task_output_root=str(output_root),
working_directory=str(output_root),
task_start_time=time.time() - 10,
)
)
assert files == []
def test_task_changes_endpoint_freezes_completed_run_manifest(
monkeypatch, tmp_path
):
monkeypatch.setenv("EIGENT_RUNTIME", "electron")
monkeypatch.setenv("EIGENT_LOCAL_CONTROL_CAPABILITY", "secret-1")
started_at = time.time() - 120
ended_at = started_at + 30
changed_file = tmp_path / "report.csv"
changed_file.write_text("report", encoding="utf-8")
os.utime(changed_file, (started_at + 10, started_at + 10))
snapshot = SimpleNamespace(
project_id="project-1",
task_output_root=str(tmp_path / "outputs"),
working_directory=str(tmp_path),
task_start_time=started_at,
artifact_manifest=None,
)
resolver = MagicMock()
resolver.store.get_snapshot.return_value = snapshot
journal = MagicMock()
journal.get_run.return_value = SimpleNamespace(
project_id="project-1",
status="completed",
created_at=started_at,
updated_at=ended_at,
)
journal.list_run_attempts.return_value = [
SimpleNamespace(started_at=started_at, ended_at=ended_at)
]
monkeypatch.setattr(
file_controller, "get_workspace_resolver", lambda: resolver
)
monkeypatch.setattr(
file_controller, "get_default_run_journal", lambda: journal
)
app = FastAPI()
app.include_router(file_controller.router)
client = TestClient(app, client=("127.0.0.1", 50000))
response = client.get(
"/files/changes",
params={
"task_id": "task-1",
"project_id": "project-1",
"email": "user@example.com",
},
headers={LOCAL_CONTROL_CAPABILITY_HEADER: "secret-1"},
)
assert response.status_code == 200
body = response.json()
assert [item["relativePath"] for item in body["artifacts"]] == [
"report.csv"
]
assert body["scan_status"] == "complete"
assert body["truncated"] is False
resolver.store.freeze_artifact_manifest.assert_called_once_with(
"user@example.com", snapshot, body["artifacts"]
)
def test_task_changes_endpoint_reads_canonical_manifest_without_filesystem_scan(
monkeypatch,
):
monkeypatch.setenv("EIGENT_RUNTIME", "electron")
monkeypatch.setenv("EIGENT_LOCAL_CONTROL_CAPABILITY", "secret-1")
resolver = MagicMock()
journal = MagicMock()
journal.get_run_artifact_manifest_event.return_value = SimpleNamespace(
payload={
"scan_status": "complete",
"artifacts": [
{
"artifact_id": "art-1",
"filename": "report.csv",
"path": "/workspace/report.csv",
"relativePath": "report.csv",
"changeType": "generated",
"size": 12,
"modifiedAt": 1234,
"supportsRanges": True,
}
],
}
)
monkeypatch.setattr(
file_controller, "get_workspace_resolver", lambda: resolver
)
monkeypatch.setattr(
file_controller, "get_default_run_journal", lambda: journal
)
monkeypatch.setattr(
file_controller,
"_list_task_changed_files",
MagicMock(side_effect=AssertionError("filesystem scan must not run")),
)
app = FastAPI()
app.include_router(file_controller.router)
client = TestClient(app, client=("127.0.0.1", 50000))
response = client.get(
"/files/changes",
params={
"task_id": "task-1",
"project_id": "project-1",
"email": "user@example.com",
},
headers={LOCAL_CONTROL_CAPABILITY_HEADER: "secret-1"},
)
assert response.status_code == 200
assert response.json()["artifacts"][0]["artifact_id"] == "art-1"
assert response.json()["scan_status"] == "complete"
assert response.json()["truncated"] is False
resolver.store.get_snapshot.assert_not_called()
def test_task_changes_treats_unavailable_manifest_as_finalized_history(
monkeypatch,
):
monkeypatch.setenv("EIGENT_RUNTIME", "electron")
monkeypatch.setenv("EIGENT_LOCAL_CONTROL_CAPABILITY", "secret-1")
resolver = MagicMock()
journal = MagicMock()
journal.get_run_artifact_manifest_event.return_value = SimpleNamespace(
payload={
"scan_status": "workspace_unavailable",
"artifacts": [],
}
)
monkeypatch.setattr(
file_controller, "get_workspace_resolver", lambda: resolver
)
monkeypatch.setattr(
file_controller, "get_default_run_journal", lambda: journal
)
app = FastAPI()
app.include_router(file_controller.router)
client = TestClient(app, client=("127.0.0.1", 50000))
response = client.get(
"/files/changes",
params={
"task_id": "task-1",
"project_id": "project-1",
"email": "user@example.com",
},
headers={LOCAL_CONTROL_CAPABILITY_HEADER: "secret-1"},
)
assert response.status_code == 200
assert response.json() == {
"artifacts": [],
"scan_status": "workspace_unavailable",
"truncated": False,
}
resolver.store.get_snapshot.assert_not_called()
def test_stream_file_supports_byte_ranges(monkeypatch, tmp_path):
project_root = tmp_path / "project"
project_root.mkdir()
payload = b"0123456789"
(project_root / "report.pdf").write_bytes(payload)
monkeypatch.setattr(
file_controller,
"_resolve_file_root",
lambda *_args, **_kwargs: project_root,
)
app = FastAPI()
app.include_router(file_controller.router)
client = TestClient(app)
response = client.get(
"/files/stream",
params={
"path": "report.pdf",
"project_id": "project-1",
"email": "user@example.com",
},
headers={"Range": "bytes=2-5"},
)
assert response.status_code == 206
assert response.content == b"2345"
assert response.headers["accept-ranges"] == "bytes"
assert response.headers["content-range"] == "bytes 2-5/10"
head = client.head(
"/files/stream",
params={
"path": "report.pdf",
"project_id": "project-1",
"email": "user@example.com",
},
)
assert head.status_code == 200
assert head.content == b""
assert head.headers["content-length"] == str(len(payload))
assert head.headers["accept-ranges"] == "bytes"
def test_task_changes_endpoint_requires_local_capability(
monkeypatch, tmp_path
):
monkeypatch.setenv("EIGENT_RUNTIME", "electron")
monkeypatch.setenv("EIGENT_LOCAL_CONTROL_CAPABILITY", "secret-1")
snapshot = SimpleNamespace(
project_id="project-1",
task_output_root=str(tmp_path),
working_directory=str(tmp_path),
task_start_time=time.time(),
)
resolver = MagicMock()
resolver.store.get_snapshot.return_value = snapshot
journal = MagicMock()
journal.get_run.return_value = None
monkeypatch.setattr(
file_controller, "get_workspace_resolver", lambda: resolver
)
monkeypatch.setattr(
file_controller, "get_default_run_journal", lambda: journal
)
app = FastAPI()
app.include_router(file_controller.router)
client = TestClient(app, client=("127.0.0.1", 50000))
params = {
"task_id": "task-1",
"project_id": "project-1",
"email": "user@example.com",
}
assert client.get("/files/changes", params=params).status_code == 401
response = client.get(
"/files/changes",
params=params,
headers={LOCAL_CONTROL_CAPABILITY_HEADER: "secret-1"},
)
assert response.status_code == 200
assert response.json() == {
"artifacts": [],
"scan_status": "complete",
"truncated": False,
}