pyquipu-application 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyquipu_application-0.1.0/.gitignore +75 -0
- pyquipu_application-0.1.0/PKG-INFO +14 -0
- pyquipu_application-0.1.0/pyproject.toml +32 -0
- pyquipu_application-0.1.0/src/quipu/__init__.py +3 -0
- pyquipu_application-0.1.0/src/quipu/application/__init__.py +0 -0
- pyquipu_application-0.1.0/src/quipu/application/controller.py +214 -0
- pyquipu_application-0.1.0/src/quipu/application/controller.stitcher.yaml +41 -0
- pyquipu_application-0.1.0/src/quipu/application/factory.py +56 -0
- pyquipu_application-0.1.0/src/quipu/application/factory.stitcher.yaml +10 -0
- pyquipu_application-0.1.0/src/quipu/application/plugin_manager.py +40 -0
- pyquipu_application-0.1.0/src/quipu/application/plugin_manager.stitcher.yaml +5 -0
- pyquipu_application-0.1.0/src/quipu/application/utils.py +16 -0
- pyquipu_application-0.1.0/src/quipu/application/utils.stitcher.yaml +2 -0
- pyquipu_application-0.1.0/stitcher.lock +178 -0
- pyquipu_application-0.1.0/tests/unit/conftest.py +3 -0
- pyquipu_application-0.1.0/tests/unit/test_controller.py +100 -0
- pyquipu_application-0.1.0/tests/unit/test_controller.stitcher.yaml +9 -0
- pyquipu_application-0.1.0/tests/unit/test_utils.py +26 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# tmpfiles
|
|
7
|
+
o.md
|
|
8
|
+
a.md
|
|
9
|
+
a
|
|
10
|
+
o
|
|
11
|
+
t
|
|
12
|
+
tmpfile
|
|
13
|
+
|
|
14
|
+
# Scaffoldings
|
|
15
|
+
context_builder.yml
|
|
16
|
+
context_builder.py
|
|
17
|
+
current_prompts
|
|
18
|
+
|
|
19
|
+
# Distribution / Packaging
|
|
20
|
+
.Python
|
|
21
|
+
build/
|
|
22
|
+
develop-eggs/
|
|
23
|
+
dist/
|
|
24
|
+
downloads/
|
|
25
|
+
eggs/
|
|
26
|
+
.eggs/
|
|
27
|
+
lib/
|
|
28
|
+
lib64/
|
|
29
|
+
parts/
|
|
30
|
+
sdist/
|
|
31
|
+
var/
|
|
32
|
+
wheels/
|
|
33
|
+
*.egg-info/
|
|
34
|
+
.installed.cfg
|
|
35
|
+
*.egg
|
|
36
|
+
|
|
37
|
+
# Virtual Environments
|
|
38
|
+
venv/
|
|
39
|
+
env/
|
|
40
|
+
.env
|
|
41
|
+
.venv/
|
|
42
|
+
|
|
43
|
+
# Testing
|
|
44
|
+
.tox/
|
|
45
|
+
.coverage
|
|
46
|
+
.coverage.*
|
|
47
|
+
.cache
|
|
48
|
+
nosetests.xml
|
|
49
|
+
coverage.xml
|
|
50
|
+
*.cover
|
|
51
|
+
.hypothesis/
|
|
52
|
+
.pytest_cache/
|
|
53
|
+
|
|
54
|
+
# IDEs
|
|
55
|
+
.idea/
|
|
56
|
+
.vscode/
|
|
57
|
+
*.swp
|
|
58
|
+
*.swo
|
|
59
|
+
|
|
60
|
+
# Project Specific
|
|
61
|
+
*.log
|
|
62
|
+
axon.log
|
|
63
|
+
# Ignore test vaults or scratchpads if created in root
|
|
64
|
+
vault/
|
|
65
|
+
temp/
|
|
66
|
+
scratch/
|
|
67
|
+
# --- Quipu Dev Infra ---
|
|
68
|
+
.envs/
|
|
69
|
+
.uv/
|
|
70
|
+
sandbox/
|
|
71
|
+
*.egg-info/
|
|
72
|
+
__pycache__/
|
|
73
|
+
.pytest_cache/
|
|
74
|
+
.coverage
|
|
75
|
+
htmlcov/
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyquipu-application
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Core application logic for Quipu, orchestrating engine and runtime.
|
|
5
|
+
Author-email: doucx <doucxldh@gmail.com>
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Classifier: Operating System :: OS Independent
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Requires-Dist: pyquipu-bus~=0.1.0
|
|
11
|
+
Requires-Dist: pyquipu-common~=0.1.0
|
|
12
|
+
Requires-Dist: pyquipu-engine~=0.1.0
|
|
13
|
+
Requires-Dist: pyquipu-interfaces~=0.1.0
|
|
14
|
+
Requires-Dist: pyquipu-runtime~=0.1.0
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pyquipu-application"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
authors = [{ name="doucx", email="doucxldh@gmail.com" }]
|
|
9
|
+
description = "Core application logic for Quipu, orchestrating engine and runtime."
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "Apache-2.0"
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"Operating System :: OS Independent",
|
|
15
|
+
]
|
|
16
|
+
dependencies = [
|
|
17
|
+
"pyquipu-interfaces ~= 0.1.0",
|
|
18
|
+
"pyquipu-engine ~= 0.1.0",
|
|
19
|
+
"pyquipu-runtime ~= 0.1.0",
|
|
20
|
+
"pyquipu-common ~= 0.1.0",
|
|
21
|
+
"pyquipu-bus ~= 0.1.0",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[tool.uv.sources]
|
|
25
|
+
pyquipu-interfaces = { workspace = true }
|
|
26
|
+
pyquipu-engine = { workspace = true }
|
|
27
|
+
pyquipu-runtime = { workspace = true }
|
|
28
|
+
pyquipu-common = { workspace = true }
|
|
29
|
+
pyquipu-bus = { workspace = true }
|
|
30
|
+
|
|
31
|
+
[tool.hatch.build.targets.wheel]
|
|
32
|
+
packages = ["src/quipu"]
|
|
File without changes
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import re
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Callable, Dict, List
|
|
5
|
+
|
|
6
|
+
from quipu.acts import register_core_acts
|
|
7
|
+
from quipu.engine.state_machine import Engine
|
|
8
|
+
from quipu.interfaces.exceptions import ExecutionError as CoreExecutionError
|
|
9
|
+
from quipu.interfaces.exceptions import OperationCancelledError
|
|
10
|
+
from quipu.interfaces.result import QuipuResult
|
|
11
|
+
from quipu.runtime.executor import Executor
|
|
12
|
+
from quipu.runtime.parser import detect_best_parser, get_parser
|
|
13
|
+
|
|
14
|
+
from .factory import create_engine
|
|
15
|
+
from .plugin_manager import PluginManager
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_available_acts(work_dir: Path) -> Dict[str, str]:
|
|
21
|
+
# A dummy confirmation handler is used as it's not required for listing.
|
|
22
|
+
# Yolo=True ensures no interactive prompts can be triggered.
|
|
23
|
+
executor = Executor(
|
|
24
|
+
root_dir=work_dir,
|
|
25
|
+
yolo=True,
|
|
26
|
+
confirmation_handler=lambda diff, prompt: True,
|
|
27
|
+
)
|
|
28
|
+
register_core_acts(executor)
|
|
29
|
+
PluginManager().load_from_sources(executor, work_dir)
|
|
30
|
+
return executor.get_registered_acts()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# 定义 ConfirmationHandler 类型别名: (diff_lines, prompt) -> bool
|
|
34
|
+
# 注意: Executor 期望如果不确认则抛出异常,或者返回 False (取决于 Executor 实现)。
|
|
35
|
+
# 为了保持与 CLI 行为一致,调用方传入的 handler 应该在用户拒绝时抛出 OperationCancelledError。
|
|
36
|
+
ConfirmationHandler = Callable[[List[str], str], bool]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class QuipuApplication:
|
|
40
|
+
def __init__(self, work_dir: Path, confirmation_handler: ConfirmationHandler, yolo: bool = False):
|
|
41
|
+
self.work_dir = work_dir
|
|
42
|
+
self.confirmation_handler = confirmation_handler
|
|
43
|
+
self.yolo = yolo
|
|
44
|
+
self.engine: Engine = create_engine(work_dir)
|
|
45
|
+
logger.info(f"Operation boundary set to: {self.work_dir}")
|
|
46
|
+
|
|
47
|
+
def _prepare_workspace(self) -> str:
|
|
48
|
+
current_hash = self.engine.git_db.get_tree_hash()
|
|
49
|
+
|
|
50
|
+
# 1. 正常 Clean: current_node 存在且与当前 hash 一致
|
|
51
|
+
is_node_clean = (self.engine.current_node is not None) and (
|
|
52
|
+
self.engine.current_node.output_tree == current_hash
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# 2. 创世 Clean: 历史为空 且 当前是空树 (即没有任何文件被追踪)
|
|
56
|
+
EMPTY_TREE_HASH = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
|
|
57
|
+
is_genesis_clean = (not self.engine.history_graph) and (current_hash == EMPTY_TREE_HASH)
|
|
58
|
+
|
|
59
|
+
is_clean = is_node_clean or is_genesis_clean
|
|
60
|
+
|
|
61
|
+
if not is_clean:
|
|
62
|
+
self.engine.capture_drift(current_hash)
|
|
63
|
+
|
|
64
|
+
if self.engine.current_node:
|
|
65
|
+
return self.engine.current_node.output_tree
|
|
66
|
+
else:
|
|
67
|
+
return current_hash
|
|
68
|
+
|
|
69
|
+
def _setup_executor(self) -> Executor:
|
|
70
|
+
executor = Executor(
|
|
71
|
+
root_dir=self.work_dir,
|
|
72
|
+
yolo=self.yolo,
|
|
73
|
+
confirmation_handler=self.confirmation_handler,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# 加载核心 acts
|
|
77
|
+
register_core_acts(executor)
|
|
78
|
+
|
|
79
|
+
# 加载外部插件
|
|
80
|
+
plugin_manager = PluginManager()
|
|
81
|
+
plugin_manager.load_from_sources(executor, self.work_dir)
|
|
82
|
+
|
|
83
|
+
return executor
|
|
84
|
+
|
|
85
|
+
def run(self, content: str, parser_name: str) -> QuipuResult:
|
|
86
|
+
# --- Phase 1 & 2: Perception & Decision (Lazy Capture) ---
|
|
87
|
+
input_tree_hash = self._prepare_workspace()
|
|
88
|
+
|
|
89
|
+
# --- Phase 3: Action (Execution) ---
|
|
90
|
+
# 3.1 Parser
|
|
91
|
+
final_parser_name = parser_name
|
|
92
|
+
if parser_name == "auto":
|
|
93
|
+
final_parser_name = detect_best_parser(content)
|
|
94
|
+
if final_parser_name != "backtick":
|
|
95
|
+
logger.info(f"🔍 自动检测到解析器: {final_parser_name}")
|
|
96
|
+
|
|
97
|
+
parser = get_parser(final_parser_name)
|
|
98
|
+
statements = parser.parse(content)
|
|
99
|
+
|
|
100
|
+
if not statements:
|
|
101
|
+
return QuipuResult(
|
|
102
|
+
success=True, # No failure, just nothing to do
|
|
103
|
+
exit_code=0,
|
|
104
|
+
message="axon.warning.noStatements",
|
|
105
|
+
msg_kwargs={"parser": final_parser_name},
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# 3.2 Executor Setup
|
|
109
|
+
executor = self._setup_executor()
|
|
110
|
+
|
|
111
|
+
# 3.3 Execute
|
|
112
|
+
executor.execute(statements)
|
|
113
|
+
|
|
114
|
+
# --- Phase 4: Recording (Plan Crystallization) ---
|
|
115
|
+
final_summary = None
|
|
116
|
+
# 优先级 1: 从 Markdown 内容中提取 # 标题
|
|
117
|
+
title_match = re.search(r"^\s*#{1,6}\s+(.*)", content, re.MULTILINE)
|
|
118
|
+
if title_match:
|
|
119
|
+
final_summary = title_match.group(1).strip()
|
|
120
|
+
# 优先级 2: 从第一个 act 指令生成摘要
|
|
121
|
+
elif statements:
|
|
122
|
+
final_summary = executor.summarize_statement(statements[0])
|
|
123
|
+
|
|
124
|
+
output_tree_hash = self.engine.git_db.get_tree_hash()
|
|
125
|
+
|
|
126
|
+
self.engine.create_plan_node(
|
|
127
|
+
input_tree=input_tree_hash,
|
|
128
|
+
output_tree=output_tree_hash,
|
|
129
|
+
plan_content=content,
|
|
130
|
+
summary_override=final_summary,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
return QuipuResult(success=True, exit_code=0, message="run.success")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def run_quipu(
|
|
137
|
+
content: str,
|
|
138
|
+
work_dir: Path,
|
|
139
|
+
confirmation_handler: ConfirmationHandler,
|
|
140
|
+
parser_name: str = "auto",
|
|
141
|
+
yolo: bool = False,
|
|
142
|
+
) -> QuipuResult:
|
|
143
|
+
app = None
|
|
144
|
+
try:
|
|
145
|
+
app = QuipuApplication(work_dir=work_dir, confirmation_handler=confirmation_handler, yolo=yolo)
|
|
146
|
+
return app.run(content=content, parser_name=parser_name)
|
|
147
|
+
|
|
148
|
+
except OperationCancelledError as e:
|
|
149
|
+
logger.info(f"🚫 操作已取消: {e}")
|
|
150
|
+
return QuipuResult(
|
|
151
|
+
success=False, exit_code=2, message="run.error.cancelled", msg_kwargs={"error": str(e)}, error=e
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
except CoreExecutionError as e:
|
|
155
|
+
logger.error(f"❌ 操作失败: {e}")
|
|
156
|
+
return QuipuResult(
|
|
157
|
+
success=False, exit_code=1, message="run.error.execution", msg_kwargs={"error": str(e)}, error=e
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
except Exception as e:
|
|
161
|
+
logger.error(f"运行时错误: {e}", exc_info=True)
|
|
162
|
+
return QuipuResult(
|
|
163
|
+
success=False, exit_code=1, message="run.error.system", msg_kwargs={"error": str(e)}, error=e
|
|
164
|
+
)
|
|
165
|
+
finally:
|
|
166
|
+
# 确保无论成功或失败,引擎资源都被关闭
|
|
167
|
+
if app and hasattr(app, "engine") and app.engine:
|
|
168
|
+
app.engine.close()
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def run_stateless_plan(
|
|
172
|
+
content: str,
|
|
173
|
+
work_dir: Path,
|
|
174
|
+
confirmation_handler: ConfirmationHandler,
|
|
175
|
+
parser_name: str = "auto",
|
|
176
|
+
yolo: bool = False,
|
|
177
|
+
) -> QuipuResult:
|
|
178
|
+
try:
|
|
179
|
+
executor = Executor(
|
|
180
|
+
root_dir=work_dir,
|
|
181
|
+
yolo=yolo,
|
|
182
|
+
confirmation_handler=confirmation_handler,
|
|
183
|
+
)
|
|
184
|
+
register_core_acts(executor)
|
|
185
|
+
PluginManager().load_from_sources(executor, work_dir)
|
|
186
|
+
|
|
187
|
+
final_parser_name = parser_name
|
|
188
|
+
if parser_name == "auto":
|
|
189
|
+
final_parser_name = detect_best_parser(content)
|
|
190
|
+
|
|
191
|
+
parser = get_parser(final_parser_name)
|
|
192
|
+
statements = parser.parse(content)
|
|
193
|
+
|
|
194
|
+
if not statements:
|
|
195
|
+
return QuipuResult(
|
|
196
|
+
success=True,
|
|
197
|
+
exit_code=0,
|
|
198
|
+
message="axon.warning.noStatements",
|
|
199
|
+
msg_kwargs={"parser": final_parser_name},
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
executor.execute(statements)
|
|
203
|
+
return QuipuResult(success=True, exit_code=0, message="axon.success")
|
|
204
|
+
|
|
205
|
+
except CoreExecutionError as e:
|
|
206
|
+
logger.error(f"❌ 操作失败: {e}")
|
|
207
|
+
return QuipuResult(
|
|
208
|
+
success=False, exit_code=1, message="run.error.execution", msg_kwargs={"error": str(e)}, error=e
|
|
209
|
+
)
|
|
210
|
+
except Exception as e:
|
|
211
|
+
logger.error(f"运行时错误: {e}", exc_info=True)
|
|
212
|
+
return QuipuResult(
|
|
213
|
+
success=False, exit_code=1, message="run.error.system", msg_kwargs={"error": str(e)}, error=e
|
|
214
|
+
)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"QuipuApplication": |-
|
|
2
|
+
封装了 Quipu 核心业务流程的高层应用对象。
|
|
3
|
+
负责协调 Engine, Parser, Executor。
|
|
4
|
+
"QuipuApplication._prepare_workspace": |-
|
|
5
|
+
检查并准备工作区,处理状态漂移。
|
|
6
|
+
返回执行前的 input_tree_hash。
|
|
7
|
+
"QuipuApplication._setup_executor": |-
|
|
8
|
+
创建、配置并返回一个 Executor 实例,并注入确认处理器。
|
|
9
|
+
"QuipuApplication.run": |-
|
|
10
|
+
执行一个完整的 Plan。
|
|
11
|
+
"run_quipu": |-
|
|
12
|
+
Quipu 核心业务逻辑的入口包装器。
|
|
13
|
+
|
|
14
|
+
实例化并运行 QuipuApplication,捕获所有异常并转化为 QuipuResult。
|
|
15
|
+
确保资源被安全释放。
|
|
16
|
+
get_available_acts: |-
|
|
17
|
+
Statelessly discovers and returns all available acts.
|
|
18
|
+
|
|
19
|
+
This function initializes a temporary, stateless executor to discover
|
|
20
|
+
core acts and acts from plugins found relative to the working directory.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
work_dir: The directory from which to discover project-level plugins.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
A dictionary mapping act names to their docstrings.
|
|
27
|
+
run_stateless_plan: |-
|
|
28
|
+
Executes a plan in a stateless manner, bypassing the Quipu engine.
|
|
29
|
+
|
|
30
|
+
This function sets up a temporary executor, loads plugins, parses the content,
|
|
31
|
+
and executes the statements against the specified working directory.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
content: The string content of the plan to execute.
|
|
35
|
+
work_dir: The root directory for the execution.
|
|
36
|
+
confirmation_handler: A callable to handle user confirmations.
|
|
37
|
+
parser_name: The name of the parser to use ('auto' by default).
|
|
38
|
+
yolo: If True, skips all confirmation prompts.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
A QuipuResult object indicating the outcome of the execution.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from quipu.engine.config import ConfigManager
|
|
5
|
+
from quipu.engine.git_db import GitDB
|
|
6
|
+
from quipu.engine.git_object_storage import GitObjectHistoryReader, GitObjectHistoryWriter
|
|
7
|
+
from quipu.engine.state_machine import Engine
|
|
8
|
+
|
|
9
|
+
from .utils import find_git_repository_root
|
|
10
|
+
|
|
11
|
+
# 迟延导入以避免循环依赖
|
|
12
|
+
try:
|
|
13
|
+
from quipu.engine.sqlite_db import DatabaseManager
|
|
14
|
+
from quipu.engine.sqlite_storage import SQLiteHistoryReader, SQLiteHistoryWriter
|
|
15
|
+
except ImportError:
|
|
16
|
+
DatabaseManager = None
|
|
17
|
+
SQLiteHistoryWriter = None
|
|
18
|
+
SQLiteHistoryReader = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def create_engine(work_dir: Path, lazy: bool = False) -> Engine:
|
|
25
|
+
project_root = find_git_repository_root(work_dir) or work_dir
|
|
26
|
+
config = ConfigManager(project_root)
|
|
27
|
+
storage_type = config.get("storage.type", "git_object")
|
|
28
|
+
logger.debug(f"Engine factory configured with storage type: '{storage_type}'")
|
|
29
|
+
git_db = GitDB(project_root)
|
|
30
|
+
db_manager = None
|
|
31
|
+
|
|
32
|
+
# 默认和备用后端
|
|
33
|
+
reader = GitObjectHistoryReader(git_db)
|
|
34
|
+
writer = GitObjectHistoryWriter(git_db)
|
|
35
|
+
|
|
36
|
+
if storage_type == "sqlite":
|
|
37
|
+
if not DatabaseManager or not SQLiteHistoryWriter or not SQLiteHistoryReader:
|
|
38
|
+
raise ImportError("SQLite dependencies could not be loaded. Please check your installation.")
|
|
39
|
+
|
|
40
|
+
logger.debug("Using SQLite storage format for reads and writes.")
|
|
41
|
+
db_manager = DatabaseManager(project_root)
|
|
42
|
+
db_manager.init_schema()
|
|
43
|
+
|
|
44
|
+
# 切换到 SQLite 后端
|
|
45
|
+
reader = SQLiteHistoryReader(db_manager=db_manager, git_db=git_db)
|
|
46
|
+
writer = SQLiteHistoryWriter(git_writer=writer, db_manager=db_manager)
|
|
47
|
+
|
|
48
|
+
elif storage_type != "git_object":
|
|
49
|
+
raise NotImplementedError(f"Storage type '{storage_type}' is not supported.")
|
|
50
|
+
|
|
51
|
+
# 将所有资源注入 Engine
|
|
52
|
+
engine = Engine(project_root, db=git_db, reader=reader, writer=writer, db_manager=db_manager)
|
|
53
|
+
if not lazy:
|
|
54
|
+
engine.align()
|
|
55
|
+
|
|
56
|
+
return engine
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from quipu.runtime.executor import Executor
|
|
5
|
+
from quipu.runtime.plugin_loader import load_plugins
|
|
6
|
+
|
|
7
|
+
from .utils import find_git_repository_root
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PluginManager:
|
|
11
|
+
def load_from_sources(self, executor: Executor, work_dir: Path):
|
|
12
|
+
plugin_sources = []
|
|
13
|
+
|
|
14
|
+
# 优先级由低到高添加,后面的会覆盖前面的
|
|
15
|
+
# 1. User Home (Lowest priority)
|
|
16
|
+
home_acts = Path.home() / ".quipu" / "acts"
|
|
17
|
+
plugin_sources.append(("🏠 Global", home_acts))
|
|
18
|
+
|
|
19
|
+
# 2. Config / Env
|
|
20
|
+
env_path = os.getenv("AXON_EXTRA_ACTS_DIR")
|
|
21
|
+
if env_path:
|
|
22
|
+
plugin_sources.append(("🔧 Env", Path(env_path)))
|
|
23
|
+
|
|
24
|
+
# 3. Project Root (Highest priority)
|
|
25
|
+
project_root_for_plugins = find_git_repository_root(work_dir)
|
|
26
|
+
if project_root_for_plugins:
|
|
27
|
+
proj_acts = project_root_for_plugins / ".quipu" / "acts"
|
|
28
|
+
plugin_sources.append(("📦 Project", proj_acts))
|
|
29
|
+
|
|
30
|
+
seen_paths = set()
|
|
31
|
+
for label, path in plugin_sources:
|
|
32
|
+
if not path.exists() or not path.is_dir():
|
|
33
|
+
continue
|
|
34
|
+
|
|
35
|
+
resolved_path = path.resolve()
|
|
36
|
+
if resolved_path in seen_paths:
|
|
37
|
+
continue
|
|
38
|
+
|
|
39
|
+
load_plugins(executor, path)
|
|
40
|
+
seen_paths.add(resolved_path)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def find_git_repository_root(start_path: Path) -> Optional[Path]:
|
|
9
|
+
try:
|
|
10
|
+
current = start_path.resolve()
|
|
11
|
+
for parent in [current] + list(current.parents):
|
|
12
|
+
if (parent / ".git").exists():
|
|
13
|
+
return parent
|
|
14
|
+
except Exception:
|
|
15
|
+
pass
|
|
16
|
+
return None
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
{
|
|
2
|
+
"fingerprints": {
|
|
3
|
+
"py://packages/pyquipu-application/src/quipu/application/controller.py#QuipuApplication": {
|
|
4
|
+
"baseline_code_signature_text": "class QuipuApplication:",
|
|
5
|
+
"baseline_code_structure_hash": "d2fb5198cd42afc8c323a52537de5b33164c9a4bb8afa8c3405447eddbafe483",
|
|
6
|
+
"baseline_yaml_content_hash": "2097b05b0863d0d255508bf524ec72afc5bb74f8a8472a9d5c42db1d78375647"
|
|
7
|
+
},
|
|
8
|
+
"py://packages/pyquipu-application/src/quipu/application/controller.py#QuipuApplication.__init__": {
|
|
9
|
+
"baseline_code_signature_text": "def __init__(self, work_dir: Path, confirmation_handler: ConfirmationHandler, yolo: bool = False):",
|
|
10
|
+
"baseline_code_structure_hash": "b2ac0b22c850eed01f7838721b9a9040bba40f58237545f3f86d306fa4327f22"
|
|
11
|
+
},
|
|
12
|
+
"py://packages/pyquipu-application/src/quipu/application/controller.py#QuipuApplication._prepare_workspace": {
|
|
13
|
+
"baseline_code_signature_text": "def _prepare_workspace(self) -> str:",
|
|
14
|
+
"baseline_code_structure_hash": "528a79ec9be83b31b3016bc3a8c5492b13b106464c5bcdca495b4fae187b574d",
|
|
15
|
+
"baseline_yaml_content_hash": "81dd6b666cf091c621c87fe727fe2f20be9183179073f9c25993d0781ac9ac58"
|
|
16
|
+
},
|
|
17
|
+
"py://packages/pyquipu-application/src/quipu/application/controller.py#QuipuApplication._setup_executor": {
|
|
18
|
+
"baseline_code_signature_text": "def _setup_executor(self) -> Executor:",
|
|
19
|
+
"baseline_code_structure_hash": "2cccb8bc4e91794d693f8f3f1f9c53109587bcdd7f5b158ca72f92234e3d74cd",
|
|
20
|
+
"baseline_yaml_content_hash": "9881f5ff456f048f0f958362c910fedcfb0738b4f461dd8a0e04d6fbc30d0944"
|
|
21
|
+
},
|
|
22
|
+
"py://packages/pyquipu-application/src/quipu/application/controller.py#QuipuApplication.run": {
|
|
23
|
+
"baseline_code_signature_text": "def run(self, content: str, parser_name: str) -> QuipuResult:",
|
|
24
|
+
"baseline_code_structure_hash": "458e8779b9f0df2331eada9d10d5fa4fe729a00ed7a5fde70b771d9beb5bfa8b",
|
|
25
|
+
"baseline_yaml_content_hash": "ba87b5c38065687528f0d842b3c042da0e4c164a6019a46707ead5b7f2488997"
|
|
26
|
+
},
|
|
27
|
+
"py://packages/pyquipu-application/src/quipu/application/controller.py#get_available_acts": {
|
|
28
|
+
"baseline_code_signature_text": "def get_available_acts(work_dir: Path) -> Dict[str, str]:",
|
|
29
|
+
"baseline_code_structure_hash": "442aa8ba3388add2d240e0f9bc612bf35528e11d40c765c6ec3a44e4eb874f8b",
|
|
30
|
+
"baseline_yaml_content_hash": "bd97f637450a7cf01f330f5106a83d9e916dde59d64f2fe89ba667ea8e23e4ac"
|
|
31
|
+
},
|
|
32
|
+
"py://packages/pyquipu-application/src/quipu/application/controller.py#run_quipu": {
|
|
33
|
+
"baseline_code_signature_text": "def run_quipu(content: str, work_dir: Path, confirmation_handler: ConfirmationHandler, parser_name: str = 'auto', yolo: bool = False) -> QuipuResult:",
|
|
34
|
+
"baseline_code_structure_hash": "d977d58a9db1f2c9d7fb2d5fabb9cccc5edcdb1aeff988f1fa78442b229c699c",
|
|
35
|
+
"baseline_yaml_content_hash": "316165fbe76e51b117eb3c9c983c8d479df4490b601609f2b0aa5176c9cbd96b"
|
|
36
|
+
},
|
|
37
|
+
"py://packages/pyquipu-application/src/quipu/application/controller.py#run_stateless_plan": {
|
|
38
|
+
"baseline_code_signature_text": "def run_stateless_plan(content: str, work_dir: Path, confirmation_handler: ConfirmationHandler, parser_name: str = 'auto', yolo: bool = False) -> QuipuResult:",
|
|
39
|
+
"baseline_code_structure_hash": "58320dcc77cb54d7ab31bafcc383b0c8dd3953aaeaf9f8208dab0af5094ffd4e",
|
|
40
|
+
"baseline_yaml_content_hash": "aa58fd5cec8b7c127c3a729915c6d4f99c875e4405edba365a5f03f5dff992f7"
|
|
41
|
+
},
|
|
42
|
+
"py://packages/pyquipu-application/src/quipu/application/factory.py#create_engine": {
|
|
43
|
+
"baseline_code_signature_text": "def create_engine(work_dir: Path, lazy: bool = False) -> Engine:",
|
|
44
|
+
"baseline_code_structure_hash": "4565a1494d7662227444205f34a6e4458c80db2856e465388e6d987a15c015c2",
|
|
45
|
+
"baseline_yaml_content_hash": "3dba786e050a1669e0b95d77fdb347afd214cb596dec260790fbfad6963db840"
|
|
46
|
+
},
|
|
47
|
+
"py://packages/pyquipu-application/src/quipu/application/plugin_manager.py#PluginManager": {
|
|
48
|
+
"baseline_code_signature_text": "class PluginManager:",
|
|
49
|
+
"baseline_code_structure_hash": "8d211ba7f4db76c8fa4afc820902583d5f7df5c7e482f99b988dba61ece4a2f5",
|
|
50
|
+
"baseline_yaml_content_hash": "c22797fdf50ccd3ac64731ca93183e2f70839b0611a049639bdc77b4f8fe7f15"
|
|
51
|
+
},
|
|
52
|
+
"py://packages/pyquipu-application/src/quipu/application/plugin_manager.py#PluginManager.load_from_sources": {
|
|
53
|
+
"baseline_code_signature_text": "def load_from_sources(self, executor: Executor, work_dir: Path):",
|
|
54
|
+
"baseline_code_structure_hash": "e1015eaa8d17516183baa58ec0239c567900a308ec70f60b9ce80fa9e1c72fd4",
|
|
55
|
+
"baseline_yaml_content_hash": "34b1c59e7fa9a03d089fccdafa494a216e5a7746e0aff3b5891a262b88986369"
|
|
56
|
+
},
|
|
57
|
+
"py://packages/pyquipu-application/src/quipu/application/utils.py#find_git_repository_root": {
|
|
58
|
+
"baseline_code_signature_text": "def find_git_repository_root(start_path: Path) -> Optional[Path]:",
|
|
59
|
+
"baseline_code_structure_hash": "9f19014baa92289d9b4f04d4ee8a92df475d31c38820edc4ffc20efc9ce218a8",
|
|
60
|
+
"baseline_yaml_content_hash": "8ab446ed5e94789aede646cc369436537ffdd64fc5da3229937ba99d5bc83bb7"
|
|
61
|
+
},
|
|
62
|
+
"py://packages/pyquipu-application/tests/unit/test_controller.py#TestControllerUnit": {
|
|
63
|
+
"baseline_code_signature_text": "class TestControllerUnit:",
|
|
64
|
+
"baseline_code_structure_hash": "b8668020e45ac212142e3e2194f2ce94369784ba3969a4757515a4059960e650",
|
|
65
|
+
"baseline_yaml_content_hash": "e2a45f750adc799b997d14d1c706d2542d3856ce9aba7773a49c5ba2ab15f1d3"
|
|
66
|
+
},
|
|
67
|
+
"py://packages/pyquipu-application/tests/unit/test_controller.py#TestControllerUnit.test_run_quipu_empty_plan": {
|
|
68
|
+
"baseline_code_signature_text": "def test_run_quipu_empty_plan(self, tmp_path, mock_engine, mock_runtime):",
|
|
69
|
+
"baseline_code_structure_hash": "d5d7459ace504109a1e35f058714a51eb8119c0ea9051168960d877179082f55",
|
|
70
|
+
"baseline_yaml_content_hash": "1071d0c4058fdf8a5b23344da1028be9c9f008be34eb421479e19174b14c4b6a"
|
|
71
|
+
},
|
|
72
|
+
"py://packages/pyquipu-application/tests/unit/test_controller.py#TestControllerUnit.test_run_quipu_execution_error": {
|
|
73
|
+
"baseline_code_signature_text": "def test_run_quipu_execution_error(self, tmp_path, mock_engine, mock_runtime):",
|
|
74
|
+
"baseline_code_structure_hash": "97f2e3585c826243e7b61c6b4e3ae485f9986b81c50ef6c72058494a5b12960b",
|
|
75
|
+
"baseline_yaml_content_hash": "b8ffd9eec97c064fabd6851169bed6211c276c164ec09f3747a2203127b28bf4"
|
|
76
|
+
},
|
|
77
|
+
"py://packages/pyquipu-application/tests/unit/test_controller.py#TestControllerUnit.test_run_quipu_success": {
|
|
78
|
+
"baseline_code_signature_text": "def test_run_quipu_success(self, tmp_path, mock_engine, mock_runtime):",
|
|
79
|
+
"baseline_code_structure_hash": "eed582f286876d423157f91ac81d1e7963b67cf3eaf66750e14526adf7832146",
|
|
80
|
+
"baseline_yaml_content_hash": "51d669c5b333dde1c9c4080b4e32e99ee7bfc81f069c896b5dc234b543787810"
|
|
81
|
+
},
|
|
82
|
+
"py://packages/pyquipu-application/tests/unit/test_utils.py#TestRootDiscovery": {
|
|
83
|
+
"baseline_code_signature_text": "class TestRootDiscovery:",
|
|
84
|
+
"baseline_code_structure_hash": "3b99ed1f64e93ef2a1c0050627ddd0d49734803e0b2e6f16697299dd446a4888"
|
|
85
|
+
},
|
|
86
|
+
"py://packages/pyquipu-application/tests/unit/test_utils.py#TestRootDiscovery.test_find_git_repository_root": {
|
|
87
|
+
"baseline_code_signature_text": "def test_find_git_repository_root(self, tmp_path: Path):",
|
|
88
|
+
"baseline_code_structure_hash": "512797d245dc49e41a5d3c7c638481b2b0bc3efa3c6be7b89b058842beea7641"
|
|
89
|
+
},
|
|
90
|
+
"py://packages/quipu-application/src/pyquipu/application/controller.py#ConfirmationHandler": {},
|
|
91
|
+
"py://packages/quipu-application/src/pyquipu/application/controller.py#QuipuApplication": {
|
|
92
|
+
"baseline_code_signature_text": "class QuipuApplication:",
|
|
93
|
+
"baseline_code_structure_hash": "d2fb5198cd42afc8c323a52537de5b33164c9a4bb8afa8c3405447eddbafe483",
|
|
94
|
+
"baseline_yaml_content_hash": "2097b05b0863d0d255508bf524ec72afc5bb74f8a8472a9d5c42db1d78375647"
|
|
95
|
+
},
|
|
96
|
+
"py://packages/quipu-application/src/pyquipu/application/controller.py#QuipuApplication.__init__": {
|
|
97
|
+
"baseline_code_signature_text": "def __init__(self, work_dir: Path, confirmation_handler: ConfirmationHandler, yolo: bool = False):",
|
|
98
|
+
"baseline_code_structure_hash": "b2ac0b22c850eed01f7838721b9a9040bba40f58237545f3f86d306fa4327f22"
|
|
99
|
+
},
|
|
100
|
+
"py://packages/quipu-application/src/pyquipu/application/controller.py#QuipuApplication._prepare_workspace": {
|
|
101
|
+
"baseline_code_signature_text": "def _prepare_workspace(self) -> str:",
|
|
102
|
+
"baseline_code_structure_hash": "528a79ec9be83b31b3016bc3a8c5492b13b106464c5bcdca495b4fae187b574d",
|
|
103
|
+
"baseline_yaml_content_hash": "81dd6b666cf091c621c87fe727fe2f20be9183179073f9c25993d0781ac9ac58"
|
|
104
|
+
},
|
|
105
|
+
"py://packages/quipu-application/src/pyquipu/application/controller.py#QuipuApplication._setup_executor": {
|
|
106
|
+
"baseline_code_signature_text": "def _setup_executor(self) -> Executor:",
|
|
107
|
+
"baseline_code_structure_hash": "2cccb8bc4e91794d693f8f3f1f9c53109587bcdd7f5b158ca72f92234e3d74cd",
|
|
108
|
+
"baseline_yaml_content_hash": "9881f5ff456f048f0f958362c910fedcfb0738b4f461dd8a0e04d6fbc30d0944"
|
|
109
|
+
},
|
|
110
|
+
"py://packages/quipu-application/src/pyquipu/application/controller.py#QuipuApplication.confirmation_handler": {},
|
|
111
|
+
"py://packages/quipu-application/src/pyquipu/application/controller.py#QuipuApplication.engine": {},
|
|
112
|
+
"py://packages/quipu-application/src/pyquipu/application/controller.py#QuipuApplication.run": {
|
|
113
|
+
"baseline_code_signature_text": "def run(self, content: str, parser_name: str) -> QuipuResult:",
|
|
114
|
+
"baseline_code_structure_hash": "458e8779b9f0df2331eada9d10d5fa4fe729a00ed7a5fde70b771d9beb5bfa8b",
|
|
115
|
+
"baseline_yaml_content_hash": "ba87b5c38065687528f0d842b3c042da0e4c164a6019a46707ead5b7f2488997"
|
|
116
|
+
},
|
|
117
|
+
"py://packages/quipu-application/src/pyquipu/application/controller.py#QuipuApplication.work_dir": {},
|
|
118
|
+
"py://packages/quipu-application/src/pyquipu/application/controller.py#QuipuApplication.yolo": {},
|
|
119
|
+
"py://packages/quipu-application/src/pyquipu/application/controller.py#logger": {},
|
|
120
|
+
"py://packages/quipu-application/src/pyquipu/application/controller.py#run_quipu": {
|
|
121
|
+
"baseline_code_signature_text": "def run_quipu(content: str, work_dir: Path, confirmation_handler: ConfirmationHandler, parser_name: str = 'auto', yolo: bool = False) -> QuipuResult:",
|
|
122
|
+
"baseline_code_structure_hash": "d977d58a9db1f2c9d7fb2d5fabb9cccc5edcdb1aeff988f1fa78442b229c699c",
|
|
123
|
+
"baseline_yaml_content_hash": "316165fbe76e51b117eb3c9c983c8d479df4490b601609f2b0aa5176c9cbd96b"
|
|
124
|
+
},
|
|
125
|
+
"py://packages/quipu-application/src/pyquipu/application/factory.py#create_engine": {
|
|
126
|
+
"baseline_code_signature_text": "def create_engine(work_dir: Path, lazy: bool = False) -> Engine:",
|
|
127
|
+
"baseline_code_structure_hash": "4565a1494d7662227444205f34a6e4458c80db2856e465388e6d987a15c015c2",
|
|
128
|
+
"baseline_yaml_content_hash": "3dba786e050a1669e0b95d77fdb347afd214cb596dec260790fbfad6963db840"
|
|
129
|
+
},
|
|
130
|
+
"py://packages/quipu-application/src/pyquipu/application/factory.py#logger": {},
|
|
131
|
+
"py://packages/quipu-application/src/pyquipu/application/plugin_manager.py#PluginManager": {
|
|
132
|
+
"baseline_code_signature_text": "class PluginManager:",
|
|
133
|
+
"baseline_code_structure_hash": "8d211ba7f4db76c8fa4afc820902583d5f7df5c7e482f99b988dba61ece4a2f5",
|
|
134
|
+
"baseline_yaml_content_hash": "c22797fdf50ccd3ac64731ca93183e2f70839b0611a049639bdc77b4f8fe7f15"
|
|
135
|
+
},
|
|
136
|
+
"py://packages/quipu-application/src/pyquipu/application/plugin_manager.py#PluginManager.load_from_sources": {
|
|
137
|
+
"baseline_code_signature_text": "def load_from_sources(self, executor: Executor, work_dir: Path):",
|
|
138
|
+
"baseline_code_structure_hash": "e1015eaa8d17516183baa58ec0239c567900a308ec70f60b9ce80fa9e1c72fd4",
|
|
139
|
+
"baseline_yaml_content_hash": "34b1c59e7fa9a03d089fccdafa494a216e5a7746e0aff3b5891a262b88986369"
|
|
140
|
+
},
|
|
141
|
+
"py://packages/quipu-application/src/pyquipu/application/utils.py#find_git_repository_root": {
|
|
142
|
+
"baseline_code_signature_text": "def find_git_repository_root(start_path: Path) -> Optional[Path]:",
|
|
143
|
+
"baseline_code_structure_hash": "9f19014baa92289d9b4f04d4ee8a92df475d31c38820edc4ffc20efc9ce218a8",
|
|
144
|
+
"baseline_yaml_content_hash": "8ab446ed5e94789aede646cc369436537ffdd64fc5da3229937ba99d5bc83bb7"
|
|
145
|
+
},
|
|
146
|
+
"py://packages/quipu-application/src/pyquipu/application/utils.py#logger": {},
|
|
147
|
+
"py://packages/quipu-application/tests/unit/conftest.py#__all__": {},
|
|
148
|
+
"py://packages/quipu-application/tests/unit/test_controller.py#TestControllerUnit": {
|
|
149
|
+
"baseline_code_signature_text": "class TestControllerUnit:",
|
|
150
|
+
"baseline_code_structure_hash": "b8668020e45ac212142e3e2194f2ce94369784ba3969a4757515a4059960e650",
|
|
151
|
+
"baseline_yaml_content_hash": "e2a45f750adc799b997d14d1c706d2542d3856ce9aba7773a49c5ba2ab15f1d3"
|
|
152
|
+
},
|
|
153
|
+
"py://packages/quipu-application/tests/unit/test_controller.py#TestControllerUnit.test_run_quipu_empty_plan": {
|
|
154
|
+
"baseline_code_signature_text": "def test_run_quipu_empty_plan(self, tmp_path, mock_engine, mock_runtime):",
|
|
155
|
+
"baseline_code_structure_hash": "d5d7459ace504109a1e35f058714a51eb8119c0ea9051168960d877179082f55",
|
|
156
|
+
"baseline_yaml_content_hash": "1071d0c4058fdf8a5b23344da1028be9c9f008be34eb421479e19174b14c4b6a"
|
|
157
|
+
},
|
|
158
|
+
"py://packages/quipu-application/tests/unit/test_controller.py#TestControllerUnit.test_run_quipu_execution_error": {
|
|
159
|
+
"baseline_code_signature_text": "def test_run_quipu_execution_error(self, tmp_path, mock_engine, mock_runtime):",
|
|
160
|
+
"baseline_code_structure_hash": "97f2e3585c826243e7b61c6b4e3ae485f9986b81c50ef6c72058494a5b12960b",
|
|
161
|
+
"baseline_yaml_content_hash": "b8ffd9eec97c064fabd6851169bed6211c276c164ec09f3747a2203127b28bf4"
|
|
162
|
+
},
|
|
163
|
+
"py://packages/quipu-application/tests/unit/test_controller.py#TestControllerUnit.test_run_quipu_success": {
|
|
164
|
+
"baseline_code_signature_text": "def test_run_quipu_success(self, tmp_path, mock_engine, mock_runtime):",
|
|
165
|
+
"baseline_code_structure_hash": "eed582f286876d423157f91ac81d1e7963b67cf3eaf66750e14526adf7832146",
|
|
166
|
+
"baseline_yaml_content_hash": "51d669c5b333dde1c9c4080b4e32e99ee7bfc81f069c896b5dc234b543787810"
|
|
167
|
+
},
|
|
168
|
+
"py://packages/quipu-application/tests/unit/test_utils.py#TestRootDiscovery": {
|
|
169
|
+
"baseline_code_signature_text": "class TestRootDiscovery:",
|
|
170
|
+
"baseline_code_structure_hash": "3b99ed1f64e93ef2a1c0050627ddd0d49734803e0b2e6f16697299dd446a4888"
|
|
171
|
+
},
|
|
172
|
+
"py://packages/quipu-application/tests/unit/test_utils.py#TestRootDiscovery.test_find_git_repository_root": {
|
|
173
|
+
"baseline_code_signature_text": "def test_find_git_repository_root(self, tmp_path: Path):",
|
|
174
|
+
"baseline_code_structure_hash": "512797d245dc49e41a5d3c7c638481b2b0bc3efa3c6be7b89b058842beea7641"
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
"version": "1.0"
|
|
178
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from unittest.mock import MagicMock, patch
|
|
2
|
+
|
|
3
|
+
from quipu.application.controller import run_quipu
|
|
4
|
+
from quipu.interfaces.exceptions import ExecutionError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TestControllerUnit:
|
|
8
|
+
def test_run_quipu_success(self, tmp_path, mock_engine, mock_runtime):
|
|
9
|
+
plan_content = """
|
|
10
|
+
```act
|
|
11
|
+
echo
|
|
12
|
+
```
|
|
13
|
+
```text
|
|
14
|
+
hello
|
|
15
|
+
```
|
|
16
|
+
"""
|
|
17
|
+
# 配置 Mock Engine 的状态以通过 _prepare_workspace 检查
|
|
18
|
+
# 模拟当前是干净状态 (clean)
|
|
19
|
+
mock_engine.git_db.get_tree_hash.return_value = "hash_123"
|
|
20
|
+
mock_node = MagicMock()
|
|
21
|
+
mock_node.output_tree = "hash_123"
|
|
22
|
+
mock_engine.current_node = mock_node
|
|
23
|
+
mock_engine.history_graph = {"hash_123": mock_node}
|
|
24
|
+
|
|
25
|
+
# Patch 工厂函数和 Executor 类
|
|
26
|
+
# 注意:controller 直接导入了 Executor 类,所以我们要 patch 这个类
|
|
27
|
+
with (
|
|
28
|
+
patch("quipu.application.controller.create_engine", return_value=mock_engine) as mk_eng_fac,
|
|
29
|
+
patch("quipu.application.controller.Executor", return_value=mock_runtime) as mk_exec_cls,
|
|
30
|
+
):
|
|
31
|
+
# 执行
|
|
32
|
+
result = run_quipu(content=plan_content, work_dir=tmp_path, yolo=True, confirmation_handler=lambda *a: True)
|
|
33
|
+
|
|
34
|
+
# 验证结果
|
|
35
|
+
assert result.success is True
|
|
36
|
+
assert result.exit_code == 0
|
|
37
|
+
|
|
38
|
+
# 验证交互
|
|
39
|
+
mk_eng_fac.assert_called_once_with(tmp_path)
|
|
40
|
+
# Executor 类被实例化
|
|
41
|
+
mk_exec_cls.assert_called_once()
|
|
42
|
+
|
|
43
|
+
# 验证编排顺序
|
|
44
|
+
# 1. _prepare_workspace 调用了 get_tree_hash
|
|
45
|
+
mock_engine.git_db.get_tree_hash.assert_called()
|
|
46
|
+
|
|
47
|
+
# 2. Executor 执行
|
|
48
|
+
mock_runtime.execute.assert_called_once()
|
|
49
|
+
|
|
50
|
+
# 3. 最后生成 Plan Node
|
|
51
|
+
mock_engine.create_plan_node.assert_called_once()
|
|
52
|
+
|
|
53
|
+
def test_run_quipu_execution_error(self, tmp_path, mock_engine, mock_runtime):
|
|
54
|
+
plan_content = """
|
|
55
|
+
```act
|
|
56
|
+
fail_act
|
|
57
|
+
```
|
|
58
|
+
"""
|
|
59
|
+
# 配置 Mock Engine
|
|
60
|
+
mock_engine.git_db.get_tree_hash.return_value = "hash_123"
|
|
61
|
+
mock_engine.current_node = MagicMock()
|
|
62
|
+
mock_engine.current_node.output_tree = "hash_123"
|
|
63
|
+
|
|
64
|
+
with (
|
|
65
|
+
patch("quipu.application.controller.create_engine", return_value=mock_engine),
|
|
66
|
+
patch("quipu.application.controller.Executor", return_value=mock_runtime),
|
|
67
|
+
):
|
|
68
|
+
# 模拟 Runtime 抛出业务异常
|
|
69
|
+
mock_runtime.execute.side_effect = ExecutionError("Task failed successfully")
|
|
70
|
+
|
|
71
|
+
result = run_quipu(content=plan_content, work_dir=tmp_path, yolo=True, confirmation_handler=lambda *a: True)
|
|
72
|
+
|
|
73
|
+
# 验证错误被捕获并封装到 Result 中
|
|
74
|
+
assert result.success is False
|
|
75
|
+
assert result.exit_code == 1
|
|
76
|
+
assert result.message == "run.error.execution"
|
|
77
|
+
assert isinstance(result.error, ExecutionError)
|
|
78
|
+
assert "Task failed successfully" in str(result.error)
|
|
79
|
+
|
|
80
|
+
def test_run_quipu_empty_plan(self, tmp_path, mock_engine, mock_runtime):
|
|
81
|
+
plan_content = "Just some text, no acts."
|
|
82
|
+
|
|
83
|
+
# 配置 Mock Engine
|
|
84
|
+
mock_engine.git_db.get_tree_hash.return_value = "hash_123"
|
|
85
|
+
mock_engine.current_node = MagicMock()
|
|
86
|
+
mock_engine.current_node.output_tree = "hash_123"
|
|
87
|
+
|
|
88
|
+
with (
|
|
89
|
+
patch("quipu.application.controller.create_engine", return_value=mock_engine),
|
|
90
|
+
patch("quipu.application.controller.Executor", return_value=mock_runtime),
|
|
91
|
+
):
|
|
92
|
+
result = run_quipu(content=plan_content, work_dir=tmp_path, yolo=True, confirmation_handler=lambda *a: True)
|
|
93
|
+
|
|
94
|
+
# 空计划通常不算失败,但也没有副作用
|
|
95
|
+
assert result.success is True
|
|
96
|
+
assert result.exit_code == 0
|
|
97
|
+
assert result.message == "axon.warning.noStatements"
|
|
98
|
+
|
|
99
|
+
# 验证没有调用 execute
|
|
100
|
+
mock_runtime.execute.assert_not_called()
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"TestControllerUnit": |-
|
|
2
|
+
对 Application 层 Controller 的纯单元测试。
|
|
3
|
+
使用 Mock 替代真实的 Engine 和 Runtime,仅验证编排逻辑。
|
|
4
|
+
"TestControllerUnit.test_run_quipu_empty_plan": |-
|
|
5
|
+
测试空计划的处理。
|
|
6
|
+
"TestControllerUnit.test_run_quipu_execution_error": |-
|
|
7
|
+
测试执行器抛出异常时的错误处理流程。
|
|
8
|
+
"TestControllerUnit.test_run_quipu_success": |-
|
|
9
|
+
测试正常执行流程:应正确初始化组件并按顺序调用。
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from quipu.application.utils import find_git_repository_root
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TestRootDiscovery:
|
|
7
|
+
def test_find_git_repository_root(self, tmp_path: Path):
|
|
8
|
+
# /project/.git
|
|
9
|
+
# /project/src/subdir
|
|
10
|
+
project = tmp_path / "project"
|
|
11
|
+
project.mkdir()
|
|
12
|
+
(project / ".git").mkdir()
|
|
13
|
+
|
|
14
|
+
subdir = project / "src" / "subdir"
|
|
15
|
+
subdir.mkdir(parents=True)
|
|
16
|
+
|
|
17
|
+
# Case 1: From subdir
|
|
18
|
+
assert find_git_repository_root(subdir) == project.resolve()
|
|
19
|
+
|
|
20
|
+
# Case 2: From root
|
|
21
|
+
assert find_git_repository_root(project) == project.resolve()
|
|
22
|
+
|
|
23
|
+
# Case 3: Outside
|
|
24
|
+
outside = tmp_path / "outside"
|
|
25
|
+
outside.mkdir()
|
|
26
|
+
assert find_git_repository_root(outside) is None
|