初始提交:QuickTeam 工单接口导入工具

用「WCF + 通用存储过程网关」接口替代 Selenium(D://Learning//Python//工单自动化)导入工单:
单 Sprint 12 个项目 175s 跑完(旧方案 25~35 分钟),失败可断点续跑且不重复建单。

- 分层:transport(HTTP/协议)/ api(存储过程语义)/ domain(纯规则)/
  workflow(阶段编排)/ store(SQLite 台账)/ report / CLI
- 幂等:台账驱动断点续跑;dry-run 与跳过行不污染进度;--rollback-on-fail 清理半成品
- 命令:plan / apply / report / status,真实写入需 --yes
- tools:只读探针、存储过程源码导出(sp_helptext)、导入诊断
- docs:调研与设计文档(含联调踩到的 5 个坑)+ 6 个关键存储过程源码

实测:Sprint 2026 Sprint 10 的 12 个项目全部完成建单→导入→确认→推送 TFS,
工单 225134–225145,35 个功能模块,重跑 plan 全部 skip-pushed。69 个单测通过。
This commit is contained in:
chenshoulin
2026-11-05 19:14:25 +08:00
commit 58850314cf
32 changed files with 4590 additions and 0 deletions
+63
View File
@@ -0,0 +1,63 @@
"""传输层与错误解析的单元测试(不联网)。"""
from __future__ import annotations
import unittest
from qtimport.errors import Category, QtError, extract_exception_message
from qtimport.transport import Transport
WCF_ERROR_PAGE = """\ufeff<?xml version="1.0" encoding="utf-8"?>
<html><body><div id="content">
<p class="heading1">Request Error</p>
<p>The server encountered an error processing the request. The exception message is
'Could not find stored procedure 'QuickTeam.usp_Nope'.'. See server logs for more details.</p>
</div></body></html>"""
class DecodeTest(unittest.TestCase):
def test_plain_json(self) -> None:
self.assertEqual(Transport.decode('[{"A":1}]'), [{"A": 1}])
def test_double_encoded(self) -> None:
raw = '"[{\\"WorkSheetId\\": 225072}]"'
self.assertEqual(Transport.decode(raw)[0]["WorkSheetId"], 225072)
def test_triple_encoded(self) -> None:
raw = '"\\"[{\\\\\\"A\\\\\\": 1}]\\""'
self.assertEqual(Transport.decode(raw), [{"A": 1}])
def test_scalar_string(self) -> None:
self.assertEqual(Transport.decode('"0"'), 0)
def test_not_json(self) -> None:
self.assertEqual(Transport.decode("<html/>"), "<html/>")
class ErrorTest(unittest.TestCase):
def test_extract_message(self) -> None:
message = extract_exception_message(WCF_ERROR_PAGE)
self.assertIn("Could not find stored procedure", message)
def test_missing_procedure_not_retryable(self) -> None:
error = QtError.from_http(400, WCF_ERROR_PAGE)
self.assertEqual(error.category, Category.SP_MISSING)
self.assertFalse(error.retryable)
def test_param_error(self) -> None:
body = "<p>The exception message is 'Procedure or function usp_X has too many arguments "
body += "specified.'. See server logs</p>"
self.assertEqual(QtError.from_http(400, body).category, Category.PARAM)
def test_server_error_retryable(self) -> None:
error = QtError.from_http(503, "<p>Service Unavailable</p>")
self.assertTrue(error.retryable)
def test_html_unescaped(self) -> None:
body = "<p>The exception message is 'Parameter &#39;UserId&#39; is missing.'. See server logs</p>"
self.assertIn("UserId", extract_exception_message(body))
if __name__ == "__main__":
unittest.main()