@opengsd/gsd-pi 1.3.0-dev.65546769 → 1.3.0-dev.eed73bea
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/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +11 -2
- package/dist/resources/extensions/google-cli/stream-adapter.js +82 -15
- package/dist/resources/extensions/gsd/auto/orchestrator.js +12 -3
- package/dist/resources/extensions/gsd/auto-dispatch.js +17 -14
- package/dist/resources/extensions/gsd/auto-prompts.js +43 -12
- package/dist/resources/extensions/gsd/auto-recovery.js +13 -6
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +103 -13
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +6 -1
- package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +2 -0
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +8 -3
- package/dist/resources/extensions/gsd/bootstrap/system-context.js +46 -19
- package/dist/resources/extensions/gsd/bootstrap/tool-call-loop-guard.js +75 -1
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +1 -1
- package/dist/resources/extensions/gsd/commands-context.js +19 -1
- package/dist/resources/extensions/gsd/commands-prefs-wizard.js +16 -10
- package/dist/resources/extensions/gsd/commands-worktree.js +12 -10
- package/dist/resources/extensions/gsd/dashboard-overlay.js +32 -3
- package/dist/resources/extensions/gsd/db/queries.js +60 -0
- package/dist/resources/extensions/gsd/doctor-providers.js +92 -8
- package/dist/resources/extensions/gsd/exec-sandbox.js +45 -9
- package/dist/resources/extensions/gsd/forensics.js +2 -32
- package/dist/resources/extensions/gsd/git-service.js +4 -4
- package/dist/resources/extensions/gsd/guided-flow-queue.js +59 -5
- package/dist/resources/extensions/gsd/health-widget.js +55 -29
- package/dist/resources/extensions/gsd/markdown-renderer.js +6 -2
- package/dist/resources/extensions/gsd/memory-consolidation-scanner.js +44 -21
- package/dist/resources/extensions/gsd/milestone-implementation-evidence.js +26 -20
- package/dist/resources/extensions/gsd/quick.js +45 -2
- package/dist/resources/extensions/gsd/session-forensics.js +11 -1
- package/dist/resources/extensions/gsd/state-reconciliation/drift/stale-render.js +52 -3
- package/dist/resources/extensions/gsd/tools/complete-slice.js +34 -3
- package/dist/resources/extensions/gsd/tools/complete-task.js +78 -16
- package/dist/resources/extensions/gsd/tools/exec-tool.js +7 -2
- package/dist/resources/extensions/gsd/unit-context-composer.js +23 -7
- package/dist/resources/extensions/gsd/unit-registry.js +25 -3
- package/dist/resources/extensions/gsd/unmerged-milestone-guard.js +33 -3
- package/dist/resources/extensions/gsd/validation-block-guard.js +9 -4
- package/dist/resources/extensions/gsd/workspace-git-preflight.js +30 -1
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +14 -14
- package/dist/web/standalone/.next/build-manifest.json +3 -3
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/react-loadable-manifest.json +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +14 -14
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/.next/static/chunks/{796.e0bdc932325d7e03.js → 796.3976108148518f7d.js} +3 -3
- package/dist/web/standalone/.next/static/chunks/{webpack-f46ea08200a0227e.js → webpack-7c1d97e39be2da11.js} +1 -1
- package/package.json +1 -1
- package/packages/cloud-mcp-gateway/package.json +2 -2
- package/packages/contracts/dist/workflow.d.ts +1 -0
- package/packages/contracts/dist/workflow.d.ts.map +1 -1
- package/packages/contracts/dist/workflow.js +2 -0
- package/packages/contracts/dist/workflow.js.map +1 -1
- package/packages/contracts/package.json +1 -1
- package/packages/daemon/package.json +4 -4
- package/packages/gsd-agent-core/package.json +5 -5
- package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js +21 -9
- package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
- package/packages/gsd-agent-modes/package.json +7 -7
- package/packages/mcp-server/README.md +1 -1
- package/packages/mcp-server/dist/server.d.ts +1 -1
- package/packages/mcp-server/dist/server.d.ts.map +1 -1
- package/packages/mcp-server/dist/server.js +3 -3
- package/packages/mcp-server/dist/server.js.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.d.ts +13 -1
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +34 -20
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/package.json +4 -4
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-coding-agent/package.json +7 -7
- package/packages/pi-tui/package.json +2 -2
- package/packages/rpc-client/package.json +2 -2
- package/pkg/package.json +1 -1
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +20 -2
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +80 -0
- package/src/resources/extensions/google-cli/stream-adapter.ts +106 -19
- package/src/resources/extensions/gsd/auto/orchestrator.ts +25 -11
- package/src/resources/extensions/gsd/auto-dispatch.ts +18 -17
- package/src/resources/extensions/gsd/auto-prompts.ts +54 -12
- package/src/resources/extensions/gsd/auto-recovery.ts +13 -6
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +125 -12
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +6 -1
- package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +2 -0
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +9 -3
- package/src/resources/extensions/gsd/bootstrap/system-context.ts +52 -18
- package/src/resources/extensions/gsd/bootstrap/tool-call-loop-guard.ts +82 -1
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +1 -1
- package/src/resources/extensions/gsd/commands-context.ts +18 -1
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +14 -9
- package/src/resources/extensions/gsd/commands-worktree.ts +12 -10
- package/src/resources/extensions/gsd/dashboard-overlay.ts +32 -3
- package/src/resources/extensions/gsd/db/queries.ts +79 -0
- package/src/resources/extensions/gsd/doctor-providers.ts +103 -9
- package/src/resources/extensions/gsd/exec-sandbox.ts +49 -9
- package/src/resources/extensions/gsd/forensics.ts +2 -33
- package/src/resources/extensions/gsd/git-service.ts +5 -5
- package/src/resources/extensions/gsd/guided-flow-queue.ts +82 -4
- package/src/resources/extensions/gsd/health-widget.ts +69 -32
- package/src/resources/extensions/gsd/markdown-renderer.ts +6 -1
- package/src/resources/extensions/gsd/memory-consolidation-scanner.ts +51 -19
- package/src/resources/extensions/gsd/milestone-implementation-evidence.ts +35 -21
- package/src/resources/extensions/gsd/quick.ts +43 -2
- package/src/resources/extensions/gsd/session-forensics.ts +11 -1
- package/src/resources/extensions/gsd/state-reconciliation/drift/stale-render.ts +76 -8
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +111 -1
- package/src/resources/extensions/gsd/tests/commands-context.test.ts +26 -0
- package/src/resources/extensions/gsd/tests/commands-worktree-clean.test.ts +80 -0
- package/src/resources/extensions/gsd/tests/complete-slice.test.ts +11 -0
- package/src/resources/extensions/gsd/tests/complete-task-rollback-evidence.test.ts +48 -8
- package/src/resources/extensions/gsd/tests/complete-task.test.ts +75 -0
- package/src/resources/extensions/gsd/tests/dashboard-overlay.test.ts +55 -2
- package/src/resources/extensions/gsd/tests/dispatch-rule-coverage.test.ts +26 -1
- package/src/resources/extensions/gsd/tests/doctor-forensics-db-open-regression.test.ts +70 -2
- package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +107 -0
- package/src/resources/extensions/gsd/tests/exec-graceful-kill.test.ts +38 -0
- package/src/resources/extensions/gsd/tests/exec-tool.test.ts +45 -1
- package/src/resources/extensions/gsd/tests/forensics-error-filter.test.ts +88 -0
- package/src/resources/extensions/gsd/tests/guided-discuss-milestone-prompt-rendering.test.ts +42 -0
- package/src/resources/extensions/gsd/tests/health-widget.test.ts +268 -3
- package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +119 -1
- package/src/resources/extensions/gsd/tests/integration/queue-active-milestone-context-budget.test.ts +93 -0
- package/src/resources/extensions/gsd/tests/integration/quick-branch-lifecycle.test.ts +56 -9
- package/src/resources/extensions/gsd/tests/knowledge-cold-start.test.ts +14 -0
- package/src/resources/extensions/gsd/tests/memory-consolidation-scanner.test.ts +78 -0
- package/src/resources/extensions/gsd/tests/orchestrator-logs.test.ts +43 -1
- package/src/resources/extensions/gsd/tests/parallel-research-dispatch.test.ts +26 -0
- package/src/resources/extensions/gsd/tests/pipeline-variant-dispatch.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +54 -1
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +195 -1
- package/src/resources/extensions/gsd/tests/read-uat-gate-verdict.test.ts +185 -0
- package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +87 -0
- package/src/resources/extensions/gsd/tests/state-reconciliation-drift.test.ts +76 -0
- package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +68 -0
- package/src/resources/extensions/gsd/tests/tool-param-optionality.test.ts +26 -0
- package/src/resources/extensions/gsd/tests/unit-context-composer.test.ts +193 -14
- package/src/resources/extensions/gsd/tests/unmerged-milestone-guard.test.ts +25 -0
- package/src/resources/extensions/gsd/tests/validation-block-guard.test.ts +79 -0
- package/src/resources/extensions/gsd/tests/verify-artifact-tightened.test.ts +66 -0
- package/src/resources/extensions/gsd/tests/workspace-git-preflight.test.ts +151 -2
- package/src/resources/extensions/gsd/tools/complete-slice.ts +30 -3
- package/src/resources/extensions/gsd/tools/complete-task.ts +86 -16
- package/src/resources/extensions/gsd/tools/exec-tool.ts +7 -3
- package/src/resources/extensions/gsd/unit-context-composer.ts +33 -7
- package/src/resources/extensions/gsd/unit-registry.ts +25 -3
- package/src/resources/extensions/gsd/unmerged-milestone-guard.ts +41 -5
- package/src/resources/extensions/gsd/validation-block-guard.ts +13 -7
- package/src/resources/extensions/gsd/workspace-git-preflight.ts +31 -0
- /package/dist/web/standalone/.next/static/{BTKtGFF1Y-hvVJEGhBRo9 → SzEuqWX37DR9MEpEuQjP1}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{BTKtGFF1Y-hvVJEGhBRo9 → SzEuqWX37DR9MEpEuQjP1}/_ssgManifest.js +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, afterEach } from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { mkdirSync, readFileSync, rmSync, writeFileSync, promises as fsPromises } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
@@ -41,7 +41,7 @@ const VALID_PARAMS = {
|
|
|
41
41
|
],
|
|
42
42
|
};
|
|
43
43
|
|
|
44
|
-
describe("complete-task projection failures
|
|
44
|
+
describe("complete-task projection failures roll back DB completion", () => {
|
|
45
45
|
let base: string;
|
|
46
46
|
|
|
47
47
|
afterEach(() => {
|
|
@@ -75,7 +75,44 @@ describe("complete-task projection failures keep DB completion committed", () =>
|
|
|
75
75
|
assert.equal(rows.length, 2, "should have 2 evidence rows after success");
|
|
76
76
|
});
|
|
77
77
|
|
|
78
|
-
it("
|
|
78
|
+
it("reopens the workflow DB when it disappears between SUMMARY.md write and plan render", async (t) => {
|
|
79
|
+
base = makeTmpBase();
|
|
80
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
81
|
+
insertMilestone({ id: "M001" });
|
|
82
|
+
insertSlice({ id: "S01", milestoneId: "M001" });
|
|
83
|
+
|
|
84
|
+
const planPath = join(base, ".gsd", "milestones", "M001", "slices", "S01", "S01-PLAN.md");
|
|
85
|
+
writeFileSync(
|
|
86
|
+
planPath,
|
|
87
|
+
"# S01 Plan\n\n## Tasks\n\n- [ ] **T01: Test task**\n",
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const originalRename = fsPromises.rename.bind(fsPromises);
|
|
91
|
+
let closedAfterSummaryWrite = false;
|
|
92
|
+
t.mock.method(fsPromises, "rename", async (...args: Parameters<typeof fsPromises.rename>) => {
|
|
93
|
+
await originalRename(...args);
|
|
94
|
+
const target = String(args[1]);
|
|
95
|
+
if (!closedAfterSummaryWrite && target.endsWith("T01-SUMMARY.md")) {
|
|
96
|
+
closedAfterSummaryWrite = true;
|
|
97
|
+
closeDatabase();
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const result = await handleCompleteTask(VALID_PARAMS, base);
|
|
102
|
+
assert.ok(closedAfterSummaryWrite, "test fixture should close DB after SUMMARY.md atomic rename");
|
|
103
|
+
assert.ok(!("error" in result), `unexpected error: ${"error" in result ? result.error : ""}`);
|
|
104
|
+
|
|
105
|
+
const content = readFileSync(planPath, "utf-8");
|
|
106
|
+
assert.match(content, /\[x\][^\n]*\*\*T01\*\*/, "PLAN.md checkbox should be rendered after DB reopen");
|
|
107
|
+
|
|
108
|
+
const adapter = _getAdapter()!;
|
|
109
|
+
const task = adapter.prepare(
|
|
110
|
+
`SELECT status FROM tasks WHERE milestone_id = 'M001' AND slice_id = 'S01' AND id = 'T01'`,
|
|
111
|
+
).get() as { status: string } | undefined;
|
|
112
|
+
assert.equal(task?.status, "complete", "task should remain complete after successful projection");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("rolls back DB completion and clears verification_evidence when disk projection write fails", async () => {
|
|
79
116
|
base = makeTmpBase();
|
|
80
117
|
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
81
118
|
insertMilestone({ id: "M001" });
|
|
@@ -87,19 +124,22 @@ describe("complete-task projection failures keep DB completion committed", () =>
|
|
|
87
124
|
writeFileSync(tasksDir, "not-a-directory");
|
|
88
125
|
|
|
89
126
|
const result = await handleCompleteTask(VALID_PARAMS, base);
|
|
90
|
-
assert.ok(
|
|
91
|
-
assert.
|
|
127
|
+
assert.ok("error" in result, "expected rollback error when projection write fails");
|
|
128
|
+
assert.ok(
|
|
129
|
+
(result as { error: string }).error.includes("rolled completion back to pending"),
|
|
130
|
+
`error should mention rollback; got: ${"error" in result ? result.error : ""}`,
|
|
131
|
+
);
|
|
92
132
|
|
|
93
133
|
const adapter = _getAdapter()!;
|
|
94
134
|
const task = adapter.prepare(
|
|
95
135
|
`SELECT status FROM tasks WHERE milestone_id = 'M001' AND slice_id = 'S01' AND id = 'T01'`,
|
|
96
136
|
).get() as { status: string } | undefined;
|
|
97
|
-
assert.ok(task, "task row should still exist");
|
|
98
|
-
assert.equal(task!.status, "
|
|
137
|
+
assert.ok(task, "task row should still exist after rollback");
|
|
138
|
+
assert.equal(task!.status, "pending", "task status should be rolled back to pending");
|
|
99
139
|
|
|
100
140
|
const evidenceRows = adapter.prepare(
|
|
101
141
|
`SELECT * FROM verification_evidence WHERE task_id = 'T01' AND slice_id = 'S01' AND milestone_id = 'M001'`,
|
|
102
142
|
).all();
|
|
103
|
-
assert.equal(evidenceRows.length,
|
|
143
|
+
assert.equal(evidenceRows.length, 0, "verification_evidence should be deleted after rollback");
|
|
104
144
|
});
|
|
105
145
|
});
|
|
@@ -380,6 +380,81 @@ console.log('\n=== complete-task: handler happy path ===');
|
|
|
380
380
|
cleanup(dbPath);
|
|
381
381
|
}
|
|
382
382
|
|
|
383
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
384
|
+
// complete-task: Projection failure rolls DB completion back
|
|
385
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
386
|
+
|
|
387
|
+
console.log('\n=== complete-task: projection failure rolls DB completion back ===');
|
|
388
|
+
{
|
|
389
|
+
const dbPath = tempDbPath();
|
|
390
|
+
openDatabase(dbPath);
|
|
391
|
+
|
|
392
|
+
const { basePath, planPath } = createTempProject();
|
|
393
|
+
|
|
394
|
+
insertMilestone({ id: 'M001', title: 'Test Milestone' });
|
|
395
|
+
insertSlice({ id: 'S01', milestoneId: 'M001', title: 'Test Slice' });
|
|
396
|
+
|
|
397
|
+
fs.unlinkSync(planPath);
|
|
398
|
+
fs.mkdirSync(planPath, { recursive: true });
|
|
399
|
+
|
|
400
|
+
const result = await handleCompleteTask(makeValidParams(), basePath);
|
|
401
|
+
|
|
402
|
+
assertTrue('error' in result, 'projection failure should return an error');
|
|
403
|
+
if ('error' in result) {
|
|
404
|
+
assertMatch(result.error, /projection write failed/, 'error should mention projection write failure');
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const task = getTask('M001', 'S01', 'T01');
|
|
408
|
+
assertTrue(task !== null, 'task row should remain for retry');
|
|
409
|
+
assertEq(task!.status, 'pending', 'task status should be rolled back to pending');
|
|
410
|
+
assertEq(task!.completed_at, null, 'rolled back task should not keep completed_at');
|
|
411
|
+
|
|
412
|
+
const adapter = _getAdapter()!;
|
|
413
|
+
const evRows = adapter.prepare(
|
|
414
|
+
"SELECT * FROM verification_evidence WHERE task_id = 'T01' AND slice_id = 'S01' AND milestone_id = 'M001'"
|
|
415
|
+
).all();
|
|
416
|
+
assertEq(evRows.length, 0, 'verification evidence should be deleted when projection rollback runs');
|
|
417
|
+
|
|
418
|
+
const summaryPath = path.join(path.dirname(planPath), 'T01-SUMMARY.md');
|
|
419
|
+
assertTrue(!fs.existsSync(summaryPath), 'SUMMARY.md should be removed so disk state stays pending');
|
|
420
|
+
|
|
421
|
+
cleanupDir(basePath);
|
|
422
|
+
cleanup(dbPath);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
426
|
+
// complete-task: Handler does not re-render completed sibling summaries
|
|
427
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
428
|
+
|
|
429
|
+
console.log('\n=== complete-task: handler leaves completed sibling summaries untouched ===');
|
|
430
|
+
{
|
|
431
|
+
const dbPath = tempDbPath();
|
|
432
|
+
openDatabase(dbPath);
|
|
433
|
+
|
|
434
|
+
const { basePath, planPath } = createTempProject();
|
|
435
|
+
|
|
436
|
+
insertMilestone({ id: 'M001', title: 'Test Milestone' });
|
|
437
|
+
insertSlice({ id: 'S01', milestoneId: 'M001', title: 'Test Slice', risk: 'high', depends: [], demo: 'basic functionality works', sequence: 1 });
|
|
438
|
+
insertTask({ id: 'T00', sliceId: 'S01', milestoneId: 'M001', status: 'complete', title: 'Already complete task', oneLiner: 'Previously completed' });
|
|
439
|
+
insertTask({ id: 'T02', sliceId: 'S01', milestoneId: 'M001', status: 'pending', title: 'Second task' });
|
|
440
|
+
|
|
441
|
+
const siblingSummaryPath = path.join(path.dirname(planPath), 'T00-SUMMARY.md');
|
|
442
|
+
const siblingSummaryContent = 'existing sibling summary marker\n';
|
|
443
|
+
fs.writeFileSync(siblingSummaryPath, siblingSummaryContent);
|
|
444
|
+
|
|
445
|
+
const result = await handleCompleteTask(makeValidParams(), basePath);
|
|
446
|
+
|
|
447
|
+
assertTrue(!('error' in result), 'handler should succeed without error');
|
|
448
|
+
assertEq(
|
|
449
|
+
fs.readFileSync(siblingSummaryPath, 'utf-8'),
|
|
450
|
+
siblingSummaryContent,
|
|
451
|
+
'complete-task should not re-render summaries for already-completed sibling tasks',
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
cleanupDir(basePath);
|
|
455
|
+
cleanup(dbPath);
|
|
456
|
+
}
|
|
457
|
+
|
|
383
458
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
384
459
|
// complete-task: hard-blocker escalation with mid-execution escalation disabled
|
|
385
460
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
import test from "node:test";
|
|
6
6
|
import assert from "node:assert/strict";
|
|
7
|
-
import { mkdirSync, rmSync } from "node:fs";
|
|
8
|
-
import { join } from "node:path";
|
|
7
|
+
import { chmodSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { delimiter, join } from "node:path";
|
|
9
9
|
import { tmpdir } from "node:os";
|
|
10
|
+
import { performance } from "node:perf_hooks";
|
|
10
11
|
|
|
11
12
|
import { GSDDashboardOverlay } from "../dashboard-overlay.ts";
|
|
12
13
|
import type { UnitMetrics } from "../metrics.ts";
|
|
@@ -85,6 +86,58 @@ test("GSDDashboardOverlay non-identity refresh avoids reparsing preferences", as
|
|
|
85
86
|
);
|
|
86
87
|
});
|
|
87
88
|
|
|
89
|
+
test("GSDDashboardOverlay render and scroll do not run environment doctor subprocesses", (t) => {
|
|
90
|
+
const basePath = join(
|
|
91
|
+
tmpdir(),
|
|
92
|
+
`gsd-dashboard-overlay-env-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
93
|
+
);
|
|
94
|
+
const shimDir = join(
|
|
95
|
+
tmpdir(),
|
|
96
|
+
`gsd-dashboard-overlay-shim-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
97
|
+
);
|
|
98
|
+
mkdirSync(basePath, { recursive: true });
|
|
99
|
+
mkdirSync(shimDir, { recursive: true });
|
|
100
|
+
writeFileSync(join(basePath, "package.json"), JSON.stringify({ engines: { node: ">=22.0.0" } }));
|
|
101
|
+
|
|
102
|
+
const posixNodeShim = join(shimDir, "node");
|
|
103
|
+
writeFileSync(posixNodeShim, "#!/bin/sh\nsleep 1\nexit 1\n");
|
|
104
|
+
chmodSync(posixNodeShim, 0o755);
|
|
105
|
+
writeFileSync(join(shimDir, "node.cmd"), "@echo off\r\nping -n 2 127.0.0.1 > nul\r\nexit /b 1\r\n");
|
|
106
|
+
|
|
107
|
+
const originalPath = process.env.PATH;
|
|
108
|
+
const overlay = new GSDDashboardOverlay({ requestRender() {} }, fakeTheme as any, () => {});
|
|
109
|
+
overlay.dispose();
|
|
110
|
+
|
|
111
|
+
t.after(() => {
|
|
112
|
+
if (originalPath === undefined) {
|
|
113
|
+
delete process.env.PATH;
|
|
114
|
+
} else {
|
|
115
|
+
process.env.PATH = originalPath;
|
|
116
|
+
}
|
|
117
|
+
rmSync(basePath, { recursive: true, force: true });
|
|
118
|
+
rmSync(shimDir, { recursive: true, force: true });
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
(overlay as any).loading = false;
|
|
122
|
+
(overlay as any).milestoneData = null;
|
|
123
|
+
(overlay as any).dashData = {
|
|
124
|
+
...(overlay as any).dashData,
|
|
125
|
+
basePath,
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
process.env.PATH = `${shimDir}${delimiter}${originalPath ?? ""}`;
|
|
129
|
+
const start = performance.now();
|
|
130
|
+
overlay.render(100);
|
|
131
|
+
overlay.handleInput("j");
|
|
132
|
+
overlay.render(100);
|
|
133
|
+
const elapsed = performance.now() - start;
|
|
134
|
+
|
|
135
|
+
assert.ok(
|
|
136
|
+
elapsed < 500,
|
|
137
|
+
`rendering and scrolling should not wait for environment subprocesses, took ${Math.round(elapsed)}ms`,
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
|
|
88
141
|
function makeUnit(id: string, cost: number): UnitMetrics {
|
|
89
142
|
return {
|
|
90
143
|
type: "execute-task",
|
|
@@ -232,7 +232,7 @@ test("dispatch-rule-coverage: planning boundary without planner handoff → rese
|
|
|
232
232
|
assertMatch(
|
|
233
233
|
match,
|
|
234
234
|
{
|
|
235
|
-
ruleName: "planning (no research
|
|
235
|
+
ruleName: "planning (no research) → research-slice",
|
|
236
236
|
action: "dispatch",
|
|
237
237
|
unitType: "research-slice",
|
|
238
238
|
},
|
|
@@ -240,6 +240,31 @@ test("dispatch-rule-coverage: planning boundary without planner handoff → rese
|
|
|
240
240
|
);
|
|
241
241
|
});
|
|
242
242
|
|
|
243
|
+
test("dispatch-rule-coverage: S01 still researches when only milestone research exists", async (t) => {
|
|
244
|
+
const tmp = mkdtempSync(join(tmpdir(), "gsd-disp-cov-s01-research-"));
|
|
245
|
+
t.after(() => rmSync(tmp, { recursive: true, force: true }));
|
|
246
|
+
|
|
247
|
+
writeMilestoneFile(tmp, "M001", "CONTEXT", "# Context\n");
|
|
248
|
+
writeMilestoneFile(tmp, "M001", "ROADMAP", "# Roadmap\n");
|
|
249
|
+
writeMilestoneFile(tmp, "M001", "RESEARCH", "# Milestone Research\n");
|
|
250
|
+
|
|
251
|
+
const state = makeState({
|
|
252
|
+
phase: "planning",
|
|
253
|
+
activeSlice: { id: "S01", title: "First Slice" },
|
|
254
|
+
nextAction: "Plan slice S01 (First Slice).",
|
|
255
|
+
});
|
|
256
|
+
const match = await findFirstMatch(makeCtx(tmp, state));
|
|
257
|
+
assertMatch(
|
|
258
|
+
match,
|
|
259
|
+
{
|
|
260
|
+
ruleName: "planning (no research) → research-slice",
|
|
261
|
+
action: "dispatch",
|
|
262
|
+
unitType: "research-slice",
|
|
263
|
+
},
|
|
264
|
+
"S01 missing slice research despite milestone research",
|
|
265
|
+
);
|
|
266
|
+
});
|
|
267
|
+
|
|
243
268
|
test("dispatch-rule-coverage: executing with task plan present → execute-task", async (t) => {
|
|
244
269
|
const tmp = mkdtempSync(join(tmpdir(), "gsd-disp-cov-exec-"));
|
|
245
270
|
t.after(() => rmSync(tmp, { recursive: true, force: true }));
|
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
_getAdapter,
|
|
9
|
+
closeDatabase,
|
|
10
|
+
getHierarchyCompletionCounts,
|
|
11
|
+
insertMilestone,
|
|
12
|
+
insertSlice,
|
|
13
|
+
insertTask,
|
|
14
|
+
openDatabase,
|
|
15
|
+
} from "../gsd-db.ts";
|
|
8
16
|
import { buildForensicReport } from "../forensics.ts";
|
|
9
17
|
import { handleDoctor } from "../commands-handlers.ts";
|
|
10
18
|
import { withCommandCwd } from "../commands/context.ts";
|
|
@@ -23,6 +31,66 @@ test("#5194 forensics opens DB before computing completion counts", async (t) =>
|
|
|
23
31
|
assert.equal(report.dbCompletionCounts?.tasksTotal, 0);
|
|
24
32
|
});
|
|
25
33
|
|
|
34
|
+
test("#968 completion counts use fixed aggregate queries and canonical closed statuses", (t) => {
|
|
35
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-forensics-counts-"));
|
|
36
|
+
const dbPath = join(base, "gsd.db");
|
|
37
|
+
t.after(() => {
|
|
38
|
+
closeDatabase();
|
|
39
|
+
rmSync(base, { recursive: true, force: true });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
openDatabase(dbPath);
|
|
43
|
+
|
|
44
|
+
insertMilestone({ id: "M001", status: "complete" });
|
|
45
|
+
insertMilestone({ id: "M002", status: "active" });
|
|
46
|
+
insertMilestone({ id: "M003", status: "closed" });
|
|
47
|
+
|
|
48
|
+
insertSlice({ milestoneId: "M001", id: "S01", status: "done" });
|
|
49
|
+
insertSlice({ milestoneId: "M001", id: "S02", status: "pending" });
|
|
50
|
+
insertSlice({ milestoneId: "M002", id: "S01", status: "skipped" });
|
|
51
|
+
insertSlice({ milestoneId: "M003", id: "S01", status: "active" });
|
|
52
|
+
|
|
53
|
+
insertTask({ milestoneId: "M001", sliceId: "S01", id: "T01", status: "complete" });
|
|
54
|
+
insertTask({ milestoneId: "M001", sliceId: "S01", id: "T02", status: "done" });
|
|
55
|
+
insertTask({ milestoneId: "M001", sliceId: "S02", id: "T01", status: "skipped" });
|
|
56
|
+
insertTask({ milestoneId: "M002", sliceId: "S01", id: "T01", status: "closed" });
|
|
57
|
+
insertTask({ milestoneId: "M002", sliceId: "S01", id: "T02", status: "pending" });
|
|
58
|
+
insertTask({ milestoneId: "M003", sliceId: "S01", id: "T01", status: "active" });
|
|
59
|
+
|
|
60
|
+
const adapter = _getAdapter()!;
|
|
61
|
+
const originalPrepare = adapter.prepare.bind(adapter);
|
|
62
|
+
const preparedSql: string[] = [];
|
|
63
|
+
adapter.prepare = (sql) => {
|
|
64
|
+
preparedSql.push(sql);
|
|
65
|
+
return originalPrepare(sql);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
assert.deepEqual(getHierarchyCompletionCounts(), {
|
|
69
|
+
milestones: 2,
|
|
70
|
+
milestonesTotal: 3,
|
|
71
|
+
slices: 2,
|
|
72
|
+
slicesTotal: 4,
|
|
73
|
+
tasks: 4,
|
|
74
|
+
tasksTotal: 6,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
assert.equal(preparedSql.length, 3);
|
|
78
|
+
assert.ok(preparedSql.every((sql) => /COUNT\(\*\)/i.test(sql)));
|
|
79
|
+
assert.doesNotMatch(preparedSql.join("\n"), /SELECT\s+\*/i);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("#968 forensics completion counts do not re-query slices and tasks", () => {
|
|
83
|
+
const source = readFileSync(join(process.cwd(), "src/resources/extensions/gsd/forensics.ts"), "utf-8");
|
|
84
|
+
const start = source.indexOf("function getDbCompletionCounts()");
|
|
85
|
+
const end = source.indexOf("// ─── Anomaly Detectors", start);
|
|
86
|
+
assert.notEqual(start, -1);
|
|
87
|
+
assert.notEqual(end, -1);
|
|
88
|
+
const body = source.slice(start, end);
|
|
89
|
+
|
|
90
|
+
assert.match(body, /getHierarchyCompletionCounts\(\)/);
|
|
91
|
+
assert.doesNotMatch(body, /getAllMilestones|getMilestoneSlices|getSliceTasks/);
|
|
92
|
+
});
|
|
93
|
+
|
|
26
94
|
test("#5194 doctor command does not emit false db_unavailable when gsd.db exists", async (t) => {
|
|
27
95
|
const base = mkdtempSync(join(tmpdir(), "gsd-doctor-db-open-"));
|
|
28
96
|
t.after(() => rmSync(base, { recursive: true, force: true }));
|
|
@@ -20,6 +20,7 @@ import { tmpdir } from "node:os";
|
|
|
20
20
|
|
|
21
21
|
import {
|
|
22
22
|
runProviderChecks,
|
|
23
|
+
runProviderChecksAsync,
|
|
23
24
|
formatProviderReport,
|
|
24
25
|
summariseProviderIssues,
|
|
25
26
|
type ProviderCheckResult,
|
|
@@ -47,6 +48,26 @@ function withEnv(vars: Record<string, string | undefined>, fn: () => void): void
|
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
async function withEnvAsync(vars: Record<string, string | undefined>, fn: () => Promise<void>): Promise<void> {
|
|
52
|
+
const saved: Record<string, string | undefined> = {};
|
|
53
|
+
for (const [k, v] of Object.entries(vars)) {
|
|
54
|
+
saved[k] = process.env[k];
|
|
55
|
+
if (v === undefined) {
|
|
56
|
+
delete process.env[k];
|
|
57
|
+
} else {
|
|
58
|
+
process.env[k] = v;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
await fn();
|
|
63
|
+
} finally {
|
|
64
|
+
for (const [k, v] of Object.entries(saved)) {
|
|
65
|
+
if (v === undefined) delete process.env[k];
|
|
66
|
+
else process.env[k] = v;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
50
71
|
function withCwd(nextCwd: string, fn: () => void): void {
|
|
51
72
|
const saved = process.cwd();
|
|
52
73
|
process.chdir(nextCwd);
|
|
@@ -57,6 +78,22 @@ function withCwd(nextCwd: string, fn: () => void): void {
|
|
|
57
78
|
}
|
|
58
79
|
}
|
|
59
80
|
|
|
81
|
+
async function withCwdAsync(nextCwd: string, fn: () => Promise<void>): Promise<void> {
|
|
82
|
+
const saved = process.cwd();
|
|
83
|
+
process.chdir(nextCwd);
|
|
84
|
+
try {
|
|
85
|
+
await fn();
|
|
86
|
+
} finally {
|
|
87
|
+
process.chdir(saved);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function normalizeProviderResults(results: ProviderCheckResult[]): string[] {
|
|
92
|
+
return results
|
|
93
|
+
.map(r => `${r.name}|${r.label}|${r.category}|${r.status}|${r.message}|${r.detail ?? ""}|${r.required}`)
|
|
94
|
+
.sort();
|
|
95
|
+
}
|
|
96
|
+
|
|
60
97
|
const PRESENT_TEST_VALUE = "configured";
|
|
61
98
|
|
|
62
99
|
// ─── formatProviderReport ─────────────────────────────────────────────────────
|
|
@@ -377,6 +414,76 @@ test("runProviderChecks detects custom provider keys from models.json", () => {
|
|
|
377
414
|
rmSync(tmpHome, { recursive: true, force: true });
|
|
378
415
|
});
|
|
379
416
|
|
|
417
|
+
test("runProviderChecksAsync matches sync for models.json keys and PATH CLI checks", async () => {
|
|
418
|
+
const tmpHome = realpathSync(mkdtempSync(join(tmpdir(), "gsd-providers-async-home-")));
|
|
419
|
+
const repo = realpathSync(mkdtempSync(join(tmpdir(), "gsd-providers-async-repo-")));
|
|
420
|
+
const agentDir = join(tmpHome, ".gsd", "agent");
|
|
421
|
+
const binDir = join(tmpHome, "bin");
|
|
422
|
+
mkdirSync(agentDir, { recursive: true });
|
|
423
|
+
mkdirSync(binDir, { recursive: true });
|
|
424
|
+
mkdirSync(join(repo, ".gsd"), { recursive: true });
|
|
425
|
+
|
|
426
|
+
writeFileSync(
|
|
427
|
+
join(repo, ".gsd", "PREFERENCES.md"),
|
|
428
|
+
[
|
|
429
|
+
"---",
|
|
430
|
+
"models:",
|
|
431
|
+
" execution:",
|
|
432
|
+
" model: custom-model",
|
|
433
|
+
" provider: custom-provider",
|
|
434
|
+
" validation:",
|
|
435
|
+
" model: gemini-2.5-pro",
|
|
436
|
+
" provider: google-gemini-cli",
|
|
437
|
+
"---",
|
|
438
|
+
"",
|
|
439
|
+
].join("\n"),
|
|
440
|
+
);
|
|
441
|
+
writeFileSync(join(agentDir, "models.json"), JSON.stringify({
|
|
442
|
+
providers: {
|
|
443
|
+
"custom-provider": {
|
|
444
|
+
api: "openai-completions",
|
|
445
|
+
apiKey: "x",
|
|
446
|
+
baseUrl: "https://example.invalid/v1",
|
|
447
|
+
models: [{ id: "custom-model", name: "Custom Model" }],
|
|
448
|
+
},
|
|
449
|
+
},
|
|
450
|
+
}));
|
|
451
|
+
|
|
452
|
+
const fakeGemini = join(binDir, "gemini");
|
|
453
|
+
writeFileSync(fakeGemini, "#!/bin/sh\necho mock\n");
|
|
454
|
+
chmodSync(fakeGemini, 0o755);
|
|
455
|
+
|
|
456
|
+
try {
|
|
457
|
+
await withEnvAsync({
|
|
458
|
+
HOME: tmpHome,
|
|
459
|
+
CUSTOM_PROVIDER_API_KEY: undefined,
|
|
460
|
+
GEMINI_API_KEY: undefined,
|
|
461
|
+
GOOGLE_API_KEY: undefined,
|
|
462
|
+
PATH: binDir,
|
|
463
|
+
}, async () => {
|
|
464
|
+
await withCwdAsync(repo, async () => {
|
|
465
|
+
const syncResults = runProviderChecks();
|
|
466
|
+
const asyncResults = await runProviderChecksAsync();
|
|
467
|
+
|
|
468
|
+
assert.deepEqual(normalizeProviderResults(asyncResults), normalizeProviderResults(syncResults));
|
|
469
|
+
assert.equal(
|
|
470
|
+
asyncResults.find(r => r.name === "custom-provider")?.status,
|
|
471
|
+
"ok",
|
|
472
|
+
"models.json apiKey should satisfy custom provider auth",
|
|
473
|
+
);
|
|
474
|
+
assert.equal(
|
|
475
|
+
asyncResults.find(r => r.name === "google-gemini-cli")?.status,
|
|
476
|
+
"ok",
|
|
477
|
+
"gemini CLI binary should satisfy explicit CLI provider",
|
|
478
|
+
);
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
} finally {
|
|
482
|
+
rmSync(repo, { recursive: true, force: true });
|
|
483
|
+
rmSync(tmpHome, { recursive: true, force: true });
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
|
|
380
487
|
test("runProviderChecks reports missing custom provider key without models.json apiKey", () => {
|
|
381
488
|
const tmpHome = realpathSync(mkdtempSync(join(tmpdir(), "gsd-providers-custom-missing-home-")));
|
|
382
489
|
const repo = realpathSync(mkdtempSync(join(tmpdir(), "gsd-providers-custom-missing-repo-")));
|
|
@@ -63,6 +63,44 @@ function wallClockGuard<T>(promise: Promise<T>, ms: number): Promise<T> {
|
|
|
63
63
|
]);
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
test(
|
|
67
|
+
'runExecSandbox: aborted signal terminates promptly without waiting for timeout',
|
|
68
|
+
{ timeout: 10_000 },
|
|
69
|
+
async (t) => {
|
|
70
|
+
const base = freshBase();
|
|
71
|
+
t.after(() => cleanup(base));
|
|
72
|
+
|
|
73
|
+
const controller = new AbortController();
|
|
74
|
+
controller.abort();
|
|
75
|
+
const TIMEOUT_MS = 10_000;
|
|
76
|
+
|
|
77
|
+
const start = Date.now();
|
|
78
|
+
const result = await wallClockGuard(
|
|
79
|
+
runExecSandbox(
|
|
80
|
+
{ runtime: 'node', script: 'setTimeout(() => {}, 30_000);', timeout_ms: TIMEOUT_MS },
|
|
81
|
+
baseOpts(base, {
|
|
82
|
+
signal: controller.signal,
|
|
83
|
+
kill_grace_ms: 500,
|
|
84
|
+
force_resolve_delay_ms: 2_000,
|
|
85
|
+
}),
|
|
86
|
+
),
|
|
87
|
+
5_000,
|
|
88
|
+
);
|
|
89
|
+
const elapsed = Date.now() - start;
|
|
90
|
+
|
|
91
|
+
assert.equal(result.aborted, true, 'aborted signal must be recorded on the sandbox result');
|
|
92
|
+
assert.equal(result.timed_out, false, 'client abort must not be reported as a sandbox timeout');
|
|
93
|
+
assert.ok(elapsed < TIMEOUT_MS, `expected abort before ${TIMEOUT_MS}ms timeout, took ${elapsed}ms`);
|
|
94
|
+
assert.ok(
|
|
95
|
+
result.duration_ms < TIMEOUT_MS,
|
|
96
|
+
`expected result duration before timeout, got ${result.duration_ms}ms`,
|
|
97
|
+
);
|
|
98
|
+
const meta = JSON.parse(readFileSync(result.meta_path, 'utf-8'));
|
|
99
|
+
assert.equal(meta.aborted, true, 'meta.json must persist the aborted marker');
|
|
100
|
+
assert.equal(meta.timed_out, false, 'meta.json must not report timed_out when abort caused the kill');
|
|
101
|
+
},
|
|
102
|
+
);
|
|
103
|
+
|
|
66
104
|
// Clean up a detached child (and its process group) that a large kill_grace_ms
|
|
67
105
|
// intentionally leaves alive past the force-resolve deadline.
|
|
68
106
|
function cleanupByPidFile(pidFile: string): void {
|
|
@@ -3,7 +3,7 @@ import assert from "node:assert/strict";
|
|
|
3
3
|
|
|
4
4
|
import { registerExecTools } from "../bootstrap/exec-tools.ts";
|
|
5
5
|
import { executeGsdExec, executeUatExec } from "../tools/exec-tool.ts";
|
|
6
|
-
import type { ExecSandboxRequest, ExecSandboxResult } from "../exec-sandbox.ts";
|
|
6
|
+
import type { ExecSandboxOptions, ExecSandboxRequest, ExecSandboxResult } from "../exec-sandbox.ts";
|
|
7
7
|
|
|
8
8
|
function makeExecResult(request: ExecSandboxRequest): ExecSandboxResult {
|
|
9
9
|
return {
|
|
@@ -12,6 +12,7 @@ function makeExecResult(request: ExecSandboxRequest): ExecSandboxResult {
|
|
|
12
12
|
exit_code: 0,
|
|
13
13
|
signal: null,
|
|
14
14
|
timed_out: false,
|
|
15
|
+
aborted: false,
|
|
15
16
|
force_resolved: false,
|
|
16
17
|
duration_ms: 1,
|
|
17
18
|
stdout_bytes: 12,
|
|
@@ -52,6 +53,49 @@ test("executeUatExec accepts evidence-mode aliases for intent", async () => {
|
|
|
52
53
|
assert.equal(requests[0]?.metadata?.intent, "uat-artifact-check");
|
|
53
54
|
});
|
|
54
55
|
|
|
56
|
+
test("executeGsdExec passes AbortSignal into sandbox options", async () => {
|
|
57
|
+
const controller = new AbortController();
|
|
58
|
+
let capturedSignal: AbortSignal | undefined;
|
|
59
|
+
|
|
60
|
+
const result = await executeGsdExec(
|
|
61
|
+
{ runtime: "bash", script: "sleep 60" },
|
|
62
|
+
{
|
|
63
|
+
baseDir: "/tmp/gsd-exec-abort-signal-test",
|
|
64
|
+
preferences: null,
|
|
65
|
+
signal: controller.signal,
|
|
66
|
+
run: async (request, opts: ExecSandboxOptions) => {
|
|
67
|
+
capturedSignal = opts.signal;
|
|
68
|
+
return makeExecResult(request);
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
assert.equal(result.isError, false);
|
|
74
|
+
assert.equal(capturedSignal, controller.signal);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("gsd_exec surfaces aborted child termination distinctly from a clean exit", async () => {
|
|
78
|
+
const result = await executeGsdExec(
|
|
79
|
+
{ runtime: "bash", script: "trap 'exit 0' TERM; sleep 60" },
|
|
80
|
+
{
|
|
81
|
+
baseDir: "/tmp/gsd-exec-aborted-test",
|
|
82
|
+
preferences: null,
|
|
83
|
+
run: async (request) => ({
|
|
84
|
+
...makeExecResult(request),
|
|
85
|
+
aborted: true,
|
|
86
|
+
exit_code: 0,
|
|
87
|
+
signal: null,
|
|
88
|
+
digest: "[no stdout — aborted]",
|
|
89
|
+
}),
|
|
90
|
+
},
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
assert.equal(result.isError, true, "an aborted run must be an error even if the child exits 0");
|
|
94
|
+
assert.equal(result.details?.aborted, true, "details must expose aborted");
|
|
95
|
+
const text = result.content.map((c) => c.text ?? "").join("\n");
|
|
96
|
+
assert.match(text, /exit=aborted/, "summary must distinguish an aborted result");
|
|
97
|
+
});
|
|
98
|
+
|
|
55
99
|
test("gsd_exec surfaces a force-resolved (D-state) kill distinctly from a clean exit", async () => {
|
|
56
100
|
// A hard-deadline force-resolve sets force_resolved=true with a synthetic SIGKILL
|
|
57
101
|
// signal and null exit code. The tool result must carry that flag in details and
|