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
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression tests for #3348 secondary issues — crash handler gaps surfaced after #3696
|
|
3
|
+
*
|
|
4
|
+
* 1. register-extension.ts: writeCrashLog writes to ~/.gsd/crash/ directory
|
|
5
|
+
* 2. register-extension.ts: _gsdRejectionGuard registered for unhandledRejection
|
|
6
|
+
* 3. register-extension.ts: _gsdEpipeGuard exits with code 1 for unrecoverable errors (no log-and-continue)
|
|
7
|
+
* 4. crash-recovery.ts: emitCrashRecoveredUnitEnd closes open unit-start journal entries
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, test } from 'node:test';
|
|
11
|
+
import assert from 'node:assert/strict';
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync } from 'node:fs';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { tmpdir } from 'node:os';
|
|
15
|
+
import { randomUUID } from 'node:crypto';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
import { dirname } from 'node:path';
|
|
18
|
+
|
|
19
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
20
|
+
const __dirname = dirname(__filename);
|
|
21
|
+
|
|
22
|
+
function makeTmpBase(): string {
|
|
23
|
+
const base = join(tmpdir(), `gsd-test-${randomUUID()}`);
|
|
24
|
+
mkdirSync(join(base, '.gsd'), { recursive: true });
|
|
25
|
+
return base;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ─── register-extension source assertions ────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
const registerExtSrc = readFileSync(
|
|
31
|
+
join(__dirname, '..', 'bootstrap', 'register-extension.ts'),
|
|
32
|
+
'utf-8',
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
describe('register-extension crash handler secondary fixes (#3348)', () => {
|
|
36
|
+
test('writeCrashLog is exported and writes a file to the crash directory', async () => {
|
|
37
|
+
// Dynamic import so GSD_HOME can be pointed at a temp dir without polluting ~/.gsd
|
|
38
|
+
const tmpHome = join(tmpdir(), `gsd-crash-test-${randomUUID()}`);
|
|
39
|
+
const origHome = process.env.GSD_HOME;
|
|
40
|
+
process.env.GSD_HOME = tmpHome;
|
|
41
|
+
try {
|
|
42
|
+
const { writeCrashLog } = await import('../bootstrap/crash-log.ts');
|
|
43
|
+
const err = new Error('test crash from secondary regression test');
|
|
44
|
+
writeCrashLog(err, 'uncaughtException');
|
|
45
|
+
|
|
46
|
+
const crashDir = join(tmpHome, 'crash');
|
|
47
|
+
assert.ok(existsSync(crashDir), 'crash directory should be created');
|
|
48
|
+
|
|
49
|
+
const logs = readdirSync(crashDir).filter((f) => f.endsWith('.log'));
|
|
50
|
+
assert.equal(logs.length, 1, 'exactly one crash log should be written');
|
|
51
|
+
|
|
52
|
+
const content = readFileSync(join(crashDir, logs[0]), 'utf-8');
|
|
53
|
+
assert.ok(content.includes('test crash from secondary regression test'), 'log should contain error message');
|
|
54
|
+
assert.ok(content.includes('uncaughtException'), 'log should identify the source');
|
|
55
|
+
assert.ok(content.includes('pid:'), 'log should include process pid');
|
|
56
|
+
} finally {
|
|
57
|
+
process.env.GSD_HOME = origHome;
|
|
58
|
+
rmSync(tmpHome, { recursive: true, force: true });
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('_gsdRejectionGuard is registered for unhandledRejection', () => {
|
|
63
|
+
assert.match(
|
|
64
|
+
registerExtSrc,
|
|
65
|
+
/_gsdRejectionGuard/,
|
|
66
|
+
'_gsdRejectionGuard handler should be defined',
|
|
67
|
+
);
|
|
68
|
+
assert.match(
|
|
69
|
+
registerExtSrc,
|
|
70
|
+
/unhandledRejection/,
|
|
71
|
+
'installEpipeGuard should register an unhandledRejection handler',
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('_gsdEpipeGuard calls process.exit(1) for unrecoverable errors, not log-and-continue', () => {
|
|
76
|
+
// The original #3696 fix replaced "throw err" with a log-and-continue.
|
|
77
|
+
// The secondary fix replaces that with writeCrashLog + process.exit(1).
|
|
78
|
+
assert.ok(
|
|
79
|
+
!registerExtSrc.includes('process.stderr.write(`[gsd] uncaught extension error (non-fatal)'),
|
|
80
|
+
'_gsdEpipeGuard should NOT log errors as non-fatal and continue',
|
|
81
|
+
);
|
|
82
|
+
assert.match(
|
|
83
|
+
registerExtSrc,
|
|
84
|
+
/process\.exit\(1\)/,
|
|
85
|
+
'_gsdEpipeGuard should call process.exit(1) for unrecoverable errors',
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('writeCrashLog never throws even when directory is unwritable', async () => {
|
|
90
|
+
const { writeCrashLog } = await import('../bootstrap/crash-log.ts');
|
|
91
|
+
const origHome = process.env.GSD_HOME;
|
|
92
|
+
// Point at a path that will fail to mkdir (e.g. a file that exists as non-dir)
|
|
93
|
+
const tmpFile = join(tmpdir(), `gsd-not-a-dir-${randomUUID()}`);
|
|
94
|
+
// Don't create it — mkdirSync with bad path should be caught internally
|
|
95
|
+
process.env.GSD_HOME = join(tmpFile, 'nested', 'deeply');
|
|
96
|
+
try {
|
|
97
|
+
// Should not throw
|
|
98
|
+
assert.doesNotThrow(() => {
|
|
99
|
+
writeCrashLog(new Error('should not throw'), 'test');
|
|
100
|
+
});
|
|
101
|
+
} finally {
|
|
102
|
+
process.env.GSD_HOME = origHome;
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// ─── emitCrashRecoveredUnitEnd ────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
describe('emitCrashRecoveredUnitEnd (#3348)', () => {
|
|
110
|
+
test('emits synthetic unit-end when unit-start has no matching unit-end', async () => {
|
|
111
|
+
const base = makeTmpBase();
|
|
112
|
+
try {
|
|
113
|
+
const { emitJournalEvent, queryJournal } = await import('../journal.ts');
|
|
114
|
+
const { emitCrashRecoveredUnitEnd } = await import('../crash-recovery.ts');
|
|
115
|
+
|
|
116
|
+
const flowId = randomUUID();
|
|
117
|
+
const unitStartSeq = 5;
|
|
118
|
+
|
|
119
|
+
// Emit a unit-start with no corresponding unit-end (simulating a crash)
|
|
120
|
+
emitJournalEvent(base, {
|
|
121
|
+
ts: new Date().toISOString(),
|
|
122
|
+
flowId,
|
|
123
|
+
seq: unitStartSeq,
|
|
124
|
+
eventType: 'unit-start',
|
|
125
|
+
data: { unitType: 'execute-task', unitId: 'M001/S01/T01' },
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const lock = {
|
|
129
|
+
pid: 99999,
|
|
130
|
+
startedAt: new Date().toISOString(),
|
|
131
|
+
unitType: 'execute-task',
|
|
132
|
+
unitId: 'M001/S01/T01',
|
|
133
|
+
unitStartedAt: new Date().toISOString(),
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
emitCrashRecoveredUnitEnd(base, lock);
|
|
137
|
+
|
|
138
|
+
const events = queryJournal(base);
|
|
139
|
+
const ends = events.filter((e) => e.eventType === 'unit-end');
|
|
140
|
+
assert.equal(ends.length, 1, 'should emit exactly one unit-end');
|
|
141
|
+
assert.equal(ends[0].data?.unitId, 'M001/S01/T01');
|
|
142
|
+
assert.equal(ends[0].data?.status, 'crash-recovered');
|
|
143
|
+
assert.equal(ends[0].causedBy?.flowId, flowId);
|
|
144
|
+
assert.equal(ends[0].causedBy?.seq, unitStartSeq);
|
|
145
|
+
assert.ok(ends[0].seq > unitStartSeq, 'unit-end seq must be higher than unit-start seq');
|
|
146
|
+
} finally {
|
|
147
|
+
rmSync(base, { recursive: true, force: true });
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('is a no-op when unit-end was already emitted (e.g. hard timeout fired)', async () => {
|
|
152
|
+
const base = makeTmpBase();
|
|
153
|
+
try {
|
|
154
|
+
const { emitJournalEvent, queryJournal } = await import('../journal.ts');
|
|
155
|
+
const { emitCrashRecoveredUnitEnd } = await import('../crash-recovery.ts');
|
|
156
|
+
|
|
157
|
+
const flowId = randomUUID();
|
|
158
|
+
emitJournalEvent(base, {
|
|
159
|
+
ts: new Date().toISOString(),
|
|
160
|
+
flowId,
|
|
161
|
+
seq: 3,
|
|
162
|
+
eventType: 'unit-start',
|
|
163
|
+
data: { unitType: 'plan-slice', unitId: 'M001/S02' },
|
|
164
|
+
});
|
|
165
|
+
// Hard timeout already emitted a unit-end
|
|
166
|
+
emitJournalEvent(base, {
|
|
167
|
+
ts: new Date().toISOString(),
|
|
168
|
+
flowId,
|
|
169
|
+
seq: 4,
|
|
170
|
+
eventType: 'unit-end',
|
|
171
|
+
data: { unitType: 'plan-slice', unitId: 'M001/S02', status: 'cancelled' },
|
|
172
|
+
causedBy: { flowId, seq: 3 },
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const lock = {
|
|
176
|
+
pid: 99999,
|
|
177
|
+
startedAt: new Date().toISOString(),
|
|
178
|
+
unitType: 'plan-slice',
|
|
179
|
+
unitId: 'M001/S02',
|
|
180
|
+
unitStartedAt: new Date().toISOString(),
|
|
181
|
+
};
|
|
182
|
+
emitCrashRecoveredUnitEnd(base, lock);
|
|
183
|
+
|
|
184
|
+
const ends = queryJournal(base).filter((e) => e.eventType === 'unit-end');
|
|
185
|
+
assert.equal(ends.length, 1, 'should not emit a duplicate unit-end');
|
|
186
|
+
assert.equal(ends[0].data?.status, 'cancelled', 'original unit-end should be preserved');
|
|
187
|
+
} finally {
|
|
188
|
+
rmSync(base, { recursive: true, force: true });
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('is a no-op for "starting" pseudo-units (bootstrap crash)', async () => {
|
|
193
|
+
const base = makeTmpBase();
|
|
194
|
+
try {
|
|
195
|
+
const { queryJournal } = await import('../journal.ts');
|
|
196
|
+
const { emitCrashRecoveredUnitEnd } = await import('../crash-recovery.ts');
|
|
197
|
+
|
|
198
|
+
const lock = {
|
|
199
|
+
pid: 99999,
|
|
200
|
+
startedAt: new Date().toISOString(),
|
|
201
|
+
unitType: 'starting',
|
|
202
|
+
unitId: 'bootstrap',
|
|
203
|
+
unitStartedAt: new Date().toISOString(),
|
|
204
|
+
};
|
|
205
|
+
emitCrashRecoveredUnitEnd(base, lock);
|
|
206
|
+
|
|
207
|
+
const events = queryJournal(base);
|
|
208
|
+
assert.equal(events.length, 0, 'should emit nothing for starting/bootstrap pseudo-units');
|
|
209
|
+
} finally {
|
|
210
|
+
rmSync(base, { recursive: true, force: true });
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('is a no-op when no unit-start exists in the journal', async () => {
|
|
215
|
+
const base = makeTmpBase();
|
|
216
|
+
try {
|
|
217
|
+
const { queryJournal } = await import('../journal.ts');
|
|
218
|
+
const { emitCrashRecoveredUnitEnd } = await import('../crash-recovery.ts');
|
|
219
|
+
|
|
220
|
+
const lock = {
|
|
221
|
+
pid: 99999,
|
|
222
|
+
startedAt: new Date().toISOString(),
|
|
223
|
+
unitType: 'execute-task',
|
|
224
|
+
unitId: 'M002/S01/T03',
|
|
225
|
+
unitStartedAt: new Date().toISOString(),
|
|
226
|
+
};
|
|
227
|
+
emitCrashRecoveredUnitEnd(base, lock);
|
|
228
|
+
|
|
229
|
+
const events = queryJournal(base);
|
|
230
|
+
assert.equal(events.length, 0, 'should emit nothing when there is no journal entry to close');
|
|
231
|
+
} finally {
|
|
232
|
+
rmSync(base, { recursive: true, force: true });
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
});
|
|
@@ -351,8 +351,9 @@ skills_used: []
|
|
|
351
351
|
const dbState = await deriveStateFromDb(base);
|
|
352
352
|
|
|
353
353
|
assertStatesEqual(dbState, fileState, 'E-blocked');
|
|
354
|
-
|
|
355
|
-
assert.
|
|
354
|
+
// With partial-dep fallback, circular deps no longer block — fallback picks first eligible slice
|
|
355
|
+
assert.deepStrictEqual(dbState.phase, 'planning', 'E-blocked: phase is planning (fallback picks a slice)');
|
|
356
|
+
assert.ok(dbState.activeSlice !== null, 'E-blocked: activeSlice is set via fallback');
|
|
356
357
|
|
|
357
358
|
closeDatabase();
|
|
358
359
|
} finally {
|
|
@@ -616,9 +616,10 @@ describe('derive-state-db', async () => {
|
|
|
616
616
|
invalidateStateCache();
|
|
617
617
|
const dbState = await deriveStateFromDb(base);
|
|
618
618
|
|
|
619
|
-
|
|
619
|
+
// With partial-dep fallback, circular deps no longer block — fallback picks first eligible slice
|
|
620
|
+
assert.deepStrictEqual(dbState.phase, 'planning', 'blocked-db: phase is planning (fallback picks a slice)');
|
|
620
621
|
assert.deepStrictEqual(dbState.phase, fileState.phase, 'blocked-db: phase matches filesystem');
|
|
621
|
-
assert.ok(dbState.
|
|
622
|
+
assert.ok(dbState.activeSlice !== null, 'blocked-db: activeSlice is set via fallback');
|
|
622
623
|
|
|
623
624
|
closeDatabase();
|
|
624
625
|
} finally {
|
|
@@ -307,27 +307,87 @@ describe('derive-state-helpers', () => {
|
|
|
307
307
|
}
|
|
308
308
|
});
|
|
309
309
|
|
|
310
|
-
// ─── buildCompletenessSet:
|
|
311
|
-
test('buildCompletenessSet:
|
|
310
|
+
// ─── buildCompletenessSet: DB status is authoritative ──────────────
|
|
311
|
+
test('buildCompletenessSet: DB status=complete marks milestone complete', async () => {
|
|
312
312
|
const base = createFixtureBase();
|
|
313
313
|
try {
|
|
314
|
-
// M001 has summary on disk but DB status is still 'active'
|
|
315
314
|
writeFile(base, 'milestones/M001/M001-ROADMAP.md', ROADMAP_CONTENT);
|
|
316
315
|
writeFile(base, 'milestones/M001/M001-SUMMARY.md', '# M001 Summary\n\nDone.');
|
|
317
|
-
// M002 is the real active milestone
|
|
318
316
|
writeFile(base, 'milestones/M002/M002-CONTEXT.md', '# M002\n\nActive.');
|
|
319
317
|
|
|
320
318
|
openDatabase(':memory:');
|
|
321
|
-
insertMilestone({ id: 'M001', title: 'First', status: '
|
|
319
|
+
insertMilestone({ id: 'M001', title: 'First', status: 'complete' });
|
|
322
320
|
insertMilestone({ id: 'M002', title: 'Second', status: 'active' });
|
|
323
321
|
|
|
324
322
|
invalidateStateCache();
|
|
325
323
|
const state = await deriveStateFromDb(base);
|
|
326
324
|
|
|
327
|
-
// M001 should be complete (summary on disk), M002 should be active
|
|
328
325
|
const m1 = state.registry.find(e => e.id === 'M001');
|
|
329
|
-
assert.equal(m1?.status, 'complete', '
|
|
330
|
-
assert.equal(state.activeMilestone?.id, 'M002', '
|
|
326
|
+
assert.equal(m1?.status, 'complete', 'DB status=complete → registry entry complete');
|
|
327
|
+
assert.equal(state.activeMilestone?.id, 'M002', 'M002 is the active milestone');
|
|
328
|
+
} finally {
|
|
329
|
+
closeDatabase();
|
|
330
|
+
cleanup(base);
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// ─── Regression #4179: orphan SUMMARY must NOT flip DB-active milestone ───
|
|
335
|
+
// A crashed complete-milestone turn (or stale/manual SUMMARY.md) can leave
|
|
336
|
+
// a milestone SUMMARY on disk while the DB row still reads 'active'. The
|
|
337
|
+
// read-side of state derivation must NOT treat the orphan SUMMARY as a
|
|
338
|
+
// completion signal, or the auto-loop advances and merges work that was
|
|
339
|
+
// never actually finished (same failure class as #4175, read-side twin).
|
|
340
|
+
test('buildCompletenessSet (#4179): orphan SUMMARY on disk does not mark DB-active milestone complete', async () => {
|
|
341
|
+
const base = createFixtureBase();
|
|
342
|
+
try {
|
|
343
|
+
writeFile(base, 'milestones/M001/M001-ROADMAP.md', ROADMAP_CONTENT);
|
|
344
|
+
writeFile(base, 'milestones/M001/M001-SUMMARY.md', '# M001 Orphan Summary\n\nLeft over from crashed turn.');
|
|
345
|
+
|
|
346
|
+
openDatabase(':memory:');
|
|
347
|
+
insertMilestone({ id: 'M001', title: 'First', status: 'active' });
|
|
348
|
+
// Slice still in-flight — auto should resume, not merge.
|
|
349
|
+
insertSlice({ id: 'S01', milestoneId: 'M001', title: 'First', status: 'active', risk: 'low', depends: [] });
|
|
350
|
+
insertSlice({ id: 'S02', milestoneId: 'M001', title: 'Second', status: 'pending', risk: 'low', depends: ['S01'] });
|
|
351
|
+
insertTask({ id: 'T01', sliceId: 'S01', milestoneId: 'M001', title: 'In-flight', status: 'pending' });
|
|
352
|
+
|
|
353
|
+
invalidateStateCache();
|
|
354
|
+
const state = await deriveStateFromDb(base);
|
|
355
|
+
|
|
356
|
+
const m1 = state.registry.find(e => e.id === 'M001');
|
|
357
|
+
assert.notEqual(m1?.status, 'complete', 'orphan SUMMARY must not mark milestone complete');
|
|
358
|
+
assert.equal(m1?.status, 'active', 'M001 remains active — DB is authoritative');
|
|
359
|
+
assert.equal(state.activeMilestone?.id, 'M001', 'M001 is still the active milestone');
|
|
360
|
+
assert.notEqual(state.phase, 'completing-milestone', 'must not short-circuit into completion');
|
|
361
|
+
} finally {
|
|
362
|
+
closeDatabase();
|
|
363
|
+
cleanup(base);
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// Regression #4179 (companion): DB-active milestone with all slices done +
|
|
368
|
+
// validation terminal + orphan SUMMARY must still flow through completing-milestone
|
|
369
|
+
// (re-runs complete-milestone), not be reported as already-complete.
|
|
370
|
+
test('buildRegistryAndFindActive (#4179): orphan SUMMARY with validation-terminal falls through to completing-milestone', async () => {
|
|
371
|
+
const base = createFixtureBase();
|
|
372
|
+
try {
|
|
373
|
+
writeFile(base, 'milestones/M001/M001-ROADMAP.md', ROADMAP_CONTENT);
|
|
374
|
+
writeFile(base, 'milestones/M001/slices/S01/S01-PLAN.md', PLAN_CONTENT);
|
|
375
|
+
writeFile(base, 'milestones/M001/slices/S02/S02-PLAN.md', PLAN_CONTENT);
|
|
376
|
+
writeFile(base, 'milestones/M001/M001-VALIDATION.md', '---\nverdict: passed\n---\n# Validation\nAll good.');
|
|
377
|
+
writeFile(base, 'milestones/M001/M001-SUMMARY.md', '# M001 Orphan Summary\n\nLeft over.');
|
|
378
|
+
|
|
379
|
+
openDatabase(':memory:');
|
|
380
|
+
insertMilestone({ id: 'M001', title: 'First', status: 'active' });
|
|
381
|
+
insertSlice({ id: 'S01', milestoneId: 'M001', title: 'First', status: 'complete', risk: 'low', depends: [] });
|
|
382
|
+
insertSlice({ id: 'S02', milestoneId: 'M001', title: 'Second', status: 'complete', risk: 'low', depends: ['S01'] });
|
|
383
|
+
|
|
384
|
+
invalidateStateCache();
|
|
385
|
+
const state = await deriveStateFromDb(base);
|
|
386
|
+
|
|
387
|
+
const m1 = state.registry.find(e => e.id === 'M001');
|
|
388
|
+
assert.equal(m1?.status, 'active', 'M001 stays active despite orphan SUMMARY + validation-terminal');
|
|
389
|
+
assert.equal(state.activeMilestone?.id, 'M001', 'M001 is still the active milestone');
|
|
390
|
+
assert.equal(state.phase, 'completing-milestone', 'phase flows through completing-milestone (re-run)');
|
|
331
391
|
} finally {
|
|
332
392
|
closeDatabase();
|
|
333
393
|
cleanup(base);
|
|
@@ -446,9 +446,9 @@ Continue from step 2.
|
|
|
446
446
|
|
|
447
447
|
const state2 = await deriveState(base2);
|
|
448
448
|
|
|
449
|
-
|
|
450
|
-
assert.deepStrictEqual(state2.
|
|
451
|
-
assert.
|
|
449
|
+
// With partial-dep fallback, S01 is picked despite unmet dep on S99
|
|
450
|
+
assert.deepStrictEqual(state2.phase, 'planning', 'blocked-B: phase is planning (fallback picks S01)');
|
|
451
|
+
assert.deepStrictEqual(state2.activeSlice?.id, 'S01', 'blocked-B: activeSlice is S01 via fallback');
|
|
452
452
|
} finally {
|
|
453
453
|
cleanup(base2);
|
|
454
454
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { describe, test } from "node:test";
|
|
8
8
|
import assert from "node:assert/strict";
|
|
9
|
-
import { isFlatRateProvider, resolvePreferredModelConfig } from "../auto-model-selection.ts";
|
|
9
|
+
import { buildFlatRateContext, isFlatRateProvider, resolvePreferredModelConfig } from "../auto-model-selection.ts";
|
|
10
10
|
|
|
11
11
|
describe("flat-rate provider routing guard (#3453)", () => {
|
|
12
12
|
|
|
@@ -48,3 +48,139 @@ describe("flat-rate provider routing guard (#3453)", () => {
|
|
|
48
48
|
assert.equal(result, undefined, "Should not create routing config for copilot");
|
|
49
49
|
});
|
|
50
50
|
});
|
|
51
|
+
|
|
52
|
+
describe("flat-rate provider extensibility (any/all/custom)", () => {
|
|
53
|
+
test("regression: built-in providers still flat-rate with no context", () => {
|
|
54
|
+
assert.equal(isFlatRateProvider("github-copilot"), true);
|
|
55
|
+
assert.equal(isFlatRateProvider("copilot"), true);
|
|
56
|
+
assert.equal(isFlatRateProvider("claude-code"), true);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("regression: non-flat-rate API providers return false with no context", () => {
|
|
60
|
+
assert.equal(isFlatRateProvider("anthropic"), false);
|
|
61
|
+
assert.equal(isFlatRateProvider("openai"), false);
|
|
62
|
+
assert.equal(isFlatRateProvider("google-vertex"), false);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("auto-detection: externalCli auth mode marks provider flat-rate", () => {
|
|
66
|
+
// Any provider registered with authMode: "externalCli" is a local
|
|
67
|
+
// CLI wrapper around the user's subscription — every request costs
|
|
68
|
+
// the same regardless of model, so dynamic routing provides no benefit.
|
|
69
|
+
assert.equal(
|
|
70
|
+
isFlatRateProvider("my-private-cli", { authMode: "externalCli" }),
|
|
71
|
+
true,
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("auto-detection: non-externalCli auth modes do not mark provider flat-rate", () => {
|
|
76
|
+
assert.equal(
|
|
77
|
+
isFlatRateProvider("my-http-proxy", { authMode: "apiKey" }),
|
|
78
|
+
false,
|
|
79
|
+
);
|
|
80
|
+
assert.equal(
|
|
81
|
+
isFlatRateProvider("my-http-proxy", { authMode: "oauth" }),
|
|
82
|
+
false,
|
|
83
|
+
);
|
|
84
|
+
assert.equal(
|
|
85
|
+
isFlatRateProvider("my-http-proxy", { authMode: "none" }),
|
|
86
|
+
false,
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("user preference: custom provider listed in userFlatRate is flat-rate", () => {
|
|
91
|
+
assert.equal(
|
|
92
|
+
isFlatRateProvider("my-ollama-proxy", { userFlatRate: ["my-ollama-proxy"] }),
|
|
93
|
+
true,
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("user preference: case-insensitive match against userFlatRate list", () => {
|
|
98
|
+
assert.equal(
|
|
99
|
+
isFlatRateProvider("My-Proxy", { userFlatRate: ["my-proxy"] }),
|
|
100
|
+
true,
|
|
101
|
+
);
|
|
102
|
+
assert.equal(
|
|
103
|
+
isFlatRateProvider("my-proxy", { userFlatRate: ["MY-PROXY"] }),
|
|
104
|
+
true,
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("user preference: provider not in userFlatRate list is not flat-rate", () => {
|
|
109
|
+
assert.equal(
|
|
110
|
+
isFlatRateProvider("other-proxy", { userFlatRate: ["my-proxy"] }),
|
|
111
|
+
false,
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("combined signals: built-in list wins even when context is empty", () => {
|
|
116
|
+
assert.equal(
|
|
117
|
+
isFlatRateProvider("claude-code", { authMode: "apiKey", userFlatRate: [] }),
|
|
118
|
+
true,
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("combined signals: externalCli auto-detection wins alongside userFlatRate miss", () => {
|
|
123
|
+
assert.equal(
|
|
124
|
+
isFlatRateProvider("my-cli", {
|
|
125
|
+
authMode: "externalCli",
|
|
126
|
+
userFlatRate: ["a-different-cli"],
|
|
127
|
+
}),
|
|
128
|
+
true,
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe("buildFlatRateContext()", () => {
|
|
134
|
+
test("builds a context from ctx.modelRegistry.getProviderAuthMode + prefs", () => {
|
|
135
|
+
const ctx = {
|
|
136
|
+
modelRegistry: {
|
|
137
|
+
getProviderAuthMode: (p: string) =>
|
|
138
|
+
p === "my-cli" ? "externalCli" : "apiKey",
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
const prefs = { flat_rate_providers: ["my-proxy"] };
|
|
142
|
+
|
|
143
|
+
const ctxForCli = buildFlatRateContext("my-cli", ctx, prefs);
|
|
144
|
+
assert.equal(ctxForCli.authMode, "externalCli");
|
|
145
|
+
assert.deepEqual(ctxForCli.userFlatRate, ["my-proxy"]);
|
|
146
|
+
assert.equal(isFlatRateProvider("my-cli", ctxForCli), true);
|
|
147
|
+
|
|
148
|
+
const ctxForProxy = buildFlatRateContext("my-proxy", ctx, prefs);
|
|
149
|
+
assert.equal(ctxForProxy.authMode, "apiKey");
|
|
150
|
+
assert.equal(isFlatRateProvider("my-proxy", ctxForProxy), true);
|
|
151
|
+
|
|
152
|
+
const ctxForOther = buildFlatRateContext("anthropic", ctx, prefs);
|
|
153
|
+
assert.equal(ctxForOther.authMode, "apiKey");
|
|
154
|
+
assert.equal(isFlatRateProvider("anthropic", ctxForOther), false);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("survives missing ctx and missing prefs", () => {
|
|
158
|
+
const empty = buildFlatRateContext("anything");
|
|
159
|
+
assert.equal(empty.authMode, undefined);
|
|
160
|
+
assert.equal(empty.userFlatRate, undefined);
|
|
161
|
+
assert.equal(isFlatRateProvider("anything", empty), false);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("survives a registry lookup that throws", () => {
|
|
165
|
+
const ctx = {
|
|
166
|
+
modelRegistry: {
|
|
167
|
+
getProviderAuthMode: () => {
|
|
168
|
+
throw new Error("registry boom");
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
const result = buildFlatRateContext("anything", ctx);
|
|
173
|
+
// Error must be swallowed — authMode left undefined, function returns.
|
|
174
|
+
assert.equal(result.authMode, undefined);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("registry returning a non-canonical auth mode is ignored", () => {
|
|
178
|
+
const ctx = {
|
|
179
|
+
modelRegistry: {
|
|
180
|
+
getProviderAuthMode: () => "weird-mode",
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
const result = buildFlatRateContext("anything", ctx);
|
|
184
|
+
assert.equal(result.authMode, undefined);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
@@ -15,10 +15,14 @@ import {
|
|
|
15
15
|
getRequirementById,
|
|
16
16
|
getActiveDecisions,
|
|
17
17
|
getActiveRequirements,
|
|
18
|
-
getTask,
|
|
19
18
|
transaction,
|
|
20
19
|
_getAdapter,
|
|
21
20
|
_resetProvider,
|
|
21
|
+
insertMilestone,
|
|
22
|
+
insertSlice,
|
|
23
|
+
insertTask,
|
|
24
|
+
getTask,
|
|
25
|
+
getSliceTasks,
|
|
22
26
|
} from '../gsd-db.ts';
|
|
23
27
|
|
|
24
28
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -460,6 +464,60 @@ describe('gsd-db', () => {
|
|
|
460
464
|
assert.ok(!wasDbOpenAttempted(), 'wasDbOpenAttempted should reset after closeDatabase');
|
|
461
465
|
});
|
|
462
466
|
|
|
467
|
+
test('gsd-db: rowToTask tolerates corrupt comma-separated task arrays', () => {
|
|
468
|
+
openDatabase(':memory:');
|
|
469
|
+
insertMilestone({ id: 'M001', status: 'active' });
|
|
470
|
+
insertSlice({ milestoneId: 'M001', id: 'S01', status: 'active' });
|
|
471
|
+
insertTask({
|
|
472
|
+
milestoneId: 'M001',
|
|
473
|
+
sliceId: 'S01',
|
|
474
|
+
id: 'T01',
|
|
475
|
+
title: 'Recover corrupt arrays',
|
|
476
|
+
planning: {
|
|
477
|
+
description: 'desc',
|
|
478
|
+
estimate: 'small',
|
|
479
|
+
files: ['src/original.ts'],
|
|
480
|
+
verify: 'npm test',
|
|
481
|
+
inputs: ['docs/original.md'],
|
|
482
|
+
expectedOutput: ['dist/original.md'],
|
|
483
|
+
observabilityImpact: '',
|
|
484
|
+
},
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
const adapter = _getAdapter()!;
|
|
488
|
+
adapter.prepare(
|
|
489
|
+
`UPDATE tasks
|
|
490
|
+
SET files = ?, inputs = ?, expected_output = ?, key_files = ?, key_decisions = ?
|
|
491
|
+
WHERE milestone_id = ? AND slice_id = ? AND id = ?`,
|
|
492
|
+
).run(
|
|
493
|
+
'src-erf/Models/foo.cs, src-erf/Models/bar.cs',
|
|
494
|
+
'docs/input-a.md, docs/input-b.md',
|
|
495
|
+
'dist/out-a.md, dist/out-b.md',
|
|
496
|
+
'src/resources/extensions/gsd/gsd-db.ts, src/resources/extensions/gsd/state.ts',
|
|
497
|
+
'"decision-1"',
|
|
498
|
+
'M001',
|
|
499
|
+
'S01',
|
|
500
|
+
'T01',
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
const task = getTask('M001', 'S01', 'T01');
|
|
504
|
+
assert.ok(task, 'getTask should still return the corrupt row');
|
|
505
|
+
assert.deepStrictEqual(task!.files, ['src-erf/Models/foo.cs', 'src-erf/Models/bar.cs']);
|
|
506
|
+
assert.deepStrictEqual(task!.inputs, ['docs/input-a.md', 'docs/input-b.md']);
|
|
507
|
+
assert.deepStrictEqual(task!.expected_output, ['dist/out-a.md', 'dist/out-b.md']);
|
|
508
|
+
assert.deepStrictEqual(
|
|
509
|
+
task!.key_files,
|
|
510
|
+
['src/resources/extensions/gsd/gsd-db.ts', 'src/resources/extensions/gsd/state.ts'],
|
|
511
|
+
);
|
|
512
|
+
assert.deepStrictEqual(task!.key_decisions, ['decision-1']);
|
|
513
|
+
|
|
514
|
+
const sliceTasks = getSliceTasks('M001', 'S01');
|
|
515
|
+
assert.equal(sliceTasks.length, 1, 'getSliceTasks should also survive corrupt rows');
|
|
516
|
+
assert.deepStrictEqual(sliceTasks[0]!.files, task!.files);
|
|
517
|
+
|
|
518
|
+
closeDatabase();
|
|
519
|
+
});
|
|
520
|
+
|
|
463
521
|
// ─── Final Report ──────────────────────────────────────────────────────────
|
|
464
522
|
|
|
465
523
|
});
|
|
@@ -691,7 +691,7 @@ describe("transition boundary failures", () => {
|
|
|
691
691
|
);
|
|
692
692
|
});
|
|
693
693
|
|
|
694
|
-
test("blocked state: all slices have unmet deps →
|
|
694
|
+
test("blocked state: all slices have unmet deps → fallback picks slice", async () => {
|
|
695
695
|
base = makeTempDir();
|
|
696
696
|
const mDir = join(base, ".gsd", "milestones", "M001");
|
|
697
697
|
mkdirSync(join(mDir, "slices", "S01", "tasks"), { recursive: true });
|
|
@@ -736,7 +736,9 @@ describe("transition boundary failures", () => {
|
|
|
736
736
|
|
|
737
737
|
invalidateAllCaches();
|
|
738
738
|
const state = await deriveStateFromDb(base);
|
|
739
|
-
|
|
739
|
+
// With partial-dep fallback, circular deps no longer block — fallback picks first eligible slice
|
|
740
|
+
assert.equal(state.phase, "planning", "circular deps: fallback picks a slice instead of blocking");
|
|
741
|
+
assert.ok(state.activeSlice !== null, "activeSlice set via fallback");
|
|
740
742
|
});
|
|
741
743
|
});
|
|
742
744
|
|