# ============================================================ # Web 本地工程数据文件上传黄金测试(AG-08 扩展) # 覆盖:multipart 上传 → 项目 workDir 自动落位 → 目录分析可见 # ============================================================ from __future__ import annotations import hashlib import os from pathlib import Path import pytest from fastapi.testclient import TestClient from server.aps_domain.folder_pack import analyze_work_dir, prepare_folder_schedule from server.state.seed import seed_world from tests.auth_provider import install_test_auth from tests.external_data import external_dir @pytest.fixture() def secure_app(tmp_path, monkeypatch): monkeypatch.setenv("APS_DB_PATH", str(tmp_path / "tenant.db")) monkeypatch.setenv("APS_WORLD_PATH", str(tmp_path / "world.json")) monkeypatch.setenv("APS_DATA_DIR", str(tmp_path / "aps-data")) import server.state.store as world_store from server.db.database import reset_engine install_test_auth(monkeypatch, "tenant-a-000000000000000000000001") world_store._stores.clear() reset_engine() from server.gateway.app import create_app app = create_app() yield app reset_engine() world_store._stores.clear() def _login(client: TestClient) -> None: response = client.post("/api/auth/login", json={ "method": "password", "username": "planner", "password": "test", }) assert response.status_code == 200 def test_upload_local_files_then_folder_analysis(secure_app, monkeypatch): client = TestClient(secure_app) _login(client) created = client.post("/api/projects", json={"name": "浏览器项目"}).json() pid = created["project"]["id"] sid = created["session"]["id"] csv = "订单号,客户,产品编码,数量,交期\nSO-DEMO-1,比亚迪,A0050101-00280,1,2026-08-30\n" response = client.post( f"/api/projects/{pid}/files/upload", files=[("files", ("订单-样例.csv", csv.encode("utf-8"), "text/csv"))], ) assert response.status_code == 200, response.text body = response.json() assert body["saved"] == ["订单-样例.csv"] assert body["workDir"] is None assert body["workDirConfigured"] is True assert all(project["workDir"] is None for project in body["workspace"]["projects"]) snap = client.get("/api/workspace").json() project = next(p for p in snap["projects"] if p["id"] == pid) assert project["workDir"] assert os.path.isfile(os.path.join(project["workDir"], "订单-样例.csv")) assert any(f["projectId"] == pid and f["name"] == "订单-样例.csv" for f in snap["files"]) class FakePS: def snapshot(self, include_messages=False): return { "projects": [{"id": pid, "name": "浏览器项目", "workDir": project["workDir"]}], "sessions": [{"id": sid, "projectId": pid}], "files": [], } monkeypatch.setattr("server.state.projects.get_project_store", lambda: FakePS()) report = analyze_work_dir(seed_world(), sid) assert report["ok"] is True assert any(f["name"] == "订单-样例.csv" for f in report["files"]) KANGNI_DATA_DIR = external_dir("KANGNI_DATA_DIR", "kangni") KANGNI_WORKBOOKS = ( "订单.xlsx", "工艺路线.xlsx", "工时.xlsx", "BOM.xlsx", "设备.xlsx", "模具.xlsx", "物料.xlsx", "设备能力映射模板.xlsx", "模具适配映射模板.xlsx", ) @pytest.mark.skipif( not all((KANGNI_DATA_DIR / name).is_file() for name in KANGNI_WORKBOOKS), reason="康尼现场只读数据不完整", ) def test_upload_kangni_workbooks_builds_frozen_trial_payload(secure_app, monkeypatch): client = TestClient(secure_app) _login(client) created = client.post("/api/projects", json={"name": "康尼上传试排"}).json() pid = created["project"]["id"] sid = created["session"]["id"] source_hashes = { name: hashlib.sha256((KANGNI_DATA_DIR / name).read_bytes()).hexdigest() for name in KANGNI_WORKBOOKS } files = [ ( "files", ( name, (KANGNI_DATA_DIR / name).read_bytes(), "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ), ) for name in KANGNI_WORKBOOKS ] response = client.post(f"/api/projects/{pid}/files/upload", files=files) assert response.status_code == 200, response.text assert set(response.json()["saved"]) == set(KANGNI_WORKBOOKS) snap = client.get("/api/workspace").json() project = next(row for row in snap["projects"] if row["id"] == pid) class FakePS: def snapshot(self, include_messages=False): return { "projects": [{"id": pid, "name": "康尼上传试排", "workDir": project["workDir"]}], "sessions": [{"id": sid, "projectId": pid}], "files": [], } monkeypatch.setattr("server.state.projects.get_project_store", lambda: FakePS()) report = prepare_folder_schedule(seed_world(), sid) assert report["kangniDetected"] is True assert report["trialReady"] is True assert report["productionReady"] is False assert report["kangniMeta"]["orderCount"] == 10 assert report["kangniMeta"]["routingRecordCount"] == 72 assert len(report["sourceManifest"]) == len(KANGNI_WORKBOOKS) assert { name: hashlib.sha256((KANGNI_DATA_DIR / name).read_bytes()).hexdigest() for name in KANGNI_WORKBOOKS } == source_hashes def test_upload_rejects_unsupported_extension(secure_app): client = TestClient(secure_app) _login(client) created = client.post("/api/projects", json={"name": "拒绝测试"}).json() pid = created["project"]["id"] response = client.post( f"/api/projects/{pid}/files/upload", files=[("files", ("说明.docx", b"docx", "application/octet-stream"))], ) assert response.status_code == 400 assert "仅支持" in response.json()["detail"] def test_same_name_upload_preserves_file_and_metadata_when_registration_fails( secure_app, monkeypatch, tmp_path, ): from server.state.projects import ProjectStore client = TestClient(secure_app) _login(client) created = client.post( "/api/projects", json={"name": "原子上传", "workDir": str(tmp_path)} ).json() pid = created["project"]["id"] first = client.post( f"/api/projects/{pid}/files/upload", files=[("files", ("same.dxf", b"original", "application/octet-stream"))], ) assert first.status_code == 200, first.text before = client.get("/api/workspace").json() old_rows = [ row for row in before["files"] if row["projectId"] == pid and row["name"] == "same.dxf" ] assert len(old_rows) == 1 def fail_create_file(self, project_id, name, kind="other", note="", file_id=None): raise RuntimeError("injected metadata failure") monkeypatch.setattr(ProjectStore, "create_file", fail_create_file) failed = client.post( f"/api/projects/{pid}/files/upload", files=[("files", ("same.dxf", b"replacement", "application/octet-stream"))], ) assert failed.status_code == 200, failed.text assert failed.json()["saved"] == [] assert "injected metadata failure" in failed.json()["errors"][0]["error"] assert (tmp_path / "same.dxf").read_bytes() == b"original" after = client.get("/api/workspace").json() rows = [ row for row in after["files"] if row["projectId"] == pid and row["name"] == "same.dxf" ] assert rows == old_rows assert not list(tmp_path.glob(".*.upload")) assert not list(tmp_path.glob(".*.backup"))