gsd-pi 2.73.0-dev.e1c09f2 → 2.73.1-dev.06e4302
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-web-branch.d.ts +4 -3
- package/dist/cli-web-branch.js +10 -7
- package/dist/cli.js +99 -206
- package/dist/logo.d.ts +1 -1
- package/dist/logo.js +1 -1
- package/dist/onboarding.js +59 -53
- package/dist/resource-loader.js +2 -2
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +68 -4
- package/dist/resources/extensions/gsd/auto/phases.js +15 -9
- package/dist/resources/extensions/gsd/auto-dispatch.js +11 -3
- package/dist/resources/extensions/gsd/auto-model-selection.js +54 -11
- package/dist/resources/extensions/gsd/auto-post-unit.js +41 -1
- package/dist/resources/extensions/gsd/auto-start.js +23 -6
- package/dist/resources/extensions/gsd/auto-timeout-recovery.js +13 -0
- package/dist/resources/extensions/gsd/auto-verification.js +88 -3
- package/dist/resources/extensions/gsd/auto.js +34 -9
- package/dist/resources/extensions/gsd/bootstrap/crash-log.js +31 -0
- package/dist/resources/extensions/gsd/bootstrap/register-extension.js +18 -7
- package/dist/resources/extensions/gsd/commands-handlers.js +8 -2
- package/dist/resources/extensions/gsd/crash-recovery.js +51 -0
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +1 -1
- package/dist/resources/extensions/gsd/gsd-db.js +36 -2
- package/dist/resources/extensions/gsd/milestone-actions.js +19 -1
- package/dist/resources/extensions/gsd/notification-widget.js +2 -2
- package/dist/resources/extensions/gsd/preferences-models.js +43 -0
- package/dist/resources/extensions/gsd/preferences-types.js +1 -0
- package/dist/resources/extensions/gsd/preferences-validation.js +22 -0
- package/dist/resources/extensions/gsd/state.js +61 -14
- package/dist/update-check.d.ts +1 -0
- package/dist/update-check.js +13 -5
- package/dist/update-cmd.js +4 -3
- 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/middleware-manifest.json +5 -5
- 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/package.json +1 -2
- package/packages/pi-ai/dist/index.d.ts +1 -0
- package/packages/pi-ai/dist/index.d.ts.map +1 -1
- package/packages/pi-ai/dist/index.js +1 -0
- package/packages/pi-ai/dist/index.js.map +1 -1
- package/packages/pi-ai/dist/utils/overflow.d.ts.map +1 -1
- package/packages/pi-ai/dist/utils/overflow.js +12 -0
- package/packages/pi-ai/dist/utils/overflow.js.map +1 -1
- package/packages/pi-ai/dist/utils/tests/overflow.test.d.ts +2 -0
- package/packages/pi-ai/dist/utils/tests/overflow.test.d.ts.map +1 -0
- package/packages/pi-ai/dist/utils/tests/overflow.test.js +50 -0
- package/packages/pi-ai/dist/utils/tests/overflow.test.js.map +1 -0
- package/packages/pi-ai/src/index.ts +4 -0
- package/packages/pi-ai/src/utils/overflow.ts +14 -1
- package/packages/pi-ai/src/utils/tests/overflow.test.ts +58 -0
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js +313 -8
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/compaction/utils.js +5 -5
- package/packages/pi-coding-agent/dist/core/compaction/utils.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/compaction-utils.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/compaction-utils.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/compaction-utils.test.js +45 -0
- package/packages/pi-coding-agent/dist/core/compaction-utils.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts +12 -2
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js +61 -28
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.d.ts +2 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.js +9 -3
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.js +52 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +94 -16
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +11 -3
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +355 -8
- package/packages/pi-coding-agent/src/core/compaction/utils.ts +5 -5
- package/packages/pi-coding-agent/src/core/compaction-utils.test.ts +50 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/assistant-message.ts +74 -32
- package/packages/pi-coding-agent/src/modes/interactive/components/dynamic-border.test.ts +73 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/dynamic-border.ts +9 -3
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +113 -21
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +11 -3
- package/packages/pi-tui/dist/__tests__/tui.test.js +60 -1
- package/packages/pi-tui/dist/__tests__/tui.test.js.map +1 -1
- package/packages/pi-tui/dist/tui.d.ts +8 -0
- package/packages/pi-tui/dist/tui.d.ts.map +1 -1
- package/packages/pi-tui/dist/tui.js +32 -3
- package/packages/pi-tui/dist/tui.js.map +1 -1
- package/packages/pi-tui/src/__tests__/tui.test.ts +76 -1
- package/packages/pi-tui/src/tui.ts +31 -3
- package/pkg/package.json +1 -1
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +107 -5
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +111 -2
- package/src/resources/extensions/gsd/auto/phases.ts +22 -9
- package/src/resources/extensions/gsd/auto-dispatch.ts +10 -4
- package/src/resources/extensions/gsd/auto-model-selection.ts +85 -11
- package/src/resources/extensions/gsd/auto-post-unit.ts +47 -1
- package/src/resources/extensions/gsd/auto-start.ts +30 -6
- package/src/resources/extensions/gsd/auto-timeout-recovery.ts +17 -0
- package/src/resources/extensions/gsd/auto-verification.ts +98 -3
- package/src/resources/extensions/gsd/auto.ts +36 -14
- package/src/resources/extensions/gsd/bootstrap/crash-log.ts +32 -0
- package/src/resources/extensions/gsd/bootstrap/register-extension.ts +19 -7
- package/src/resources/extensions/gsd/commands-handlers.ts +8 -2
- package/src/resources/extensions/gsd/crash-recovery.ts +59 -0
- package/src/resources/extensions/gsd/docs/preferences-reference.md +1 -1
- package/src/resources/extensions/gsd/gsd-db.ts +52 -2
- package/src/resources/extensions/gsd/milestone-actions.ts +19 -1
- package/src/resources/extensions/gsd/notification-widget.ts +2 -2
- package/src/resources/extensions/gsd/preferences-models.ts +41 -0
- package/src/resources/extensions/gsd/preferences-types.ts +12 -0
- package/src/resources/extensions/gsd/preferences-validation.ts +23 -0
- package/src/resources/extensions/gsd/state.ts +71 -15
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/auto-post-unit-step-message.test.ts +53 -0
- package/src/resources/extensions/gsd/tests/auto-start-model-capture.test.ts +51 -2
- package/src/resources/extensions/gsd/tests/complete-milestone-false-merge.test.ts +142 -0
- package/src/resources/extensions/gsd/tests/completed-at-reconcile.test.ts +42 -0
- package/src/resources/extensions/gsd/tests/crash-handler-secondary.test.ts +235 -0
- package/src/resources/extensions/gsd/tests/derive-state-crossval.test.ts +3 -2
- package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +3 -2
- package/src/resources/extensions/gsd/tests/derive-state-helpers.test.ts +68 -8
- package/src/resources/extensions/gsd/tests/derive-state.test.ts +3 -3
- package/src/resources/extensions/gsd/tests/flat-rate-routing-guard.test.ts +137 -1
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +59 -1
- package/src/resources/extensions/gsd/tests/integration/state-machine-edge-cases.test.ts +4 -2
- package/src/resources/extensions/gsd/tests/model-isolation.test.ts +91 -2
- package/src/resources/extensions/gsd/tests/park-milestone.test.ts +64 -0
- package/src/resources/extensions/gsd/tests/preferences.test.ts +47 -0
- package/src/resources/extensions/gsd/tests/state-machine-full-walkthrough.test.ts +5 -7
- package/src/resources/extensions/gsd/tests/token-profile.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/validate-milestone-stuck-guard.test.ts +179 -0
- /package/dist/web/standalone/.next/static/{_XD_gUDcZNBbWV5rI8RgS → RXD20AQgB9BHSQJ07MDdd}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{_XD_gUDcZNBbWV5rI8RgS → RXD20AQgB9BHSQJ07MDdd}/_ssgManifest.js +0 -0
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
10
12
|
import type { DynamicRoutingConfig } from "./model-router.js";
|
|
11
13
|
import { defaultRoutingConfig } from "./model-router.js";
|
|
12
14
|
import type { TokenProfile, InlineLevel } from "./types.js";
|
|
@@ -185,6 +187,45 @@ export function resolveDefaultSessionModel(
|
|
|
185
187
|
return undefined;
|
|
186
188
|
}
|
|
187
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Returns true if `provider` is defined as a custom provider in the user's
|
|
192
|
+
* `~/.gsd/agent/models.json` (Ollama, vLLM, LM Studio, OpenAI-compatible
|
|
193
|
+
* proxies, etc.).
|
|
194
|
+
*
|
|
195
|
+
* Used by auto-mode bootstrap to decide whether the session model
|
|
196
|
+
* (set via `/gsd model`) should override `PREFERENCES.md`. Custom providers
|
|
197
|
+
* are never reachable from `PREFERENCES.md` (which only knows built-in
|
|
198
|
+
* providers), so when the user has explicitly selected one, it must take
|
|
199
|
+
* priority — otherwise auto-mode tries to start the built-in provider from
|
|
200
|
+
* PREFERENCES.md and fails with "Not logged in · Please run /login" (#4122).
|
|
201
|
+
*
|
|
202
|
+
* Reads models.json directly with a lightweight JSON parse to avoid
|
|
203
|
+
* pulling in the full model-registry at this call site. Falls back to
|
|
204
|
+
* `~/.pi/agent/models.json` for parity with `resolveModelsJsonPath()`.
|
|
205
|
+
* Any read or parse error yields `false` (treat as not-custom) so a
|
|
206
|
+
* malformed models.json never breaks the session bootstrap.
|
|
207
|
+
*/
|
|
208
|
+
export function isCustomProvider(provider: string | undefined): boolean {
|
|
209
|
+
if (!provider) return false;
|
|
210
|
+
const candidates = [
|
|
211
|
+
join(homedir(), ".gsd", "agent", "models.json"),
|
|
212
|
+
join(homedir(), ".pi", "agent", "models.json"),
|
|
213
|
+
];
|
|
214
|
+
for (const path of candidates) {
|
|
215
|
+
if (!existsSync(path)) continue;
|
|
216
|
+
try {
|
|
217
|
+
const raw = readFileSync(path, "utf-8");
|
|
218
|
+
const parsed = JSON.parse(raw) as { providers?: Record<string, unknown> };
|
|
219
|
+
if (parsed?.providers && Object.prototype.hasOwnProperty.call(parsed.providers, provider)) {
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
} catch {
|
|
223
|
+
// Ignore — malformed models.json must not break bootstrap.
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
|
|
188
229
|
/**
|
|
189
230
|
* Determines the next fallback model to try when the current model fails.
|
|
190
231
|
* If the current model is not in the configured list, returns the primary model.
|
|
@@ -113,6 +113,7 @@ export const KNOWN_PREFERENCE_KEYS = new Set<string>([
|
|
|
113
113
|
"discuss_preparation",
|
|
114
114
|
"discuss_web_research",
|
|
115
115
|
"discuss_depth",
|
|
116
|
+
"flat_rate_providers",
|
|
116
117
|
]);
|
|
117
118
|
|
|
118
119
|
/** Canonical list of all dispatch unit types. */
|
|
@@ -359,6 +360,17 @@ export interface GSDPreferences {
|
|
|
359
360
|
* Default: "standard".
|
|
360
361
|
*/
|
|
361
362
|
discuss_depth?: "quick" | "standard" | "thorough";
|
|
363
|
+
/**
|
|
364
|
+
* Extra provider IDs to treat as flat-rate (no cost benefit from dynamic
|
|
365
|
+
* routing). Dynamic routing is suppressed for any provider listed here,
|
|
366
|
+
* in addition to the built-in list (github-copilot, copilot, claude-code)
|
|
367
|
+
* and any provider auto-detected via `authMode: "externalCli"`.
|
|
368
|
+
*
|
|
369
|
+
* Intended for private subscription-backed proxies, enterprise-gated
|
|
370
|
+
* deployments, and custom CLI wrappers where every request costs the
|
|
371
|
+
* same regardless of model. Case-insensitive.
|
|
372
|
+
*/
|
|
373
|
+
flat_rate_providers?: string[];
|
|
362
374
|
}
|
|
363
375
|
|
|
364
376
|
export interface LoadedGSDPreferences {
|
|
@@ -180,6 +180,29 @@ export function validatePreferences(preferences: GSDPreferences): {
|
|
|
180
180
|
}
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
// ─── Flat-rate Providers ────────────────────────────────────────────
|
|
184
|
+
// User-declared flat-rate providers for dynamic routing suppression.
|
|
185
|
+
// Built-in providers (github-copilot, copilot, claude-code) and any
|
|
186
|
+
// externalCli provider are already auto-detected; this list layers on
|
|
187
|
+
// top for private subscription proxies and custom CLI wrappers.
|
|
188
|
+
if (preferences.flat_rate_providers !== undefined) {
|
|
189
|
+
if (Array.isArray(preferences.flat_rate_providers)) {
|
|
190
|
+
const allStrings = preferences.flat_rate_providers.every(
|
|
191
|
+
(item: unknown) => typeof item === "string",
|
|
192
|
+
);
|
|
193
|
+
if (allStrings) {
|
|
194
|
+
// Strip empty/whitespace-only entries to avoid false matches.
|
|
195
|
+
validated.flat_rate_providers = preferences.flat_rate_providers
|
|
196
|
+
.map((s: string) => s.trim())
|
|
197
|
+
.filter((s: string) => s.length > 0);
|
|
198
|
+
} else {
|
|
199
|
+
errors.push("flat_rate_providers must be an array of strings");
|
|
200
|
+
}
|
|
201
|
+
} else {
|
|
202
|
+
errors.push("flat_rate_providers must be an array of strings");
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
183
206
|
// ─── Phase Skip Preferences ─────────────────────────────────────────
|
|
184
207
|
if (preferences.phases !== undefined) {
|
|
185
208
|
if (typeof preferences.phases === "object" && preferences.phases !== null) {
|
|
@@ -386,6 +386,10 @@ function buildCompletenessSet(basePath: string, milestones: MilestoneRow[]) {
|
|
|
386
386
|
const completeMilestoneIds = new Set<string>();
|
|
387
387
|
const parkedMilestoneIds = new Set<string>();
|
|
388
388
|
|
|
389
|
+
// DB-authoritative: a milestone is only "complete" when its DB row says so.
|
|
390
|
+
// SUMMARY-file presence is NOT a completion signal here — an orphan SUMMARY
|
|
391
|
+
// (crashed complete-milestone turn, partial merge, manual edit) must not
|
|
392
|
+
// flip derived state to complete and cascade into a false auto-merge (#4179).
|
|
389
393
|
for (const m of milestones) {
|
|
390
394
|
const parkedFile = resolveMilestoneFile(basePath, m.id, "PARKED");
|
|
391
395
|
if (parkedFile || m.status === 'parked') {
|
|
@@ -396,11 +400,6 @@ function buildCompletenessSet(basePath: string, milestones: MilestoneRow[]) {
|
|
|
396
400
|
completeMilestoneIds.add(m.id);
|
|
397
401
|
continue;
|
|
398
402
|
}
|
|
399
|
-
const summaryFile = resolveMilestoneFile(basePath, m.id, "SUMMARY");
|
|
400
|
-
if (summaryFile) {
|
|
401
|
-
completeMilestoneIds.add(m.id);
|
|
402
|
-
continue;
|
|
403
|
-
}
|
|
404
403
|
}
|
|
405
404
|
return { completeMilestoneIds, parkedMilestoneIds };
|
|
406
405
|
}
|
|
@@ -429,18 +428,22 @@ async function buildRegistryAndFindActive(
|
|
|
429
428
|
if (isGhostMilestone(basePath, m.id)) continue;
|
|
430
429
|
}
|
|
431
430
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
431
|
+
// DB-authoritative completeness (#4179): only trust completeMilestoneIds,
|
|
432
|
+
// which is itself derived from DB status. SUMMARY-file presence alone must
|
|
433
|
+
// not imply completion. The summary file may still be consulted below as a
|
|
434
|
+
// title source for legitimately-complete milestones whose DB row has no title.
|
|
435
|
+
if (completeMilestoneIds.has(m.id)) {
|
|
435
436
|
let title = stripMilestonePrefix(m.title) || m.id;
|
|
436
|
-
if (
|
|
437
|
-
const
|
|
438
|
-
if (
|
|
439
|
-
|
|
437
|
+
if (!m.title) {
|
|
438
|
+
const summaryFile = resolveMilestoneFile(basePath, m.id, "SUMMARY");
|
|
439
|
+
if (summaryFile) {
|
|
440
|
+
const summaryContent = await loadFile(summaryFile);
|
|
441
|
+
if (summaryContent) {
|
|
442
|
+
title = parseSummary(summaryContent).title || m.id;
|
|
443
|
+
}
|
|
440
444
|
}
|
|
441
445
|
}
|
|
442
446
|
registry.push({ id: m.id, title, status: 'complete' });
|
|
443
|
-
completeMilestoneIds.add(m.id);
|
|
444
447
|
continue;
|
|
445
448
|
}
|
|
446
449
|
|
|
@@ -481,7 +484,14 @@ async function buildRegistryAndFindActive(
|
|
|
481
484
|
const validationContent = validationFile ? await loadFile(validationFile) : null;
|
|
482
485
|
const validationTerminal = validationContent ? isValidationTerminal(validationContent) : false;
|
|
483
486
|
|
|
484
|
-
|
|
487
|
+
// DB-authoritative (#4179): completeness is already decided by
|
|
488
|
+
// completeMilestoneIds above. If we reached this branch, the DB says
|
|
489
|
+
// the milestone is NOT complete — so any SUMMARY file on disk is an
|
|
490
|
+
// orphan (crashed complete-milestone, partial merge, manual edit) and
|
|
491
|
+
// must not short-circuit this path. When validation is terminal, fall
|
|
492
|
+
// through to the default active-push below so `complete-milestone` can
|
|
493
|
+
// re-run idempotently.
|
|
494
|
+
if (!validationTerminal) {
|
|
485
495
|
activeMilestone = { id: m.id, title };
|
|
486
496
|
activeMilestoneSlices = slices;
|
|
487
497
|
activeMilestoneFound = true;
|
|
@@ -630,13 +640,39 @@ function resolveSliceDependencies(activeMilestoneSlices: SliceRow[]): { activeSl
|
|
|
630
640
|
}
|
|
631
641
|
}
|
|
632
642
|
|
|
643
|
+
// First pass: find a slice with ALL dependencies satisfied (strict)
|
|
644
|
+
let bestFallback: SliceRow | null = null;
|
|
645
|
+
let bestFallbackSatisfied = -1;
|
|
646
|
+
|
|
633
647
|
for (const s of activeMilestoneSlices) {
|
|
634
648
|
if (isStatusDone(s.status)) continue;
|
|
635
649
|
if (isDeferredStatus(s.status)) continue;
|
|
636
650
|
if (s.depends.every(dep => doneSliceIds.has(dep))) {
|
|
637
651
|
return { activeSlice: { id: s.id, title: s.title }, activeSliceRow: s };
|
|
638
652
|
}
|
|
653
|
+
// Track the slice with the most satisfied dependencies as fallback
|
|
654
|
+
const satisfied = s.depends.filter(dep => doneSliceIds.has(dep)).length;
|
|
655
|
+
if (satisfied > bestFallbackSatisfied || (satisfied === bestFallbackSatisfied && !bestFallback)) {
|
|
656
|
+
bestFallback = s;
|
|
657
|
+
bestFallbackSatisfied = satisfied;
|
|
658
|
+
}
|
|
639
659
|
}
|
|
660
|
+
|
|
661
|
+
// Fallback: if no slice has all deps met but there ARE incomplete non-deferred
|
|
662
|
+
// slices, pick the one with the most deps satisfied. This prevents hard-blocking
|
|
663
|
+
// when dependency metadata is stale (e.g. after reassessment added/removed slices)
|
|
664
|
+
// or when deps reference slices from previous milestones.
|
|
665
|
+
if (bestFallback) {
|
|
666
|
+
const unmet = bestFallback.depends.filter(dep => !doneSliceIds.has(dep));
|
|
667
|
+
logWarning("state",
|
|
668
|
+
`No slice has all deps satisfied — falling back to ${bestFallback.id} ` +
|
|
669
|
+
`(${bestFallbackSatisfied}/${bestFallback.depends.length} deps met, ` +
|
|
670
|
+
`unmet: ${unmet.join(", ")})`,
|
|
671
|
+
{ mid: activeMilestoneSlices[0]?.milestone_id, sid: bestFallback.id },
|
|
672
|
+
);
|
|
673
|
+
return { activeSlice: { id: bestFallback.id, title: bestFallback.title }, activeSliceRow: bestFallback };
|
|
674
|
+
}
|
|
675
|
+
|
|
640
676
|
return { activeSlice: null, activeSliceRow: null };
|
|
641
677
|
}
|
|
642
678
|
|
|
@@ -684,7 +720,7 @@ async function reconcileSliceTasks(
|
|
|
684
720
|
const summaryPath = resolveTaskFile(basePath, milestoneId, sliceId, t.id, "SUMMARY");
|
|
685
721
|
if (summaryPath && existsSync(summaryPath)) {
|
|
686
722
|
try {
|
|
687
|
-
updateTaskStatus(milestoneId, sliceId, t.id, "complete");
|
|
723
|
+
updateTaskStatus(milestoneId, sliceId, t.id, "complete", new Date().toISOString());
|
|
688
724
|
logWarning("reconcile", `task ${milestoneId}/${sliceId}/${t.id} status reconciled from "${t.status}" to "complete" (#2514)`, { mid: milestoneId, sid: sliceId, tid: t.id });
|
|
689
725
|
reconciled = true;
|
|
690
726
|
} catch (e) {
|
|
@@ -1431,12 +1467,32 @@ export async function _deriveStateImpl(basePath: string): Promise<GSDState> {
|
|
|
1431
1467
|
};
|
|
1432
1468
|
}
|
|
1433
1469
|
} else {
|
|
1470
|
+
let bestFallbackLegacy: { id: string; title: string; depends: string[] } | null = null;
|
|
1471
|
+
let bestFallbackLegacySatisfied = -1;
|
|
1472
|
+
|
|
1434
1473
|
for (const s of activeRoadmap.slices) {
|
|
1435
1474
|
if (s.done) continue;
|
|
1436
1475
|
if (s.depends.every(dep => doneSliceIds.has(dep))) {
|
|
1437
1476
|
activeSlice = { id: s.id, title: s.title };
|
|
1438
1477
|
break;
|
|
1439
1478
|
}
|
|
1479
|
+
// Track best fallback
|
|
1480
|
+
const satisfied = s.depends.filter(dep => doneSliceIds.has(dep)).length;
|
|
1481
|
+
if (satisfied > bestFallbackLegacySatisfied) {
|
|
1482
|
+
bestFallbackLegacy = s;
|
|
1483
|
+
bestFallbackLegacySatisfied = satisfied;
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
// Fallback: if no slice has all deps met, pick the one with the most deps satisfied
|
|
1488
|
+
if (!activeSlice && bestFallbackLegacy) {
|
|
1489
|
+
const unmet = bestFallbackLegacy.depends.filter(dep => !doneSliceIds.has(dep));
|
|
1490
|
+
logWarning("state",
|
|
1491
|
+
`No slice has all deps satisfied — falling back to ${bestFallbackLegacy.id} ` +
|
|
1492
|
+
`(${bestFallbackLegacySatisfied}/${bestFallbackLegacy.depends.length} deps met, ` +
|
|
1493
|
+
`unmet: ${unmet.join(", ")})`,
|
|
1494
|
+
);
|
|
1495
|
+
activeSlice = { id: bestFallbackLegacy.id, title: bestFallbackLegacy.title };
|
|
1440
1496
|
}
|
|
1441
1497
|
}
|
|
1442
1498
|
|
|
@@ -688,8 +688,8 @@ test("autoLoop exits on terminal blocked state", async (t) => {
|
|
|
688
688
|
|
|
689
689
|
assert.ok(deps.callLog.includes("deriveState"), "should have derived state");
|
|
690
690
|
assert.ok(
|
|
691
|
-
deps.callLog.includes("
|
|
692
|
-
"should have called
|
|
691
|
+
deps.callLog.includes("pauseAuto"),
|
|
692
|
+
"should have called pauseAuto for blocked state",
|
|
693
693
|
);
|
|
694
694
|
assert.ok(
|
|
695
695
|
!deps.callLog.includes("resolveDispatch"),
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// GSD-2 — Tests for step-mode completion messages in auto-post-unit
|
|
2
|
+
|
|
3
|
+
import test from "node:test";
|
|
4
|
+
import assert from "node:assert/strict";
|
|
5
|
+
|
|
6
|
+
import { buildStepCompleteMessage, STEP_COMPLETE_FALLBACK_MESSAGE } from "../auto-post-unit.ts";
|
|
7
|
+
import type { GSDState } from "../types.ts";
|
|
8
|
+
|
|
9
|
+
function makeState(overrides: Partial<GSDState>): GSDState {
|
|
10
|
+
return {
|
|
11
|
+
activeMilestone: null,
|
|
12
|
+
activeSlice: null,
|
|
13
|
+
activeTask: null,
|
|
14
|
+
phase: "executing",
|
|
15
|
+
recentDecisions: [],
|
|
16
|
+
blockers: [],
|
|
17
|
+
nextAction: "",
|
|
18
|
+
registry: [],
|
|
19
|
+
...overrides,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
test("buildStepCompleteMessage: milestone complete surfaces review guidance", () => {
|
|
24
|
+
const msg = buildStepCompleteMessage(makeState({ phase: "complete" }));
|
|
25
|
+
assert.match(msg, /milestone finished/);
|
|
26
|
+
assert.match(msg, /\/gsd status/);
|
|
27
|
+
assert.doesNotMatch(msg, /Next:/);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("buildStepCompleteMessage: mid-flight step includes next unit label and /clear hint", () => {
|
|
31
|
+
const state = makeState({
|
|
32
|
+
phase: "executing",
|
|
33
|
+
activeSlice: { id: "S01", title: "Core" },
|
|
34
|
+
activeTask: { id: "T03", title: "Wire notify" },
|
|
35
|
+
});
|
|
36
|
+
const msg = buildStepCompleteMessage(state);
|
|
37
|
+
assert.match(msg, /Next: Execute T03: Wire notify/);
|
|
38
|
+
assert.match(msg, /\/clear/);
|
|
39
|
+
assert.match(msg, /\/gsd to continue/);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("buildStepCompleteMessage: unknown phase falls back to generic continue label", () => {
|
|
43
|
+
// Cast to bypass Phase union so we exercise the default branch of describeNextUnit.
|
|
44
|
+
const state = makeState({ phase: "totally-unknown" as unknown as GSDState["phase"] });
|
|
45
|
+
const msg = buildStepCompleteMessage(state);
|
|
46
|
+
assert.match(msg, /Next: Continue/);
|
|
47
|
+
assert.match(msg, /\/clear/);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("STEP_COMPLETE_FALLBACK_MESSAGE: used when deriveState throws, still points users at /clear + /gsd", () => {
|
|
51
|
+
assert.match(STEP_COMPLETE_FALLBACK_MESSAGE, /\/clear/);
|
|
52
|
+
assert.match(STEP_COMPLETE_FALLBACK_MESSAGE, /\/gsd/);
|
|
53
|
+
});
|
|
@@ -33,8 +33,12 @@ test("bootstrapAutoSession checks manual session override before preferences", (
|
|
|
33
33
|
assert.ok(manualIdx > -1, "auto-start.ts should read session model override first");
|
|
34
34
|
|
|
35
35
|
// resolveDefaultSessionModel() should still be called for fallback behavior
|
|
36
|
-
const preferredIdx = source.indexOf("const preferredModel =
|
|
37
|
-
assert.ok(preferredIdx > -1, "auto-start.ts should
|
|
36
|
+
const preferredIdx = source.indexOf("const preferredModel = ");
|
|
37
|
+
assert.ok(preferredIdx > -1, "auto-start.ts should build preferredModel");
|
|
38
|
+
assert.ok(
|
|
39
|
+
source.indexOf("resolveDefaultSessionModel(") > -1,
|
|
40
|
+
"auto-start.ts should call resolveDefaultSessionModel()",
|
|
41
|
+
);
|
|
38
42
|
|
|
39
43
|
// Session provider should be passed for bare model ID resolution
|
|
40
44
|
const withProviderIdx = source.indexOf("resolveDefaultSessionModel(ctx.model?.provider)");
|
|
@@ -47,6 +51,51 @@ test("bootstrapAutoSession checks manual session override before preferences", (
|
|
|
47
51
|
manualIdx < snapshotIdx && preferredIdx < snapshotIdx,
|
|
48
52
|
"manual override and preference fallback must be resolved before building startModelSnapshot",
|
|
49
53
|
);
|
|
54
|
+
|
|
55
|
+
// The validated preferred model must still appear as one of the snapshot
|
|
56
|
+
// sources so PREFERENCES.md continues to win over a stale settings.json
|
|
57
|
+
// default for built-in providers.
|
|
58
|
+
const snapshotBlock = source.slice(snapshotIdx, snapshotIdx + 400);
|
|
59
|
+
assert.ok(
|
|
60
|
+
snapshotBlock.includes("validatedPreferredModel") || snapshotBlock.includes("preferredModel"),
|
|
61
|
+
"startModelSnapshot must still consider preferredModel for built-in providers",
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("bootstrapAutoSession prefers session model over PREFERENCES.md when provider is custom (#4122)", () => {
|
|
66
|
+
// Custom providers (Ollama, vLLM, OpenAI-compatible proxies) live in
|
|
67
|
+
// ~/.gsd/agent/models.json, not PREFERENCES.md. When the user picks one
|
|
68
|
+
// via /gsd model, that selection must win over any preferredModel from
|
|
69
|
+
// PREFERENCES.md, otherwise auto-mode tries to start a built-in provider
|
|
70
|
+
// the user is not logged into and pauses with "Not logged in".
|
|
71
|
+
const customCheckIdx = source.indexOf("isCustomProvider(ctx.model?.provider)");
|
|
72
|
+
assert.ok(
|
|
73
|
+
customCheckIdx > -1,
|
|
74
|
+
"auto-start.ts should call isCustomProvider() to detect custom-model sessions",
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
// sessionProviderIsCustom must gate preferredModel resolution so that when the
|
|
78
|
+
// session provider is custom, preferredModel is null and PREFERENCES.md is
|
|
79
|
+
// skipped entirely — the snapshot then falls through to ctx.model.
|
|
80
|
+
const gateIdx = source.indexOf("sessionProviderIsCustom");
|
|
81
|
+
assert.ok(gateIdx > -1, "auto-start.ts should bind sessionProviderIsCustom");
|
|
82
|
+
|
|
83
|
+
const preferredIdx = source.indexOf("const preferredModel = ");
|
|
84
|
+
assert.ok(preferredIdx > -1, "auto-start.ts should build preferredModel");
|
|
85
|
+
|
|
86
|
+
const preferredBlock = source.slice(preferredIdx, preferredIdx + 200);
|
|
87
|
+
assert.ok(
|
|
88
|
+
preferredBlock.includes("sessionProviderIsCustom"),
|
|
89
|
+
"preferredModel must be gated on sessionProviderIsCustom so PREFERENCES.md is skipped for custom providers",
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const snapshotIdx = source.indexOf("const startModelSnapshot = ");
|
|
93
|
+
assert.ok(snapshotIdx > -1, "auto-start.ts should build startModelSnapshot");
|
|
94
|
+
|
|
95
|
+
assert.ok(
|
|
96
|
+
customCheckIdx < preferredIdx && preferredIdx < snapshotIdx,
|
|
97
|
+
"isCustomProvider() must be evaluated before preferredModel, which must be resolved before startModelSnapshot",
|
|
98
|
+
);
|
|
50
99
|
});
|
|
51
100
|
|
|
52
101
|
test("bootstrapAutoSession validates preferred model against live registry auth (#unconfigured-models)", () => {
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* complete-milestone-false-merge.test.ts — Regression test for #4175.
|
|
3
|
+
*
|
|
4
|
+
* Before the fix, a failed complete-milestone unit could leave a stub
|
|
5
|
+
* SUMMARY blocker placeholder on disk. stopAuto's SUMMARY-presence check
|
|
6
|
+
* then treated the milestone as complete and merged the worktree branch
|
|
7
|
+
* into main — emitting a misleading metadata-only merge warning for a
|
|
8
|
+
* milestone that was never legitimately finished.
|
|
9
|
+
*
|
|
10
|
+
* The fix has three cooperating parts:
|
|
11
|
+
* 1. stopAuto uses DB status (authoritative) instead of SUMMARY presence
|
|
12
|
+
* when the project DB is available.
|
|
13
|
+
* 2. postUnitPreVerification pauses auto-mode for complete-milestone
|
|
14
|
+
* after retries are exhausted instead of writing a blocker placeholder.
|
|
15
|
+
* 3. recoverTimedOutUnit pauses for complete-milestone instead of
|
|
16
|
+
* writing a blocker placeholder.
|
|
17
|
+
*
|
|
18
|
+
* This test guards all three via source inspection so a future refactor
|
|
19
|
+
* cannot silently reintroduce the false-merge path.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import test from "node:test";
|
|
23
|
+
import assert from "node:assert/strict";
|
|
24
|
+
import { readFileSync } from "node:fs";
|
|
25
|
+
import { join } from "node:path";
|
|
26
|
+
|
|
27
|
+
const gsdDir = join(import.meta.dirname, "..");
|
|
28
|
+
const autoSrc = readFileSync(join(gsdDir, "auto.ts"), "utf-8");
|
|
29
|
+
const postUnitSrc = readFileSync(join(gsdDir, "auto-post-unit.ts"), "utf-8");
|
|
30
|
+
const timeoutSrc = readFileSync(join(gsdDir, "auto-timeout-recovery.ts"), "utf-8");
|
|
31
|
+
|
|
32
|
+
test("#4175: stopAuto uses DB status as the authoritative milestone-complete signal", () => {
|
|
33
|
+
const step4Idx = autoSrc.indexOf("Step 4: Auto-worktree exit");
|
|
34
|
+
assert.ok(step4Idx !== -1, "Step 4 comment exists in stopAuto");
|
|
35
|
+
const step5Idx = autoSrc.indexOf("Step 5:", step4Idx);
|
|
36
|
+
const step4Block = autoSrc.slice(step4Idx, step5Idx);
|
|
37
|
+
|
|
38
|
+
assert.ok(
|
|
39
|
+
step4Block.includes("isDbAvailable()"),
|
|
40
|
+
"Step 4 should branch on isDbAvailable() so DB is consulted when present",
|
|
41
|
+
);
|
|
42
|
+
assert.ok(
|
|
43
|
+
step4Block.includes("getMilestone(s.currentMilestoneId)"),
|
|
44
|
+
"Step 4 should read authoritative milestone status via getMilestone()",
|
|
45
|
+
);
|
|
46
|
+
assert.ok(
|
|
47
|
+
/status\s*===\s*"complete"/.test(step4Block),
|
|
48
|
+
'Step 4 should compare the DB row status to "complete"',
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("#4175: stopAuto imports getMilestone from gsd-db", () => {
|
|
53
|
+
assert.ok(
|
|
54
|
+
/import\s*\{[^}]*\bgetMilestone\b[^}]*\}\s*from\s*"\.\/gsd-db\.js"/.test(autoSrc),
|
|
55
|
+
"auto.ts should import getMilestone from ./gsd-db.js",
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("#4175: stopAuto still falls back to SUMMARY presence when DB is unavailable", () => {
|
|
60
|
+
const step4Idx = autoSrc.indexOf("Step 4: Auto-worktree exit");
|
|
61
|
+
const step5Idx = autoSrc.indexOf("Step 5:", step4Idx);
|
|
62
|
+
const step4Block = autoSrc.slice(step4Idx, step5Idx);
|
|
63
|
+
|
|
64
|
+
assert.ok(
|
|
65
|
+
step4Block.includes("resolveMilestoneFile"),
|
|
66
|
+
"Step 4 should keep SUMMARY-file resolution for DB-unavailable projects",
|
|
67
|
+
);
|
|
68
|
+
assert.ok(
|
|
69
|
+
step4Block.includes("preserveBranch"),
|
|
70
|
+
"Step 4 should still preserve branch for incomplete milestones (fallback path)",
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("#4175: postUnitPreVerification pauses complete-milestone after retries exhausted", () => {
|
|
75
|
+
// The pause branch must live inside the retries-exhausted block, above the
|
|
76
|
+
// writeBlockerPlaceholder call — otherwise the stub SUMMARY is still written.
|
|
77
|
+
const retriesExhaustedIdx = postUnitSrc.indexOf(
|
|
78
|
+
"if (attempt > MAX_VERIFICATION_RETRIES)",
|
|
79
|
+
);
|
|
80
|
+
assert.ok(
|
|
81
|
+
retriesExhaustedIdx !== -1,
|
|
82
|
+
"retries-exhausted guard exists in postUnitPreVerification",
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const blockerCallIdx = postUnitSrc.indexOf("writeBlockerPlaceholder", retriesExhaustedIdx);
|
|
86
|
+
assert.ok(
|
|
87
|
+
blockerCallIdx !== -1,
|
|
88
|
+
"blocker placeholder call still exists for non-milestone units",
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const exhaustedBlock = postUnitSrc.slice(retriesExhaustedIdx, blockerCallIdx);
|
|
92
|
+
|
|
93
|
+
assert.ok(
|
|
94
|
+
/s\.currentUnit\.type\s*===\s*"complete-milestone"/.test(exhaustedBlock),
|
|
95
|
+
"retries-exhausted block should specifically handle complete-milestone",
|
|
96
|
+
);
|
|
97
|
+
assert.ok(
|
|
98
|
+
/pauseAuto\s*\(\s*ctx\s*,\s*pi\s*\)/.test(exhaustedBlock),
|
|
99
|
+
"complete-milestone path should call pauseAuto instead of falling through",
|
|
100
|
+
);
|
|
101
|
+
// The pause branch must return so execution never reaches writeBlockerPlaceholder.
|
|
102
|
+
assert.ok(
|
|
103
|
+
/return\s+"dispatched"\s*;/.test(exhaustedBlock),
|
|
104
|
+
"complete-milestone pause branch should return before the placeholder call",
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("#4175: recoverTimedOutUnit pauses complete-milestone instead of writing a blocker placeholder", () => {
|
|
109
|
+
// The complete-milestone pause branch must sit immediately above the
|
|
110
|
+
// "retries exhausted" writeBlockerPlaceholder call so a failed
|
|
111
|
+
// complete-milestone never produces a stub SUMMARY. Anchor on the
|
|
112
|
+
// comment that precedes that specific placeholder call rather than the
|
|
113
|
+
// function's earlier writeBlockerPlaceholder use sites or its import.
|
|
114
|
+
// Use lastIndexOf so we find the final retries-exhausted block in
|
|
115
|
+
// recoverTimedOutUnit, not an earlier helper with the same comment.
|
|
116
|
+
const exhaustedAnchor = "Retries exhausted — write a blocker placeholder";
|
|
117
|
+
const exhaustedIdx = timeoutSrc.lastIndexOf(exhaustedAnchor);
|
|
118
|
+
assert.ok(
|
|
119
|
+
exhaustedIdx !== -1,
|
|
120
|
+
"retries-exhausted blocker-placeholder path still exists for non-milestone units",
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const guardIdx = timeoutSrc.lastIndexOf(
|
|
124
|
+
'unitType === "complete-milestone"',
|
|
125
|
+
exhaustedIdx,
|
|
126
|
+
);
|
|
127
|
+
assert.ok(
|
|
128
|
+
guardIdx !== -1,
|
|
129
|
+
"complete-milestone guard should appear above the retries-exhausted placeholder call",
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const guardBlock = timeoutSrc.slice(guardIdx, exhaustedIdx);
|
|
133
|
+
assert.ok(
|
|
134
|
+
/return\s+"paused"\s*;/.test(guardBlock),
|
|
135
|
+
"complete-milestone guard should return 'paused' before the placeholder call",
|
|
136
|
+
);
|
|
137
|
+
// The guard itself must not call writeBlockerPlaceholder.
|
|
138
|
+
assert.ok(
|
|
139
|
+
!guardBlock.includes("writeBlockerPlaceholder"),
|
|
140
|
+
"complete-milestone guard must not write a blocker placeholder",
|
|
141
|
+
);
|
|
142
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression test for #4129: tasks.completed_at stays NULL when status is
|
|
3
|
+
* reconciled to 'complete' via the file-existence path in state.ts.
|
|
4
|
+
*
|
|
5
|
+
* Root cause: reconcileSliceTasks called
|
|
6
|
+
* updateTaskStatus(milestoneId, sliceId, t.id, "complete")
|
|
7
|
+
* without a completedAt timestamp, so the column stays NULL.
|
|
8
|
+
*
|
|
9
|
+
* Fix: pass new Date().toISOString() as the 5th argument.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, test } from "node:test";
|
|
13
|
+
import assert from "node:assert/strict";
|
|
14
|
+
import { readFileSync } from "node:fs";
|
|
15
|
+
import { join, dirname } from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
|
|
18
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const stateSource = readFileSync(join(__dirname, "..", "state.ts"), "utf-8");
|
|
20
|
+
|
|
21
|
+
describe("completed-at reconcile (#4129)", () => {
|
|
22
|
+
test("reconcileSliceTasks passes a completedAt timestamp when setting status to complete", () => {
|
|
23
|
+
// Before the fix, state.ts had:
|
|
24
|
+
// updateTaskStatus(milestoneId, sliceId, t.id, "complete")
|
|
25
|
+
// which leaves completed_at NULL in the DB.
|
|
26
|
+
// After the fix, a timestamp must be passed as the 5th argument.
|
|
27
|
+
assert.doesNotMatch(
|
|
28
|
+
stateSource,
|
|
29
|
+
/updateTaskStatus\(\s*milestoneId\s*,\s*sliceId\s*,\s*t\.id\s*,\s*["']complete["']\s*\)/,
|
|
30
|
+
"updateTaskStatus must not be called without a completedAt timestamp when reconciling tasks to 'complete' (#4129)",
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("reconcileSliceTasks passes new Date().toISOString() as the completedAt argument", () => {
|
|
35
|
+
// Positive assertion: the fixed call must include a timestamp.
|
|
36
|
+
assert.match(
|
|
37
|
+
stateSource,
|
|
38
|
+
/updateTaskStatus\(\s*milestoneId\s*,\s*sliceId\s*,\s*t\.id\s*,\s*["']complete["']\s*,\s*new Date\(\)\.toISOString\(\)\s*\)/,
|
|
39
|
+
"reconcileSliceTasks must pass new Date().toISOString() as completedAt when setting task status to 'complete' (#4129)",
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
});
|