fix: restore Python 3.12 startup and release 2.0.1
This commit is contained in:
1 parent
bf6247703d
commit
b584dae80a
19 files changed
+223
-20
No files matched your search
@@ -21,7 +21,7 @@
|
|||||||
| 探索、示范、用例提炼与复跑 | [探索说明](docs/exploration-mode.md) |
|
| 探索、示范、用例提炼与复跑 | [探索说明](docs/exploration-mode.md) |
|
||||||
| 插件协议、适配或市场协同 | [插件开发规范](docs/plugin-development.md)及插件仓库自身的 AGENTS.md、DESIGN.md |
|
| 插件协议、适配或市场协同 | [插件开发规范](docs/plugin-development.md)及插件仓库自身的 AGENTS.md、DESIGN.md |
|
||||||
| 远程 Python SDK | [SDK 设计](packages/artemis-client/DESIGN.md),保持标准库薄客户端边界 |
|
| 远程 Python SDK | [SDK 设计](packages/artemis-client/DESIGN.md),保持标准库薄客户端边界 |
|
||||||
| 继续历史开发、确认版本或发布 | [迁入时的开发快照](docs/development-handoff.md)、[2.0.0 发行说明](docs/releases/2.0.0.md),再核对实时 Git 状态;`docs/plans/` 保留各自阶段的状态 |
|
| 继续历史开发、确认版本或发布 | [迁入时的开发快照](docs/development-handoff.md)、[2.0.1 发行说明](docs/releases/2.0.1.md),再核对实时 Git 状态;`docs/plans/` 保留各自阶段的状态 |
|
||||||
|
|
||||||
## Quick Commands
|
## Quick Commands
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ components:
|
|||||||
|
|
||||||
ARTEMIS 面向使用自然语言执行、探索和复跑测试的人。当前产品包含 Flash、Pro、探索三种入口,按需调用安卓设备和电脑插件,提供批次、精选任务、历史、报告及证据。用户应能看懂当前在做什么、何时需要回答、哪些结果已有证据。
|
ARTEMIS 面向使用自然语言执行、探索和复跑测试的人。当前产品包含 Flash、Pro、探索三种入口,按需调用安卓设备和电脑插件,提供批次、精选任务、历史、报告及证据。用户应能看懂当前在做什么、何时需要回答、哪些结果已有证据。
|
||||||
|
|
||||||
本文记录主程序的职责边界及 `apps/showcase_ui` 的现有视觉与交互规则,不提出新的主题改版。远程 SDK 的协议边界见[局部设计](packages/artemis-client/DESIGN.md);独立插件仓库维护适配器设计和构建规则。阶段性进度见[开发快照](docs/development-handoff.md),发行状态见[2.0.0 说明](docs/releases/2.0.0.md)。
|
本文记录主程序的职责边界及 `apps/showcase_ui` 的现有视觉与交互规则,不提出新的主题改版。远程 SDK 的协议边界见[局部设计](packages/artemis-client/DESIGN.md);独立插件仓库维护适配器设计和构建规则。阶段性进度见[开发快照](docs/development-handoff.md),发行状态见[2.0.1 说明](docs/releases/2.0.1.md)。
|
||||||
|
|
||||||
## System architecture
|
## System architecture
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
基于 [ARTEMIS](https://github.com/google/artemis) 扩展的 Android AI 自动化测试平台。用自然语言描述任务,即可通过简体中文网页或 MCP 驱动真机、模拟器执行操作并收集结果。模型统一使用 **OpenAI Responses API**。
|
基于 [ARTEMIS](https://github.com/google/artemis) 扩展的 Android AI 自动化测试平台。用自然语言描述任务,即可通过简体中文网页或 MCP 驱动真机、模拟器执行操作并收集结果。模型统一使用 **OpenAI Responses API**。
|
||||||
|
|
||||||
**2.0.0 已发布**:新增交互探索、插件扩展和精选任务,增强跨端证据与 AI 接入。完整变化及升级步骤见 [2.0.0 发行说明](docs/releases/2.0.0.md)。
|
**2.0.1 已发布**:修复 Python 3.12 全新安装时网页服务无法启动的问题,并统一不同 Python 版本的 MCP 工具描述。修复与升级步骤见 [2.0.1 发行说明](docs/releases/2.0.1.md);探索、插件和精选任务等功能见 [2.0.0 发行说明](docs/releases/2.0.0.md)。
|
||||||
|
|
||||||
## 主要功能
|
## 主要功能
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
"""Independent favorite snapshots dispatched through the existing task services."""
|
"""Independent favorite snapshots dispatched through the existing task services."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
import asyncio
|
import asyncio
|
||||||
import hashlib
|
import hashlib
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "artemis-showcase-ui",
|
"name": "artemis-showcase-ui",
|
||||||
"version": "2.0.0",
|
"version": "2.0.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "artemis-showcase-ui",
|
"name": "artemis-showcase-ui",
|
||||||
"version": "2.0.0",
|
"version": "2.0.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/cdk": "^22.1.5",
|
"@angular/cdk": "^22.1.5",
|
||||||
"@angular/common": "^22.1.4",
|
"@angular/common": "^22.1.4",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "artemis-showcase-ui",
|
"name": "artemis-showcase-ui",
|
||||||
"version": "2.0.0",
|
"version": "2.0.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
"start": "ng serve --proxy-config proxy.conf.json",
|
"start": "ng serve --proxy-config proxy.conf.json",
|
||||||
|
|||||||
@@ -145,16 +145,16 @@ class ReadinessEngine:
|
|||||||
device check.
|
device check.
|
||||||
"""
|
"""
|
||||||
cacheable = categories is None
|
cacheable = categories is None
|
||||||
request_started = time.monotonic()
|
cache_at_request = self._report_cache
|
||||||
if cacheable and not force_refresh:
|
if cacheable and not force_refresh:
|
||||||
cached = self._cached_report(self._REPORT_CACHE_TTL_SECONDS)
|
cached = self._cached_report(self._REPORT_CACHE_TTL_SECONDS)
|
||||||
if cached is not None:
|
if cached is not None:
|
||||||
return cached
|
return cached
|
||||||
|
|
||||||
async with self._report_lock:
|
async with self._report_lock:
|
||||||
# A refresh that completed while this caller waited satisfies even a
|
# A replacement report completed while this caller waited. Compare
|
||||||
# forced request that began before it, coalescing concurrent clicks.
|
# the snapshot itself: sequential requests can share a clock tick.
|
||||||
if cacheable and self._report_cache_time >= request_started:
|
if cacheable and self._report_cache is not cache_at_request:
|
||||||
cached = self._cached_report(float("inf"))
|
cached = self._cached_report(float("inf"))
|
||||||
if cached is not None:
|
if cached is not None:
|
||||||
return cached
|
return cached
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
"""Durable manual-demonstration commands, sharing the automation transaction."""
|
"""Durable manual-demonstration commands, sharing the automation transaction."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import time
|
import time
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ The queue remains the sole scheduler. These records describe a conversation and
|
|||||||
its immutable evidence/case versions, not a second execution state machine.
|
its immutable evidence/case versions, not a second execution state machine.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
import hashlib
|
import hashlib
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ if _REPO_ROOT not in sys.path:
|
|||||||
|
|
||||||
# pylint: disable=wrong-import-position
|
# pylint: disable=wrong-import-position
|
||||||
from adbutils import AdbClient
|
from adbutils import AdbClient
|
||||||
from mcp.server.fastmcp import Context, FastMCP
|
from mcp.server.fastmcp import Context
|
||||||
|
|
||||||
|
from artemis.mcp.tool_server import ArtemisFastMCP
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -86,7 +88,7 @@ def configure_stdio_mode() -> None:
|
|||||||
|
|
||||||
|
|
||||||
# Create minimal MCP server
|
# Create minimal MCP server
|
||||||
mcp = FastMCP("Android_ADB_Controller")
|
mcp = ArtemisFastMCP("Android_ADB_Controller")
|
||||||
|
|
||||||
_GLOBAL_CONTROLLER = None
|
_GLOBAL_CONTROLLER = None
|
||||||
_CONTROLLERS: dict[str, Any] = {}
|
_CONTROLLERS: dict[str, Any] = {}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
"""Keep ARTEMIS tool descriptions stable across supported Python versions."""
|
||||||
|
|
||||||
|
from textwrap import dedent
|
||||||
|
|
||||||
|
from mcp.server.fastmcp import FastMCP
|
||||||
|
|
||||||
|
|
||||||
|
class ArtemisFastMCP(FastMCP):
|
||||||
|
"""Normalize docstring indentation before FastMCP publishes tool metadata."""
|
||||||
|
|
||||||
|
def add_tool(
|
||||||
|
self,
|
||||||
|
fn,
|
||||||
|
name=None,
|
||||||
|
title=None,
|
||||||
|
description=None,
|
||||||
|
annotations=None,
|
||||||
|
icons=None,
|
||||||
|
meta=None,
|
||||||
|
structured_output=None,
|
||||||
|
):
|
||||||
|
if not description:
|
||||||
|
# Python 3.13+ removes this indentation at compilation; 3.12 leaves
|
||||||
|
# it in __doc__. Retain blank lines, including the final newline,
|
||||||
|
# so the existing catalog and pinned schemas stay byte-for-byte equal.
|
||||||
|
first, separator, body = (fn.__doc__ or "").expandtabs().partition("\n")
|
||||||
|
description = first.lstrip() + separator + dedent(body)
|
||||||
|
super().add_tool(
|
||||||
|
fn,
|
||||||
|
name=name,
|
||||||
|
title=title,
|
||||||
|
description=description,
|
||||||
|
annotations=annotations,
|
||||||
|
icons=icons,
|
||||||
|
meta=meta,
|
||||||
|
structured_output=structured_output,
|
||||||
|
)
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# ARTEMIS-Eapil 2.0.1
|
||||||
|
|
||||||
|
2.0.1 修复 Windows 全新安装使用 Python 3.12 时,网页构建成功后管理服务立即退出的问题。已有 2.0.0 安装也建议升级;探索、插件和精选任务等功能继续沿用 [2.0.0](2.0.0.md)。
|
||||||
|
|
||||||
|
## 修复内容
|
||||||
|
|
||||||
|
- **全新安装可以启动网页服务**:探索存储、人工示范和收藏服务中的 `list` 方法遮蔽了同名内置类型,导致 Python 3.12 在加载后续类型注解时抛出 `TypeError: 'function' object is not subscriptable`。统一延迟求值,并增加冷启动与运行时类型注解回归,覆盖较新 Python 延迟暴露同一问题的情况。
|
||||||
|
- **诊断强制刷新按请求生效**:连续请求落在同一个时钟刻度时,仍会重新扫描;并发请求继续合并,避免重复检查。
|
||||||
|
- **MCP 工具描述跨版本一致**:统一 Python 3.12 与较新版本编译后文档字符串的缩进差异,保持工具目录、固定协议快照及现有显式描述一致。
|
||||||
|
|
||||||
|
本版不要求升级 Python,也不更改已有配置、任务历史或插件版本。
|
||||||
|
|
||||||
|
## 升级与安装
|
||||||
|
|
||||||
|
1. 先结束正在运行的任务,再运行 `update.bat`;国内网络使用 `update-cn.bat`。升级程序会停止当前项目的服务与任务进程,并同步依赖环境。
|
||||||
|
2. 完成后运行 `start.bat` / `start-cn.bat`,等待网页构建并打开页面。启动脚本只检查更新,不代替升级脚本。
|
||||||
|
3. 全新安装下载 `artemis-eapil-2.0.1-source.zip`,解压后运行上述启动脚本。用附件 `SHA256SUMS.txt` 校验下载完整性。
|
||||||
|
|
||||||
|
源码包保留浅层 Git 更新能力,不包含依赖环境、用户密钥、任务历史或预编译网页。升级主程序不会自动升级已安装插件。
|
||||||
|
|
||||||
|
## 发布验证
|
||||||
|
|
||||||
|
- Python 3.12 与 3.14 的默认后端测试分别为 **2952 项通过、6 项跳过、8 项按标记排除**。新增启动回归、同一时钟刻度下的诊断刷新回归及跨版本 MCP 描述回归;现有工具目录与协议快照无需修改。
|
||||||
|
- 在 Python 3.12 隔离环境运行真实服务生命周期,首页、API 文档、收藏和探索接口均返回 HTTP 200,服务正常关闭。仅替代硬件预热和防休眠入口,数据库、队列、IPC、MCP 会话及 HTTP 服务沿用真实实现。
|
||||||
|
- 前端 **290 项测试通过**,2.0.1 生产构建成功。Ruff 格式与静态检查、质量基线和核心类型检查通过。
|
||||||
|
|
||||||
|
本次未调用付费模型或执行设备业务任务,未替用户升级或重启现有安装。
|
||||||
+2
-2
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from mcp.server.fastmcp import FastMCP
|
from artemis.mcp.tool_server import ArtemisFastMCP
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ except Exception as exc:
|
|||||||
logger.debug("FastMCP Settings.model_rebuild skipped: %s", exc, exc_info=True)
|
logger.debug("FastMCP Settings.model_rebuild skipped: %s", exc, exc_info=True)
|
||||||
|
|
||||||
# Define the shared FastMCP instance for external IDE and agent clients.
|
# Define the shared FastMCP instance for external IDE and agent clients.
|
||||||
mcp = FastMCP(
|
mcp = ArtemisFastMCP(
|
||||||
"artemis",
|
"artemis",
|
||||||
instructions=(
|
instructions=(
|
||||||
"ARTEMIS executes Android, computer and mixed tests with Flash, Pro or interactive exploration. The user starts the service; MCP only connects. Use the official artemis-use Skill for workflows and plugin development. "
|
"ARTEMIS executes Android, computer and mixed tests with Flash, Pro or interactive exploration. The user starts the service; MCP only connects. Use the official artemis-use Skill for workflows and plugin development. "
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ import logging
|
|||||||
import threading
|
import threading
|
||||||
from typing import get_type_hints
|
from typing import get_type_hints
|
||||||
|
|
||||||
from mcp.server.fastmcp import FastMCP
|
|
||||||
from starlette.responses import JSONResponse
|
from starlette.responses import JSONResponse
|
||||||
|
|
||||||
|
from artemis.mcp.tool_server import ArtemisFastMCP
|
||||||
from artemis.service_registry import TOKEN_HEADER
|
from artemis.service_registry import TOKEN_HEADER
|
||||||
|
|
||||||
|
|
||||||
@@ -85,7 +85,7 @@ def create_shared_mcp():
|
|||||||
from mcp_server import tools
|
from mcp_server import tools
|
||||||
|
|
||||||
executor = ToolExecutor()
|
executor = ToolExecutor()
|
||||||
shared = FastMCP(
|
shared = ArtemisFastMCP(
|
||||||
"artemis",
|
"artemis",
|
||||||
instructions=definitions.instructions,
|
instructions=definitions.instructions,
|
||||||
streamable_http_path="/",
|
streamable_http_path="/",
|
||||||
|
|||||||
+1
-1
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "artemis"
|
name = "artemis"
|
||||||
version = "2.0.0"
|
version = "2.0.1"
|
||||||
description = "Autonomous AI Assistant for Mobile Automation"
|
description = "Autonomous AI Assistant for Mobile Automation"
|
||||||
|
|
||||||
authors = [
|
authors = [
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"""Unit tests for Artemis System Diagnostics & Readiness Engine."""
|
"""Unit tests for Artemis System Diagnostics & Readiness Engine."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from types import SimpleNamespace
|
||||||
from unittest.mock import AsyncMock, Mock
|
from unittest.mock import AsyncMock, Mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -173,7 +174,13 @@ async def test_dashboard_reuses_recent_confirmed_state_on_one_timeout(monkeypatc
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_readiness_engine_coalesces_concurrent_full_scans():
|
@pytest.mark.parametrize("same_clock_tick", [False, True])
|
||||||
|
async def test_readiness_engine_coalesces_concurrent_full_scans(same_clock_tick, monkeypatch):
|
||||||
|
if same_clock_tick:
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"artemis.core.diagnostics.engine.time",
|
||||||
|
SimpleNamespace(monotonic=lambda: 100.0, time=lambda: 100.0),
|
||||||
|
)
|
||||||
engine = ReadinessEngine()
|
engine = ReadinessEngine()
|
||||||
result = ProbeResult(
|
result = ProbeResult(
|
||||||
id="test_probe",
|
id="test_probe",
|
||||||
@@ -200,7 +207,13 @@ async def test_readiness_engine_coalesces_concurrent_full_scans():
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_readiness_engine_reuses_cache_until_forced():
|
@pytest.mark.parametrize("same_clock_tick", [False, True])
|
||||||
|
async def test_readiness_engine_reuses_cache_until_forced(same_clock_tick, monkeypatch):
|
||||||
|
if same_clock_tick:
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"artemis.core.diagnostics.engine.time",
|
||||||
|
SimpleNamespace(monotonic=lambda: 100.0, time=lambda: 100.0),
|
||||||
|
)
|
||||||
engine = ReadinessEngine()
|
engine = ReadinessEngine()
|
||||||
result = ProbeResult(
|
result = ProbeResult(
|
||||||
id="test_probe",
|
id="test_probe",
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
from mcp.types import ToolAnnotations
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from artemis.mcp.tool_server import ArtemisFastMCP
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.parametrize("registration", ["decorator", "add_tool"])
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("docstring", "expected"),
|
||||||
|
[
|
||||||
|
("Summary.\n\n Details.\n Nested.\n ", "Summary.\n\nDetails.\n Nested.\n"),
|
||||||
|
("Summary.\n\nDetails.\n Nested.\n", "Summary.\n\nDetails.\n Nested.\n"),
|
||||||
|
("One line.", "One line."),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_tool_descriptions_match_across_docstring_formats(registration, docstring, expected):
|
||||||
|
server = ArtemisFastMCP("description-test")
|
||||||
|
|
||||||
|
def probe(count: int = 1) -> str:
|
||||||
|
return str(count)
|
||||||
|
|
||||||
|
probe.__doc__ = docstring
|
||||||
|
if registration == "decorator":
|
||||||
|
assert server.tool()(probe) is probe
|
||||||
|
else:
|
||||||
|
server.add_tool(probe)
|
||||||
|
(tool,) = await server.list_tools()
|
||||||
|
assert tool.description == expected
|
||||||
|
assert tool.inputSchema["properties"]["count"]["type"] == "integer"
|
||||||
|
assert probe.__doc__ == docstring
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_explicit_tool_metadata_is_preserved():
|
||||||
|
server = ArtemisFastMCP("description-test")
|
||||||
|
|
||||||
|
def probe() -> str:
|
||||||
|
"""The fallback docstring."""
|
||||||
|
return "ok"
|
||||||
|
|
||||||
|
description = "Explicit description.\n Keep this indentation.\n"
|
||||||
|
server.add_tool(
|
||||||
|
probe,
|
||||||
|
"renamed_probe",
|
||||||
|
"Probe title",
|
||||||
|
description,
|
||||||
|
annotations=ToolAnnotations(readOnlyHint=True),
|
||||||
|
meta={"source": "test"},
|
||||||
|
structured_output=False,
|
||||||
|
)
|
||||||
|
(tool,) = await server.list_tools()
|
||||||
|
assert tool.name == "renamed_probe"
|
||||||
|
assert tool.title == "Probe title"
|
||||||
|
assert tool.description == description
|
||||||
|
assert tool.annotations.readOnlyHint is True
|
||||||
|
assert tool.meta == {"source": "test"}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
"""Cold imports and runtime annotations must work on the installer's Python."""
|
||||||
|
|
||||||
|
from importlib import import_module
|
||||||
|
import inspect
|
||||||
|
from pathlib import Path
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from textwrap import dedent
|
||||||
|
from typing import get_type_hints
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("module_name", "class_name"),
|
||||||
|
[
|
||||||
|
("artemis.exploration.store", "ExplorationStore"),
|
||||||
|
("artemis.exploration.demonstrations", "Demonstrations"),
|
||||||
|
("apps.admin_console.services.favorites", "FavoriteService"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_startup_service_annotations_do_not_shadow_builtin_collections(module_name, class_name):
|
||||||
|
service = getattr(import_module(module_name), class_name)
|
||||||
|
# Python 3.14 defers these errors until annotations are read. Evaluating them
|
||||||
|
# also detects the import failure seen immediately on Python 3.12 and 3.13.
|
||||||
|
assert get_type_hints(service.list)["return"] == list[dict]
|
||||||
|
for method in vars(service).values():
|
||||||
|
if inspect.isfunction(method):
|
||||||
|
get_type_hints(method)
|
||||||
|
|
||||||
|
|
||||||
|
def test_ui_server_cold_import_serves_empty_workspace(tmp_path, monkeypatch):
|
||||||
|
monkeypatch.setenv("ARTEMIS_APP_DIR", str(tmp_path / "app"))
|
||||||
|
monkeypatch.setenv("ARTEMIS_TRACES_DIR", str(tmp_path / "traces"))
|
||||||
|
code = dedent("""
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
from apps.admin_console.server import app
|
||||||
|
|
||||||
|
# No lifespan: validate startup imports and HTTP routes without device
|
||||||
|
# discovery, background workers, or a running user's service.
|
||||||
|
client = TestClient(app, base_url="http://localhost")
|
||||||
|
try:
|
||||||
|
for path in ("/api/automation/favorites", "/api/explorations"):
|
||||||
|
response = client.get(path)
|
||||||
|
assert response.status_code == 200, response.text
|
||||||
|
assert response.json() == [], response.text
|
||||||
|
response = client.get("/openapi.json")
|
||||||
|
assert response.status_code == 200, response.text
|
||||||
|
assert "/api/explorations" in response.json()["paths"]
|
||||||
|
finally:
|
||||||
|
client.close()
|
||||||
|
""")
|
||||||
|
result = subprocess.run(
|
||||||
|
[sys.executable, "-X", "utf8", "-c", code],
|
||||||
|
cwd=Path(__file__).resolve().parents[2],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
encoding="utf-8",
|
||||||
|
timeout=60,
|
||||||
|
)
|
||||||
|
assert result.returncode == 0, result.stdout + result.stderr
|
||||||
Reference in new issue
Block a user