refactor(fam-core): 重构第二阶段 - img_proxy 收尾 + 稳定性小修 + 工程质量
img_proxy 收尾: 提交此前未提交的图片代理蓝图(app.py 注册),改为复用 oracle_sync
已解析好的 base_url/token/timeout,不再独立读一份配置(避免配置改动时两处不同步)。
稳定性/正确性: chat_handler 问答失败时不再把 Oracle 内部 HTTP 状态码等细节透传给
客户端,改为通用错误信息(详细原因仍记服务端日志); 删除 logger.py 里旧任务架构
遗留的死函数 log_task; 更新 config.yaml.example 到当前 v2 架构(原文件还是重构前
的 scheduler/dispatcher/video_server 旧结构,当前代码完全不读这些字段)。
工程质量: 新增 fam-core/tests(9 个单元测试,覆盖 _format_events 上下文格式化和
config_loader 的 ${ENV_VAR} 解析)。
部署时发现并修复一个和这次改动无关的运维问题: NAS .env 文件缺 export 关键字,
plain source 只在当前 shell 生效不会被子进程(gunicorn)继承,导致 Oracle-Sync
token 校验失败;用 set -a/set +a 强制导出重启,非代码改动。
已部署 NAS 并验证:/health、/api/status、/api/proxy/frame、/api/proxy/avatar
全部通过;浏览器实测事件时间轴、人物管理页正常渲染。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
7
fam-core/tests/conftest.py
Normal file
7
fam-core/tests/conftest.py
Normal file
@@ -0,0 +1,7 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
# 让测试能直接 `from fam_core.xxx import yyy`,无需先 pip install -e .
|
||||
_SRC = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'src')
|
||||
if _SRC not in sys.path:
|
||||
sys.path.insert(0, _SRC)
|
||||
55
fam-core/tests/test_chat_handler.py
Normal file
55
fam-core/tests/test_chat_handler.py
Normal file
@@ -0,0 +1,55 @@
|
||||
from fam_core.chat_handler.chat_handler import _format_events
|
||||
|
||||
|
||||
def test_multiple_persons_and_attention():
|
||||
rows = [{
|
||||
"ts": "2026-08-21 16:11:25", "camera_name": "客厅",
|
||||
"description": "两人在客厅玩耍",
|
||||
"person_list_json": '["汤圆", "爷爷"]',
|
||||
"is_attention_event": True,
|
||||
}]
|
||||
out = _format_events(rows)
|
||||
assert out == "[2026-08-21 16:11 客厅] 汤圆,爷爷: 两人在客厅玩耍 [关注事件]"
|
||||
|
||||
|
||||
def test_no_person_defaults_to_wu_ren():
|
||||
rows = [{
|
||||
"ts": "2026-08-21 16:11:25", "camera_name": "客厅",
|
||||
"description": "空房间", "person_list_json": '[]',
|
||||
"is_attention_event": False,
|
||||
}]
|
||||
out = _format_events(rows)
|
||||
assert "无人:" in out
|
||||
assert "[关注事件]" not in out
|
||||
|
||||
|
||||
def test_person_list_already_decoded_list():
|
||||
"""pymysql 某些驱动/字段类型可能已经把 JSON 解码成 list,不是字符串。"""
|
||||
rows = [{
|
||||
"ts": "2026-08-21 16:11:25", "camera_name": "客厅",
|
||||
"description": "在客厅", "person_list_json": ["汤圆"],
|
||||
"is_attention_event": False,
|
||||
}]
|
||||
out = _format_events(rows)
|
||||
assert "汤圆:" in out
|
||||
|
||||
|
||||
def test_malformed_person_list_json_degrades_gracefully():
|
||||
rows = [{
|
||||
"ts": "2026-08-21 16:11:25", "camera_name": "客厅",
|
||||
"description": "在客厅", "person_list_json": "not valid json",
|
||||
"is_attention_event": False,
|
||||
}]
|
||||
out = _format_events(rows)
|
||||
assert "无人:" in out
|
||||
|
||||
|
||||
def test_multiple_rows_joined_by_newline():
|
||||
rows = [
|
||||
{"ts": "2026-08-21 16:11:25", "camera_name": "客厅",
|
||||
"description": "第一条", "person_list_json": '["汤圆"]', "is_attention_event": False},
|
||||
{"ts": "2026-08-21 16:12:00", "camera_name": "客厅",
|
||||
"description": "第二条", "person_list_json": '["汤圆"]', "is_attention_event": False},
|
||||
]
|
||||
out = _format_events(rows)
|
||||
assert len(out.split("\n")) == 2
|
||||
25
fam-core/tests/test_config_loader.py
Normal file
25
fam-core/tests/test_config_loader.py
Normal file
@@ -0,0 +1,25 @@
|
||||
import os
|
||||
|
||||
from fam_core.config_loader import _resolve_env_vars
|
||||
|
||||
|
||||
def test_resolves_string_env_var(monkeypatch):
|
||||
monkeypatch.setenv("FAM_CORE_TEST_VAR", "hello")
|
||||
assert _resolve_env_vars("${FAM_CORE_TEST_VAR}") == "hello"
|
||||
|
||||
|
||||
def test_unset_env_var_left_as_literal():
|
||||
assert _resolve_env_vars("${SOME_TOTALLY_UNSET_VAR_XYZ}") == "${SOME_TOTALLY_UNSET_VAR_XYZ}"
|
||||
|
||||
|
||||
def test_resolves_nested_dict(monkeypatch):
|
||||
monkeypatch.setenv("FAM_CORE_TEST_VAR", "secret")
|
||||
data = {"a": {"b": "${FAM_CORE_TEST_VAR}"}, "c": ["${FAM_CORE_TEST_VAR}", "plain"]}
|
||||
out = _resolve_env_vars(data)
|
||||
assert out == {"a": {"b": "secret"}, "c": ["secret", "plain"]}
|
||||
|
||||
|
||||
def test_non_string_values_passed_through():
|
||||
assert _resolve_env_vars(42) == 42
|
||||
assert _resolve_env_vars(True) is True
|
||||
assert _resolve_env_vars(None) is None
|
||||
Reference in New Issue
Block a user