patchwarden 0.6.0 → 0.6.1
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.md +3 -3
- package/dist/doctor.js +1 -1
- 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/performance-notes.md +55 -0
- package/docs/release-v0.6.1.md +75 -0
- package/package.json +3 -2
- package/scripts/http-mcp-smoke.js +10 -2
- package/scripts/lifecycle-smoke.js +336 -2
- package/scripts/mcp-manifest-check.js +30 -7
- package/scripts/mcp-smoke.js +11 -8
- package/scripts/pack-clean.js +157 -1
- package/scripts/unit-tests.js +36 -0
- package/src/doctor.ts +1 -1
- 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
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
2
|
+
import { strict as assert } from "node:assert";
|
|
3
|
+
import { mkdtempSync, rmSync, mkdirSync, writeFileSync, readFileSync, existsSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { syncFile } from "../../tools/syncFile.js";
|
|
7
|
+
import { computeFileSha256 } from "../../direct/directPatch.js";
|
|
8
|
+
function makeConfig(workspaceRoot) {
|
|
9
|
+
return {
|
|
10
|
+
workspaceRoot,
|
|
11
|
+
plansDir: ".patchwarden/plans",
|
|
12
|
+
tasksDir: ".patchwarden/tasks",
|
|
13
|
+
assessmentsDir: ".patchwarden/assessments",
|
|
14
|
+
assessmentTtlSeconds: 3600,
|
|
15
|
+
agents: { codex: { command: "codex", args: ["exec", "{prompt}"] } },
|
|
16
|
+
allowedTestCommands: ["npm test"],
|
|
17
|
+
repoAllowedTestCommands: {},
|
|
18
|
+
maxReadFileBytes: 200_000,
|
|
19
|
+
defaultTaskTimeoutSeconds: 900,
|
|
20
|
+
maxTaskTimeoutSeconds: 3600,
|
|
21
|
+
watcherStaleSeconds: 30,
|
|
22
|
+
directSessionsDir: ".patchwarden/direct-sessions",
|
|
23
|
+
directSessionTtlSeconds: 3600,
|
|
24
|
+
directMaxPatchBytes: 200_000,
|
|
25
|
+
directMaxFileBytes: 500_000,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
describe("syncFile", () => {
|
|
29
|
+
let tempDir;
|
|
30
|
+
let config;
|
|
31
|
+
let repoPath;
|
|
32
|
+
let sessionsDir;
|
|
33
|
+
let sessionId;
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
tempDir = mkdtempSync(join(tmpdir(), "pw-syncfile-"));
|
|
36
|
+
config = makeConfig(tempDir);
|
|
37
|
+
repoPath = join(tempDir, "my-repo");
|
|
38
|
+
sessionsDir = join(tempDir, ".patchwarden", "direct-sessions");
|
|
39
|
+
sessionId = "test-session-001";
|
|
40
|
+
mkdirSync(repoPath, { recursive: true });
|
|
41
|
+
mkdirSync(join(sessionsDir, sessionId), { recursive: true });
|
|
42
|
+
writeFileSync(join(sessionsDir, sessionId, "session.json"), JSON.stringify({
|
|
43
|
+
session_id: sessionId,
|
|
44
|
+
repo_path: repoPath,
|
|
45
|
+
status: "active",
|
|
46
|
+
created_at: new Date().toISOString(),
|
|
47
|
+
}), "utf-8");
|
|
48
|
+
});
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
51
|
+
});
|
|
52
|
+
it("copies a file from source to target", () => {
|
|
53
|
+
const sourceContent = "console.log('hello world');";
|
|
54
|
+
mkdirSync(join(repoPath, "mobile_app"), { recursive: true });
|
|
55
|
+
writeFileSync(join(repoPath, "mobile_app", "app.js"), sourceContent, "utf-8");
|
|
56
|
+
const result = syncFile(sessionId, "mobile_app/app.js", "windows_app/app/app.js", undefined, config);
|
|
57
|
+
assert.equal(result.changed, true);
|
|
58
|
+
assert.equal(result.copied_bytes, sourceContent.length);
|
|
59
|
+
assert.equal(result.before_target_sha256, null);
|
|
60
|
+
assert.ok(result.after_target_sha256);
|
|
61
|
+
const targetContent = readFileSync(join(repoPath, "windows_app", "app", "app.js"), "utf-8");
|
|
62
|
+
assert.equal(targetContent, sourceContent);
|
|
63
|
+
});
|
|
64
|
+
it("returns changed=false when target already has same content", () => {
|
|
65
|
+
const sourceContent = "same content";
|
|
66
|
+
mkdirSync(join(repoPath, "src"), { recursive: true });
|
|
67
|
+
mkdirSync(join(repoPath, "dst"), { recursive: true });
|
|
68
|
+
writeFileSync(join(repoPath, "src", "file.ts"), sourceContent, "utf-8");
|
|
69
|
+
writeFileSync(join(repoPath, "dst", "file.ts"), sourceContent, "utf-8");
|
|
70
|
+
const result = syncFile(sessionId, "src/file.ts", "dst/file.ts", undefined, config);
|
|
71
|
+
assert.equal(result.changed, false);
|
|
72
|
+
assert.ok(result.before_target_sha256);
|
|
73
|
+
assert.equal(result.before_target_sha256, result.after_target_sha256);
|
|
74
|
+
});
|
|
75
|
+
it("returns changed=true when target has different content", () => {
|
|
76
|
+
mkdirSync(join(repoPath, "src"), { recursive: true });
|
|
77
|
+
mkdirSync(join(repoPath, "dst"), { recursive: true });
|
|
78
|
+
writeFileSync(join(repoPath, "src", "file.ts"), "new content", "utf-8");
|
|
79
|
+
writeFileSync(join(repoPath, "dst", "file.ts"), "old content", "utf-8");
|
|
80
|
+
const result = syncFile(sessionId, "src/file.ts", "dst/file.ts", undefined, config);
|
|
81
|
+
assert.equal(result.changed, true);
|
|
82
|
+
assert.notEqual(result.before_target_sha256, result.after_target_sha256);
|
|
83
|
+
});
|
|
84
|
+
it("rejects source path outside repo", () => {
|
|
85
|
+
mkdirSync(join(repoPath, "src"), { recursive: true });
|
|
86
|
+
writeFileSync(join(repoPath, "src", "file.ts"), "content", "utf-8");
|
|
87
|
+
assert.throws(() => syncFile(sessionId, "../../../etc/passwd", "dst/file.ts", undefined, config), Error);
|
|
88
|
+
});
|
|
89
|
+
it("rejects target path outside repo", () => {
|
|
90
|
+
mkdirSync(join(repoPath, "src"), { recursive: true });
|
|
91
|
+
writeFileSync(join(repoPath, "src", "file.ts"), "content", "utf-8");
|
|
92
|
+
assert.throws(() => syncFile(sessionId, "src/file.ts", "../../../etc/evil", undefined, config), Error);
|
|
93
|
+
});
|
|
94
|
+
it("rejects sensitive source files", () => {
|
|
95
|
+
writeFileSync(join(repoPath, ".env"), "SECRET=abc123", "utf-8");
|
|
96
|
+
assert.throws(() => syncFile(sessionId, ".env", "dst/.env", undefined, config), Error);
|
|
97
|
+
});
|
|
98
|
+
it("rejects sensitive target files", () => {
|
|
99
|
+
mkdirSync(join(repoPath, "src"), { recursive: true });
|
|
100
|
+
writeFileSync(join(repoPath, "src", "file.ts"), "content", "utf-8");
|
|
101
|
+
assert.throws(() => syncFile(sessionId, "src/file.ts", "config.json", undefined, config), Error);
|
|
102
|
+
});
|
|
103
|
+
it("rejects writing to node_modules", () => {
|
|
104
|
+
mkdirSync(join(repoPath, "src"), { recursive: true });
|
|
105
|
+
writeFileSync(join(repoPath, "src", "file.ts"), "content", "utf-8");
|
|
106
|
+
assert.throws(() => syncFile(sessionId, "src/file.ts", "node_modules/evil/index.js", undefined, config), Error);
|
|
107
|
+
});
|
|
108
|
+
it("rejects writing to dist", () => {
|
|
109
|
+
mkdirSync(join(repoPath, "src"), { recursive: true });
|
|
110
|
+
writeFileSync(join(repoPath, "src", "file.ts"), "content", "utf-8");
|
|
111
|
+
assert.throws(() => syncFile(sessionId, "src/file.ts", "dist/main.js", undefined, config), Error);
|
|
112
|
+
});
|
|
113
|
+
it("rejects non-existent session", () => {
|
|
114
|
+
mkdirSync(join(repoPath, "src"), { recursive: true });
|
|
115
|
+
writeFileSync(join(repoPath, "src", "file.ts"), "content", "utf-8");
|
|
116
|
+
assert.throws(() => syncFile("non-existent-session", "src/file.ts", "dst/file.ts", undefined, config), Error);
|
|
117
|
+
});
|
|
118
|
+
it("rejects non-existent source file", () => {
|
|
119
|
+
assert.throws(() => syncFile(sessionId, "non-existent/file.ts", "dst/file.ts", undefined, config), Error);
|
|
120
|
+
});
|
|
121
|
+
it("validates expected_source_sha256", () => {
|
|
122
|
+
mkdirSync(join(repoPath, "src"), { recursive: true });
|
|
123
|
+
writeFileSync(join(repoPath, "src", "file.ts"), "content", "utf-8");
|
|
124
|
+
const correctHash = computeFileSha256(join(repoPath, "src", "file.ts"));
|
|
125
|
+
const result = syncFile(sessionId, "src/file.ts", "dst/file.ts", {
|
|
126
|
+
expected_source_sha256: correctHash,
|
|
127
|
+
}, config);
|
|
128
|
+
assert.equal(result.source_sha256, correctHash);
|
|
129
|
+
assert.throws(() => syncFile(sessionId, "src/file.ts", "dst2/file.ts", {
|
|
130
|
+
expected_source_sha256: "wronghash",
|
|
131
|
+
}, config), Error);
|
|
132
|
+
});
|
|
133
|
+
it("validates expected_target_sha256", () => {
|
|
134
|
+
mkdirSync(join(repoPath, "src"), { recursive: true });
|
|
135
|
+
mkdirSync(join(repoPath, "dst"), { recursive: true });
|
|
136
|
+
writeFileSync(join(repoPath, "src", "file.ts"), "new content", "utf-8");
|
|
137
|
+
writeFileSync(join(repoPath, "dst", "file.ts"), "old content", "utf-8");
|
|
138
|
+
const correctTargetHash = computeFileSha256(join(repoPath, "dst", "file.ts"));
|
|
139
|
+
const result = syncFile(sessionId, "src/file.ts", "dst/file.ts", {
|
|
140
|
+
expected_target_sha256: correctTargetHash,
|
|
141
|
+
}, config);
|
|
142
|
+
assert.equal(result.before_target_sha256, correctTargetHash);
|
|
143
|
+
assert.throws(() => syncFile(sessionId, "src/file.ts", "dst/file.ts", {
|
|
144
|
+
expected_target_sha256: "wronghash",
|
|
145
|
+
}, config), Error);
|
|
146
|
+
});
|
|
147
|
+
it("creates target directory if it doesn't exist", () => {
|
|
148
|
+
mkdirSync(join(repoPath, "src"), { recursive: true });
|
|
149
|
+
writeFileSync(join(repoPath, "src", "file.ts"), "content", "utf-8");
|
|
150
|
+
const result = syncFile(sessionId, "src/file.ts", "deep/nested/path/file.ts", undefined, config);
|
|
151
|
+
assert.equal(result.changed, true);
|
|
152
|
+
assert.ok(existsSync(join(repoPath, "deep", "nested", "path", "file.ts")));
|
|
153
|
+
});
|
|
154
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
2
|
+
import { strict as assert } from "node:assert";
|
|
3
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join, dirname } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { readWatcherStatus, getWatcherHeartbeatPath } from "../../watcherStatus.js";
|
|
7
|
+
function makeConfig(workspaceRoot) {
|
|
8
|
+
return {
|
|
9
|
+
workspaceRoot,
|
|
10
|
+
plansDir: ".patchwarden/plans",
|
|
11
|
+
tasksDir: ".patchwarden/tasks",
|
|
12
|
+
assessmentsDir: ".patchwarden/assessments",
|
|
13
|
+
assessmentTtlSeconds: 3600,
|
|
14
|
+
agents: { codex: { command: "codex", args: ["exec", "{prompt}"] } },
|
|
15
|
+
allowedTestCommands: ["npm test"],
|
|
16
|
+
repoAllowedTestCommands: {},
|
|
17
|
+
maxReadFileBytes: 200_000,
|
|
18
|
+
defaultTaskTimeoutSeconds: 900,
|
|
19
|
+
maxTaskTimeoutSeconds: 3600,
|
|
20
|
+
watcherStaleSeconds: 30,
|
|
21
|
+
directSessionsDir: ".patchwarden/direct-sessions",
|
|
22
|
+
directSessionTtlSeconds: 3600,
|
|
23
|
+
directMaxPatchBytes: 200_000,
|
|
24
|
+
directMaxFileBytes: 500_000,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
describe("readWatcherStatus — watcher stale fallback", () => {
|
|
28
|
+
let tempDir;
|
|
29
|
+
let config;
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
tempDir = mkdtempSync(join(tmpdir(), "pw-watcher-test-"));
|
|
32
|
+
config = makeConfig(tempDir);
|
|
33
|
+
mkdirSync(join(tempDir, ".patchwarden", "tasks"), { recursive: true });
|
|
34
|
+
});
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
37
|
+
});
|
|
38
|
+
it("returns missing when no heartbeat and no running task", () => {
|
|
39
|
+
const result = readWatcherStatus(config, Date.now());
|
|
40
|
+
assert.equal(result.status, "missing");
|
|
41
|
+
assert.equal(result.available, false);
|
|
42
|
+
assert.equal(result.activity, null);
|
|
43
|
+
});
|
|
44
|
+
it("returns healthy when watcher heartbeat is fresh", () => {
|
|
45
|
+
const heartbeatPath = getWatcherHeartbeatPath(config);
|
|
46
|
+
mkdirSync(dirname(heartbeatPath), { recursive: true });
|
|
47
|
+
writeFileSync(heartbeatPath, JSON.stringify({
|
|
48
|
+
status: "running",
|
|
49
|
+
pid: process.pid,
|
|
50
|
+
last_heartbeat_at: new Date().toISOString(),
|
|
51
|
+
}), "utf-8");
|
|
52
|
+
const result = readWatcherStatus(config, Date.now());
|
|
53
|
+
assert.equal(result.status, "healthy");
|
|
54
|
+
assert.equal(result.available, true);
|
|
55
|
+
assert.equal(result.activity, null);
|
|
56
|
+
});
|
|
57
|
+
it("returns stale when watcher heartbeat is old and no running task", () => {
|
|
58
|
+
const heartbeatPath = getWatcherHeartbeatPath(config);
|
|
59
|
+
mkdirSync(dirname(heartbeatPath), { recursive: true });
|
|
60
|
+
const oldTime = new Date(Date.now() - 120_000).toISOString();
|
|
61
|
+
writeFileSync(heartbeatPath, JSON.stringify({
|
|
62
|
+
status: "running",
|
|
63
|
+
pid: process.pid,
|
|
64
|
+
last_heartbeat_at: oldTime,
|
|
65
|
+
}), "utf-8");
|
|
66
|
+
const result = readWatcherStatus(config, Date.now());
|
|
67
|
+
assert.equal(result.status, "stale");
|
|
68
|
+
assert.equal(result.available, false);
|
|
69
|
+
assert.equal(result.activity, null);
|
|
70
|
+
});
|
|
71
|
+
it("returns healthy when watcher heartbeat is stale but a running task has fresh heartbeat", () => {
|
|
72
|
+
// Write stale watcher heartbeat
|
|
73
|
+
const heartbeatPath = getWatcherHeartbeatPath(config);
|
|
74
|
+
mkdirSync(dirname(heartbeatPath), { recursive: true });
|
|
75
|
+
const oldTime = new Date(Date.now() - 120_000).toISOString();
|
|
76
|
+
writeFileSync(heartbeatPath, JSON.stringify({
|
|
77
|
+
status: "running",
|
|
78
|
+
pid: process.pid,
|
|
79
|
+
last_heartbeat_at: oldTime,
|
|
80
|
+
}), "utf-8");
|
|
81
|
+
// Create a running task with fresh heartbeat
|
|
82
|
+
const taskDir = join(tempDir, ".patchwarden", "tasks", "test-task-001");
|
|
83
|
+
mkdirSync(taskDir, { recursive: true });
|
|
84
|
+
writeFileSync(join(taskDir, "status.json"), JSON.stringify({
|
|
85
|
+
status: "running",
|
|
86
|
+
task_id: "test-task-001",
|
|
87
|
+
}), "utf-8");
|
|
88
|
+
writeFileSync(join(taskDir, "runtime.json"), JSON.stringify({
|
|
89
|
+
phase: "executing_agent",
|
|
90
|
+
last_heartbeat_at: new Date().toISOString(),
|
|
91
|
+
current_command: "codex exec",
|
|
92
|
+
}), "utf-8");
|
|
93
|
+
const result = readWatcherStatus(config, Date.now());
|
|
94
|
+
assert.equal(result.status, "healthy");
|
|
95
|
+
assert.equal(result.available, true);
|
|
96
|
+
assert.ok(result.activity);
|
|
97
|
+
assert.match(result.activity, /test-task-001/);
|
|
98
|
+
});
|
|
99
|
+
it("returns healthy when watcher heartbeat is missing but a running task has fresh heartbeat", () => {
|
|
100
|
+
// No watcher heartbeat file at all
|
|
101
|
+
// Create a running task with fresh heartbeat
|
|
102
|
+
const taskDir = join(tempDir, ".patchwarden", "tasks", "test-task-002");
|
|
103
|
+
mkdirSync(taskDir, { recursive: true });
|
|
104
|
+
writeFileSync(join(taskDir, "status.json"), JSON.stringify({
|
|
105
|
+
status: "running",
|
|
106
|
+
task_id: "test-task-002",
|
|
107
|
+
}), "utf-8");
|
|
108
|
+
writeFileSync(join(taskDir, "runtime.json"), JSON.stringify({
|
|
109
|
+
phase: "running_tests",
|
|
110
|
+
last_heartbeat_at: new Date().toISOString(),
|
|
111
|
+
current_command: "npm test",
|
|
112
|
+
}), "utf-8");
|
|
113
|
+
const result = readWatcherStatus(config, Date.now());
|
|
114
|
+
assert.equal(result.status, "healthy");
|
|
115
|
+
assert.equal(result.available, true);
|
|
116
|
+
assert.ok(result.activity);
|
|
117
|
+
assert.match(result.activity, /test-task-002/);
|
|
118
|
+
});
|
|
119
|
+
it("returns stale when both watcher heartbeat and task heartbeat are old", () => {
|
|
120
|
+
const heartbeatPath = getWatcherHeartbeatPath(config);
|
|
121
|
+
mkdirSync(dirname(heartbeatPath), { recursive: true });
|
|
122
|
+
const oldTime = new Date(Date.now() - 120_000).toISOString();
|
|
123
|
+
writeFileSync(heartbeatPath, JSON.stringify({
|
|
124
|
+
status: "running",
|
|
125
|
+
pid: process.pid,
|
|
126
|
+
last_heartbeat_at: oldTime,
|
|
127
|
+
}), "utf-8");
|
|
128
|
+
const taskDir = join(tempDir, ".patchwarden", "tasks", "test-task-003");
|
|
129
|
+
mkdirSync(taskDir, { recursive: true });
|
|
130
|
+
writeFileSync(join(taskDir, "status.json"), JSON.stringify({
|
|
131
|
+
status: "running",
|
|
132
|
+
task_id: "test-task-003",
|
|
133
|
+
}), "utf-8");
|
|
134
|
+
writeFileSync(join(taskDir, "runtime.json"), JSON.stringify({
|
|
135
|
+
phase: "executing_agent",
|
|
136
|
+
last_heartbeat_at: oldTime,
|
|
137
|
+
current_command: "codex exec",
|
|
138
|
+
}), "utf-8");
|
|
139
|
+
const result = readWatcherStatus(config, Date.now());
|
|
140
|
+
assert.equal(result.status, "stale");
|
|
141
|
+
assert.equal(result.available, false);
|
|
142
|
+
assert.equal(result.activity, null);
|
|
143
|
+
});
|
|
144
|
+
it("does not fall back to non-running tasks", () => {
|
|
145
|
+
const heartbeatPath = getWatcherHeartbeatPath(config);
|
|
146
|
+
mkdirSync(dirname(heartbeatPath), { recursive: true });
|
|
147
|
+
const oldTime = new Date(Date.now() - 120_000).toISOString();
|
|
148
|
+
writeFileSync(heartbeatPath, JSON.stringify({
|
|
149
|
+
status: "running",
|
|
150
|
+
pid: process.pid,
|
|
151
|
+
last_heartbeat_at: oldTime,
|
|
152
|
+
}), "utf-8");
|
|
153
|
+
// Done task with fresh heartbeat should not trigger fallback
|
|
154
|
+
const taskDir = join(tempDir, ".patchwarden", "tasks", "test-task-004");
|
|
155
|
+
mkdirSync(taskDir, { recursive: true });
|
|
156
|
+
writeFileSync(join(taskDir, "status.json"), JSON.stringify({
|
|
157
|
+
status: "done",
|
|
158
|
+
task_id: "test-task-004",
|
|
159
|
+
}), "utf-8");
|
|
160
|
+
writeFileSync(join(taskDir, "runtime.json"), JSON.stringify({
|
|
161
|
+
phase: "completed",
|
|
162
|
+
last_heartbeat_at: new Date().toISOString(),
|
|
163
|
+
current_command: null,
|
|
164
|
+
}), "utf-8");
|
|
165
|
+
const result = readWatcherStatus(config, Date.now());
|
|
166
|
+
assert.equal(result.status, "stale");
|
|
167
|
+
assert.equal(result.available, false);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PatchWarden Android Doctor — read-only Android build environment diagnostics.
|
|
3
|
+
*
|
|
4
|
+
* Inspects a repository for an `android_app` directory and reports whether the
|
|
5
|
+
* local environment can build an APK. The module is strictly read-only: it never
|
|
6
|
+
* auto-downloads the Android SDK, never modifies system environment variables,
|
|
7
|
+
* and never installs global dependencies. It only runs `java -version` (read-only)
|
|
8
|
+
* and inspects files/directories/env vars that already exist.
|
|
9
|
+
*/
|
|
10
|
+
export type AndroidCheckStatus = "ok" | "warn" | "fail" | "skip";
|
|
11
|
+
export interface AndroidDiagnosticItem {
|
|
12
|
+
check: string;
|
|
13
|
+
status: AndroidCheckStatus;
|
|
14
|
+
reason: string;
|
|
15
|
+
suggested_fix: string;
|
|
16
|
+
}
|
|
17
|
+
export interface AndroidBuildDiagnosticReport {
|
|
18
|
+
status: "ok" | "warn" | "fail";
|
|
19
|
+
repo_path: string;
|
|
20
|
+
android_app_path: string;
|
|
21
|
+
checks: AndroidDiagnosticItem[];
|
|
22
|
+
checked_at: string;
|
|
23
|
+
}
|
|
24
|
+
export type AndroidBuildDiagnostic = {
|
|
25
|
+
status: "skip";
|
|
26
|
+
reason: string;
|
|
27
|
+
} | AndroidBuildDiagnosticReport;
|
|
28
|
+
/**
|
|
29
|
+
* Diagnose the Android build readiness for the repository at `repoPath`.
|
|
30
|
+
*
|
|
31
|
+
* - If `<repoPath>/android_app` does not exist, returns a skip result.
|
|
32
|
+
* - Otherwise runs 10 read-only checks and returns a structured report with an
|
|
33
|
+
* overall status: "fail" if any check failed, "warn" if any warned, else "ok".
|
|
34
|
+
*
|
|
35
|
+
* This function performs no mutations: it does not download the SDK, change env
|
|
36
|
+
* vars, or install dependencies.
|
|
37
|
+
*/
|
|
38
|
+
export declare function diagnoseAndroidBuild(repoPath: string): AndroidBuildDiagnostic;
|