2026-08-11 19:01:05 +08:00
|
|
|
|
# ============================================================
|
|
|
|
|
|
# Web 本地工程数据文件上传黄金测试(AG-08 扩展)
|
|
|
|
|
|
# 覆盖:multipart 上传 → 项目 workDir 自动落位 → 目录分析可见
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
2026-08-26 00:25:46 +08:00
|
|
|
|
import hashlib
|
2026-08-11 19:01:05 +08:00
|
|
|
|
import os
|
2026-08-26 00:25:46 +08:00
|
|
|
|
from pathlib import Path
|
2026-08-11 19:01:05 +08:00
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
|
|
|
2026-08-26 00:25:46 +08:00
|
|
|
|
from server.aps_domain.folder_pack import analyze_work_dir, prepare_folder_schedule
|
2026-08-11 19:01:05 +08:00
|
|
|
|
from server.state.seed import seed_world
|
|
|
|
|
|
from tests.auth_provider import install_test_auth
|
2026-09-08 00:07:26 +08:00
|
|
|
|
from tests.external_data import external_dir
|
2026-08-11 19:01:05 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@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"]
|
2026-08-26 00:25:46 +08:00
|
|
|
|
assert body["workDir"] is None
|
|
|
|
|
|
assert body["workDirConfigured"] is True
|
|
|
|
|
|
assert all(project["workDir"] is None for project in body["workspace"]["projects"])
|
2026-08-11 19:01:05 +08:00
|
|
|
|
|
|
|
|
|
|
snap = client.get("/api/workspace").json()
|
|
|
|
|
|
project = next(p for p in snap["projects"] if p["id"] == pid)
|
2026-08-26 00:25:46 +08:00
|
|
|
|
assert project["workDir"]
|
|
|
|
|
|
assert os.path.isfile(os.path.join(project["workDir"], "订单-样例.csv"))
|
2026-08-11 19:01:05 +08:00
|
|
|
|
assert any(f["projectId"] == pid and f["name"] == "订单-样例.csv" for f in snap["files"])
|
|
|
|
|
|
|
|
|
|
|
|
class FakePS:
|
|
|
|
|
|
def snapshot(self, include_messages=False):
|
|
|
|
|
|
return {
|
2026-08-26 00:25:46 +08:00
|
|
|
|
"projects": [{"id": pid, "name": "浏览器项目", "workDir": project["workDir"]}],
|
2026-08-11 19:01:05 +08:00
|
|
|
|
"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"])
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-09-08 00:07:26 +08:00
|
|
|
|
KANGNI_DATA_DIR = external_dir("KANGNI_DATA_DIR", "kangni")
|
2026-08-26 00:25:46 +08:00
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-11 19:01:05 +08:00
|
|
|
|
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"]
|
2026-08-26 00:25:46 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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"))
|