gsd-pi 2.80.0-dev.cf9433f56 → 2.80.0-dev.d4fc28e6b
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/cli.js +0 -19
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +29 -0
- package/dist/resources/extensions/gsd/auto/loop.js +71 -8
- package/dist/resources/extensions/gsd/auto/phases.js +150 -94
- package/dist/resources/extensions/gsd/auto/resolve.js +12 -0
- package/dist/resources/extensions/gsd/auto/run-unit.js +10 -30
- package/dist/resources/extensions/gsd/auto/session.js +8 -0
- package/dist/resources/extensions/gsd/auto/workflow-dispatch-claim.js +33 -1
- package/dist/resources/extensions/gsd/auto/workflow-worker-heartbeat.js +9 -1
- package/dist/resources/extensions/gsd/auto-direct-dispatch.js +5 -32
- package/dist/resources/extensions/gsd/auto-dispatch.js +16 -0
- package/dist/resources/extensions/gsd/auto-post-unit.js +17 -4
- package/dist/resources/extensions/gsd/auto-prompts.js +90 -15
- package/dist/resources/extensions/gsd/auto-start.js +197 -6
- package/dist/resources/extensions/gsd/auto-worktree.js +111 -1
- package/dist/resources/extensions/gsd/auto.js +18 -22
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +86 -19
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +49 -36
- package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +15 -5
- package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +9 -3
- package/dist/resources/extensions/gsd/bootstrap/journal-tools.js +7 -1
- package/dist/resources/extensions/gsd/bootstrap/memory-tools.js +9 -3
- package/dist/resources/extensions/gsd/bootstrap/query-tools.js +8 -2
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +298 -54
- package/dist/resources/extensions/gsd/bootstrap/system-context.js +82 -23
- package/dist/resources/extensions/gsd/clean-root-preflight.js +24 -6
- package/dist/resources/extensions/gsd/commands-handlers.js +23 -9
- package/dist/resources/extensions/gsd/db/unit-dispatches.js +53 -0
- package/dist/resources/extensions/gsd/ecosystem/gsd-extension-api.js +2 -0
- package/dist/resources/extensions/gsd/guided-flow.js +47 -28
- package/dist/resources/extensions/gsd/native-git-bridge.js +32 -8
- package/dist/resources/extensions/gsd/orphan-stash-audit.js +101 -0
- package/dist/resources/extensions/gsd/parallel-orchestrator.js +13 -3
- package/dist/resources/extensions/gsd/pre-execution-checks.js +15 -0
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +2 -0
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/execute-task.md +4 -2
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/replan-slice.md +2 -2
- package/dist/resources/extensions/gsd/workflow-protocol.js +131 -0
- package/dist/resources/extensions/gsd/worktree-resolver.js +35 -4
- 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 +12 -12
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- 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/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 +12 -12
- package/dist/web/standalone/.next/server/middleware-build-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/welcome-screen.d.ts +2 -0
- package/dist/welcome-screen.js +9 -7
- package/package.json +1 -1
- package/packages/pi-agent-core/dist/agent-loop.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/agent-loop.js +4 -1
- package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
- package/packages/pi-agent-core/dist/agent.d.ts +5 -0
- package/packages/pi-agent-core/dist/agent.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/agent.js +2 -0
- package/packages/pi-agent-core/dist/agent.js.map +1 -1
- package/packages/pi-agent-core/dist/index.d.ts +1 -0
- package/packages/pi-agent-core/dist/index.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/index.js +2 -0
- package/packages/pi-agent-core/dist/index.js.map +1 -1
- package/packages/pi-agent-core/dist/token-audit.d.ts +47 -0
- package/packages/pi-agent-core/dist/token-audit.d.ts.map +1 -0
- package/packages/pi-agent-core/dist/token-audit.js +221 -0
- package/packages/pi-agent-core/dist/token-audit.js.map +1 -0
- package/packages/pi-agent-core/dist/types.d.ts +9 -0
- package/packages/pi-agent-core/dist/types.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/types.js.map +1 -1
- package/packages/pi-agent-core/src/agent-loop.test.ts +128 -0
- package/packages/pi-agent-core/src/agent-loop.ts +4 -1
- package/packages/pi-agent-core/src/agent.ts +8 -0
- package/packages/pi-agent-core/src/index.ts +2 -0
- package/packages/pi-agent-core/src/token-audit.test.ts +189 -0
- package/packages/pi-agent-core/src/token-audit.ts +287 -0
- package/packages/pi-agent-core/src/types.ts +14 -0
- package/packages/pi-agent-core/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session-tool-refresh.test.js +18 -0
- package/packages/pi-coding-agent/dist/core/agent-session-tool-refresh.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts +12 -0
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +36 -7
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.js +8 -0
- package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.js +3 -6
- package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.test.js +3 -3
- package/packages/pi-coding-agent/dist/core/extensions/runner.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +32 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/hooks-runner.test.js +2 -0
- package/packages/pi-coding-agent/dist/core/hooks-runner.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk-tool-filter.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/sdk-tool-filter.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/sdk-tool-filter.test.js +46 -0
- package/packages/pi-coding-agent/dist/core/sdk-tool-filter.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/sdk.d.ts +10 -2
- package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk.js +74 -2
- package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/skill-tool.test.js +22 -0
- package/packages/pi-coding-agent/dist/core/skill-tool.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.d.ts +6 -7
- package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.js +2 -3
- package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
- package/packages/pi-coding-agent/src/core/agent-session-tool-refresh.test.ts +25 -0
- package/packages/pi-coding-agent/src/core/agent-session.ts +40 -7
- package/packages/pi-coding-agent/src/core/extensions/loader.ts +10 -0
- package/packages/pi-coding-agent/src/core/extensions/runner.test.ts +3 -3
- package/packages/pi-coding-agent/src/core/extensions/runner.ts +5 -5
- package/packages/pi-coding-agent/src/core/extensions/types.ts +35 -1
- package/packages/pi-coding-agent/src/core/hooks-runner.test.ts +2 -0
- package/packages/pi-coding-agent/src/core/sdk-tool-filter.test.ts +60 -0
- package/packages/pi-coding-agent/src/core/sdk.ts +85 -3
- package/packages/pi-coding-agent/src/core/skill-tool.test.ts +28 -0
- package/packages/pi-coding-agent/src/core/system-prompt.ts +8 -10
- package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +30 -0
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +26 -0
- package/src/resources/extensions/gsd/auto/loop-deps.ts +2 -2
- package/src/resources/extensions/gsd/auto/loop.ts +84 -8
- package/src/resources/extensions/gsd/auto/phases.ts +218 -154
- package/src/resources/extensions/gsd/auto/resolve.ts +19 -0
- package/src/resources/extensions/gsd/auto/run-unit.ts +10 -29
- package/src/resources/extensions/gsd/auto/session.ts +8 -0
- package/src/resources/extensions/gsd/auto/workflow-dispatch-claim.ts +63 -1
- package/src/resources/extensions/gsd/auto/workflow-worker-heartbeat.ts +14 -1
- package/src/resources/extensions/gsd/auto-direct-dispatch.ts +8 -34
- package/src/resources/extensions/gsd/auto-dispatch.ts +16 -0
- package/src/resources/extensions/gsd/auto-post-unit.ts +18 -4
- package/src/resources/extensions/gsd/auto-prompts.ts +95 -14
- package/src/resources/extensions/gsd/auto-start.ts +230 -9
- package/src/resources/extensions/gsd/auto-worktree.ts +123 -0
- package/src/resources/extensions/gsd/auto.ts +18 -18
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +100 -18
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +50 -36
- package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +16 -5
- package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +10 -3
- package/src/resources/extensions/gsd/bootstrap/journal-tools.ts +8 -1
- package/src/resources/extensions/gsd/bootstrap/memory-tools.ts +10 -3
- package/src/resources/extensions/gsd/bootstrap/query-tools.ts +9 -2
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +347 -54
- package/src/resources/extensions/gsd/bootstrap/system-context.ts +90 -22
- package/src/resources/extensions/gsd/clean-root-preflight.ts +32 -7
- package/src/resources/extensions/gsd/commands-handlers.ts +34 -15
- package/src/resources/extensions/gsd/db/unit-dispatches.ts +66 -0
- package/src/resources/extensions/gsd/ecosystem/gsd-extension-api.ts +3 -0
- package/src/resources/extensions/gsd/guided-flow.ts +52 -35
- package/src/resources/extensions/gsd/native-git-bridge.ts +39 -6
- package/src/resources/extensions/gsd/orphan-stash-audit.ts +117 -0
- package/src/resources/extensions/gsd/parallel-orchestrator.ts +13 -3
- package/src/resources/extensions/gsd/pre-execution-checks.ts +16 -0
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +2 -0
- package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/execute-task.md +4 -2
- package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/replan-slice.md +2 -2
- package/src/resources/extensions/gsd/tests/artifact-retry-cap.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +361 -10
- package/src/resources/extensions/gsd/tests/auto-wrapup-inflight-guard.test.ts +168 -6
- package/src/resources/extensions/gsd/tests/clean-root-preflight.test.ts +15 -6
- package/src/resources/extensions/gsd/tests/complete-milestone-excerpt.test.ts +31 -0
- package/src/resources/extensions/gsd/tests/complete-slice-composer.test.ts +3 -2
- package/src/resources/extensions/gsd/tests/context-store.test.ts +7 -1
- package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +5 -1
- package/src/resources/extensions/gsd/tests/execute-task-rendering.test.ts +5 -2
- package/src/resources/extensions/gsd/tests/fast-forward-reused-milestone-branch.test.ts +219 -0
- package/src/resources/extensions/gsd/tests/finalize-survivor-branch.test.ts +132 -0
- package/src/resources/extensions/gsd/tests/isolation-none-branch-guard.test.ts +6 -3
- package/src/resources/extensions/gsd/tests/journal-integration.test.ts +5 -1
- package/src/resources/extensions/gsd/tests/journal-query-tool.test.ts +32 -0
- package/src/resources/extensions/gsd/tests/knowledge.test.ts +47 -0
- package/src/resources/extensions/gsd/tests/merge-conflict-stops-loop.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/milestone-merge-stash-restore.test.ts +242 -0
- package/src/resources/extensions/gsd/tests/native-git-bridge-exec-fallback.test.ts +34 -2
- package/src/resources/extensions/gsd/tests/originalbase-path-comparison.test.ts +3 -0
- package/src/resources/extensions/gsd/tests/orphan-merge-bootstrap.test.ts +133 -0
- package/src/resources/extensions/gsd/tests/orphan-stash-audit.test.ts +201 -0
- package/src/resources/extensions/gsd/tests/parallel-orchestrator-fast-forward.test.ts +113 -0
- package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +7 -5
- package/src/resources/extensions/gsd/tests/prompt-duplication-cuts.test.ts +230 -0
- package/src/resources/extensions/gsd/tests/query-tools-db-open.test.ts +3 -3
- package/src/resources/extensions/gsd/tests/restore-tools-after-discuss.test.ts +38 -17
- package/src/resources/extensions/gsd/tests/select-resumable-milestone.test.ts +96 -0
- package/src/resources/extensions/gsd/tests/session-start-footer.test.ts +77 -0
- package/src/resources/extensions/gsd/tests/session-switch-abort-misclassification.test.ts +166 -0
- package/src/resources/extensions/gsd/tests/state-corruption-2945.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/system-context-memory.test.ts +112 -0
- package/src/resources/extensions/gsd/tests/system-context-message-routing.test.ts +7 -9
- package/src/resources/extensions/gsd/tests/token-tool-gating.test.ts +291 -0
- package/src/resources/extensions/gsd/tests/unit-dispatches.test.ts +50 -1
- package/src/resources/extensions/gsd/tests/unstructured-continue-context-injection.test.ts +5 -4
- package/src/resources/extensions/gsd/tests/workflow-dispatch-claim.test.ts +142 -0
- package/src/resources/extensions/gsd/tests/workflow-protocol-excerpt.test.ts +99 -0
- package/src/resources/extensions/gsd/tests/workflow-worker-heartbeat.test.ts +32 -1
- package/src/resources/extensions/gsd/tests/worktree-journal-events.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/worktree-path-injection.test.ts +22 -19
- package/src/resources/extensions/gsd/tests/worktree-project-root-degrade.test.ts +66 -0
- package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +104 -3
- package/src/resources/extensions/gsd/workflow-protocol.ts +160 -0
- package/src/resources/extensions/gsd/worktree-resolver.ts +49 -4
- package/src/resources/extensions/gsd/tests/phases-merge-error-stops-auto.test.ts +0 -97
- /package/dist/web/standalone/.next/static/{-5nHJWzSdG-WkPMul_khA → cWaxzf-sdbSSbbwYu8q7a}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{-5nHJWzSdG-WkPMul_khA → cWaxzf-sdbSSbbwYu8q7a}/_ssgManifest.js +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
// GSD-2
|
|
1
|
+
// Project/App: GSD-2
|
|
2
|
+
// File Purpose: Regression test for unstructured continue task-context injection.
|
|
2
3
|
// Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -55,12 +56,12 @@ describe("#3615 — structural: fallback exists with correct guards", () => {
|
|
|
55
56
|
);
|
|
56
57
|
});
|
|
57
58
|
|
|
58
|
-
test("fallback is intent-gated via
|
|
59
|
+
test("fallback is intent-gated via isLowEntropyResumePrompt", () => {
|
|
59
60
|
const afterFallback = fnBody.indexOf("// Fallback:");
|
|
60
61
|
const fallbackSection = fnBody.slice(afterFallback);
|
|
61
62
|
assert.ok(
|
|
62
|
-
fallbackSection.includes("
|
|
63
|
-
"fallback must check
|
|
63
|
+
fallbackSection.includes("isLowEntropyResumePrompt(prompt)"),
|
|
64
|
+
"fallback must check isLowEntropyResumePrompt before deriveState",
|
|
64
65
|
);
|
|
65
66
|
});
|
|
66
67
|
|
|
@@ -7,7 +7,9 @@ import test from "node:test";
|
|
|
7
7
|
import type { AutoSession } from "../auto/session.ts";
|
|
8
8
|
import type { IterationData } from "../auto/types.ts";
|
|
9
9
|
import {
|
|
10
|
+
ensureDispatchLease,
|
|
10
11
|
openDispatchClaim,
|
|
12
|
+
type EnsureDispatchLeaseDeps,
|
|
11
13
|
type OpenDispatchClaimDeps,
|
|
12
14
|
} from "../auto/workflow-dispatch-claim.ts";
|
|
13
15
|
|
|
@@ -49,6 +51,25 @@ function makeDeps(overrides?: Partial<OpenDispatchClaimDeps>): OpenDispatchClaim
|
|
|
49
51
|
};
|
|
50
52
|
}
|
|
51
53
|
|
|
54
|
+
function makeLeaseDeps(overrides?: Partial<EnsureDispatchLeaseDeps>): {
|
|
55
|
+
deps: EnsureDispatchLeaseDeps;
|
|
56
|
+
calls: unknown[];
|
|
57
|
+
failures: unknown[];
|
|
58
|
+
} {
|
|
59
|
+
const calls: unknown[] = [];
|
|
60
|
+
const failures: unknown[] = [];
|
|
61
|
+
const deps: EnsureDispatchLeaseDeps = {
|
|
62
|
+
claimMilestoneLease: (workerId, milestoneId) => {
|
|
63
|
+
calls.push(["claim", workerId, milestoneId]);
|
|
64
|
+
return { ok: true, token: 8, expiresAt: "2030-01-01T00:00:00.000Z" };
|
|
65
|
+
},
|
|
66
|
+
logLeaseRecovered: details => calls.push(["recovered", details]),
|
|
67
|
+
logLeaseRecoveryFailed: details => failures.push(details),
|
|
68
|
+
...overrides,
|
|
69
|
+
};
|
|
70
|
+
return { deps, calls, failures };
|
|
71
|
+
}
|
|
72
|
+
|
|
52
73
|
test("openDispatchClaim degrades when worker identity or lease token is missing", () => {
|
|
53
74
|
assert.deepEqual(
|
|
54
75
|
openDispatchClaim(makeSession({ workerId: null }), "flow", "turn", makeIterationData(), makeDeps({
|
|
@@ -156,3 +177,124 @@ test("openDispatchClaim degrades on claim write failures", () => {
|
|
|
156
177
|
assert.deepEqual(outcome, { kind: "degraded" });
|
|
157
178
|
assert.deepEqual(logged, [writeError]);
|
|
158
179
|
});
|
|
180
|
+
|
|
181
|
+
test("ensureDispatchLease degrades without worker identity or milestone id", () => {
|
|
182
|
+
const { deps, calls } = makeLeaseDeps({
|
|
183
|
+
claimMilestoneLease: () => assert.fail("claimMilestoneLease should not be called"),
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
assert.deepEqual(
|
|
187
|
+
ensureDispatchLease(makeSession({ workerId: null }), "M001", deps),
|
|
188
|
+
{ kind: "degraded", reason: "missing-worker" },
|
|
189
|
+
);
|
|
190
|
+
assert.deepEqual(
|
|
191
|
+
ensureDispatchLease(makeSession(), undefined, deps),
|
|
192
|
+
{ kind: "degraded", reason: "missing-milestone" },
|
|
193
|
+
);
|
|
194
|
+
assert.deepEqual(calls, []);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("ensureDispatchLease reuses an existing numeric token", () => {
|
|
198
|
+
const { deps, calls } = makeLeaseDeps({
|
|
199
|
+
claimMilestoneLease: () => assert.fail("claimMilestoneLease should not be called"),
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const session = makeSession({ milestoneLeaseToken: 7 });
|
|
203
|
+
const outcome = ensureDispatchLease(session, "M001", deps);
|
|
204
|
+
|
|
205
|
+
assert.deepEqual(outcome, { kind: "ready", token: 7, recovered: false });
|
|
206
|
+
assert.equal(session.milestoneLeaseToken, 7);
|
|
207
|
+
assert.deepEqual(calls, []);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("ensureDispatchLease claims a lease when the session has no token", () => {
|
|
211
|
+
const { deps, calls, failures } = makeLeaseDeps();
|
|
212
|
+
const session = makeSession({
|
|
213
|
+
currentMilestoneId: "M001",
|
|
214
|
+
milestoneLeaseToken: null,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const outcome = ensureDispatchLease(session, "M001", deps);
|
|
218
|
+
|
|
219
|
+
assert.deepEqual(outcome, { kind: "ready", token: 8, recovered: false });
|
|
220
|
+
assert.equal(session.currentMilestoneId, "M001");
|
|
221
|
+
assert.equal(session.milestoneLeaseToken, 8);
|
|
222
|
+
assert.deepEqual(calls, [
|
|
223
|
+
["claim", "worker-1", "M001"],
|
|
224
|
+
["recovered", {
|
|
225
|
+
milestoneId: "M001",
|
|
226
|
+
workerId: "worker-1",
|
|
227
|
+
token: 8,
|
|
228
|
+
recovered: false,
|
|
229
|
+
}],
|
|
230
|
+
]);
|
|
231
|
+
assert.deepEqual(failures, []);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("ensureDispatchLease force-reclaims after a stale dispatch claim", () => {
|
|
235
|
+
const { deps, calls } = makeLeaseDeps({
|
|
236
|
+
claimMilestoneLease: (workerId, milestoneId) => {
|
|
237
|
+
calls.push(["claim", workerId, milestoneId]);
|
|
238
|
+
return { ok: true, token: 9, expiresAt: "2030-01-01T00:00:00.000Z" };
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
const session = makeSession({ milestoneLeaseToken: 7 });
|
|
242
|
+
|
|
243
|
+
const outcome = ensureDispatchLease(session, "M001", deps, { forceReclaim: true });
|
|
244
|
+
|
|
245
|
+
assert.deepEqual(outcome, { kind: "ready", token: 9, recovered: true });
|
|
246
|
+
assert.equal(session.milestoneLeaseToken, 9);
|
|
247
|
+
assert.deepEqual(calls, [
|
|
248
|
+
["claim", "worker-1", "M001"],
|
|
249
|
+
["recovered", {
|
|
250
|
+
milestoneId: "M001",
|
|
251
|
+
workerId: "worker-1",
|
|
252
|
+
token: 9,
|
|
253
|
+
recovered: true,
|
|
254
|
+
}],
|
|
255
|
+
]);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("ensureDispatchLease blocks when another worker holds the lease", () => {
|
|
259
|
+
const { deps, failures } = makeLeaseDeps({
|
|
260
|
+
claimMilestoneLease: () => ({
|
|
261
|
+
ok: false,
|
|
262
|
+
error: "held_by",
|
|
263
|
+
byWorker: "worker-2",
|
|
264
|
+
expiresAt: "2030-01-01T00:00:00.000Z",
|
|
265
|
+
}),
|
|
266
|
+
});
|
|
267
|
+
const session = makeSession({ milestoneLeaseToken: null });
|
|
268
|
+
|
|
269
|
+
const outcome = ensureDispatchLease(session, "M001", deps);
|
|
270
|
+
|
|
271
|
+
assert.deepEqual(outcome, {
|
|
272
|
+
kind: "blocked",
|
|
273
|
+
reason: "Milestone M001 is held by worker worker-2 until 2030-01-01T00:00:00.000Z.",
|
|
274
|
+
});
|
|
275
|
+
assert.equal(session.milestoneLeaseToken, null);
|
|
276
|
+
assert.deepEqual(failures, [{
|
|
277
|
+
milestoneId: "M001",
|
|
278
|
+
workerId: "worker-1",
|
|
279
|
+
reason: "Milestone M001 is held by worker worker-2 until 2030-01-01T00:00:00.000Z.",
|
|
280
|
+
}]);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test("ensureDispatchLease fails closed on claim errors", () => {
|
|
284
|
+
const { deps, failures } = makeLeaseDeps({
|
|
285
|
+
claimMilestoneLease: () => {
|
|
286
|
+
throw new Error("db unavailable");
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
const session = makeSession({ milestoneLeaseToken: null });
|
|
290
|
+
|
|
291
|
+
const outcome = ensureDispatchLease(session, "M001", deps);
|
|
292
|
+
|
|
293
|
+
assert.deepEqual(outcome, { kind: "failed", reason: "db unavailable" });
|
|
294
|
+
assert.equal(session.milestoneLeaseToken, null);
|
|
295
|
+
assert.deepEqual(failures, [{
|
|
296
|
+
milestoneId: "M001",
|
|
297
|
+
workerId: "worker-1",
|
|
298
|
+
reason: "db unavailable",
|
|
299
|
+
}]);
|
|
300
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Project/App: GSD-2
|
|
2
|
+
// File Purpose: Tests for capped GSD workflow protocol and doctor-heal payload helpers.
|
|
3
|
+
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
import assert from "node:assert/strict";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
buildDoctorHealIssuePayload,
|
|
9
|
+
buildDoctorHealSummary,
|
|
10
|
+
buildWorkflowDispatchContent,
|
|
11
|
+
buildWorkflowProtocolExcerpt,
|
|
12
|
+
} from "../workflow-protocol.ts";
|
|
13
|
+
|
|
14
|
+
test("workflow protocol helper emits capped excerpt plus source path", () => {
|
|
15
|
+
const workflow = `# Protocol\n${"FULL_WORKFLOW_BODY ".repeat(500)}`;
|
|
16
|
+
const excerpt = buildWorkflowProtocolExcerpt(workflow, "/tmp/GSD-WORKFLOW.md", { maxChars: 1200 });
|
|
17
|
+
|
|
18
|
+
assert.match(excerpt, /Source: `\/tmp\/GSD-WORKFLOW\.md`/);
|
|
19
|
+
assert.match(excerpt, /\[Workflow Protocol Truncated\]/);
|
|
20
|
+
assert.ok(excerpt.length < workflow.length);
|
|
21
|
+
assert.ok(excerpt.length < 1600);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("workflow dispatch uses excerpt instead of full workflow body", () => {
|
|
25
|
+
const workflow = `# Protocol\n${"FULL_WORKFLOW_BODY ".repeat(500)}`;
|
|
26
|
+
const content = buildWorkflowDispatchContent({
|
|
27
|
+
workflow,
|
|
28
|
+
workflowPath: "/tmp/GSD-WORKFLOW.md",
|
|
29
|
+
task: "Run the selected unit.",
|
|
30
|
+
maxProtocolChars: 1200,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
assert.match(content, /## GSD Workflow Protocol Excerpt/);
|
|
34
|
+
assert.match(content, /## Your Task/);
|
|
35
|
+
assert.match(content, /Run the selected unit/);
|
|
36
|
+
assert.ok(content.length < workflow.length);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("workflow protocol excerpt includes late verification and advance rules", () => {
|
|
40
|
+
const workflow = [
|
|
41
|
+
"# GSD Workflow",
|
|
42
|
+
"intro",
|
|
43
|
+
"## Quick Start",
|
|
44
|
+
"quick",
|
|
45
|
+
"## File Format Reference",
|
|
46
|
+
"format ".repeat(400),
|
|
47
|
+
"## The Phases",
|
|
48
|
+
"phase overview",
|
|
49
|
+
"### Phase 4: Execute",
|
|
50
|
+
"execute rules",
|
|
51
|
+
"### Phase 5: Verify",
|
|
52
|
+
"verification rules",
|
|
53
|
+
"### Phase 7: Advance",
|
|
54
|
+
"advance rules",
|
|
55
|
+
].join("\n");
|
|
56
|
+
|
|
57
|
+
const excerpt = buildWorkflowProtocolExcerpt(workflow, "/tmp/GSD-WORKFLOW.md", { maxChars: 1300 });
|
|
58
|
+
|
|
59
|
+
assert.match(excerpt, /Quick Start/);
|
|
60
|
+
assert.match(excerpt, /Phase 5: Verify/);
|
|
61
|
+
assert.match(excerpt, /Phase 7: Advance/);
|
|
62
|
+
assert.doesNotMatch(excerpt, /format format format format format/);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("doctor heal summary omits duplicated full report body", () => {
|
|
66
|
+
const report = [
|
|
67
|
+
"# GSD doctor heal prep.",
|
|
68
|
+
"Scope: M001",
|
|
69
|
+
"Status: warning",
|
|
70
|
+
"Warnings: 9",
|
|
71
|
+
"",
|
|
72
|
+
"VERY_LONG_FULL_REPORT_BODY ".repeat(300),
|
|
73
|
+
].join("\n");
|
|
74
|
+
|
|
75
|
+
const summary = buildDoctorHealSummary(report, { maxChars: 900 });
|
|
76
|
+
|
|
77
|
+
assert.match(summary, /GSD doctor heal prep/);
|
|
78
|
+
assert.match(summary, /Warnings: 9/);
|
|
79
|
+
assert.ok(summary.length <= 900);
|
|
80
|
+
assert.doesNotMatch(summary, /VERY_LONG_FULL_REPORT_BODY VERY_LONG_FULL_REPORT_BODY VERY_LONG_FULL_REPORT_BODY/);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("doctor heal issue payload keeps top actionable issues and caps detail", () => {
|
|
84
|
+
const issues = Array.from({ length: 20 }, (_, index) =>
|
|
85
|
+
`### Issue ${index + 1}\n${`detail ${index + 1} `.repeat(80)}`,
|
|
86
|
+
).join("\n");
|
|
87
|
+
|
|
88
|
+
const payload = buildDoctorHealIssuePayload(issues, {
|
|
89
|
+
maxIssues: 3,
|
|
90
|
+
maxIssueChars: 180,
|
|
91
|
+
maxChars: 900,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
assert.match(payload, /Issue 1/);
|
|
95
|
+
assert.match(payload, /Issue 3/);
|
|
96
|
+
assert.doesNotMatch(payload, /Issue 4/);
|
|
97
|
+
assert.match(payload, /17 additional actionable issue/);
|
|
98
|
+
assert.ok(payload.length <= 900);
|
|
99
|
+
});
|
|
@@ -14,9 +14,11 @@ function makeDeps(overrides?: Partial<MaintainWorkerHeartbeatDeps>): {
|
|
|
14
14
|
deps: MaintainWorkerHeartbeatDeps;
|
|
15
15
|
calls: unknown[];
|
|
16
16
|
errors: unknown[];
|
|
17
|
+
misses: unknown[];
|
|
17
18
|
} {
|
|
18
19
|
const calls: unknown[] = [];
|
|
19
20
|
const errors: unknown[] = [];
|
|
21
|
+
const misses: unknown[] = [];
|
|
20
22
|
const deps: MaintainWorkerHeartbeatDeps = {
|
|
21
23
|
heartbeatAutoWorker: workerId => calls.push(["heartbeat", workerId]),
|
|
22
24
|
refreshMilestoneLease: (workerId, milestoneId, token) => {
|
|
@@ -24,9 +26,10 @@ function makeDeps(overrides?: Partial<MaintainWorkerHeartbeatDeps>): {
|
|
|
24
26
|
return true;
|
|
25
27
|
},
|
|
26
28
|
logHeartbeatFailure: err => errors.push(err),
|
|
29
|
+
logLeaseRefreshMiss: details => misses.push(details),
|
|
27
30
|
...overrides,
|
|
28
31
|
};
|
|
29
|
-
return { deps, calls, errors };
|
|
32
|
+
return { deps, calls, errors, misses };
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
test("maintainWorkerHeartbeat no-ops without a worker id", () => {
|
|
@@ -121,3 +124,31 @@ test("maintainWorkerHeartbeat logs and suppresses lease refresh failures", () =>
|
|
|
121
124
|
]);
|
|
122
125
|
assert.deepEqual(errors, [failure]);
|
|
123
126
|
});
|
|
127
|
+
|
|
128
|
+
test("maintainWorkerHeartbeat clears stale lease tokens when refresh misses", () => {
|
|
129
|
+
const { deps, calls, errors, misses } = makeDeps({
|
|
130
|
+
refreshMilestoneLease: (workerId, milestoneId, token) => {
|
|
131
|
+
calls.push(["refresh", workerId, milestoneId, token]);
|
|
132
|
+
return false;
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
const session: WorkerHeartbeatSession = {
|
|
136
|
+
workerId: "worker-1",
|
|
137
|
+
currentMilestoneId: "M001",
|
|
138
|
+
milestoneLeaseToken: 7,
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
maintainWorkerHeartbeat(session, deps);
|
|
142
|
+
|
|
143
|
+
assert.deepEqual(calls, [
|
|
144
|
+
["heartbeat", "worker-1"],
|
|
145
|
+
["refresh", "worker-1", "M001", 7],
|
|
146
|
+
]);
|
|
147
|
+
assert.deepEqual(errors, []);
|
|
148
|
+
assert.deepEqual(misses, [{
|
|
149
|
+
workerId: "worker-1",
|
|
150
|
+
milestoneId: "M001",
|
|
151
|
+
fencingToken: 7,
|
|
152
|
+
}]);
|
|
153
|
+
assert.equal(session.milestoneLeaseToken, null);
|
|
154
|
+
});
|
|
@@ -39,6 +39,7 @@ function makeDeps(
|
|
|
39
39
|
getAutoWorktreePath: () => null,
|
|
40
40
|
autoCommitCurrentBranch: () => {},
|
|
41
41
|
getCurrentBranch: () => "main",
|
|
42
|
+
checkoutBranch: () => {},
|
|
42
43
|
autoWorktreeBranch: (milestoneId: string) => `milestone/${milestoneId}`,
|
|
43
44
|
resolveMilestoneFile: (_basePath: string, milestoneId: string) =>
|
|
44
45
|
`/project/.gsd/milestones/${milestoneId}/${milestoneId}-ROADMAP.md`,
|
|
@@ -79,7 +79,7 @@ async function waitFor(condition: () => boolean, label: string): Promise<void> {
|
|
|
79
79
|
assert.fail(`Timed out waiting for ${label} after ${timeoutMs}ms`);
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
test("runUnit
|
|
82
|
+
test("runUnit passes basePath as workspaceRoot without changing process cwd", async (t) => {
|
|
83
83
|
_resetPendingResolve();
|
|
84
84
|
|
|
85
85
|
const originalCwd = process.cwd();
|
|
@@ -93,13 +93,15 @@ test("runUnit changes cwd to basePath before creating a new session", async (t)
|
|
|
93
93
|
|
|
94
94
|
process.chdir(drifted);
|
|
95
95
|
|
|
96
|
+
let newSessionWorkspaceRoot: string | undefined;
|
|
96
97
|
let cwdAtNewSession: string | undefined;
|
|
97
98
|
const session = {
|
|
98
99
|
active: true,
|
|
99
100
|
basePath: base,
|
|
100
101
|
verbose: false,
|
|
101
102
|
cmdCtx: {
|
|
102
|
-
newSession: () => {
|
|
103
|
+
newSession: (options?: { workspaceRoot?: string }) => {
|
|
104
|
+
newSessionWorkspaceRoot = options?.workspaceRoot;
|
|
103
105
|
cwdAtNewSession = process.cwd();
|
|
104
106
|
return Promise.resolve({ cancelled: false });
|
|
105
107
|
},
|
|
@@ -119,10 +121,12 @@ test("runUnit changes cwd to basePath before creating a new session", async (t)
|
|
|
119
121
|
|
|
120
122
|
const result = await resultPromise;
|
|
121
123
|
assert.equal(result.status, "completed");
|
|
122
|
-
assert.equal(
|
|
124
|
+
assert.equal(newSessionWorkspaceRoot, base);
|
|
125
|
+
assert.equal(cwdAtNewSession, drifted);
|
|
126
|
+
assert.equal(process.cwd(), drifted);
|
|
123
127
|
});
|
|
124
128
|
|
|
125
|
-
test("runUnit
|
|
129
|
+
test("runUnit does not chdir or cancel when basePath is not a live directory", async (t) => {
|
|
126
130
|
_resetPendingResolve();
|
|
127
131
|
|
|
128
132
|
const originalCwd = process.cwd();
|
|
@@ -136,14 +140,14 @@ test("runUnit cancels before creating a session when basePath chdir fails", asyn
|
|
|
136
140
|
|
|
137
141
|
process.chdir(drifted);
|
|
138
142
|
|
|
139
|
-
let
|
|
143
|
+
let newSessionWorkspaceRoot: string | undefined;
|
|
140
144
|
const session = {
|
|
141
145
|
active: true,
|
|
142
146
|
basePath: base,
|
|
143
147
|
verbose: false,
|
|
144
148
|
cmdCtx: {
|
|
145
|
-
newSession: () => {
|
|
146
|
-
|
|
149
|
+
newSession: (options?: { workspaceRoot?: string }) => {
|
|
150
|
+
newSessionWorkspaceRoot = options?.workspaceRoot;
|
|
147
151
|
return Promise.resolve({ cancelled: false });
|
|
148
152
|
},
|
|
149
153
|
},
|
|
@@ -156,15 +160,14 @@ test("runUnit cancels before creating a session when basePath chdir fails", asyn
|
|
|
156
160
|
} as any;
|
|
157
161
|
const ctx = { ui: { notify: () => {} }, model: { id: "test-model" } } as any;
|
|
158
162
|
|
|
159
|
-
const
|
|
163
|
+
const resultPromise = runUnit(ctx, pi, session, "task", "T01", "prompt");
|
|
164
|
+
await waitFor(() => pi.calls.length === 1, "runUnit dispatch");
|
|
165
|
+
resolveAgentEnd({ messages: [{ role: "assistant" }] });
|
|
160
166
|
|
|
161
|
-
|
|
162
|
-
assert.equal(result.
|
|
163
|
-
assert.equal(
|
|
164
|
-
assert.
|
|
165
|
-
assert.ok(result.errorContext?.message.includes(base), "error should include the failed basePath");
|
|
166
|
-
assert.equal(newSessionCalled, false, "newSession must not run after chdir failure");
|
|
167
|
-
assert.equal(pi.calls.length, 0, "unit must not dispatch after chdir failure");
|
|
167
|
+
const result = await resultPromise;
|
|
168
|
+
assert.equal(result.status, "completed");
|
|
169
|
+
assert.equal(newSessionWorkspaceRoot, base);
|
|
170
|
+
assert.equal(process.cwd(), drifted);
|
|
168
171
|
});
|
|
169
172
|
|
|
170
173
|
test("direct dispatch redirects to the canonical milestone worktree before newSession", async (t) => {
|
|
@@ -185,12 +188,12 @@ test("direct dispatch redirects to the canonical milestone worktree before newSe
|
|
|
185
188
|
|
|
186
189
|
process.chdir(drifted);
|
|
187
190
|
|
|
188
|
-
let
|
|
191
|
+
let newSessionWorkspaceRoot: string | undefined;
|
|
189
192
|
let sentPrompt: string | undefined;
|
|
190
193
|
const ctx = {
|
|
191
194
|
ui: { notify: () => {} },
|
|
192
|
-
newSession: async () => {
|
|
193
|
-
|
|
195
|
+
newSession: async (options?: { workspaceRoot?: string }) => {
|
|
196
|
+
newSessionWorkspaceRoot = options?.workspaceRoot;
|
|
194
197
|
return { cancelled: false };
|
|
195
198
|
},
|
|
196
199
|
} as any;
|
|
@@ -202,7 +205,7 @@ test("direct dispatch redirects to the canonical milestone worktree before newSe
|
|
|
202
205
|
|
|
203
206
|
await dispatchDirectPhase(ctx, pi, "research-milestone", base);
|
|
204
207
|
|
|
205
|
-
assert.equal(
|
|
208
|
+
assert.equal(newSessionWorkspaceRoot, worktreeRoot);
|
|
206
209
|
assert.equal(process.cwd(), drifted);
|
|
207
210
|
assert.ok(sentPrompt?.includes(worktreeRoot), "prompt should name the canonical worktree root");
|
|
208
211
|
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// GSD-2 + Worktree dispatch guard: degrade empty worktrees over real project roots.
|
|
2
|
+
|
|
3
|
+
import { describe, test } from "node:test";
|
|
4
|
+
import assert from "node:assert/strict";
|
|
5
|
+
|
|
6
|
+
import { shouldDegradeEmptyWorktreeToProjectRoot } from "../auto/phases.ts";
|
|
7
|
+
import type { ProjectClassification } from "../detection.ts";
|
|
8
|
+
|
|
9
|
+
function classification(kind: ProjectClassification["kind"]): ProjectClassification {
|
|
10
|
+
return {
|
|
11
|
+
kind,
|
|
12
|
+
signals: {
|
|
13
|
+
detectedFiles: [],
|
|
14
|
+
isGitRepo: true,
|
|
15
|
+
isMonorepo: false,
|
|
16
|
+
xcodePlatforms: [],
|
|
17
|
+
hasCI: false,
|
|
18
|
+
hasTests: false,
|
|
19
|
+
verificationCommands: [],
|
|
20
|
+
},
|
|
21
|
+
trackedFiles: [],
|
|
22
|
+
untrackedFiles: [],
|
|
23
|
+
contentFiles: [],
|
|
24
|
+
markers: [],
|
|
25
|
+
reason: kind,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("worktree project-root degradation", () => {
|
|
30
|
+
test("degrades when worktree is greenfield but project root has content", () => {
|
|
31
|
+
assert.equal(
|
|
32
|
+
shouldDegradeEmptyWorktreeToProjectRoot(
|
|
33
|
+
classification("greenfield"),
|
|
34
|
+
classification("typed-existing"),
|
|
35
|
+
),
|
|
36
|
+
true,
|
|
37
|
+
);
|
|
38
|
+
assert.equal(
|
|
39
|
+
shouldDegradeEmptyWorktreeToProjectRoot(
|
|
40
|
+
classification("greenfield"),
|
|
41
|
+
classification("untyped-existing"),
|
|
42
|
+
),
|
|
43
|
+
true,
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("keeps true greenfield worktrees in worktree mode", () => {
|
|
48
|
+
assert.equal(
|
|
49
|
+
shouldDegradeEmptyWorktreeToProjectRoot(
|
|
50
|
+
classification("greenfield"),
|
|
51
|
+
classification("greenfield"),
|
|
52
|
+
),
|
|
53
|
+
false,
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("does not degrade when project root classification is invalid", () => {
|
|
58
|
+
assert.equal(
|
|
59
|
+
shouldDegradeEmptyWorktreeToProjectRoot(
|
|
60
|
+
classification("greenfield"),
|
|
61
|
+
classification("invalid-repo"),
|
|
62
|
+
),
|
|
63
|
+
false,
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -118,6 +118,9 @@ function makeDeps(
|
|
|
118
118
|
calls.push({ fn: "getCurrentBranch", args: [basePath] });
|
|
119
119
|
return "main";
|
|
120
120
|
},
|
|
121
|
+
checkoutBranch: (basePath: string, branch: string) => {
|
|
122
|
+
calls.push({ fn: "checkoutBranch", args: [basePath, branch] });
|
|
123
|
+
},
|
|
121
124
|
autoWorktreeBranch: (milestoneId: string) => {
|
|
122
125
|
calls.push({ fn: "autoWorktreeBranch", args: [milestoneId] });
|
|
123
126
|
return `milestone/${milestoneId}`;
|
|
@@ -802,21 +805,92 @@ test("mergeAndExit in branch mode merges when on milestone branch", () => {
|
|
|
802
805
|
assert.ok(ctx.messages.some((m) => m.msg.includes("branch mode")));
|
|
803
806
|
});
|
|
804
807
|
|
|
805
|
-
test("mergeAndExit in branch mode
|
|
808
|
+
test("mergeAndExit in branch mode checks out the milestone branch and merges (#5538-followup)", () => {
|
|
809
|
+
// Regression: previously this case silently returned without merging,
|
|
810
|
+
// stranding the milestone's commits on the branch (the test12345 repro).
|
|
811
|
+
// The fix forces a checkout first; merge proceeds when checkout succeeds.
|
|
806
812
|
const s = makeSession({ basePath: "/project", originalBasePath: "/project" });
|
|
813
|
+
let currentBranch = "main";
|
|
814
|
+
const checkoutInvocations: Array<{ basePath: string; branch: string }> = [];
|
|
807
815
|
const deps = makeDeps({
|
|
808
816
|
isInAutoWorktree: () => false,
|
|
809
817
|
getIsolationMode: () => "branch",
|
|
810
|
-
getCurrentBranch: () =>
|
|
818
|
+
getCurrentBranch: () => currentBranch,
|
|
811
819
|
autoWorktreeBranch: () => "milestone/M001",
|
|
820
|
+
checkoutBranch: (basePath: string, branch: string) => {
|
|
821
|
+
checkoutInvocations.push({ basePath, branch });
|
|
822
|
+
currentBranch = branch;
|
|
823
|
+
},
|
|
812
824
|
});
|
|
813
825
|
const ctx = makeNotifyCtx();
|
|
814
826
|
const resolver = new WorktreeResolver(s, deps);
|
|
815
827
|
|
|
816
828
|
resolver.mergeAndExit("M001", ctx);
|
|
817
829
|
|
|
830
|
+
assert.equal(checkoutInvocations.length, 1, "must attempt checkout when on wrong branch");
|
|
831
|
+
assert.deepEqual(checkoutInvocations[0], { basePath: "/project", branch: "milestone/M001" });
|
|
832
|
+
assert.equal(findCalls(deps.calls, "mergeMilestoneToMain").length, 1);
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
test("mergeAndExit in branch mode throws when checkout fails", () => {
|
|
836
|
+
// Regression for the silent-skip bug: if the working tree is on the wrong
|
|
837
|
+
// branch and checkout fails, we must throw so the caller pauses auto-mode
|
|
838
|
+
// — never silently advance with the milestone unmerged.
|
|
839
|
+
const s = makeSession({ basePath: "/project", originalBasePath: "/project" });
|
|
840
|
+
const deps = makeDeps({
|
|
841
|
+
isInAutoWorktree: () => false,
|
|
842
|
+
getIsolationMode: () => "branch",
|
|
843
|
+
getCurrentBranch: () => "main",
|
|
844
|
+
autoWorktreeBranch: () => "milestone/M001",
|
|
845
|
+
checkoutBranch: () => {
|
|
846
|
+
throw new Error("dirty working tree blocks checkout");
|
|
847
|
+
},
|
|
848
|
+
});
|
|
849
|
+
const ctx = makeNotifyCtx();
|
|
850
|
+
const resolver = new WorktreeResolver(s, deps);
|
|
851
|
+
|
|
852
|
+
assert.throws(
|
|
853
|
+
() => resolver.mergeAndExit("M001", ctx),
|
|
854
|
+
/dirty working tree blocks checkout/,
|
|
855
|
+
);
|
|
856
|
+
assert.equal(
|
|
857
|
+
findCalls(deps.calls, "mergeMilestoneToMain").length,
|
|
858
|
+
0,
|
|
859
|
+
"merge must not run when checkout failed",
|
|
860
|
+
);
|
|
861
|
+
const errorNotify = ctx.messages.find((m) => m.level === "error");
|
|
862
|
+
assert.ok(errorNotify, "an error notification must be emitted");
|
|
863
|
+
assert.match(errorNotify!.msg, /milestone\/M001 failed/);
|
|
864
|
+
assert.match(errorNotify!.msg, /Resolve manually/);
|
|
865
|
+
assert.equal(
|
|
866
|
+
ctx.messages.some((m) => m.level === "warning" && m.msg.includes("Milestone merge failed")),
|
|
867
|
+
false,
|
|
868
|
+
"checkout failures with explicit recovery guidance must not emit a duplicate warning",
|
|
869
|
+
);
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
test("mergeAndExit in branch mode throws when checkout reports success but HEAD is still wrong", () => {
|
|
873
|
+
// Defense in depth: even if checkoutBranch returns without throwing, we
|
|
874
|
+
// re-verify and throw if HEAD didn't actually move. Prevents merging on
|
|
875
|
+
// top of the wrong branch on platforms where the checkout is a no-op.
|
|
876
|
+
const s = makeSession({ basePath: "/project", originalBasePath: "/project" });
|
|
877
|
+
const deps = makeDeps({
|
|
878
|
+
isInAutoWorktree: () => false,
|
|
879
|
+
getIsolationMode: () => "branch",
|
|
880
|
+
getCurrentBranch: () => "main", // never changes — simulates no-op checkout
|
|
881
|
+
autoWorktreeBranch: () => "milestone/M001",
|
|
882
|
+
checkoutBranch: () => {
|
|
883
|
+
// Pretend success — but getCurrentBranch will still return "main".
|
|
884
|
+
},
|
|
885
|
+
});
|
|
886
|
+
const ctx = makeNotifyCtx();
|
|
887
|
+
const resolver = new WorktreeResolver(s, deps);
|
|
888
|
+
|
|
889
|
+
assert.throws(
|
|
890
|
+
() => resolver.mergeAndExit("M001", ctx),
|
|
891
|
+
/reported success but current branch is main/,
|
|
892
|
+
);
|
|
818
893
|
assert.equal(findCalls(deps.calls, "mergeMilestoneToMain").length, 0);
|
|
819
|
-
assert.equal(ctx.messages.length, 0);
|
|
820
894
|
});
|
|
821
895
|
|
|
822
896
|
test("mergeAndExit in branch mode handles merge failure gracefully", () => {
|
|
@@ -1039,6 +1113,33 @@ test("mergeAndEnterNext enters next milestone even if merge fails", () => {
|
|
|
1039
1113
|
);
|
|
1040
1114
|
});
|
|
1041
1115
|
|
|
1116
|
+
test("mergeAndEnterNext halts after branch-mode user-notified checkout failure", () => {
|
|
1117
|
+
const s = makeSession({ basePath: "/project", originalBasePath: "/project" });
|
|
1118
|
+
const deps = makeDeps({
|
|
1119
|
+
isInAutoWorktree: () => false,
|
|
1120
|
+
getIsolationMode: () => "branch",
|
|
1121
|
+
getCurrentBranch: () => "main",
|
|
1122
|
+
autoWorktreeBranch: () => "milestone/M001",
|
|
1123
|
+
checkoutBranch: () => {
|
|
1124
|
+
throw new Error("dirty working tree blocks checkout");
|
|
1125
|
+
},
|
|
1126
|
+
});
|
|
1127
|
+
const ctx = makeNotifyCtx();
|
|
1128
|
+
const resolver = new WorktreeResolver(s, deps);
|
|
1129
|
+
|
|
1130
|
+
assert.throws(
|
|
1131
|
+
() => resolver.mergeAndEnterNext("M001", "M002", ctx),
|
|
1132
|
+
/dirty working tree blocks checkout/,
|
|
1133
|
+
);
|
|
1134
|
+
assert.equal(
|
|
1135
|
+
findCalls(deps.calls, "enterBranchModeForMilestone").length,
|
|
1136
|
+
0,
|
|
1137
|
+
"must not enter the next milestone after a user-notified branch-mode failure",
|
|
1138
|
+
);
|
|
1139
|
+
assert.equal(findCalls(deps.calls, "mergeMilestoneToMain").length, 0);
|
|
1140
|
+
assert.ok(ctx.messages.some((m) => m.level === "error" && m.msg.includes("Resolve manually")));
|
|
1141
|
+
});
|
|
1142
|
+
|
|
1042
1143
|
// ─── GitService Rebuild Atomicity ────────────────────────────────────────────
|
|
1043
1144
|
|
|
1044
1145
|
test("GitService is rebuilt with the NEW basePath after enterMilestone", () => {
|