patchwarden 0.6.4 → 1.5.0
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.
- package/README.en.md +54 -20
- package/README.md +48 -21
- package/dist/agents/agentRouter.d.ts +40 -0
- package/dist/agents/agentRouter.js +95 -0
- package/dist/config.d.ts +1 -1
- package/dist/config.js +6 -2
- package/dist/controlCenter.js +212 -3
- package/dist/direct/directSessionStore.d.ts +2 -0
- package/dist/direct/directVerification.js +7 -0
- package/dist/doctor.js +32 -1
- package/dist/goal/acceptanceEngine.d.ts +62 -0
- package/dist/goal/acceptanceEngine.js +103 -0
- package/dist/goal/acceptanceTemplate.d.ts +10 -0
- package/dist/goal/acceptanceTemplate.js +104 -0
- package/dist/goal/goalGraph.d.ts +58 -0
- package/dist/goal/goalGraph.js +189 -0
- package/dist/goal/goalProgress.d.ts +81 -0
- package/dist/goal/goalProgress.js +216 -0
- package/dist/goal/goalStatus.d.ts +60 -0
- package/dist/goal/goalStatus.js +137 -0
- package/dist/goal/goalStore.d.ts +79 -0
- package/dist/goal/goalStore.js +211 -0
- package/dist/goal/handoffExport.d.ts +34 -0
- package/dist/goal/handoffExport.js +183 -0
- package/dist/goal/subgoalSync.d.ts +40 -0
- package/dist/goal/subgoalSync.js +72 -0
- package/dist/goal/worktreeManager.d.ts +96 -0
- package/dist/goal/worktreeManager.js +292 -0
- package/dist/logging.d.ts +44 -0
- package/dist/logging.js +68 -0
- package/dist/policy/projectPolicy.d.ts +55 -0
- package/dist/policy/projectPolicy.js +286 -0
- package/dist/release/releaseGate.d.ts +99 -0
- package/dist/release/releaseGate.js +475 -0
- package/dist/runner/postTaskCleanup.d.ts +13 -0
- package/dist/runner/postTaskCleanup.js +147 -0
- package/dist/runner/runTask.js +50 -10
- package/dist/security/discoveryTokenStore.d.ts +62 -0
- package/dist/security/discoveryTokenStore.js +100 -0
- package/dist/security/toolInvocationGuard.d.ts +35 -0
- package/dist/security/toolInvocationGuard.js +153 -0
- package/dist/smoke-test.js +18 -10
- package/dist/taskRuntime.d.ts +17 -0
- package/dist/test/unit/acceptance-engine.test.d.ts +1 -0
- package/dist/test/unit/acceptance-engine.test.js +228 -0
- package/dist/test/unit/agent-router.test.d.ts +1 -0
- package/dist/test/unit/agent-router.test.js +287 -0
- package/dist/test/unit/audit-checks.test.d.ts +1 -0
- package/dist/test/unit/audit-checks.test.js +350 -0
- package/dist/test/unit/diagnose-task.test.d.ts +1 -0
- package/dist/test/unit/diagnose-task.test.js +457 -0
- package/dist/test/unit/discovery-token-store.test.d.ts +1 -0
- package/dist/test/unit/discovery-token-store.test.js +139 -0
- package/dist/test/unit/evidence-pack.test.d.ts +1 -0
- package/dist/test/unit/evidence-pack.test.js +130 -0
- package/dist/test/unit/goal-graph.test.d.ts +1 -0
- package/dist/test/unit/goal-graph.test.js +298 -0
- package/dist/test/unit/goal-progress.test.d.ts +1 -0
- package/dist/test/unit/goal-progress.test.js +381 -0
- package/dist/test/unit/goal-status.test.d.ts +1 -0
- package/dist/test/unit/goal-status.test.js +215 -0
- package/dist/test/unit/goal-store.test.d.ts +1 -0
- package/dist/test/unit/goal-store.test.js +253 -0
- package/dist/test/unit/goal-subgoal-task.test.d.ts +1 -0
- package/dist/test/unit/goal-subgoal-task.test.js +55 -0
- package/dist/test/unit/goal-tools-registry.test.d.ts +1 -0
- package/dist/test/unit/goal-tools-registry.test.js +190 -0
- package/dist/test/unit/handoff-export.test.d.ts +1 -0
- package/dist/test/unit/handoff-export.test.js +263 -0
- package/dist/test/unit/invoke-discovered-tool.test.d.ts +1 -0
- package/dist/test/unit/invoke-discovered-tool.test.js +167 -0
- package/dist/test/unit/logging.test.js +127 -5
- package/dist/test/unit/post-task-cleanup.test.d.ts +1 -0
- package/dist/test/unit/post-task-cleanup.test.js +48 -0
- package/dist/test/unit/project-policy-release-mode.test.d.ts +1 -0
- package/dist/test/unit/project-policy-release-mode.test.js +125 -0
- package/dist/test/unit/reconcile-tasks.test.d.ts +1 -0
- package/dist/test/unit/reconcile-tasks.test.js +456 -0
- package/dist/test/unit/release-gate.test.d.ts +1 -0
- package/dist/test/unit/release-gate.test.js +242 -0
- package/dist/test/unit/run-task-loop.test.d.ts +1 -0
- package/dist/test/unit/run-task-loop.test.js +380 -0
- package/dist/test/unit/safe-views.test.d.ts +1 -0
- package/dist/test/unit/safe-views.test.js +171 -0
- package/dist/test/unit/schema-drift-check.test.d.ts +1 -0
- package/dist/test/unit/schema-drift-check.test.js +176 -0
- package/dist/test/unit/subgoal-sync.test.d.ts +1 -0
- package/dist/test/unit/subgoal-sync.test.js +183 -0
- package/dist/test/unit/tool-invocation-guard.test.d.ts +1 -0
- package/dist/test/unit/tool-invocation-guard.test.js +432 -0
- package/dist/test/unit/tool-usage-stats.test.d.ts +1 -0
- package/dist/test/unit/tool-usage-stats.test.js +300 -0
- package/dist/test/unit/toolSearch.test.d.ts +1 -0
- package/dist/test/unit/toolSearch.test.js +571 -0
- package/dist/test/unit/worktree-manager.test.d.ts +1 -0
- package/dist/test/unit/worktree-manager.test.js +176 -0
- package/dist/tools/auditTask.d.ts +103 -1
- package/dist/tools/auditTask.js +461 -8
- package/dist/tools/cancelTask.js +1 -1
- package/dist/tools/checkReleaseGate.d.ts +21 -0
- package/dist/tools/checkReleaseGate.js +22 -0
- package/dist/tools/createTask.d.ts +18 -2
- package/dist/tools/createTask.js +29 -1
- package/dist/tools/diagnoseTask.d.ts +45 -0
- package/dist/tools/diagnoseTask.js +347 -0
- package/dist/tools/discardWorktree.d.ts +24 -0
- package/dist/tools/discardWorktree.js +27 -0
- package/dist/tools/discoverTools.d.ts +11 -0
- package/dist/tools/discoverTools.js +39 -0
- package/dist/tools/evidencePack.d.ts +39 -0
- package/dist/tools/evidencePack.js +168 -0
- package/dist/tools/explainTool.d.ts +11 -0
- package/dist/tools/explainTool.js +21 -0
- package/dist/tools/getTaskSummary.js +2 -1
- package/dist/tools/goalSubgoalTask.d.ts +51 -0
- package/dist/tools/goalSubgoalTask.js +110 -0
- package/dist/tools/healthCheck.d.ts +1 -0
- package/dist/tools/healthCheck.js +5 -1
- package/dist/tools/invokeDiscoveredTool.d.ts +37 -0
- package/dist/tools/invokeDiscoveredTool.js +112 -0
- package/dist/tools/listTasks.d.ts +3 -1
- package/dist/tools/listTasks.js +14 -1
- package/dist/tools/mergeWorktree.d.ts +24 -0
- package/dist/tools/mergeWorktree.js +27 -0
- package/dist/tools/recommendAgentForTask.d.ts +19 -0
- package/dist/tools/recommendAgentForTask.js +56 -0
- package/dist/tools/reconcileTasks.d.ts +41 -0
- package/dist/tools/reconcileTasks.js +352 -0
- package/dist/tools/registry.js +926 -2
- package/dist/tools/releaseMode.d.ts +50 -0
- package/dist/tools/releaseMode.js +370 -0
- package/dist/tools/runDirectVerificationBundle.d.ts +26 -0
- package/dist/tools/runDirectVerificationBundle.js +64 -0
- package/dist/tools/runTaskLoop.d.ts +57 -0
- package/dist/tools/runTaskLoop.js +417 -0
- package/dist/tools/runVerification.d.ts +4 -0
- package/dist/tools/runVerification.js +4 -0
- package/dist/tools/safeStatus.d.ts +31 -1
- package/dist/tools/safeStatus.js +43 -1
- package/dist/tools/safeViews.d.ts +262 -0
- package/dist/tools/safeViews.js +252 -0
- package/dist/tools/schemaDriftCheck.d.ts +46 -0
- package/dist/tools/schemaDriftCheck.js +80 -0
- package/dist/tools/taskLineage.d.ts +91 -0
- package/dist/tools/taskLineage.js +175 -0
- package/dist/tools/toolCatalog.d.ts +4 -3
- package/dist/tools/toolCatalog.js +39 -11
- package/dist/tools/toolRegistry.d.ts +61 -0
- package/dist/tools/toolRegistry.js +834 -0
- package/dist/tools/toolSearch.d.ts +110 -0
- package/dist/tools/toolSearch.js +331 -0
- package/dist/tools/toolUsageStats.d.ts +41 -0
- package/dist/tools/toolUsageStats.js +116 -0
- package/dist/tools/waitForTask.js +1 -0
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/dist/watcherStatus.d.ts +29 -0
- package/dist/watcherStatus.js +92 -1
- package/docs/chatgpt-usage.md +31 -0
- package/docs/control-center/README.md +9 -0
- package/package.json +3 -2
- package/scripts/checks/control-center-smoke.js +87 -0
- package/scripts/checks/control-smoke.js +2 -2
- package/scripts/checks/doctor-smoke.js +1 -0
- package/scripts/checks/lifecycle-smoke.js +13 -9
- package/scripts/checks/mcp-manifest-check.js +65 -6
- package/scripts/checks/mcp-smoke.js +60 -9
- package/scripts/checks/watcher-supervisor-smoke.js +2 -2
- package/src/agents/agentRouter.ts +149 -0
- package/src/config.ts +9 -3
- package/src/controlCenter.ts +215 -4
- package/src/direct/directSessionStore.ts +2 -0
- package/src/direct/directVerification.ts +7 -0
- package/src/doctor.ts +36 -1
- package/src/goal/acceptanceEngine.ts +160 -0
- package/src/goal/acceptanceTemplate.ts +121 -0
- package/src/goal/goalGraph.ts +225 -0
- package/src/goal/goalProgress.ts +301 -0
- package/src/goal/goalStatus.ts +234 -0
- package/src/goal/goalStore.ts +306 -0
- package/src/goal/handoffExport.ts +211 -0
- package/src/goal/subgoalSync.ts +82 -0
- package/src/goal/worktreeManager.ts +404 -0
- package/src/logging.ts +91 -0
- package/src/policy/projectPolicy.ts +344 -0
- package/src/release/releaseGate.ts +567 -0
- package/src/runner/postTaskCleanup.ts +154 -0
- package/src/runner/runTask.ts +49 -10
- package/src/security/discoveryTokenStore.ts +157 -0
- package/src/security/toolInvocationGuard.ts +251 -0
- package/src/smoke-test.ts +15 -7
- package/src/taskRuntime.ts +17 -0
- package/src/test/unit/acceptance-engine.test.ts +261 -0
- package/src/test/unit/agent-router.test.ts +342 -0
- package/src/test/unit/audit-checks.test.ts +567 -0
- package/src/test/unit/diagnose-task.test.ts +544 -0
- package/src/test/unit/discovery-token-store.test.ts +181 -0
- package/src/test/unit/evidence-pack.test.ts +142 -0
- package/src/test/unit/goal-graph.test.ts +347 -0
- package/src/test/unit/goal-progress.test.ts +538 -0
- package/src/test/unit/goal-status.test.ts +270 -0
- package/src/test/unit/goal-store.test.ts +318 -0
- package/src/test/unit/goal-subgoal-task.test.ts +72 -0
- package/src/test/unit/goal-tools-registry.test.ts +243 -0
- package/src/test/unit/handoff-export.test.ts +295 -0
- package/src/test/unit/invoke-discovered-tool.test.ts +216 -0
- package/src/test/unit/logging.test.ts +177 -5
- package/src/test/unit/post-task-cleanup.test.ts +53 -0
- package/src/test/unit/project-policy-release-mode.test.ts +156 -0
- package/src/test/unit/reconcile-tasks.test.ts +551 -0
- package/src/test/unit/release-gate.test.ts +314 -0
- package/src/test/unit/run-task-loop.test.ts +425 -0
- package/src/test/unit/safe-views.test.ts +184 -0
- package/src/test/unit/schema-drift-check.test.ts +260 -0
- package/src/test/unit/subgoal-sync.test.ts +236 -0
- package/src/test/unit/tool-invocation-guard.test.ts +542 -0
- package/src/test/unit/tool-usage-stats.test.ts +384 -0
- package/src/test/unit/toolSearch.test.ts +756 -0
- package/src/test/unit/worktree-manager.test.ts +247 -0
- package/src/tools/auditTask.ts +831 -402
- package/src/tools/cancelTask.ts +1 -1
- package/src/tools/checkReleaseGate.ts +45 -0
- package/src/tools/createTask.ts +64 -6
- package/src/tools/diagnoseTask.ts +460 -0
- package/src/tools/discardWorktree.ts +42 -0
- package/src/tools/discoverTools.ts +51 -0
- package/src/tools/evidencePack.ts +205 -0
- package/src/tools/explainTool.ts +26 -0
- package/src/tools/getTaskSummary.ts +2 -1
- package/src/tools/goalSubgoalTask.ts +170 -0
- package/src/tools/healthCheck.ts +4 -1
- package/src/tools/invokeDiscoveredTool.ts +163 -0
- package/src/tools/listTasks.ts +16 -2
- package/src/tools/listWorkspace.ts +71 -71
- package/src/tools/mergeWorktree.ts +42 -0
- package/src/tools/recommendAgentForTask.ts +79 -0
- package/src/tools/reconcileTasks.ts +464 -0
- package/src/tools/registry.ts +1093 -73
- package/src/tools/releaseMode.ts +450 -0
- package/src/tools/runDirectVerificationBundle.ts +98 -0
- package/src/tools/runTaskLoop.ts +526 -0
- package/src/tools/runVerification.ts +8 -0
- package/src/tools/safeStatus.ts +73 -2
- package/src/tools/safeViews.ts +273 -0
- package/src/tools/schemaDriftCheck.ts +120 -0
- package/src/tools/taskLineage.ts +300 -0
- package/src/tools/toolCatalog.ts +40 -11
- package/src/tools/toolRegistry.ts +896 -0
- package/src/tools/toolSearch.ts +464 -0
- package/src/tools/toolUsageStats.ts +151 -0
- package/src/tools/waitForTask.ts +1 -0
- package/src/version.ts +2 -2
- package/src/watcherStatus.ts +97 -1
- package/ui/pages/dashboard.html +143 -2
|
@@ -0,0 +1,756 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import { strict as assert } from "node:assert";
|
|
3
|
+
import {
|
|
4
|
+
buildToolRegistry,
|
|
5
|
+
computeSchemaDigest,
|
|
6
|
+
TOOL_RISK_RANK,
|
|
7
|
+
type PatchWardenToolMeta,
|
|
8
|
+
type ToolRisk,
|
|
9
|
+
} from "../../tools/toolRegistry.js";
|
|
10
|
+
import {
|
|
11
|
+
discoverTools,
|
|
12
|
+
explainTool,
|
|
13
|
+
computeFinalScore,
|
|
14
|
+
riskBonusOrPenalty,
|
|
15
|
+
classifyQueryIntent,
|
|
16
|
+
INTENT_PRIORITY_RISK,
|
|
17
|
+
type QueryIntent,
|
|
18
|
+
} from "../../tools/toolSearch.js";
|
|
19
|
+
import {
|
|
20
|
+
selectToolsForProfile,
|
|
21
|
+
resolveToolProfile,
|
|
22
|
+
CHATGPT_SEARCH_TOOL_NAMES,
|
|
23
|
+
} from "../../tools/toolCatalog.js";
|
|
24
|
+
import type { ToolDef } from "../../tools/registry.js";
|
|
25
|
+
import type { ToolUsageStats } from "../../tools/toolUsageStats.js";
|
|
26
|
+
|
|
27
|
+
// ── Mock tool definitions ─────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
function makeTool(name: string, inputSchema: Record<string, unknown> = {}): ToolDef {
|
|
30
|
+
return {
|
|
31
|
+
name,
|
|
32
|
+
description: `Description for ${name}`,
|
|
33
|
+
inputSchema: {
|
|
34
|
+
type: "object" as const,
|
|
35
|
+
properties: inputSchema,
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const MOCK_TOOLS: ToolDef[] = [
|
|
41
|
+
makeTool("health_check", { detail: { type: "string" } }),
|
|
42
|
+
makeTool("list_agents"),
|
|
43
|
+
makeTool("list_workspace"),
|
|
44
|
+
makeTool("list_tasks", { repo_path: { type: "string" }, active_only: { type: "boolean" } }),
|
|
45
|
+
makeTool("get_task_status", { task_id: { type: "string" } }),
|
|
46
|
+
makeTool("get_diff", { task_id: { type: "string" } }),
|
|
47
|
+
makeTool("get_result", { task_id: { type: "string" } }),
|
|
48
|
+
makeTool("get_result_json", { task_id: { type: "string" } }),
|
|
49
|
+
makeTool("get_test_log", { task_id: { type: "string" } }),
|
|
50
|
+
makeTool("get_task_log_tail", { task_id: { type: "string" } }),
|
|
51
|
+
makeTool("get_task_progress", { task_id: { type: "string" } }),
|
|
52
|
+
makeTool("get_task_summary", { task_id: { type: "string" } }),
|
|
53
|
+
makeTool("safe_status", { task_id: { type: "string" } }),
|
|
54
|
+
makeTool("diagnose_task", { task_id: { type: "string" }, include_logs: { type: "boolean" } }),
|
|
55
|
+
makeTool("reconcile_tasks", { mode: { type: "string" }, max_age_minutes: { type: "number" } }),
|
|
56
|
+
makeTool("wait_for_task", { task_id: { type: "string" }, timeout_seconds: { type: "number" } }),
|
|
57
|
+
makeTool("get_plan", { plan_id: { type: "string" } }),
|
|
58
|
+
makeTool("get_task_stdout_tail", { task_id: { type: "string" } }),
|
|
59
|
+
makeTool("discover_tools", { query: { type: "string" } }),
|
|
60
|
+
makeTool("explain_tool", { name: { type: "string" } }),
|
|
61
|
+
makeTool("read_workspace_file", { path: { type: "string" } }),
|
|
62
|
+
makeTool("search_workspace", { session_id: { type: "string" }, query: { type: "string" } }),
|
|
63
|
+
makeTool("save_plan", { title: { type: "string" }, content: { type: "string" } }),
|
|
64
|
+
makeTool("create_task", { plan_id: { type: "string" }, agent: { type: "string" } }),
|
|
65
|
+
makeTool("sync_file", { session_id: { type: "string" }, source: { type: "string" } }),
|
|
66
|
+
makeTool("apply_patch", { session_id: { type: "string" }, path: { type: "string" } }),
|
|
67
|
+
makeTool("create_direct_session", { repo_path: { type: "string" } }),
|
|
68
|
+
makeTool("finalize_direct_session", { session_id: { type: "string" } }),
|
|
69
|
+
makeTool("audit_session", { session_id: { type: "string" } }),
|
|
70
|
+
makeTool("audit_task", { task_id: { type: "string" } }),
|
|
71
|
+
makeTool("cancel_task", { task_id: { type: "string" } }),
|
|
72
|
+
makeTool("kill_task", { task_id: { type: "string" } }),
|
|
73
|
+
makeTool("retry_task", { task_id: { type: "string" } }),
|
|
74
|
+
makeTool("run_verification", { session_id: { type: "string" }, command: { type: "string" } }),
|
|
75
|
+
// v0.8.0 goal 工具
|
|
76
|
+
makeTool("create_goal", { title: { type: "string" }, goal_description: { type: "string" }, repo_path: { type: "string" } }),
|
|
77
|
+
makeTool("list_goals"),
|
|
78
|
+
makeTool("read_goal", { goal_id: { type: "string" } }),
|
|
79
|
+
makeTool("create_subgoal_task", { goal_id: { type: "string" }, subgoal_title: { type: "string" }, depends_on: { type: "array", items: { type: "string" } }, repo_path: { type: "string" } }),
|
|
80
|
+
makeTool("accept_subgoal", { goal_id: { type: "string" }, subgoal_id: { type: "string" } }),
|
|
81
|
+
makeTool("reject_subgoal", { goal_id: { type: "string" }, subgoal_id: { type: "string" }, reason: { type: "string" } }),
|
|
82
|
+
makeTool("suggest_next_subgoal", { goal_id: { type: "string" } }),
|
|
83
|
+
makeTool("summarize_goal_progress", { goal_id: { type: "string" } }),
|
|
84
|
+
makeTool("export_handoff", { goal_id: { type: "string" } }),
|
|
85
|
+
// v0.8.1 invoke_discovered_tool
|
|
86
|
+
makeTool("invoke_discovered_tool", { tool_name: { type: "string" }, token: { type: "string" }, input: { type: "object" } }),
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
const REGISTRY: PatchWardenToolMeta[] = buildToolRegistry(MOCK_TOOLS);
|
|
90
|
+
const TOOL_DEFS_MAP = new Map(MOCK_TOOLS.map((t) => [t.name, t]));
|
|
91
|
+
|
|
92
|
+
// ── Tests ─────────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
describe("toolSearch", () => {
|
|
95
|
+
|
|
96
|
+
describe("discoverTools — 中文意图搜索", () => {
|
|
97
|
+
it("「验收」应命中 safe_status / get_diff / get_result / get_test_log / audit_task", () => {
|
|
98
|
+
const result = discoverTools({ query: "验收" }, REGISTRY);
|
|
99
|
+
const names = result.results.map((r) => r.name);
|
|
100
|
+
assert.ok(names.includes("safe_status"), `expected safe_status in results, got: ${names.join(", ")}`);
|
|
101
|
+
assert.ok(names.includes("get_diff"), `expected get_diff in results, got: ${names.join(", ")}`);
|
|
102
|
+
assert.ok(names.includes("get_result"), `expected get_result in results, got: ${names.join(", ")}`);
|
|
103
|
+
assert.ok(names.includes("get_test_log"), `expected get_test_log in results, got: ${names.join(", ")}`);
|
|
104
|
+
assert.ok(names.includes("audit_task"), `expected audit_task in results, got: ${names.join(", ")}`);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("「改文件」应命中 apply_patch / sync_file", () => {
|
|
108
|
+
const result = discoverTools({ query: "改文件" }, REGISTRY);
|
|
109
|
+
const names = result.results.map((r) => r.name);
|
|
110
|
+
assert.ok(names.includes("apply_patch"), `expected apply_patch, got: ${names.join(", ")}`);
|
|
111
|
+
assert.ok(names.includes("sync_file"), `expected sync_file, got: ${names.join(", ")}`);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("「卡住/旧任务」应命中 diagnose_task / reconcile_tasks", () => {
|
|
115
|
+
const result = discoverTools({ query: "卡住 旧任务" }, REGISTRY);
|
|
116
|
+
const names = result.results.map((r) => r.name);
|
|
117
|
+
assert.ok(names.includes("diagnose_task"), `expected diagnose_task, got: ${names.join(", ")}`);
|
|
118
|
+
assert.ok(names.includes("reconcile_tasks"), `expected reconcile_tasks, got: ${names.join(", ")}`);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("「状态」应命中 safe_status / list_tasks / health_check", () => {
|
|
122
|
+
const result = discoverTools({ query: "状态" }, REGISTRY);
|
|
123
|
+
const names = result.results.map((r) => r.name);
|
|
124
|
+
assert.ok(names.includes("safe_status"), `expected safe_status, got: ${names.join(", ")}`);
|
|
125
|
+
assert.ok(names.includes("list_tasks"), `expected list_tasks, got: ${names.join(", ")}`);
|
|
126
|
+
assert.ok(names.includes("health_check"), `expected health_check, got: ${names.join(", ")}`);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("「差异」应命中 get_diff", () => {
|
|
130
|
+
const result = discoverTools({ query: "差异" }, REGISTRY);
|
|
131
|
+
const names = result.results.map((r) => r.name);
|
|
132
|
+
assert.ok(names.includes("get_diff"), `expected get_diff, got: ${names.join(", ")}`);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("「工具」应命中 discover_tools / explain_tool", () => {
|
|
136
|
+
const result = discoverTools({ query: "工具" }, REGISTRY);
|
|
137
|
+
const names = result.results.map((r) => r.name);
|
|
138
|
+
assert.ok(names.includes("discover_tools"), `expected discover_tools, got: ${names.join(", ")}`);
|
|
139
|
+
assert.ok(names.includes("explain_tool"), `expected explain_tool, got: ${names.join(", ")}`);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe("discoverTools — 英文搜索", () => {
|
|
144
|
+
it("'diff' should match get_diff", () => {
|
|
145
|
+
const result = discoverTools({ query: "diff" }, REGISTRY);
|
|
146
|
+
const names = result.results.map((r) => r.name);
|
|
147
|
+
assert.ok(names.includes("get_diff"), `expected get_diff, got: ${names.join(", ")}`);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("'cancel task' should match cancel_task and kill_task", () => {
|
|
151
|
+
const result = discoverTools({ query: "cancel task" }, REGISTRY);
|
|
152
|
+
const names = result.results.map((r) => r.name);
|
|
153
|
+
assert.ok(names.includes("cancel_task"), `expected cancel_task, got: ${names.join(", ")}`);
|
|
154
|
+
assert.ok(names.includes("kill_task"), `expected kill_task, got: ${names.join(", ")}`);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("'patch' should match apply_patch", () => {
|
|
158
|
+
const result = discoverTools({ query: "patch" }, REGISTRY);
|
|
159
|
+
const names = result.results.map((r) => r.name);
|
|
160
|
+
assert.ok(names.includes("apply_patch"), `expected apply_patch, got: ${names.join(", ")}`);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("'status' should match safe_status and get_task_status", () => {
|
|
164
|
+
const result = discoverTools({ query: "status" }, REGISTRY);
|
|
165
|
+
const names = result.results.map((r) => r.name);
|
|
166
|
+
assert.ok(names.includes("safe_status"), `expected safe_status, got: ${names.join(", ")}`);
|
|
167
|
+
assert.ok(names.includes("get_task_status"), `expected get_task_status, got: ${names.join(", ")}`);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe("discoverTools — riskCeiling 过滤", () => {
|
|
172
|
+
it("riskCeiling=readonly 时不能返回 workspace_read_sensitive 及以上工具", () => {
|
|
173
|
+
const result = discoverTools({ query: "", riskCeiling: "readonly" }, REGISTRY);
|
|
174
|
+
for (const r of result.results) {
|
|
175
|
+
assert.ok(
|
|
176
|
+
TOOL_RISK_RANK[r.risk] <= TOOL_RISK_RANK.readonly,
|
|
177
|
+
`tool ${r.name} has risk ${r.risk} which exceeds readonly`
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("riskCeiling=workspace_read_sensitive 时可返回读取类工具但不能返回写入工具", () => {
|
|
183
|
+
const result = discoverTools({ query: "", riskCeiling: "workspace_read_sensitive" }, REGISTRY);
|
|
184
|
+
for (const r of result.results) {
|
|
185
|
+
assert.ok(
|
|
186
|
+
TOOL_RISK_RANK[r.risk] <= TOOL_RISK_RANK.workspace_read_sensitive,
|
|
187
|
+
`tool ${r.name} has risk ${r.risk} which exceeds workspace_read_sensitive`
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("riskCeiling=workspace_write 时可返回写入工具但不能返回 command 工具", () => {
|
|
193
|
+
const result = discoverTools({ query: "", riskCeiling: "workspace_write" }, REGISTRY);
|
|
194
|
+
for (const r of result.results) {
|
|
195
|
+
assert.ok(
|
|
196
|
+
TOOL_RISK_RANK[r.risk] <= TOOL_RISK_RANK.workspace_write,
|
|
197
|
+
`tool ${r.name} has risk ${r.risk} which exceeds workspace_write`
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe("discoverTools — hidden_results", () => {
|
|
204
|
+
it("默认 includeHighRisk=false 时 command 工具被隐藏", () => {
|
|
205
|
+
const result = discoverTools({ query: "verify test run" }, REGISTRY);
|
|
206
|
+
const visibleNames = result.results.map((r) => r.name);
|
|
207
|
+
assert.ok(!visibleNames.includes("run_verification"), "run_verification should be hidden by default");
|
|
208
|
+
const hiddenRisks = result.hidden_results.map((h) => h.risk);
|
|
209
|
+
assert.ok(hiddenRisks.includes("command"), "command risk should be in hidden_results");
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("includeHighRisk=true 时 command 工具出现在结果中", () => {
|
|
213
|
+
const result = discoverTools({ query: "verify test run", includeHighRisk: true }, REGISTRY);
|
|
214
|
+
const visibleNames = result.results.map((r) => r.name);
|
|
215
|
+
assert.ok(visibleNames.includes("run_verification"), "run_verification should be visible when includeHighRisk=true");
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("hidden_results 包含 count 和 reason", () => {
|
|
219
|
+
const result = discoverTools({ query: "verify" }, REGISTRY);
|
|
220
|
+
assert.ok(result.hidden_results.length > 0, "should have hidden results");
|
|
221
|
+
for (const h of result.hidden_results) {
|
|
222
|
+
assert.ok(typeof h.count === "number" && h.count > 0, `count should be positive, got ${h.count}`);
|
|
223
|
+
assert.ok(h.reason.length > 0, `reason should be non-empty, got "${h.reason}"`);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("riskCeiling=readonly 时 hidden_results reason 包含 'riskCeiling'", () => {
|
|
228
|
+
const result = discoverTools({ query: "", riskCeiling: "readonly" }, REGISTRY);
|
|
229
|
+
assert.ok(result.hidden_results.length > 0, "should have hidden results");
|
|
230
|
+
for (const h of result.hidden_results) {
|
|
231
|
+
assert.ok(h.reason.includes("riskCeiling"), `reason should mention riskCeiling, got "${h.reason}"`);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
describe("discoverTools — maxResults", () => {
|
|
237
|
+
it("maxResults 限制返回数量", () => {
|
|
238
|
+
const result = discoverTools({ query: "", maxResults: 3 }, REGISTRY);
|
|
239
|
+
assert.ok(result.results.length <= 3, `expected at most 3 results, got ${result.results.length}`);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("maxResults=1 只返回一个结果", () => {
|
|
243
|
+
const result = discoverTools({ query: "task", maxResults: 1 }, REGISTRY);
|
|
244
|
+
assert.equal(result.results.length, 1);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe("discoverTools — profile 过滤", () => {
|
|
249
|
+
it("profile=chatgpt_core 只返回 core 工具", () => {
|
|
250
|
+
const result = discoverTools({ query: "", profile: "chatgpt_core" }, REGISTRY);
|
|
251
|
+
for (const r of result.results) {
|
|
252
|
+
const meta = REGISTRY.find((m) => m.name === r.name);
|
|
253
|
+
assert.ok(meta, `tool ${r.name} not in registry`);
|
|
254
|
+
assert.ok(
|
|
255
|
+
meta!.profiles.includes("chatgpt_core"),
|
|
256
|
+
`tool ${r.name} should be in chatgpt_core profile`
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("profile=chatgpt_direct 只返回 direct 工具", () => {
|
|
262
|
+
const result = discoverTools({ query: "", profile: "chatgpt_direct" }, REGISTRY);
|
|
263
|
+
for (const r of result.results) {
|
|
264
|
+
const meta = REGISTRY.find((m) => m.name === r.name);
|
|
265
|
+
assert.ok(meta, `tool ${r.name} not in registry`);
|
|
266
|
+
assert.ok(
|
|
267
|
+
meta!.profiles.includes("chatgpt_direct"),
|
|
268
|
+
`tool ${r.name} should be in chatgpt_direct profile`
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
describe("discoverTools — goal 工具可发现性(v0.8.0 补齐 + v0.8.1)", () => {
|
|
275
|
+
it("「目标 goal 子目标」应命中 create_goal / list_goals / read_goal / create_subgoal_task", () => {
|
|
276
|
+
const result = discoverTools({ query: "目标 goal 子目标" }, REGISTRY);
|
|
277
|
+
const names = result.results.map((r) => r.name);
|
|
278
|
+
assert.ok(names.includes("create_goal"), `expected create_goal, got: ${names.join(", ")}`);
|
|
279
|
+
assert.ok(names.includes("list_goals"), `expected list_goals, got: ${names.join(", ")}`);
|
|
280
|
+
assert.ok(names.includes("read_goal"), `expected read_goal, got: ${names.join(", ")}`);
|
|
281
|
+
assert.ok(names.includes("create_subgoal_task"), `expected create_subgoal_task, got: ${names.join(", ")}`);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("「交接 handoff」应命中 export_handoff", () => {
|
|
285
|
+
const result = discoverTools({ query: "交接 handoff" }, REGISTRY);
|
|
286
|
+
const names = result.results.map((r) => r.name);
|
|
287
|
+
assert.ok(names.includes("export_handoff"), `expected export_handoff, got: ${names.join(", ")}`);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("explain_tool({ name: 'create_goal' }) 返回非 null 且 risk === 'workspace_write'", () => {
|
|
291
|
+
const result = explainTool({ name: "create_goal" }, REGISTRY, TOOL_DEFS_MAP);
|
|
292
|
+
assert.ok(result, "create_goal should be explainable");
|
|
293
|
+
assert.equal(result!.risk, "workspace_write");
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("explain_tool({ name: 'invoke_discovered_tool' }) 返回非 null 且 risk === 'command'", () => {
|
|
297
|
+
const result = explainTool({ name: "invoke_discovered_tool" }, REGISTRY, TOOL_DEFS_MAP);
|
|
298
|
+
assert.ok(result, "invoke_discovered_tool should be explainable");
|
|
299
|
+
assert.equal(result!.risk, "command");
|
|
300
|
+
assert.ok(
|
|
301
|
+
result!.profiles.includes("chatgpt_search"),
|
|
302
|
+
`profiles should include 'chatgpt_search' (v0.8.1), got: ${result!.profiles.join(", ")}`
|
|
303
|
+
);
|
|
304
|
+
assert.equal(result!.requires_confirmation, true, "invoke_discovered_tool should require confirmation");
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
describe("discoverTools — discoveryToken (v0.8.1)", () => {
|
|
309
|
+
it("不传 tokenIssuer 时每个 result 的 discoveryToken 为空字符串", () => {
|
|
310
|
+
const result = discoverTools({ query: "task" }, REGISTRY);
|
|
311
|
+
assert.ok(result.results.length > 0, "should have results");
|
|
312
|
+
for (const r of result.results) {
|
|
313
|
+
assert.equal(r.discoveryToken, "", `tool ${r.name} should have empty discoveryToken`);
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it("传 tokenIssuer 时 discoveryToken 等于 issuer 返回值", () => {
|
|
318
|
+
const issuer = (tool: PatchWardenToolMeta) => `token_for_${tool.name}`;
|
|
319
|
+
const result = discoverTools({ query: "task" }, REGISTRY, issuer);
|
|
320
|
+
assert.ok(result.results.length > 0, "should have results");
|
|
321
|
+
for (const r of result.results) {
|
|
322
|
+
assert.equal(
|
|
323
|
+
r.discoveryToken,
|
|
324
|
+
`token_for_${r.name}`,
|
|
325
|
+
`tool ${r.name} should have token from issuer`
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("每个 result 都包含 discoveryToken 字段(类型为 string)", () => {
|
|
331
|
+
const result = discoverTools({ query: "diff" }, REGISTRY);
|
|
332
|
+
for (const r of result.results) {
|
|
333
|
+
assert.ok(
|
|
334
|
+
Object.prototype.hasOwnProperty.call(r, "discoveryToken"),
|
|
335
|
+
`tool ${r.name} should have discoveryToken field`
|
|
336
|
+
);
|
|
337
|
+
assert.equal(typeof r.discoveryToken, "string");
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
describe("explainTool", () => {
|
|
343
|
+
it("返回工具的完整元数据", () => {
|
|
344
|
+
const result = explainTool({ name: "create_task" }, REGISTRY, TOOL_DEFS_MAP);
|
|
345
|
+
assert.ok(result, "should return a result");
|
|
346
|
+
assert.equal(result!.name, "create_task");
|
|
347
|
+
assert.equal(result!.title, "Create Task");
|
|
348
|
+
assert.equal(result!.risk, "workspace_write");
|
|
349
|
+
assert.ok(result!.tags.length > 0, "should have tags");
|
|
350
|
+
assert.ok(result!.aliases.length > 0, "should have aliases");
|
|
351
|
+
assert.ok(result!.schema_digest.startsWith("sha256:"), "schema_digest should be sha256 prefixed");
|
|
352
|
+
assert.ok(result!.related_tools.length > 0, "should have related tools");
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it("includeSchema=true 时返回 inputSchema", () => {
|
|
356
|
+
const result = explainTool({ name: "create_task", includeSchema: true }, REGISTRY, TOOL_DEFS_MAP);
|
|
357
|
+
assert.ok(result, "should return a result");
|
|
358
|
+
assert.ok(result!.input_schema, "should include inputSchema");
|
|
359
|
+
assert.equal((result!.input_schema as any).type, "object");
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it("includeSchema=false (default) 时不返回 inputSchema", () => {
|
|
363
|
+
const result = explainTool({ name: "create_task" }, REGISTRY, TOOL_DEFS_MAP);
|
|
364
|
+
assert.ok(result, "should return a result");
|
|
365
|
+
assert.equal(result!.input_schema, undefined, "should not include inputSchema");
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it("通过别名查找工具", () => {
|
|
369
|
+
const result = explainTool({ name: "new_task" }, REGISTRY, TOOL_DEFS_MAP);
|
|
370
|
+
assert.ok(result, "should find tool by alias");
|
|
371
|
+
assert.equal(result!.name, "create_task");
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it("不存在的工具返回 null", () => {
|
|
375
|
+
const result = explainTool({ name: "nonexistent_tool" }, REGISTRY, TOOL_DEFS_MAP);
|
|
376
|
+
assert.equal(result, null);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it("risk_rank 与 TOOL_RISK_RANK 一致", () => {
|
|
380
|
+
const result = explainTool({ name: "run_verification" }, REGISTRY, TOOL_DEFS_MAP);
|
|
381
|
+
assert.ok(result, "should return a result");
|
|
382
|
+
assert.equal(result!.risk, "command");
|
|
383
|
+
assert.equal(result!.risk_rank, TOOL_RISK_RANK.command);
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
describe("explainTool — schema_drift_warning (v0.8.1)", () => {
|
|
388
|
+
it("schema 一致时不设 schema_drift_warning", () => {
|
|
389
|
+
// tokenPeeker 返回 toolDef 实际计算的 digest,与 registry 一致
|
|
390
|
+
const peeker = (toolName: string) => {
|
|
391
|
+
const toolDef = TOOL_DEFS_MAP.get(toolName);
|
|
392
|
+
if (!toolDef) return null;
|
|
393
|
+
return { schemaDigest: computeSchemaDigest(toolDef.inputSchema) };
|
|
394
|
+
};
|
|
395
|
+
const result = explainTool(
|
|
396
|
+
{ name: "create_task", includeSchema: true },
|
|
397
|
+
REGISTRY,
|
|
398
|
+
TOOL_DEFS_MAP,
|
|
399
|
+
peeker
|
|
400
|
+
);
|
|
401
|
+
assert.ok(result, "should return a result");
|
|
402
|
+
assert.equal(
|
|
403
|
+
result!.schema_drift_warning,
|
|
404
|
+
undefined,
|
|
405
|
+
"should not set drift warning when digests match"
|
|
406
|
+
);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it("schema 不一致时设 schema_drift_warning(registry digest 与 toolDef 实际 digest 不同)", () => {
|
|
410
|
+
// 构造 registry 副本,将 create_task 的 digest 改为错误的值(模拟过期 registry)
|
|
411
|
+
const driftedRegistry: PatchWardenToolMeta[] = REGISTRY.map((m) =>
|
|
412
|
+
m.name === "create_task"
|
|
413
|
+
? { ...m, inputSchemaDigest: "sha256:fake_drifted_digest_value" }
|
|
414
|
+
: m
|
|
415
|
+
);
|
|
416
|
+
// tokenPeeker 返回 toolDef 实际计算的 digest(正确值)
|
|
417
|
+
const peeker = (toolName: string) => {
|
|
418
|
+
const toolDef = TOOL_DEFS_MAP.get(toolName);
|
|
419
|
+
if (!toolDef) return null;
|
|
420
|
+
return { schemaDigest: computeSchemaDigest(toolDef.inputSchema) };
|
|
421
|
+
};
|
|
422
|
+
const result = explainTool(
|
|
423
|
+
{ name: "create_task", includeSchema: true },
|
|
424
|
+
driftedRegistry,
|
|
425
|
+
TOOL_DEFS_MAP,
|
|
426
|
+
peeker
|
|
427
|
+
);
|
|
428
|
+
assert.ok(result, "should return a result");
|
|
429
|
+
assert.ok(result!.schema_drift_warning, "should set drift warning when digests differ");
|
|
430
|
+
assert.ok(
|
|
431
|
+
result!.schema_drift_warning!.includes("mismatch"),
|
|
432
|
+
`warning should mention mismatch, got: ${result!.schema_drift_warning}`
|
|
433
|
+
);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it("不传 tokenPeeker 时不设 schema_drift_warning", () => {
|
|
437
|
+
const result = explainTool(
|
|
438
|
+
{ name: "create_task", includeSchema: true },
|
|
439
|
+
REGISTRY,
|
|
440
|
+
TOOL_DEFS_MAP
|
|
441
|
+
);
|
|
442
|
+
assert.ok(result, "should return a result");
|
|
443
|
+
assert.equal(
|
|
444
|
+
result!.schema_drift_warning,
|
|
445
|
+
undefined,
|
|
446
|
+
"should not set drift warning without tokenPeeker"
|
|
447
|
+
);
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it("includeSchema=false 时即使有 tokenPeeker 也不设 schema_drift_warning", () => {
|
|
451
|
+
const peeker = (_toolName: string) => ({ schemaDigest: "sha256:different" });
|
|
452
|
+
const result = explainTool(
|
|
453
|
+
{ name: "create_task", includeSchema: false },
|
|
454
|
+
REGISTRY,
|
|
455
|
+
TOOL_DEFS_MAP,
|
|
456
|
+
peeker
|
|
457
|
+
);
|
|
458
|
+
assert.ok(result, "should return a result");
|
|
459
|
+
assert.equal(
|
|
460
|
+
result!.schema_drift_warning,
|
|
461
|
+
undefined,
|
|
462
|
+
"should not set drift warning when includeSchema=false"
|
|
463
|
+
);
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
describe("schema_digest 稳定性", () => {
|
|
468
|
+
it("相同 inputSchema 产生相同 digest", () => {
|
|
469
|
+
const schema = { type: "object", properties: { a: { type: "string" }, b: { type: "number" } } };
|
|
470
|
+
const d1 = computeSchemaDigest(schema);
|
|
471
|
+
const d2 = computeSchemaDigest(schema);
|
|
472
|
+
assert.equal(d1, d2);
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
it("不同 key 顺序产生相同 digest(字段排序稳定性)", () => {
|
|
476
|
+
const schema1 = { type: "object", properties: { a: { type: "string" }, b: { type: "number" } } };
|
|
477
|
+
const schema2 = { type: "object", properties: { b: { type: "number" }, a: { type: "string" } } };
|
|
478
|
+
const d1 = computeSchemaDigest(schema1);
|
|
479
|
+
const d2 = computeSchemaDigest(schema2);
|
|
480
|
+
assert.equal(d1, d2, "digest should be stable regardless of key order");
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it("不同 inputSchema 产生不同 digest", () => {
|
|
484
|
+
const schema1 = { type: "object", properties: { a: { type: "string" } } };
|
|
485
|
+
const schema2 = { type: "object", properties: { a: { type: "number" } } };
|
|
486
|
+
const d1 = computeSchemaDigest(schema1);
|
|
487
|
+
const d2 = computeSchemaDigest(schema2);
|
|
488
|
+
assert.notEqual(d1, d2);
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it("registry 中每个工具的 schema_digest 格式正确", () => {
|
|
492
|
+
for (const meta of REGISTRY) {
|
|
493
|
+
assert.ok(
|
|
494
|
+
meta.inputSchemaDigest.startsWith("sha256:"),
|
|
495
|
+
`tool ${meta.name} has invalid digest format: ${meta.inputSchemaDigest}`
|
|
496
|
+
);
|
|
497
|
+
assert.equal(
|
|
498
|
+
meta.inputSchemaDigest.length,
|
|
499
|
+
"sha256:".length + 64,
|
|
500
|
+
`tool ${meta.name} has invalid digest length`
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
describe("buildToolRegistry", () => {
|
|
507
|
+
it("所有 mock 工具都被注册", () => {
|
|
508
|
+
const registeredNames = REGISTRY.map((m) => m.name);
|
|
509
|
+
for (const tool of MOCK_TOOLS) {
|
|
510
|
+
assert.ok(
|
|
511
|
+
registeredNames.includes(tool.name),
|
|
512
|
+
`tool ${tool.name} should be in registry`
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
it("每个工具有完整元数据", () => {
|
|
518
|
+
for (const meta of REGISTRY) {
|
|
519
|
+
assert.ok(meta.title.length > 0, `tool ${meta.name} missing title`);
|
|
520
|
+
assert.ok(meta.summary.length > 0, `tool ${meta.name} missing summary`);
|
|
521
|
+
assert.ok(meta.profiles.length > 0, `tool ${meta.name} missing profiles`);
|
|
522
|
+
assert.ok(meta.modes.length > 0, `tool ${meta.name} missing modes`);
|
|
523
|
+
assert.ok(meta.tags.length > 0, `tool ${meta.name} missing tags`);
|
|
524
|
+
assert.ok(meta.aliases.length > 0, `tool ${meta.name} missing aliases`);
|
|
525
|
+
assert.ok(typeof meta.risk === "string", `tool ${meta.name} missing risk`);
|
|
526
|
+
assert.ok(typeof meta.requiresConfirmation === "boolean", `tool ${meta.name} missing requiresConfirmation`);
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
describe("chatgpt_search profile 选择(v0.8.1)", () => {
|
|
532
|
+
const SEARCH_TOOLS = CHATGPT_SEARCH_TOOL_NAMES.map((name) => makeTool(name));
|
|
533
|
+
|
|
534
|
+
it("selectToolsForProfile 对 chatgpt_search 返回恰好 5 个工具,顺序与 CHATGPT_SEARCH_TOOL_NAMES 一致", () => {
|
|
535
|
+
const selected = selectToolsForProfile(SEARCH_TOOLS, "chatgpt_search");
|
|
536
|
+
assert.equal(selected.length, 5);
|
|
537
|
+
assert.deepEqual(
|
|
538
|
+
selected.map((t) => t.name),
|
|
539
|
+
[...CHATGPT_SEARCH_TOOL_NAMES]
|
|
540
|
+
);
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
it("selectToolsForProfile 缺失任一工具时抛错", () => {
|
|
544
|
+
const partial = SEARCH_TOOLS.slice(0, 4); // 丢弃 safe_status
|
|
545
|
+
assert.throws(
|
|
546
|
+
() => selectToolsForProfile(partial, "chatgpt_search"),
|
|
547
|
+
/chatgpt_search tool profile requires missing tool/
|
|
548
|
+
);
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
it("resolveToolProfile 接受 \"chatgpt_search\"", () => {
|
|
552
|
+
const saved = process.env.PATCHWARDEN_TOOL_PROFILE;
|
|
553
|
+
delete process.env.PATCHWARDEN_TOOL_PROFILE;
|
|
554
|
+
try {
|
|
555
|
+
assert.equal(resolveToolProfile("chatgpt_search"), "chatgpt_search");
|
|
556
|
+
} finally {
|
|
557
|
+
if (saved !== undefined) process.env.PATCHWARDEN_TOOL_PROFILE = saved;
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
it("resolveToolProfile 拒绝非法值并消息含 4 个合法值", () => {
|
|
562
|
+
const saved = process.env.PATCHWARDEN_TOOL_PROFILE;
|
|
563
|
+
delete process.env.PATCHWARDEN_TOOL_PROFILE;
|
|
564
|
+
try {
|
|
565
|
+
assert.throws(
|
|
566
|
+
() => resolveToolProfile("invalid_profile"),
|
|
567
|
+
(err: Error) => {
|
|
568
|
+
assert.ok(err.message.includes('"full"'), `message should mention "full": ${err.message}`);
|
|
569
|
+
assert.ok(err.message.includes('"chatgpt_core"'), `message should mention "chatgpt_core": ${err.message}`);
|
|
570
|
+
assert.ok(err.message.includes('"chatgpt_direct"'), `message should mention "chatgpt_direct": ${err.message}`);
|
|
571
|
+
assert.ok(err.message.includes('"chatgpt_search"'), `message should mention "chatgpt_search": ${err.message}`);
|
|
572
|
+
return true;
|
|
573
|
+
}
|
|
574
|
+
);
|
|
575
|
+
} finally {
|
|
576
|
+
if (saved !== undefined) process.env.PATCHWARDEN_TOOL_PROFILE = saved;
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
// ── v0.9.0: 混合排序公式测试 ─────────────────────────────────────
|
|
582
|
+
|
|
583
|
+
// 构造自定义 PatchWardenToolMeta 的辅助函数(用于隔离测试混合排序公式)
|
|
584
|
+
function makeCustomMeta(name: string, risk: ToolRisk): PatchWardenToolMeta {
|
|
585
|
+
return {
|
|
586
|
+
name,
|
|
587
|
+
title: name,
|
|
588
|
+
summary: name,
|
|
589
|
+
description: name,
|
|
590
|
+
profiles: ["full"],
|
|
591
|
+
modes: ["delegate"],
|
|
592
|
+
tags: [],
|
|
593
|
+
aliases: [],
|
|
594
|
+
risk,
|
|
595
|
+
requiresConfirmation: false,
|
|
596
|
+
inputSchemaDigest: "sha256:fake",
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
describe("混合排序公式 (v0.9.0)", () => {
|
|
601
|
+
it("computeFinalScore 按 0.45/0.25/0.15/0.10/0.05 权重正确加权", () => {
|
|
602
|
+
// ruleScore=10, tagScore=20, profileMatch=1, historyScore=5, riskBonus=-1
|
|
603
|
+
// 10*0.45 + 20*0.25 + 1*0.15 + 5*0.10 + (-1)*0.05 = 4.5 + 5 + 0.15 + 0.5 - 0.05 = 10.1
|
|
604
|
+
const result = computeFinalScore(10, 20, 1, 5, -1);
|
|
605
|
+
assert.ok(
|
|
606
|
+
Math.abs(result - 10.1) < 1e-9,
|
|
607
|
+
`expected ~10.1, got ${result}`
|
|
608
|
+
);
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
it("computeFinalScore 各分量独立贡献正确", () => {
|
|
612
|
+
// 只 ruleScore 有值
|
|
613
|
+
assert.ok(Math.abs(computeFinalScore(10, 0, 0, 0, 0) - 4.5) < 1e-9);
|
|
614
|
+
// 只 tagScore 有值
|
|
615
|
+
assert.ok(Math.abs(computeFinalScore(0, 10, 0, 0, 0) - 2.5) < 1e-9);
|
|
616
|
+
// 只 profileMatch 有值
|
|
617
|
+
assert.ok(Math.abs(computeFinalScore(0, 0, 1, 0, 0) - 0.15) < 1e-9);
|
|
618
|
+
// 只 historyScore 有值
|
|
619
|
+
assert.ok(Math.abs(computeFinalScore(0, 0, 0, 5, 0) - 0.5) < 1e-9);
|
|
620
|
+
// 只 riskBonus 有值
|
|
621
|
+
assert.ok(Math.abs(computeFinalScore(0, 0, 0, 0, 1) - 0.05) < 1e-9);
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
it("riskBonusOrPenalty 6 种 ToolRisk 的 bonus/penalty 值正确", () => {
|
|
625
|
+
assert.equal(riskBonusOrPenalty("readonly"), 1);
|
|
626
|
+
assert.equal(riskBonusOrPenalty("workspace_read_sensitive"), 0.5);
|
|
627
|
+
assert.equal(riskBonusOrPenalty("workspace_write"), 0);
|
|
628
|
+
assert.equal(riskBonusOrPenalty("command"), -0.5);
|
|
629
|
+
assert.equal(riskBonusOrPenalty("release"), -1);
|
|
630
|
+
assert.equal(riskBonusOrPenalty("credential_sensitive"), -1);
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
it("无 usageStatsProvider 时 historyScore=0,结果与 v0.7.1 基本一致(向后兼容)", () => {
|
|
634
|
+
// "diff" 不命中任何意图触发词,intent=unknown
|
|
635
|
+
const result = discoverTools({ query: "diff" }, REGISTRY);
|
|
636
|
+
assert.equal(result.intent, "unknown");
|
|
637
|
+
const names = result.results.map((r) => r.name);
|
|
638
|
+
assert.ok(names.includes("get_diff"), `expected get_diff, got: ${names.join(", ")}`);
|
|
639
|
+
// get_diff 强匹配,应为首个结果
|
|
640
|
+
assert.equal(result.results[0].name, "get_diff");
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
it("有 usageStatsProvider 且 successRate=0.9 时,目标工具排名比无历史数据时高", () => {
|
|
644
|
+
// 构造两个 ruleScore/tagScore 完全相同的 readonly 工具
|
|
645
|
+
const customRegistry: PatchWardenToolMeta[] = [
|
|
646
|
+
makeCustomMeta("zz_one", "readonly"),
|
|
647
|
+
makeCustomMeta("zz_two", "readonly"),
|
|
648
|
+
];
|
|
649
|
+
const query = "zz";
|
|
650
|
+
// 无 provider:同分按字母序,zz_one 在前
|
|
651
|
+
const noProvider = discoverTools({ query }, customRegistry);
|
|
652
|
+
assert.equal(noProvider.results[0].name, "zz_one");
|
|
653
|
+
assert.equal(noProvider.results[1].name, "zz_two");
|
|
654
|
+
// 有 provider:zz_two 有 successRate=0.9,historyScore=4.5,应排在 zz_one 前面
|
|
655
|
+
const provider = (toolName: string): ToolUsageStats | null =>
|
|
656
|
+
toolName === "zz_two"
|
|
657
|
+
? { totalCalls: 10, successRate: 0.9, avgDurationMs: 100, lastUsedAt: "2026-06-27T00:00:00.000Z" }
|
|
658
|
+
: null;
|
|
659
|
+
const withProvider = discoverTools({ query }, customRegistry, undefined, provider);
|
|
660
|
+
assert.equal(withProvider.results[0].name, "zz_two", "有历史数据时 zz_two 应排第一");
|
|
661
|
+
assert.equal(withProvider.results[1].name, "zz_one");
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
it("高风险工具(command)即使 successRate=1.0,includeHighRisk=false 时仍进 hidden_results", () => {
|
|
665
|
+
const provider = (toolName: string): ToolUsageStats | null =>
|
|
666
|
+
toolName === "run_verification"
|
|
667
|
+
? { totalCalls: 100, successRate: 1.0, avgDurationMs: 50, lastUsedAt: "2026-06-27T00:00:00.000Z" }
|
|
668
|
+
: null;
|
|
669
|
+
const result = discoverTools({ query: "verify test run" }, REGISTRY, undefined, provider);
|
|
670
|
+
const visibleNames = result.results.map((r) => r.name);
|
|
671
|
+
assert.ok(
|
|
672
|
+
!visibleNames.includes("run_verification"),
|
|
673
|
+
"run_verification 应被隐藏(风险过滤不受 historyScore 影响)"
|
|
674
|
+
);
|
|
675
|
+
const hiddenRisks = result.hidden_results.map((h) => h.risk);
|
|
676
|
+
assert.ok(hiddenRisks.includes("command"), "command 应在 hidden_results 中");
|
|
677
|
+
});
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
// ── v0.9.0: 查询意图分类测试 ─────────────────────────────────────
|
|
681
|
+
|
|
682
|
+
describe("查询意图分类 (v0.9.0)", () => {
|
|
683
|
+
it("classifyQueryIntent('查看任务状态') 返回 'read'", () => {
|
|
684
|
+
assert.equal(classifyQueryIntent("查看任务状态"), "read");
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
it("classifyQueryIntent('修复 bug') 返回 'write'", () => {
|
|
688
|
+
assert.equal(classifyQueryIntent("修复 bug"), "write");
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
it("classifyQueryIntent('验收测试') 返回 'verify'", () => {
|
|
692
|
+
assert.equal(classifyQueryIntent("验收测试"), "verify");
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
it("classifyQueryIntent('发布版本') 返回 'release'", () => {
|
|
696
|
+
assert.equal(classifyQueryIntent("发布版本"), "release");
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
it("classifyQueryIntent('旧任务卡住') 返回 'diagnose'", () => {
|
|
700
|
+
assert.equal(classifyQueryIntent("旧任务卡住"), "diagnose");
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
it("classifyQueryIntent('random text') 返回 'unknown'", () => {
|
|
704
|
+
assert.equal(classifyQueryIntent("random text"), "unknown");
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
it("意图触发词大小写不敏感('CHECK' 命中 verify)", () => {
|
|
708
|
+
assert.equal(classifyQueryIntent("CHECK status"), "verify");
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
it("按 read → write → verify 顺序匹配('fix test' 命中 write 而非 verify)", () => {
|
|
712
|
+
// "fix" 是 write 触发词,"test" 是 verify 触发词;write 优先
|
|
713
|
+
assert.equal(classifyQueryIntent("fix test"), "write");
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
it("INTENT_PRIORITY_RISK 映射完整", () => {
|
|
717
|
+
assert.deepEqual(INTENT_PRIORITY_RISK.read, ["readonly", "workspace_read_sensitive"]);
|
|
718
|
+
assert.deepEqual(INTENT_PRIORITY_RISK.write, ["workspace_write"]);
|
|
719
|
+
assert.deepEqual(INTENT_PRIORITY_RISK.verify, ["readonly", "workspace_read_sensitive"]);
|
|
720
|
+
assert.deepEqual(INTENT_PRIORITY_RISK.release, ["release"]);
|
|
721
|
+
assert.deepEqual(INTENT_PRIORITY_RISK.diagnose, ["readonly", "workspace_read_sensitive"]);
|
|
722
|
+
assert.deepEqual(INTENT_PRIORITY_RISK.unknown, []);
|
|
723
|
+
});
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
// ── v0.9.0: discoverTools intent 集成测试 ────────────────────────
|
|
727
|
+
|
|
728
|
+
describe("discoverTools — intent 集成 (v0.9.0)", () => {
|
|
729
|
+
it("discoverTools 输出包含 intent 字段", () => {
|
|
730
|
+
const result = discoverTools({ query: "查看任务" }, REGISTRY);
|
|
731
|
+
assert.ok(
|
|
732
|
+
Object.prototype.hasOwnProperty.call(result, "intent"),
|
|
733
|
+
"应有 intent 字段"
|
|
734
|
+
);
|
|
735
|
+
assert.equal(result.intent, "read"); // "查看" → read
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
it("同分工具按意图优先风险等级微调排序(intent=read 时 readonly 排在 workspace_write 前面)", () => {
|
|
739
|
+
// 构造两个 ruleScore/tagScore 相同的工具,仅风险等级不同
|
|
740
|
+
// 名字避开意图触发词子串(如 "read"/"write"),确保基础分真正相同
|
|
741
|
+
const customRegistry: PatchWardenToolMeta[] = [
|
|
742
|
+
makeCustomMeta("aa_beta", "workspace_write"),
|
|
743
|
+
makeCustomMeta("aa_alpha", "readonly"),
|
|
744
|
+
];
|
|
745
|
+
// "查看" 触发 read 意图;"aa" 匹配两个工具的 name/title/summary/description
|
|
746
|
+
const result = discoverTools({ query: "查看 aa" }, customRegistry);
|
|
747
|
+
assert.equal(result.intent, "read");
|
|
748
|
+
assert.equal(
|
|
749
|
+
result.results[0].name,
|
|
750
|
+
"aa_alpha",
|
|
751
|
+
"readonly 工具应排在 workspace_write 前面(intent=read 优先只读)"
|
|
752
|
+
);
|
|
753
|
+
assert.equal(result.results[1].name, "aa_beta");
|
|
754
|
+
});
|
|
755
|
+
});
|
|
756
|
+
});
|