patchwarden 0.6.0 → 0.6.4
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/PatchWarden-Control-Tray.cmd +11 -0
- package/PatchWarden-Control.cmd +6 -0
- package/PatchWarden-Desktop.cmd +5 -0
- package/PatchWarden.cmd +1 -1
- package/README.en.md +106 -18
- package/README.md +30 -19
- package/Restart-PatchWarden-Control.cmd +6 -0
- package/Stop-PatchWarden.cmd +11 -0
- package/dist/controlCenter.d.ts +14 -0
- package/dist/controlCenter.js +2002 -0
- package/dist/doctor.js +4 -4
- package/dist/logging.d.ts +52 -0
- package/dist/logging.js +123 -0
- package/dist/runner/changeCapture.d.ts +41 -0
- package/dist/runner/changeCapture.js +171 -1
- package/dist/runner/runTask.js +204 -24
- package/dist/smoke-test.js +8 -8
- package/dist/test/unit/android-doctor.test.d.ts +1 -0
- package/dist/test/unit/android-doctor.test.js +118 -0
- package/dist/test/unit/chinese-path.test.d.ts +1 -0
- package/dist/test/unit/chinese-path.test.js +91 -0
- package/dist/test/unit/command-guard.test.d.ts +1 -0
- package/dist/test/unit/command-guard.test.js +160 -0
- package/dist/test/unit/direct-guards.test.d.ts +1 -0
- package/dist/test/unit/direct-guards.test.js +213 -0
- package/dist/test/unit/logging.test.d.ts +1 -0
- package/dist/test/unit/logging.test.js +275 -0
- package/dist/test/unit/path-guard.test.d.ts +1 -0
- package/dist/test/unit/path-guard.test.js +109 -0
- package/dist/test/unit/safe-status.test.d.ts +1 -0
- package/dist/test/unit/safe-status.test.js +165 -0
- package/dist/test/unit/sensitive-guard.test.d.ts +1 -0
- package/dist/test/unit/sensitive-guard.test.js +104 -0
- package/dist/test/unit/sync-file.test.d.ts +1 -0
- package/dist/test/unit/sync-file.test.js +154 -0
- package/dist/test/unit/watcher-status.test.d.ts +1 -0
- package/dist/test/unit/watcher-status.test.js +169 -0
- package/dist/tools/androidDoctor.d.ts +38 -0
- package/dist/tools/androidDoctor.js +391 -0
- package/dist/tools/auditTask.js +11 -5
- package/dist/tools/getTaskSummary.d.ts +3 -0
- package/dist/tools/getTaskSummary.js +15 -1
- package/dist/tools/healthCheck.d.ts +5 -0
- package/dist/tools/healthCheck.js +21 -0
- package/dist/tools/registry.js +53 -0
- package/dist/tools/safeStatus.d.ts +19 -0
- package/dist/tools/safeStatus.js +72 -0
- package/dist/tools/syncFile.d.ts +18 -0
- package/dist/tools/syncFile.js +65 -0
- package/dist/tools/taskOutputs.d.ts +2 -2
- package/dist/tools/toolCatalog.d.ts +2 -2
- package/dist/tools/toolCatalog.js +2 -0
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/dist/watcherStatus.d.ts +1 -0
- package/dist/watcherStatus.js +96 -4
- package/docs/control-center/README.md +33 -0
- package/docs/control-center/control-center-daily-driver.md +211 -0
- package/docs/control-center/control-center-mvp.md +205 -0
- package/docs/control-center/control-center-phase2.md +159 -0
- package/docs/demo.md +3 -0
- package/docs/performance-notes.md +55 -0
- package/docs/release-v0.6.1.md +75 -0
- package/docs/release-v0.6.4.md +45 -0
- package/examples/openai-tunnel/README.md +5 -5
- package/examples/openai-tunnel/tunnel-client.example.yaml +3 -3
- package/package.json +23 -14
- package/scripts/README.md +47 -0
- package/scripts/{brand-check.js → checks/brand-check.js} +2 -2
- package/scripts/checks/control-center-smoke.js +1098 -0
- package/scripts/{control-smoke.js → checks/control-smoke.js} +17 -3
- package/scripts/{doctor-smoke.js → checks/doctor-smoke.js} +1 -1
- package/scripts/{http-mcp-smoke.js → checks/http-mcp-smoke.js} +12 -4
- package/scripts/{lifecycle-smoke.js → checks/lifecycle-smoke.js} +347 -13
- package/scripts/{mcp-manifest-check.js → checks/mcp-manifest-check.js} +32 -9
- package/scripts/{mcp-smoke.js → checks/mcp-smoke.js} +14 -9
- package/scripts/{package-manifest-check.js → checks/package-manifest-check.js} +2 -1
- package/scripts/{tunnel-supervisor-smoke.js → checks/tunnel-supervisor-smoke.js} +2 -2
- package/scripts/checks/unit-tests.js +36 -0
- package/scripts/{watcher-supervisor-smoke.js → checks/watcher-supervisor-smoke.js} +11 -5
- package/scripts/control/control-center-tray.ps1 +281 -0
- package/scripts/{get-patchwarden-health.ps1 → control/get-patchwarden-health.ps1} +3 -3
- package/scripts/{manage-patchwarden.ps1 → control/manage-patchwarden.ps1} +30 -4
- package/scripts/control/restart-control-center.ps1 +173 -0
- package/scripts/{restart-patchwarden.ps1 → control/restart-patchwarden.ps1} +1 -1
- package/scripts/control/start-control-center.ps1 +263 -0
- package/scripts/{start-patchwarden-tunnel.ps1 → control/start-patchwarden-tunnel.ps1} +48 -6
- package/scripts/control/stop-patchwarden.ps1 +114 -0
- package/scripts/launchers/Check-PatchWarden-Health.cmd +1 -1
- package/scripts/launchers/Reset-PatchWarden-Tunnel-Key.cmd +1 -1
- package/scripts/launchers/Restart-PatchWarden.cmd +1 -1
- package/scripts/launchers/Start-PatchWarden-Direct-Tunnel.cmd +1 -1
- package/scripts/launchers/Start-PatchWarden-Tunnel.cmd +1 -1
- package/scripts/{patchwarden-mcp-direct.cmd → mcp/patchwarden-mcp-direct.cmd} +1 -1
- package/scripts/{patchwarden-mcp-stdio.cmd → mcp/patchwarden-mcp-stdio.cmd} +1 -1
- package/scripts/release/pack-clean.js +305 -0
- package/src/controlCenter.ts +2152 -0
- package/src/doctor.ts +5 -5
- package/src/logging.ts +152 -0
- package/src/runner/changeCapture.ts +212 -1
- package/src/runner/runTask.ts +220 -22
- package/src/smoke-test.ts +5 -5
- package/src/test/unit/android-doctor.test.ts +158 -0
- package/src/test/unit/chinese-path.test.ts +106 -0
- package/src/test/unit/command-guard.test.ts +221 -0
- package/src/test/unit/direct-guards.test.ts +297 -0
- package/src/test/unit/logging.test.ts +325 -0
- package/src/test/unit/path-guard.test.ts +150 -0
- package/src/test/unit/safe-status.test.ts +187 -0
- package/src/test/unit/sensitive-guard.test.ts +124 -0
- package/src/test/unit/sync-file.test.ts +231 -0
- package/src/test/unit/watcher-status.test.ts +190 -0
- package/src/tools/androidDoctor.ts +424 -0
- package/src/tools/auditTask.ts +11 -5
- package/src/tools/getTaskSummary.ts +22 -1
- package/src/tools/healthCheck.ts +22 -0
- package/src/tools/registry.ts +63 -0
- package/src/tools/safeStatus.ts +96 -0
- package/src/tools/syncFile.ts +122 -0
- package/src/tools/toolCatalog.ts +2 -0
- package/src/version.ts +2 -2
- package/src/watcherStatus.ts +101 -4
- package/ui/colors_and_type.css +141 -0
- package/ui/pages/audit.html +743 -0
- package/ui/pages/dashboard.html +1154 -0
- package/ui/pages/direct-sessions.html +652 -0
- package/ui/pages/logs.html +502 -0
- package/ui/pages/task-detail.html +1229 -0
- package/ui/pages/tasks.html +702 -0
- package/ui/pages/workspace.html +947 -0
- package/ui/partials/project-shell.html +362 -0
- package/ui/vendor/lucide.js +12 -0
- package/ui/vendor/tailwindcss-browser.js +947 -0
- package/scripts/pack-clean.js +0 -141
|
@@ -11,8 +11,8 @@ if (process.platform !== "win32") {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
const scriptDir = resolve(fileURLToPath(new URL(".", import.meta.url)));
|
|
14
|
-
const root = resolve(scriptDir, "..");
|
|
15
|
-
const manager = join(
|
|
14
|
+
const root = resolve(scriptDir, "..", "..");
|
|
15
|
+
const manager = join(root, "scripts", "control", "manage-patchwarden.ps1");
|
|
16
16
|
const temp = mkdtempSync(join(tmpdir(), "patchwarden-control-smoke-"));
|
|
17
17
|
const mockConfig = join(temp, "patchwarden.config.json");
|
|
18
18
|
writeFileSync(mockConfig, JSON.stringify({
|
|
@@ -139,7 +139,13 @@ try {
|
|
|
139
139
|
|
|
140
140
|
const expectedFiles = [
|
|
141
141
|
"PatchWarden.cmd",
|
|
142
|
-
"
|
|
142
|
+
"PatchWarden-Control.cmd",
|
|
143
|
+
"PatchWarden-Control-Tray.cmd",
|
|
144
|
+
"PatchWarden-Desktop.cmd",
|
|
145
|
+
"Restart-PatchWarden-Control.cmd",
|
|
146
|
+
"Stop-PatchWarden.cmd",
|
|
147
|
+
"scripts/control/manage-patchwarden.ps1",
|
|
148
|
+
"scripts/control/stop-patchwarden.ps1",
|
|
143
149
|
"scripts/launchers/Start-PatchWarden-Tunnel.cmd",
|
|
144
150
|
"scripts/launchers/Start-PatchWarden-Direct-Tunnel.cmd",
|
|
145
151
|
];
|
|
@@ -152,6 +158,14 @@ try {
|
|
|
152
158
|
if (!rootEntry.includes("manage-patchwarden.ps1")) {
|
|
153
159
|
throw new Error("PatchWarden.cmd does not invoke the consolidated manager");
|
|
154
160
|
}
|
|
161
|
+
const stopEntry = readFileSync(join(root, "Stop-PatchWarden.cmd"), "utf8");
|
|
162
|
+
if (!stopEntry.includes("scripts\\control\\stop-patchwarden.ps1")) {
|
|
163
|
+
throw new Error("Stop-PatchWarden.cmd does not invoke the one-click shutdown script");
|
|
164
|
+
}
|
|
165
|
+
const desktopEntry = readFileSync(join(root, "PatchWarden-Desktop.cmd"), "utf8");
|
|
166
|
+
if (!desktopEntry.includes("control-center-tray.ps1") || !desktopEntry.includes("WindowStyle Hidden")) {
|
|
167
|
+
throw new Error("PatchWarden-Desktop.cmd must launch the tray hidden as the daily desktop entry");
|
|
168
|
+
}
|
|
155
169
|
console.log("ok - control handles orphan cleanup, scoped kill, port conflicts, health fallback, and Core/Direct lifecycle actions");
|
|
156
170
|
} finally {
|
|
157
171
|
if (fakeTunnel?.exitCode === null) fakeTunnel.kill();
|
|
@@ -5,7 +5,7 @@ import { tmpdir } from "node:os";
|
|
|
5
5
|
import { join, resolve } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
|
|
8
|
-
const root = resolve(fileURLToPath(new URL("
|
|
8
|
+
const root = resolve(fileURLToPath(new URL("../..", import.meta.url)));
|
|
9
9
|
const tempRoot = mkdtempSync(join(tmpdir(), "patchwarden-doctor-"));
|
|
10
10
|
const workspaceRoot = join(tempRoot, "workspace");
|
|
11
11
|
const configPath = join(tempRoot, "patchwarden.config.json");
|
|
@@ -11,10 +11,10 @@ import { tmpdir } from "node:os";
|
|
|
11
11
|
import { dirname, join, resolve } from "node:path";
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
13
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
14
|
-
import { CHATGPT_CORE_TOOL_NAMES } from "
|
|
14
|
+
import { CHATGPT_CORE_TOOL_NAMES } from "../../dist/tools/toolCatalog.js";
|
|
15
15
|
|
|
16
16
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
|
-
const root = resolve(__dirname, "..");
|
|
17
|
+
const root = resolve(__dirname, "..", "..");
|
|
18
18
|
const serverPath = resolve(root, "dist", "httpServer.js");
|
|
19
19
|
|
|
20
20
|
const host = "127.0.0.1";
|
|
@@ -139,7 +139,7 @@ try {
|
|
|
139
139
|
serverStderr += chunk.toString();
|
|
140
140
|
});
|
|
141
141
|
|
|
142
|
-
await sleep(
|
|
142
|
+
await sleep(3000);
|
|
143
143
|
if (serverProcess.exitCode !== null) {
|
|
144
144
|
throw new Error(`HTTP server exited early: ${serverStderr}`);
|
|
145
145
|
}
|
|
@@ -274,7 +274,15 @@ try {
|
|
|
274
274
|
stdio: ["ignore", "ignore", "pipe"],
|
|
275
275
|
});
|
|
276
276
|
serverProcess.stderr.on("data", (chunk) => { serverStderr += chunk.toString(); });
|
|
277
|
-
await sleep(
|
|
277
|
+
await sleep(3000);
|
|
278
|
+
// Wait for server to be ready
|
|
279
|
+
for (let i = 0; i < 10; i++) {
|
|
280
|
+
try {
|
|
281
|
+
const r = await fetch(`${mcpUrl}/healthz`);
|
|
282
|
+
if (r.status === 200) break;
|
|
283
|
+
} catch {}
|
|
284
|
+
await sleep(500);
|
|
285
|
+
}
|
|
278
286
|
|
|
279
287
|
await test("token: no token returns 401", async () => {
|
|
280
288
|
try {
|
|
@@ -39,8 +39,8 @@ async function test(name, fn) {
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
function git(args) {
|
|
43
|
-
const result = spawnSync("git", args, { cwd
|
|
42
|
+
function git(args, cwd = repoPath) {
|
|
43
|
+
const result = spawnSync("git", args, { cwd, encoding: "utf-8" });
|
|
44
44
|
if (result.status !== 0) throw new Error(`git ${args.join(" ")} failed: ${result.stderr}`);
|
|
45
45
|
}
|
|
46
46
|
|
|
@@ -148,17 +148,18 @@ try {
|
|
|
148
148
|
);
|
|
149
149
|
process.env.PATCHWARDEN_CONFIG = configPath;
|
|
150
150
|
|
|
151
|
-
const { savePlan } = await import("
|
|
152
|
-
const { createTask } = await import("
|
|
153
|
-
const { getTaskStatus } = await import("
|
|
154
|
-
const { cancelTask } = await import("
|
|
155
|
-
const { killTask } = await import("
|
|
156
|
-
const { getDiff } = await import("
|
|
157
|
-
const { listAgents } = await import("
|
|
158
|
-
const { getTaskSummary } = await import("
|
|
159
|
-
const { auditTask } = await import("
|
|
160
|
-
const { waitForTask } = await import("
|
|
161
|
-
const { runTask } = await import("
|
|
151
|
+
const { savePlan } = await import("../../dist/tools/savePlan.js");
|
|
152
|
+
const { createTask } = await import("../../dist/tools/createTask.js");
|
|
153
|
+
const { getTaskStatus } = await import("../../dist/tools/getTaskStatus.js");
|
|
154
|
+
const { cancelTask } = await import("../../dist/tools/cancelTask.js");
|
|
155
|
+
const { killTask } = await import("../../dist/tools/killTask.js");
|
|
156
|
+
const { getDiff } = await import("../../dist/tools/taskOutputs.js");
|
|
157
|
+
const { listAgents } = await import("../../dist/tools/listAgents.js");
|
|
158
|
+
const { getTaskSummary } = await import("../../dist/tools/getTaskSummary.js");
|
|
159
|
+
const { auditTask } = await import("../../dist/tools/auditTask.js");
|
|
160
|
+
const { waitForTask } = await import("../../dist/tools/waitForTask.js");
|
|
161
|
+
const { runTask } = await import("../../dist/runner/runTask.js");
|
|
162
|
+
const { reloadConfig } = await import("../../dist/config.js");
|
|
162
163
|
|
|
163
164
|
await test("list_agents reports configured executables", async () => {
|
|
164
165
|
const result = listAgents();
|
|
@@ -454,6 +455,339 @@ try {
|
|
|
454
455
|
}
|
|
455
456
|
});
|
|
456
457
|
|
|
458
|
+
// ═══════════════════════════════════════════════════════════
|
|
459
|
+
// Phase 4: Tracked external dirty tests (separate workspace)
|
|
460
|
+
// ═══════════════════════════════════════════════════════════
|
|
461
|
+
|
|
462
|
+
await test("pre-existing tracked external dirty unchanged → warning only, done", async () => {
|
|
463
|
+
// Create a separate workspace with git at the root
|
|
464
|
+
const ws2 = join(tempRoot, "workspace-tracked-dirty");
|
|
465
|
+
const repo2 = join(ws2, "repo");
|
|
466
|
+
const ext2 = join(ws2, "external");
|
|
467
|
+
mkdirSync(repo2, { recursive: true });
|
|
468
|
+
mkdirSync(ext2, { recursive: true });
|
|
469
|
+
mkdirSync(join(ws2, ".patchwarden"), { recursive: true });
|
|
470
|
+
writeFileSync(join(ws2, ".patchwarden", "watcher-heartbeat.json"), JSON.stringify({
|
|
471
|
+
status: "running", pid: process.pid, instance_id: "td-watcher", launcher_pid: process.pid,
|
|
472
|
+
started_at: new Date().toISOString(), last_heartbeat_at: new Date().toISOString(),
|
|
473
|
+
}), "utf-8");
|
|
474
|
+
|
|
475
|
+
writeFileSync(join(repo2, "main.js"), "console.log('main');\n", "utf-8");
|
|
476
|
+
writeFileSync(join(ext2, "external-tracked.txt"), "external tracked content\n", "utf-8");
|
|
477
|
+
|
|
478
|
+
git(["init"], ws2);
|
|
479
|
+
git(["add", "repo/main.js", "external/external-tracked.txt"], ws2);
|
|
480
|
+
git(["-c", "user.name=T", "-c", "user.email=t@e", "commit", "-m", "init"], ws2);
|
|
481
|
+
|
|
482
|
+
// Now modify external file to create pre-existing dirty state
|
|
483
|
+
writeFileSync(join(ext2, "external-tracked.txt"), "pre-existing dirty content\n", "utf-8");
|
|
484
|
+
|
|
485
|
+
const cfg2 = join(tempRoot, "config-tracked-dirty.json");
|
|
486
|
+
writeFileSync(cfg2, JSON.stringify({
|
|
487
|
+
workspaceRoot: ws2,
|
|
488
|
+
plansDir: ".patchwarden/plans",
|
|
489
|
+
tasksDir: ".patchwarden/tasks",
|
|
490
|
+
agents: {
|
|
491
|
+
noop: { command: process.execPath, args: ["-e", "console.log('noop')"] },
|
|
492
|
+
},
|
|
493
|
+
allowedTestCommands: ["node --check main.js"],
|
|
494
|
+
maxReadFileBytes: 200000,
|
|
495
|
+
defaultTaskTimeoutSeconds: 10,
|
|
496
|
+
watcherStaleSeconds: 3600,
|
|
497
|
+
}, null, 2), "utf-8");
|
|
498
|
+
|
|
499
|
+
const oldConfig = process.env.PATCHWARDEN_CONFIG;
|
|
500
|
+
process.env.PATCHWARDEN_CONFIG = cfg2;
|
|
501
|
+
reloadConfig(cfg2);
|
|
502
|
+
|
|
503
|
+
const plan = savePlan({ title: "Noop with external dirty", content: "Do nothing." });
|
|
504
|
+
const task = createTask({ plan_id: plan.plan_id, agent: "noop", repo_path: "repo", verify_commands: ["node --check main.js"] });
|
|
505
|
+
const result = await runTask(task.task_id);
|
|
506
|
+
|
|
507
|
+
if (result.status !== "done") {
|
|
508
|
+
throw new Error(`Expected done, got: ${JSON.stringify(result)}`);
|
|
509
|
+
}
|
|
510
|
+
const structured = JSON.parse(readFileSync(join(task.path, "result.json"), "utf-8"));
|
|
511
|
+
|
|
512
|
+
// Must have pre-existing external dirty warning
|
|
513
|
+
if (!structured.warnings?.some((w) => w.includes("Pre-existing external dirty"))) {
|
|
514
|
+
throw new Error(`Missing pre-existing dirty warning: ${JSON.stringify(structured.warnings)}`);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// new_out_of_scope_changes must be empty (file didn't change during task)
|
|
518
|
+
if (structured.new_out_of_scope_changes?.length > 0) {
|
|
519
|
+
throw new Error(`Should have no new out-of-scope changes: ${JSON.stringify(structured.new_out_of_scope_changes)}`);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// verify_status should NOT be failed
|
|
523
|
+
if (structured.verify_status === "failed") {
|
|
524
|
+
throw new Error(`verify_status should not be failed: ${structured.verify_status}`);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// get_task_summary acceptance_status should NOT be failed
|
|
528
|
+
const summary = getTaskSummary(task.task_id);
|
|
529
|
+
if (summary.acceptance_status === "failed") {
|
|
530
|
+
throw new Error(`acceptance_status should not be failed: ${summary.acceptance_status}`);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// audit_task should NOT fail scope_changes
|
|
534
|
+
const audit = auditTask(task.task_id);
|
|
535
|
+
const scopeCheck = audit.checks.find((c) => c.name === "scope_changes");
|
|
536
|
+
if (!scopeCheck || scopeCheck.result !== "pass") {
|
|
537
|
+
throw new Error(`audit scope_changes should pass: ${JSON.stringify(scopeCheck)}`);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Restore config AFTER all assertions
|
|
541
|
+
process.env.PATCHWARDEN_CONFIG = oldConfig;
|
|
542
|
+
reloadConfig(oldConfig);
|
|
543
|
+
|
|
544
|
+
// Print result.json key fields for reporting
|
|
545
|
+
console.log(` [result.json] status=${structured.status}, verify_status=${structured.verify_status}, new_out_of_scope=${JSON.stringify(structured.new_out_of_scope_changes)}, preexisting=${JSON.stringify(structured.preexisting_external_dirty_files)}, warnings=${JSON.stringify(structured.warnings)}`);
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
await test("pre-existing tracked external dirty changed during task → failed_scope_violation", async () => {
|
|
549
|
+
const ws2 = join(tempRoot, "workspace-tracked-dirty2");
|
|
550
|
+
const repo2 = join(ws2, "repo");
|
|
551
|
+
const ext2 = join(ws2, "external");
|
|
552
|
+
mkdirSync(repo2, { recursive: true });
|
|
553
|
+
mkdirSync(ext2, { recursive: true });
|
|
554
|
+
mkdirSync(join(ws2, ".patchwarden"), { recursive: true });
|
|
555
|
+
writeFileSync(join(ws2, ".patchwarden", "watcher-heartbeat.json"), JSON.stringify({
|
|
556
|
+
status: "running", pid: process.pid, instance_id: "td-watcher2", launcher_pid: process.pid,
|
|
557
|
+
started_at: new Date().toISOString(), last_heartbeat_at: new Date().toISOString(),
|
|
558
|
+
}), "utf-8");
|
|
559
|
+
|
|
560
|
+
writeFileSync(join(repo2, "main.js"), "console.log('main');\n", "utf-8");
|
|
561
|
+
writeFileSync(join(ext2, "external-tracked.txt"), "external tracked content\n", "utf-8");
|
|
562
|
+
|
|
563
|
+
git(["init"], ws2);
|
|
564
|
+
git(["add", "repo/main.js", "external/external-tracked.txt"], ws2);
|
|
565
|
+
git(["-c", "user.name=T", "-c", "user.email=t@e", "commit", "-m", "init"], ws2);
|
|
566
|
+
|
|
567
|
+
// Modify external file BEFORE task (pre-existing dirty)
|
|
568
|
+
writeFileSync(join(ext2, "external-tracked.txt"), "pre-existing dirty content\n", "utf-8");
|
|
569
|
+
|
|
570
|
+
const cfg2 = join(tempRoot, "config-tracked-dirty2.json");
|
|
571
|
+
writeFileSync(cfg2, JSON.stringify({
|
|
572
|
+
workspaceRoot: ws2,
|
|
573
|
+
plansDir: ".patchwarden/plans",
|
|
574
|
+
tasksDir: ".patchwarden/tasks",
|
|
575
|
+
agents: {
|
|
576
|
+
extmod: { command: process.execPath, args: ["-e", "require('fs').appendFileSync('../external/external-tracked.txt',' modified by task\\n')"] },
|
|
577
|
+
},
|
|
578
|
+
allowedTestCommands: ["node --check main.js"],
|
|
579
|
+
maxReadFileBytes: 200000,
|
|
580
|
+
defaultTaskTimeoutSeconds: 10,
|
|
581
|
+
watcherStaleSeconds: 3600,
|
|
582
|
+
}, null, 2), "utf-8");
|
|
583
|
+
|
|
584
|
+
const oldConfig = process.env.PATCHWARDEN_CONFIG;
|
|
585
|
+
process.env.PATCHWARDEN_CONFIG = cfg2;
|
|
586
|
+
reloadConfig(cfg2);
|
|
587
|
+
|
|
588
|
+
const plan = savePlan({ title: "External dirty modifier", content: "Modify external tracked file." });
|
|
589
|
+
const task = createTask({ plan_id: plan.plan_id, agent: "extmod", repo_path: "repo", verify_commands: ["node --check main.js"] });
|
|
590
|
+
const result = await runTask(task.task_id);
|
|
591
|
+
|
|
592
|
+
if (result.status !== "failed_scope_violation") {
|
|
593
|
+
throw new Error(`Expected failed_scope_violation, got: ${JSON.stringify(result)}`);
|
|
594
|
+
}
|
|
595
|
+
const structured = JSON.parse(readFileSync(join(task.path, "result.json"), "utf-8"));
|
|
596
|
+
|
|
597
|
+
// new_out_of_scope_changes must contain the external tracked file
|
|
598
|
+
if (!structured.new_out_of_scope_changes?.some((f) => f.path.includes("external-tracked.txt"))) {
|
|
599
|
+
throw new Error(`Missing new out-of-scope change: ${JSON.stringify(structured.new_out_of_scope_changes)}`);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// verify_status must be failed
|
|
603
|
+
if (structured.verify_status !== "failed") {
|
|
604
|
+
throw new Error(`verify_status must be failed: ${structured.verify_status}`);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// rollback plan must contain the file
|
|
608
|
+
const rollbackPath = join(task.path, "rollback_scope_violation_plan.md");
|
|
609
|
+
if (!existsSync(rollbackPath)) throw new Error("rollback_scope_violation_plan.md missing");
|
|
610
|
+
const rollback = readFileSync(rollbackPath, "utf-8");
|
|
611
|
+
if (!rollback.includes("external-tracked.txt")) {
|
|
612
|
+
throw new Error(`Rollback plan missing external-tracked.txt: ${rollback}`);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// get_task_summary acceptance_status must be failed
|
|
616
|
+
const summary = getTaskSummary(task.task_id);
|
|
617
|
+
if (summary.acceptance_status !== "failed") {
|
|
618
|
+
throw new Error(`acceptance_status must be failed: ${summary.acceptance_status}`);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// audit_task verdict must be fail
|
|
622
|
+
const audit = auditTask(task.task_id);
|
|
623
|
+
if (audit.verdict !== "fail") {
|
|
624
|
+
throw new Error(`audit verdict must be fail: ${audit.verdict}`);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Restore config AFTER all assertions
|
|
628
|
+
process.env.PATCHWARDEN_CONFIG = oldConfig;
|
|
629
|
+
reloadConfig(oldConfig);
|
|
630
|
+
|
|
631
|
+
console.log(` [result.json] status=${structured.status}, verify_status=${structured.verify_status}, new_out_of_scope=${JSON.stringify(structured.new_out_of_scope_changes)}, preexisting=${JSON.stringify(structured.preexisting_external_dirty_files)}, warnings=${JSON.stringify(structured.warnings)}`);
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
await test("clean tracked external file changed during task → failed_scope_violation", async () => {
|
|
635
|
+
const ws2 = join(tempRoot, "workspace-tracked-dirty3");
|
|
636
|
+
const repo2 = join(ws2, "repo");
|
|
637
|
+
const ext2 = join(ws2, "external");
|
|
638
|
+
mkdirSync(repo2, { recursive: true });
|
|
639
|
+
mkdirSync(ext2, { recursive: true });
|
|
640
|
+
mkdirSync(join(ws2, ".patchwarden"), { recursive: true });
|
|
641
|
+
writeFileSync(join(ws2, ".patchwarden", "watcher-heartbeat.json"), JSON.stringify({
|
|
642
|
+
status: "running", pid: process.pid, instance_id: "td-watcher3", launcher_pid: process.pid,
|
|
643
|
+
started_at: new Date().toISOString(), last_heartbeat_at: new Date().toISOString(),
|
|
644
|
+
}), "utf-8");
|
|
645
|
+
|
|
646
|
+
writeFileSync(join(repo2, "main.js"), "console.log('main');\n", "utf-8");
|
|
647
|
+
writeFileSync(join(ext2, "external-tracked.txt"), "external tracked content\n", "utf-8");
|
|
648
|
+
|
|
649
|
+
git(["init"], ws2);
|
|
650
|
+
git(["add", "repo/main.js", "external/external-tracked.txt"], ws2);
|
|
651
|
+
git(["-c", "user.name=T", "-c", "user.email=t@e", "commit", "-m", "init"], ws2);
|
|
652
|
+
|
|
653
|
+
// External file is clean (committed state) — no pre-existing dirty
|
|
654
|
+
|
|
655
|
+
const cfg2 = join(tempRoot, "config-tracked-dirty3.json");
|
|
656
|
+
writeFileSync(cfg2, JSON.stringify({
|
|
657
|
+
workspaceRoot: ws2,
|
|
658
|
+
plansDir: ".patchwarden/plans",
|
|
659
|
+
tasksDir: ".patchwarden/tasks",
|
|
660
|
+
agents: {
|
|
661
|
+
extmod: { command: process.execPath, args: ["-e", "require('fs').appendFileSync('../external/external-tracked.txt',' modified by task\\n')"] },
|
|
662
|
+
},
|
|
663
|
+
allowedTestCommands: ["node --check main.js"],
|
|
664
|
+
maxReadFileBytes: 200000,
|
|
665
|
+
defaultTaskTimeoutSeconds: 10,
|
|
666
|
+
watcherStaleSeconds: 3600,
|
|
667
|
+
}, null, 2), "utf-8");
|
|
668
|
+
|
|
669
|
+
const oldConfig = process.env.PATCHWARDEN_CONFIG;
|
|
670
|
+
process.env.PATCHWARDEN_CONFIG = cfg2;
|
|
671
|
+
reloadConfig(cfg2);
|
|
672
|
+
|
|
673
|
+
const plan = savePlan({ title: "Clean external modifier", content: "Modify clean external tracked file." });
|
|
674
|
+
const task = createTask({ plan_id: plan.plan_id, agent: "extmod", repo_path: "repo", verify_commands: ["node --check main.js"] });
|
|
675
|
+
const result = await runTask(task.task_id);
|
|
676
|
+
|
|
677
|
+
if (result.status !== "failed_scope_violation") {
|
|
678
|
+
throw new Error(`Expected failed_scope_violation, got: ${JSON.stringify(result)}`);
|
|
679
|
+
}
|
|
680
|
+
const structured = JSON.parse(readFileSync(join(task.path, "result.json"), "utf-8"));
|
|
681
|
+
|
|
682
|
+
// new_out_of_scope_changes must contain the external tracked file (clean→dirty)
|
|
683
|
+
if (!structured.new_out_of_scope_changes?.some((f) => f.path.includes("external-tracked.txt"))) {
|
|
684
|
+
throw new Error(`Missing new out-of-scope change: ${JSON.stringify(structured.new_out_of_scope_changes)}`);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// verify_status must be failed
|
|
688
|
+
if (structured.verify_status !== "failed") {
|
|
689
|
+
throw new Error(`verify_status must be failed: ${structured.verify_status}`);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// get_task_summary acceptance_status must be failed
|
|
693
|
+
const summary = getTaskSummary(task.task_id);
|
|
694
|
+
if (summary.acceptance_status !== "failed") {
|
|
695
|
+
throw new Error(`acceptance_status must be failed: ${summary.acceptance_status}`);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// audit_task verdict must be fail
|
|
699
|
+
const audit = auditTask(task.task_id);
|
|
700
|
+
if (audit.verdict !== "fail") {
|
|
701
|
+
throw new Error(`audit verdict must be fail: ${audit.verdict}`);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// Restore config AFTER all assertions
|
|
705
|
+
process.env.PATCHWARDEN_CONFIG = oldConfig;
|
|
706
|
+
reloadConfig(oldConfig);
|
|
707
|
+
|
|
708
|
+
console.log(` [result.json] status=${structured.status}, verify_status=${structured.verify_status}, new_out_of_scope=${JSON.stringify(structured.new_out_of_scope_changes)}, preexisting=${JSON.stringify(structured.preexisting_external_dirty_files)}, warnings=${JSON.stringify(structured.warnings)}`);
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
await test("tracked external file rename → failed_scope_violation", async () => {
|
|
712
|
+
const ws2 = join(tempRoot, "workspace-tracked-rename");
|
|
713
|
+
const repo2 = join(ws2, "repo");
|
|
714
|
+
const ext2 = join(ws2, "external");
|
|
715
|
+
mkdirSync(repo2, { recursive: true });
|
|
716
|
+
mkdirSync(ext2, { recursive: true });
|
|
717
|
+
mkdirSync(join(ws2, ".patchwarden"), { recursive: true });
|
|
718
|
+
writeFileSync(join(ws2, ".patchwarden", "watcher-heartbeat.json"), JSON.stringify({
|
|
719
|
+
status: "running", pid: process.pid, instance_id: "td-watcher-rename", launcher_pid: process.pid,
|
|
720
|
+
started_at: new Date().toISOString(), last_heartbeat_at: new Date().toISOString(),
|
|
721
|
+
}), "utf-8");
|
|
722
|
+
|
|
723
|
+
writeFileSync(join(repo2, "main.js"), "console.log('main');\n", "utf-8");
|
|
724
|
+
writeFileSync(join(ext2, "external-tracked.txt"), "external tracked content\n", "utf-8");
|
|
725
|
+
|
|
726
|
+
git(["init"], ws2);
|
|
727
|
+
git(["add", "repo/main.js", "external/external-tracked.txt"], ws2);
|
|
728
|
+
git(["-c", "user.name=T", "-c", "user.email=t@e", "commit", "-m", "init"], ws2);
|
|
729
|
+
|
|
730
|
+
// External file is clean (committed state) — no pre-existing dirty
|
|
731
|
+
|
|
732
|
+
const cfg2 = join(tempRoot, "config-tracked-rename.json");
|
|
733
|
+
writeFileSync(cfg2, JSON.stringify({
|
|
734
|
+
workspaceRoot: ws2,
|
|
735
|
+
plansDir: ".patchwarden/plans",
|
|
736
|
+
tasksDir: ".patchwarden/tasks",
|
|
737
|
+
agents: {
|
|
738
|
+
extrenamer: { command: process.execPath, args: ["-e", "require('fs').renameSync('../external/external-tracked.txt','../external/external-renamed.txt')"] },
|
|
739
|
+
},
|
|
740
|
+
allowedTestCommands: ["node --check main.js"],
|
|
741
|
+
maxReadFileBytes: 200000,
|
|
742
|
+
defaultTaskTimeoutSeconds: 10,
|
|
743
|
+
watcherStaleSeconds: 3600,
|
|
744
|
+
}, null, 2), "utf-8");
|
|
745
|
+
|
|
746
|
+
const oldConfig = process.env.PATCHWARDEN_CONFIG;
|
|
747
|
+
process.env.PATCHWARDEN_CONFIG = cfg2;
|
|
748
|
+
reloadConfig(cfg2);
|
|
749
|
+
|
|
750
|
+
const plan = savePlan({ title: "External rename", content: "Rename external tracked file." });
|
|
751
|
+
const task = createTask({ plan_id: plan.plan_id, agent: "extrenamer", repo_path: "repo", verify_commands: ["node --check main.js"] });
|
|
752
|
+
const result = await runTask(task.task_id);
|
|
753
|
+
|
|
754
|
+
if (result.status !== "failed_scope_violation") {
|
|
755
|
+
throw new Error(`Expected failed_scope_violation, got: ${JSON.stringify(result)}`);
|
|
756
|
+
}
|
|
757
|
+
const structured = JSON.parse(readFileSync(join(task.path, "result.json"), "utf-8"));
|
|
758
|
+
|
|
759
|
+
// new_out_of_scope_changes must contain rename evidence (old or new path)
|
|
760
|
+
const hasRenameEvidence = structured.new_out_of_scope_changes?.some(
|
|
761
|
+
(f) => f.path.includes("external-renamed.txt") || f.path.includes("external-tracked.txt") || f.change === "renamed"
|
|
762
|
+
);
|
|
763
|
+
if (!hasRenameEvidence) {
|
|
764
|
+
throw new Error(`Missing rename evidence in new_out_of_scope_changes: ${JSON.stringify(structured.new_out_of_scope_changes)}`);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// verify_status must be failed
|
|
768
|
+
if (structured.verify_status !== "failed") {
|
|
769
|
+
throw new Error(`verify_status must be failed: ${structured.verify_status}`);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// get_task_summary acceptance_status must be failed
|
|
773
|
+
const summary = getTaskSummary(task.task_id);
|
|
774
|
+
if (summary.acceptance_status !== "failed") {
|
|
775
|
+
throw new Error(`acceptance_status must be failed: ${summary.acceptance_status}`);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// audit_task verdict must be fail
|
|
779
|
+
const audit = auditTask(task.task_id);
|
|
780
|
+
if (audit.verdict !== "fail") {
|
|
781
|
+
throw new Error(`audit verdict must be fail: ${audit.verdict}`);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// Restore config AFTER all assertions
|
|
785
|
+
process.env.PATCHWARDEN_CONFIG = oldConfig;
|
|
786
|
+
reloadConfig(oldConfig);
|
|
787
|
+
|
|
788
|
+
console.log(` [result.json] status=${structured.status}, verify_status=${structured.verify_status}, new_out_of_scope=${JSON.stringify(structured.new_out_of_scope_changes)}, preexisting=${JSON.stringify(structured.preexisting_external_dirty_files)}, warnings=${JSON.stringify(structured.warnings)}`);
|
|
789
|
+
});
|
|
790
|
+
|
|
457
791
|
await test("timeout terminates a long-running agent", async () => {
|
|
458
792
|
const plan = savePlan({ title: "Timeout", content: "Wait." });
|
|
459
793
|
const task = createTask({ plan_id: plan.plan_id, agent: "slow", repo_path: "repo", timeout_seconds: 1 });
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
2
|
+
import { existsSync, readFileSync, rmSync } from "node:fs";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
6
6
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
7
|
-
import { CHATGPT_CORE_TOOL_NAMES, CHATGPT_DIRECT_TOOL_NAMES } from "
|
|
7
|
+
import { CHATGPT_CORE_TOOL_NAMES, CHATGPT_DIRECT_TOOL_NAMES } from "../../dist/tools/toolCatalog.js";
|
|
8
8
|
|
|
9
|
-
const root = resolve(fileURLToPath(new URL("
|
|
9
|
+
const root = resolve(fileURLToPath(new URL("../..", import.meta.url)));
|
|
10
10
|
const jsonOnly = process.argv.includes("--json");
|
|
11
11
|
const profileIndex = process.argv.indexOf("--profile");
|
|
12
12
|
const profile = profileIndex >= 0 ? process.argv[profileIndex + 1] : "chatgpt_core";
|
|
@@ -21,12 +21,32 @@ const transportEnv = {
|
|
|
21
21
|
...process.env,
|
|
22
22
|
PATCHWARDEN_TOOL_PROFILE: profile,
|
|
23
23
|
};
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
|
|
25
|
+
// When checking chatgpt_direct profile, ensure enableDirectProfile is true
|
|
26
|
+
// by creating a temporary config if needed.
|
|
27
|
+
let tempConfigPath = null;
|
|
28
|
+
if (profile === "chatgpt_direct") {
|
|
29
|
+
const baseConfig = existsSync(defaultConfigPath)
|
|
30
|
+
? JSON.parse(readFileSync(defaultConfigPath, "utf-8"))
|
|
31
|
+
: {};
|
|
32
|
+
if (!baseConfig.enableDirectProfile) {
|
|
33
|
+
const { mkdtempSync, writeFileSync, rmSync } = await import("node:fs");
|
|
34
|
+
const { tmpdir } = await import("node:os");
|
|
35
|
+
const { join } = await import("node:path");
|
|
36
|
+
tempConfigPath = join(mkdtempSync(join(tmpdir(), "pw-direct-")), "config.json");
|
|
37
|
+
writeFileSync(tempConfigPath, JSON.stringify({ ...baseConfig, enableDirectProfile: true }, null, 2), "utf-8");
|
|
38
|
+
transportEnv.PATCHWARDEN_CONFIG = tempConfigPath;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!transportEnv.PATCHWARDEN_CONFIG) {
|
|
43
|
+
if (process.env.PATCHWARDEN_CONFIG) {
|
|
44
|
+
transportEnv.PATCHWARDEN_CONFIG = process.env.PATCHWARDEN_CONFIG;
|
|
45
|
+
} else if (existsSync(defaultConfigPath)) {
|
|
46
|
+
transportEnv.PATCHWARDEN_CONFIG = defaultConfigPath;
|
|
47
|
+
} else {
|
|
48
|
+
delete transportEnv.PATCHWARDEN_CONFIG;
|
|
49
|
+
}
|
|
30
50
|
}
|
|
31
51
|
|
|
32
52
|
const transport = new StdioClientTransport({
|
|
@@ -159,4 +179,7 @@ try {
|
|
|
159
179
|
process.exitCode = 1;
|
|
160
180
|
} finally {
|
|
161
181
|
await client.close().catch(() => {});
|
|
182
|
+
if (tempConfigPath) {
|
|
183
|
+
try { rmSync(tempConfigPath, { force: true }); } catch {}
|
|
184
|
+
}
|
|
162
185
|
}
|
|
@@ -14,7 +14,9 @@ import { fileURLToPath } from "node:url";
|
|
|
14
14
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
15
15
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
16
16
|
|
|
17
|
-
const root = resolve(fileURLToPath(new URL("
|
|
17
|
+
const root = resolve(fileURLToPath(new URL("../..", import.meta.url)));
|
|
18
|
+
const packageJson = JSON.parse(readFileSync(join(root, "package.json"), "utf-8"));
|
|
19
|
+
const expectedServerVersion = packageJson.version;
|
|
18
20
|
const tempRoot = mkdtempSync(join(tmpdir(), "patchwarden-mcp-"));
|
|
19
21
|
const workspaceRoot = join(tempRoot, "workspace");
|
|
20
22
|
const configPath = join(tempRoot, "patchwarden.config.json");
|
|
@@ -126,8 +128,10 @@ try {
|
|
|
126
128
|
"read_workspace_file",
|
|
127
129
|
"retry_task",
|
|
128
130
|
"run_verification",
|
|
131
|
+
"safe_status",
|
|
129
132
|
"save_plan",
|
|
130
133
|
"search_workspace",
|
|
134
|
+
"sync_file",
|
|
131
135
|
"wait_for_task",
|
|
132
136
|
];
|
|
133
137
|
if (JSON.stringify(names) !== JSON.stringify(expected)) {
|
|
@@ -139,7 +143,7 @@ try {
|
|
|
139
143
|
if (!tools._meta || typeof tools._meta.tool_manifest_sha256 !== "string" || tools._meta.tool_manifest_sha256.length !== 64) {
|
|
140
144
|
throw new Error(`tools/list _meta missing manifest hash: ${JSON.stringify(tools._meta || null)}`);
|
|
141
145
|
}
|
|
142
|
-
if (tools._meta.tool_profile !== "full" || tools._meta.tool_count !==
|
|
146
|
+
if (tools._meta.tool_profile !== "full" || tools._meta.tool_count !== 30) {
|
|
143
147
|
throw new Error(`tools/list _meta profile/count mismatch: ${JSON.stringify(tools._meta)}`);
|
|
144
148
|
}
|
|
145
149
|
if (typeof tools._meta.schema_epoch !== "string" || typeof tools._meta.server_version !== "string") {
|
|
@@ -210,7 +214,7 @@ try {
|
|
|
210
214
|
test_command: "npm test",
|
|
211
215
|
});
|
|
212
216
|
if (
|
|
213
|
-
task.server_version !==
|
|
217
|
+
task.server_version !== expectedServerVersion ||
|
|
214
218
|
!/^[a-f0-9]{64}$/.test(task.tool_manifest_sha256 || "") ||
|
|
215
219
|
task.next_tool_call?.name !== "wait_for_task" ||
|
|
216
220
|
task.next_tool_call?.arguments?.timeout_seconds !== 25
|
|
@@ -389,7 +393,7 @@ try {
|
|
|
389
393
|
await disabledClient.close();
|
|
390
394
|
ok("chatgpt_direct disabled exposes only health_check with diagnostic");
|
|
391
395
|
|
|
392
|
-
// 2. chatgpt_direct enabled:
|
|
396
|
+
// 2. chatgpt_direct enabled: 10 tools + minimal create_direct_session
|
|
393
397
|
const enabledConfigPath = join(tempRoot, "direct-enabled.json");
|
|
394
398
|
const directRepo = join(workspaceRoot, "direct-fixture");
|
|
395
399
|
mkdirSync(join(directRepo, "src"), { recursive: true });
|
|
@@ -447,12 +451,13 @@ try {
|
|
|
447
451
|
"read_workspace_file",
|
|
448
452
|
"run_verification",
|
|
449
453
|
"search_workspace",
|
|
454
|
+
"sync_file",
|
|
450
455
|
];
|
|
451
456
|
if (JSON.stringify(enabledNames) !== JSON.stringify(expectedDirect)) {
|
|
452
457
|
throw new Error(`chatgpt_direct enabled tools mismatch: ${enabledNames.join(", ")}`);
|
|
453
458
|
}
|
|
454
|
-
if (enabledTools._meta.tool_count !==
|
|
455
|
-
throw new Error(`chatgpt_direct enabled tool_count should be
|
|
459
|
+
if (enabledTools._meta.tool_count !== 10) {
|
|
460
|
+
throw new Error(`chatgpt_direct enabled tool_count should be 10, got ${enabledTools._meta.tool_count}`);
|
|
456
461
|
}
|
|
457
462
|
|
|
458
463
|
// Minimal create_direct_session
|
|
@@ -472,12 +477,12 @@ try {
|
|
|
472
477
|
if (enabledHealth.direct_profile_enabled !== true) {
|
|
473
478
|
throw new Error(`direct_profile_enabled should be true, got ${enabledHealth.direct_profile_enabled}`);
|
|
474
479
|
}
|
|
475
|
-
if (enabledHealth.direct_tool_count !==
|
|
476
|
-
throw new Error(`direct_tool_count should be
|
|
480
|
+
if (enabledHealth.direct_tool_count !== 10) {
|
|
481
|
+
throw new Error(`direct_tool_count should be 10, got ${enabledHealth.direct_tool_count}`);
|
|
477
482
|
}
|
|
478
483
|
|
|
479
484
|
await enabledClient.close();
|
|
480
|
-
ok("chatgpt_direct enabled exposes
|
|
485
|
+
ok("chatgpt_direct enabled exposes 10 tools and create_direct_session works");
|
|
481
486
|
} catch (error) {
|
|
482
487
|
fail("MCP smoke test", error);
|
|
483
488
|
} finally {
|
|
@@ -45,7 +45,8 @@ if (leaked.length > 0) {
|
|
|
45
45
|
|
|
46
46
|
const required = [
|
|
47
47
|
"PatchWarden.cmd",
|
|
48
|
-
"
|
|
48
|
+
"PatchWarden-Desktop.cmd",
|
|
49
|
+
"scripts/control/manage-patchwarden.ps1",
|
|
49
50
|
"scripts/launchers/Start-PatchWarden-Tunnel.cmd",
|
|
50
51
|
"scripts/launchers/Start-PatchWarden-Direct-Tunnel.cmd",
|
|
51
52
|
];
|
|
@@ -10,7 +10,7 @@ if (process.platform !== "win32") {
|
|
|
10
10
|
process.exit(0);
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
const root = resolve(fileURLToPath(new URL("
|
|
13
|
+
const root = resolve(fileURLToPath(new URL("../..", import.meta.url)));
|
|
14
14
|
const temp = mkdtempSync(join(tmpdir(), "patchwarden-tunnel-smoke-"));
|
|
15
15
|
const mockJs = join(temp, "mock-tunnel-client.js");
|
|
16
16
|
const mockCmd = join(temp, "mock-tunnel-client.cmd");
|
|
@@ -57,7 +57,7 @@ if(command==='run'){
|
|
|
57
57
|
MOCK_TUNNEL_STATE: stateFile,
|
|
58
58
|
};
|
|
59
59
|
const result = spawnSync("powershell.exe", [
|
|
60
|
-
"-NoProfile", "-ExecutionPolicy", "Bypass", "-File", join(root, "scripts", "start-patchwarden-tunnel.ps1"),
|
|
60
|
+
"-NoProfile", "-ExecutionPolicy", "Bypass", "-File", join(root, "scripts", "control", "start-patchwarden-tunnel.ps1"),
|
|
61
61
|
"-TunnelId", "tunnel_smoke_fixture",
|
|
62
62
|
"-TunnelClientExe", mockCmd,
|
|
63
63
|
"-ConfigPath", mockConfig,
|