gsd-pi 2.41.0-dev.cac69f9 → 2.42.0-dev.97e9e30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/resources/extensions/gsd/auto/loop.js +80 -0
- package/dist/resources/extensions/gsd/auto/phases.js +2 -2
- package/dist/resources/extensions/gsd/auto/session.js +6 -0
- package/dist/resources/extensions/gsd/auto-dashboard.js +2 -0
- package/dist/resources/extensions/gsd/auto.js +28 -1
- package/dist/resources/extensions/gsd/bootstrap/tool-call-loop-guard.js +7 -2
- package/dist/resources/extensions/gsd/commands/catalog.js +32 -0
- package/dist/resources/extensions/gsd/commands/handlers/workflow.js +146 -0
- package/dist/resources/extensions/gsd/context-injector.js +74 -0
- package/dist/resources/extensions/gsd/custom-execution-policy.js +47 -0
- package/dist/resources/extensions/gsd/custom-verification.js +145 -0
- package/dist/resources/extensions/gsd/custom-workflow-engine.js +164 -0
- package/dist/resources/extensions/gsd/dashboard-overlay.js +1 -0
- package/dist/resources/extensions/gsd/definition-loader.js +352 -0
- package/dist/resources/extensions/gsd/dev-execution-policy.js +24 -0
- package/dist/resources/extensions/gsd/dev-workflow-engine.js +82 -0
- package/dist/resources/extensions/gsd/engine-resolver.js +40 -0
- package/dist/resources/extensions/gsd/engine-types.js +8 -0
- package/dist/resources/extensions/gsd/execution-policy.js +8 -0
- package/dist/resources/extensions/gsd/graph.js +225 -0
- package/dist/resources/extensions/gsd/run-manager.js +134 -0
- package/dist/resources/extensions/gsd/workflow-engine.js +7 -0
- package/dist/resources/skills/create-workflow/SKILL.md +103 -0
- package/dist/resources/skills/create-workflow/references/feature-patterns.md +128 -0
- package/dist/resources/skills/create-workflow/references/verification-policies.md +76 -0
- package/dist/resources/skills/create-workflow/references/yaml-schema-v1.md +46 -0
- package/dist/resources/skills/create-workflow/templates/blog-post-pipeline.yaml +60 -0
- package/dist/resources/skills/create-workflow/templates/code-audit.yaml +60 -0
- package/dist/resources/skills/create-workflow/templates/release-checklist.yaml +66 -0
- package/dist/resources/skills/create-workflow/templates/workflow-definition.yaml +32 -0
- package/dist/resources/skills/create-workflow/workflows/create-from-scratch.md +104 -0
- package/dist/resources/skills/create-workflow/workflows/create-from-template.md +72 -0
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +15 -15
- 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 +2 -2
- 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 +15 -15
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +2 -2
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/package.json +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/auto/loop.ts +91 -0
- package/src/resources/extensions/gsd/auto/phases.ts +2 -2
- package/src/resources/extensions/gsd/auto/session.ts +6 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +2 -0
- package/src/resources/extensions/gsd/auto.ts +31 -1
- package/src/resources/extensions/gsd/bootstrap/tool-call-loop-guard.ts +9 -2
- package/src/resources/extensions/gsd/commands/catalog.ts +32 -0
- package/src/resources/extensions/gsd/commands/handlers/workflow.ts +164 -0
- package/src/resources/extensions/gsd/context-injector.ts +100 -0
- package/src/resources/extensions/gsd/custom-execution-policy.ts +73 -0
- package/src/resources/extensions/gsd/custom-verification.ts +180 -0
- package/src/resources/extensions/gsd/custom-workflow-engine.ts +216 -0
- package/src/resources/extensions/gsd/dashboard-overlay.ts +1 -0
- package/src/resources/extensions/gsd/definition-loader.ts +462 -0
- package/src/resources/extensions/gsd/dev-execution-policy.ts +51 -0
- package/src/resources/extensions/gsd/dev-workflow-engine.ts +110 -0
- package/src/resources/extensions/gsd/engine-resolver.ts +57 -0
- package/src/resources/extensions/gsd/engine-types.ts +71 -0
- package/src/resources/extensions/gsd/execution-policy.ts +43 -0
- package/src/resources/extensions/gsd/graph.ts +312 -0
- package/src/resources/extensions/gsd/run-manager.ts +180 -0
- package/src/resources/extensions/gsd/tests/bundled-workflow-defs.test.ts +180 -0
- package/src/resources/extensions/gsd/tests/commands-workflow-custom.test.ts +283 -0
- package/src/resources/extensions/gsd/tests/context-injector.test.ts +313 -0
- package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +540 -0
- package/src/resources/extensions/gsd/tests/custom-verification.test.ts +382 -0
- package/src/resources/extensions/gsd/tests/custom-workflow-engine.test.ts +339 -0
- package/src/resources/extensions/gsd/tests/dashboard-custom-engine.test.ts +87 -0
- package/src/resources/extensions/gsd/tests/definition-loader.test.ts +778 -0
- package/src/resources/extensions/gsd/tests/dev-engine-wrapper.test.ts +318 -0
- package/src/resources/extensions/gsd/tests/e2e-workflow-pipeline-integration.test.ts +476 -0
- package/src/resources/extensions/gsd/tests/engine-interfaces-contract.test.ts +271 -0
- package/src/resources/extensions/gsd/tests/graph-operations.test.ts +599 -0
- package/src/resources/extensions/gsd/tests/iterate-engine-integration.test.ts +429 -0
- package/src/resources/extensions/gsd/tests/run-manager.test.ts +229 -0
- package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +45 -0
- package/src/resources/extensions/gsd/workflow-engine.ts +38 -0
- package/src/resources/skills/create-workflow/SKILL.md +103 -0
- package/src/resources/skills/create-workflow/references/feature-patterns.md +128 -0
- package/src/resources/skills/create-workflow/references/verification-policies.md +76 -0
- package/src/resources/skills/create-workflow/references/yaml-schema-v1.md +46 -0
- package/src/resources/skills/create-workflow/templates/blog-post-pipeline.yaml +60 -0
- package/src/resources/skills/create-workflow/templates/code-audit.yaml +60 -0
- package/src/resources/skills/create-workflow/templates/release-checklist.yaml +66 -0
- package/src/resources/skills/create-workflow/templates/workflow-definition.yaml +32 -0
- package/src/resources/skills/create-workflow/workflows/create-from-scratch.md +104 -0
- package/src/resources/skills/create-workflow/workflows/create-from-template.md +72 -0
- /package/dist/web/standalone/.next/static/{EnGUNqHeGbE0tuuUkTJVA → PXrI5DoWsm7rwAVnEU2rD}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{EnGUNqHeGbE0tuuUkTJVA → PXrI5DoWsm7rwAVnEU2rD}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* custom-engine-loop-integration.test.ts — Integration test proving that
|
|
3
|
+
* autoLoop dispatches a 3-step custom workflow through the real pipeline.
|
|
4
|
+
*
|
|
5
|
+
* Creates a real run directory with GRAPH.yaml, mocks LoopDeps minimally,
|
|
6
|
+
* and verifies all 3 steps complete in dependency order.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, it, afterEach } from "node:test";
|
|
10
|
+
import assert from "node:assert/strict";
|
|
11
|
+
import { mkdtempSync, rmSync, existsSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
|
|
15
|
+
import { autoLoop, resolveAgentEnd, _resetPendingResolve } from "../auto-loop.js";
|
|
16
|
+
import type { LoopDeps } from "../auto/loop-deps.js";
|
|
17
|
+
import type { SessionLockStatus } from "../session-lock.js";
|
|
18
|
+
import { writeGraph, readGraph, type WorkflowGraph, type GraphStep } from "../graph.ts";
|
|
19
|
+
import { writeFileSync } from "node:fs";
|
|
20
|
+
import { stringify } from "yaml";
|
|
21
|
+
|
|
22
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
const tmpDirs: string[] = [];
|
|
25
|
+
|
|
26
|
+
function makeTmpDir(): string {
|
|
27
|
+
const dir = mkdtempSync(join(tmpdir(), "loop-integ-"));
|
|
28
|
+
tmpDirs.push(dir);
|
|
29
|
+
return dir;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
_resetPendingResolve();
|
|
34
|
+
for (const d of tmpDirs) {
|
|
35
|
+
try { rmSync(d, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }); } catch { /* Windows EPERM — OS cleans up temp dirs */ }
|
|
36
|
+
}
|
|
37
|
+
tmpDirs.length = 0;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
function makeStep(overrides: Partial<GraphStep> & { id: string }): GraphStep {
|
|
41
|
+
return {
|
|
42
|
+
title: overrides.id,
|
|
43
|
+
status: "pending",
|
|
44
|
+
prompt: `Do ${overrides.id}`,
|
|
45
|
+
dependsOn: [],
|
|
46
|
+
...overrides,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function makeGraph(steps: GraphStep[], name = "test-wf"): WorkflowGraph {
|
|
51
|
+
return {
|
|
52
|
+
steps,
|
|
53
|
+
metadata: { name, createdAt: "2026-01-01T00:00:00.000Z" },
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Write a minimal DEFINITION.yaml that matches the graph steps (needed by resolveDispatch since S06). */
|
|
58
|
+
function writeDefinition(runDir: string, steps: GraphStep[], name = "test-wf"): void {
|
|
59
|
+
const def = {
|
|
60
|
+
version: 1,
|
|
61
|
+
name,
|
|
62
|
+
description: `Test workflow: ${name}`,
|
|
63
|
+
steps: steps.map((s) => ({
|
|
64
|
+
id: s.id,
|
|
65
|
+
name: s.title ?? s.id,
|
|
66
|
+
prompt: s.prompt ?? `Do ${s.id}`,
|
|
67
|
+
produces: `${s.id}/output.md`,
|
|
68
|
+
...(s.dependsOn?.length ? { requires: s.dependsOn } : {}),
|
|
69
|
+
})),
|
|
70
|
+
};
|
|
71
|
+
writeFileSync(join(runDir, "DEFINITION.yaml"), stringify(def));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function makeMockCtx() {
|
|
75
|
+
return {
|
|
76
|
+
ui: { notify: () => {}, setStatus: () => {} },
|
|
77
|
+
model: { id: "test-model" },
|
|
78
|
+
sessionManager: { getSessionFile: () => "/tmp/session.json" },
|
|
79
|
+
} as any;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function makeMockPi() {
|
|
83
|
+
const calls: unknown[] = [];
|
|
84
|
+
return {
|
|
85
|
+
sendMessage: (...args: unknown[]) => {
|
|
86
|
+
calls.push(args);
|
|
87
|
+
},
|
|
88
|
+
calls,
|
|
89
|
+
} as any;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function makeLoopSession(overrides?: Record<string, unknown>) {
|
|
93
|
+
return {
|
|
94
|
+
active: true,
|
|
95
|
+
verbose: false,
|
|
96
|
+
stepMode: false,
|
|
97
|
+
paused: false,
|
|
98
|
+
basePath: "/tmp/project",
|
|
99
|
+
originalBasePath: "",
|
|
100
|
+
currentMilestoneId: null,
|
|
101
|
+
currentUnit: null,
|
|
102
|
+
currentUnitRouting: null,
|
|
103
|
+
completedUnits: [],
|
|
104
|
+
resourceVersionOnStart: null,
|
|
105
|
+
lastPromptCharCount: undefined,
|
|
106
|
+
lastBaselineCharCount: undefined,
|
|
107
|
+
lastBudgetAlertLevel: 0,
|
|
108
|
+
pendingVerificationRetry: null,
|
|
109
|
+
pendingCrashRecovery: null,
|
|
110
|
+
pendingQuickTasks: [],
|
|
111
|
+
sidecarQueue: [],
|
|
112
|
+
autoModeStartModel: null,
|
|
113
|
+
unitDispatchCount: new Map<string, number>(),
|
|
114
|
+
unitLifetimeDispatches: new Map<string, number>(),
|
|
115
|
+
unitRecoveryCount: new Map<string, number>(),
|
|
116
|
+
verificationRetryCount: new Map<string, number>(),
|
|
117
|
+
gitService: null,
|
|
118
|
+
autoStartTime: Date.now(),
|
|
119
|
+
activeEngineId: null,
|
|
120
|
+
activeRunDir: null,
|
|
121
|
+
rewriteAttemptCount: 0,
|
|
122
|
+
cmdCtx: {
|
|
123
|
+
newSession: () => Promise.resolve({ cancelled: false }),
|
|
124
|
+
getContextUsage: () => ({ percent: 10, tokens: 1000, limit: 10000 }),
|
|
125
|
+
},
|
|
126
|
+
clearTimers: () => {},
|
|
127
|
+
lockBasePath: "/tmp/project",
|
|
128
|
+
...overrides,
|
|
129
|
+
} as any;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function makeMockDeps(overrides?: Partial<LoopDeps>): LoopDeps & { callLog: string[] } {
|
|
133
|
+
const callLog: string[] = [];
|
|
134
|
+
|
|
135
|
+
const baseDeps: LoopDeps = {
|
|
136
|
+
lockBase: () => "/tmp/test-lock",
|
|
137
|
+
buildSnapshotOpts: () => ({}),
|
|
138
|
+
stopAuto: async (_ctx, _pi, reason) => {
|
|
139
|
+
callLog.push(`stopAuto:${reason ?? "no-reason"}`);
|
|
140
|
+
},
|
|
141
|
+
pauseAuto: async () => {
|
|
142
|
+
callLog.push("pauseAuto");
|
|
143
|
+
},
|
|
144
|
+
clearUnitTimeout: () => {},
|
|
145
|
+
updateProgressWidget: () => {},
|
|
146
|
+
syncCmuxSidebar: () => {},
|
|
147
|
+
logCmuxEvent: () => {},
|
|
148
|
+
invalidateAllCaches: () => {},
|
|
149
|
+
deriveState: async () => {
|
|
150
|
+
callLog.push("deriveState");
|
|
151
|
+
return {
|
|
152
|
+
phase: "executing",
|
|
153
|
+
activeMilestone: { id: "M001", title: "Workflow", status: "active" },
|
|
154
|
+
activeSlice: null,
|
|
155
|
+
activeTask: null,
|
|
156
|
+
registry: [],
|
|
157
|
+
blockers: [],
|
|
158
|
+
} as any;
|
|
159
|
+
},
|
|
160
|
+
rebuildState: async () => {},
|
|
161
|
+
loadEffectiveGSDPreferences: () => undefined,
|
|
162
|
+
preDispatchHealthGate: async () => ({ proceed: true, fixesApplied: [] }),
|
|
163
|
+
syncProjectRootToWorktree: () => {},
|
|
164
|
+
checkResourcesStale: () => null,
|
|
165
|
+
validateSessionLock: () => ({ valid: true } as SessionLockStatus),
|
|
166
|
+
updateSessionLock: () => {},
|
|
167
|
+
handleLostSessionLock: () => {},
|
|
168
|
+
sendDesktopNotification: () => {},
|
|
169
|
+
setActiveMilestoneId: () => {},
|
|
170
|
+
pruneQueueOrder: () => {},
|
|
171
|
+
isInAutoWorktree: () => false,
|
|
172
|
+
shouldUseWorktreeIsolation: () => false,
|
|
173
|
+
mergeMilestoneToMain: () => ({ pushed: false, codeFilesChanged: false }),
|
|
174
|
+
teardownAutoWorktree: () => {},
|
|
175
|
+
createAutoWorktree: () => "/tmp/wt",
|
|
176
|
+
captureIntegrationBranch: () => {},
|
|
177
|
+
getIsolationMode: () => "none",
|
|
178
|
+
getCurrentBranch: () => "main",
|
|
179
|
+
autoWorktreeBranch: () => "auto/M001",
|
|
180
|
+
resolveMilestoneFile: () => null,
|
|
181
|
+
reconcileMergeState: () => false,
|
|
182
|
+
getLedger: () => null,
|
|
183
|
+
getProjectTotals: () => ({ cost: 0 }),
|
|
184
|
+
formatCost: (c: number) => `$${c.toFixed(2)}`,
|
|
185
|
+
getBudgetAlertLevel: () => 0,
|
|
186
|
+
getNewBudgetAlertLevel: () => 0,
|
|
187
|
+
getBudgetEnforcementAction: () => "none",
|
|
188
|
+
getManifestStatus: async () => null,
|
|
189
|
+
collectSecretsFromManifest: async () => null,
|
|
190
|
+
resolveDispatch: async () => {
|
|
191
|
+
callLog.push("resolveDispatch");
|
|
192
|
+
return { action: "dispatch" as const, unitType: "execute-task", unitId: "M001/S01/T01", prompt: "unused" };
|
|
193
|
+
},
|
|
194
|
+
runPreDispatchHooks: () => ({ firedHooks: [], action: "proceed" }),
|
|
195
|
+
getPriorSliceCompletionBlocker: () => null,
|
|
196
|
+
getMainBranch: () => "main",
|
|
197
|
+
collectObservabilityWarnings: async () => [],
|
|
198
|
+
buildObservabilityRepairBlock: () => null,
|
|
199
|
+
closeoutUnit: async () => {},
|
|
200
|
+
verifyExpectedArtifact: () => true,
|
|
201
|
+
clearUnitRuntimeRecord: () => {},
|
|
202
|
+
writeUnitRuntimeRecord: () => {},
|
|
203
|
+
recordOutcome: () => {},
|
|
204
|
+
writeLock: () => {},
|
|
205
|
+
captureAvailableSkills: () => {},
|
|
206
|
+
ensurePreconditions: () => {},
|
|
207
|
+
updateSliceProgressCache: () => {},
|
|
208
|
+
selectAndApplyModel: async () => ({ routing: null }),
|
|
209
|
+
resolveModelId: () => undefined,
|
|
210
|
+
startUnitSupervision: () => {},
|
|
211
|
+
getDeepDiagnostic: () => null,
|
|
212
|
+
isDbAvailable: () => false,
|
|
213
|
+
reorderForCaching: (p: string) => p,
|
|
214
|
+
existsSync: (p: string) => existsSync(p),
|
|
215
|
+
readFileSync: () => "",
|
|
216
|
+
atomicWriteSync: () => {},
|
|
217
|
+
GitServiceImpl: class {} as any,
|
|
218
|
+
resolver: {
|
|
219
|
+
get workPath() { return "/tmp/project"; },
|
|
220
|
+
get projectRoot() { return "/tmp/project"; },
|
|
221
|
+
get lockPath() { return "/tmp/project"; },
|
|
222
|
+
enterMilestone: () => {},
|
|
223
|
+
exitMilestone: () => {},
|
|
224
|
+
mergeAndExit: () => {},
|
|
225
|
+
mergeAndEnterNext: () => {},
|
|
226
|
+
} as any,
|
|
227
|
+
postUnitPreVerification: async () => "continue" as const,
|
|
228
|
+
runPostUnitVerification: async () => "continue" as const,
|
|
229
|
+
postUnitPostVerification: async () => "continue" as const,
|
|
230
|
+
getSessionFile: () => "/tmp/session.json",
|
|
231
|
+
emitJournalEvent: (entry) => {
|
|
232
|
+
callLog.push(`journal:${entry.eventType}`);
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
return { ...baseDeps, ...overrides, callLog };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ─── Tests ───────────────────────────────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
describe("Custom engine loop integration", () => {
|
|
242
|
+
it("dispatches a 3-step workflow through autoLoop and all steps complete", async () => {
|
|
243
|
+
_resetPendingResolve();
|
|
244
|
+
|
|
245
|
+
// Create a real run directory with 3 steps: a → b → c
|
|
246
|
+
const runDir = makeTmpDir();
|
|
247
|
+
const graph = makeGraph([
|
|
248
|
+
makeStep({ id: "step-a" }),
|
|
249
|
+
makeStep({ id: "step-b", dependsOn: ["step-a"] }),
|
|
250
|
+
makeStep({ id: "step-c", dependsOn: ["step-b"] }),
|
|
251
|
+
], "integ-test");
|
|
252
|
+
writeGraph(runDir, graph);
|
|
253
|
+
writeDefinition(runDir, graph.steps, "integ-test");
|
|
254
|
+
|
|
255
|
+
const ctx = makeMockCtx();
|
|
256
|
+
const pi = makeMockPi();
|
|
257
|
+
|
|
258
|
+
let unitCount = 0;
|
|
259
|
+
|
|
260
|
+
const s = makeLoopSession({
|
|
261
|
+
activeEngineId: "custom",
|
|
262
|
+
activeRunDir: runDir,
|
|
263
|
+
basePath: runDir,
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const deps = makeMockDeps({
|
|
267
|
+
stopAuto: async (_ctx, _pi, reason) => {
|
|
268
|
+
deps.callLog.push(`stopAuto:${reason ?? "no-reason"}`);
|
|
269
|
+
s.active = false;
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// Start autoLoop — it will block inside runUnit awaiting resolveAgentEnd
|
|
274
|
+
const loopPromise = autoLoop(ctx, pi, s, deps);
|
|
275
|
+
|
|
276
|
+
// Each iteration: the custom engine path derives state → resolves dispatch →
|
|
277
|
+
// runs guards → runs runUnitPhase (which calls runUnit) → we resolve →
|
|
278
|
+
// engine.reconcile marks the step complete → loop continues.
|
|
279
|
+
// We need to resolve resolveAgentEnd for each step.
|
|
280
|
+
|
|
281
|
+
// Step 1: step-a
|
|
282
|
+
await new Promise((r) => setTimeout(r, 80));
|
|
283
|
+
unitCount++;
|
|
284
|
+
resolveAgentEnd({ messages: [{ role: "assistant" }] });
|
|
285
|
+
|
|
286
|
+
// Step 2: step-b
|
|
287
|
+
await new Promise((r) => setTimeout(r, 80));
|
|
288
|
+
unitCount++;
|
|
289
|
+
resolveAgentEnd({ messages: [{ role: "assistant" }] });
|
|
290
|
+
|
|
291
|
+
// Step 3: step-c
|
|
292
|
+
await new Promise((r) => setTimeout(r, 80));
|
|
293
|
+
unitCount++;
|
|
294
|
+
resolveAgentEnd({ messages: [{ role: "assistant" }] });
|
|
295
|
+
|
|
296
|
+
// After step-c completes, engine.reconcile marks it complete, then
|
|
297
|
+
// next deriveState sees isComplete=true → stopAuto → loop exits
|
|
298
|
+
await loopPromise;
|
|
299
|
+
|
|
300
|
+
// Verify GRAPH.yaml shows all 3 steps complete
|
|
301
|
+
const finalGraph = readGraph(runDir);
|
|
302
|
+
assert.equal(finalGraph.steps.length, 3, "Should have 3 steps");
|
|
303
|
+
for (const step of finalGraph.steps) {
|
|
304
|
+
assert.equal(step.status, "complete", `Step ${step.id} should be complete, got ${step.status}`);
|
|
305
|
+
assert.ok(step.finishedAt, `Step ${step.id} should have finishedAt timestamp`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Verify exactly 3 units were dispatched (3 pi.sendMessage calls)
|
|
309
|
+
assert.equal(pi.calls.length, 3, `Should have dispatched exactly 3 units, got ${pi.calls.length}`);
|
|
310
|
+
|
|
311
|
+
// Verify the loop stopped because the workflow completed
|
|
312
|
+
const stopEntry = deps.callLog.find((e: string) => e.startsWith("stopAuto:"));
|
|
313
|
+
assert.ok(stopEntry, "stopAuto should have been called");
|
|
314
|
+
assert.ok(
|
|
315
|
+
stopEntry!.includes("Workflow complete"),
|
|
316
|
+
`stopAuto reason should include "Workflow complete", got: ${stopEntry}`,
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
// Verify dev path was NOT used (resolveDispatch should not appear)
|
|
320
|
+
assert.ok(
|
|
321
|
+
!deps.callLog.includes("resolveDispatch"),
|
|
322
|
+
"Custom engine path should skip resolveDispatch (dev path not taken)",
|
|
323
|
+
);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it("stops when engine reports isComplete on first derive", async () => {
|
|
327
|
+
_resetPendingResolve();
|
|
328
|
+
|
|
329
|
+
// Create a run directory where all steps are already complete
|
|
330
|
+
const runDir = makeTmpDir();
|
|
331
|
+
const graph = makeGraph([
|
|
332
|
+
makeStep({ id: "step-a", status: "complete" }),
|
|
333
|
+
], "already-done");
|
|
334
|
+
writeGraph(runDir, graph);
|
|
335
|
+
writeDefinition(runDir, graph.steps, "already-done");
|
|
336
|
+
|
|
337
|
+
const ctx = makeMockCtx();
|
|
338
|
+
const pi = makeMockPi();
|
|
339
|
+
|
|
340
|
+
const s = makeLoopSession({
|
|
341
|
+
activeEngineId: "custom",
|
|
342
|
+
activeRunDir: runDir,
|
|
343
|
+
basePath: runDir,
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
const deps = makeMockDeps({
|
|
347
|
+
stopAuto: async (_ctx, _pi, reason) => {
|
|
348
|
+
deps.callLog.push(`stopAuto:${reason ?? "no-reason"}`);
|
|
349
|
+
s.active = false;
|
|
350
|
+
},
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
await autoLoop(ctx, pi, s, deps);
|
|
354
|
+
|
|
355
|
+
// No units should have been dispatched
|
|
356
|
+
assert.equal(pi.calls.length, 0, "Should not dispatch units for complete workflow");
|
|
357
|
+
|
|
358
|
+
// Should stop with "Workflow complete" reason
|
|
359
|
+
const stopEntry = deps.callLog.find((e: string) => e.startsWith("stopAuto:"));
|
|
360
|
+
assert.ok(stopEntry?.includes("Workflow complete"), "Should stop with 'Workflow complete'");
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it("does not call runPreDispatch or runFinalize on the custom path", async () => {
|
|
364
|
+
_resetPendingResolve();
|
|
365
|
+
|
|
366
|
+
// Single-step workflow
|
|
367
|
+
const runDir = makeTmpDir();
|
|
368
|
+
const graph = makeGraph([makeStep({ id: "only" })], "single");
|
|
369
|
+
writeGraph(runDir, graph);
|
|
370
|
+
writeDefinition(runDir, graph.steps, "single");
|
|
371
|
+
|
|
372
|
+
const ctx = makeMockCtx();
|
|
373
|
+
const pi = makeMockPi();
|
|
374
|
+
|
|
375
|
+
const s = makeLoopSession({
|
|
376
|
+
activeEngineId: "custom",
|
|
377
|
+
activeRunDir: runDir,
|
|
378
|
+
basePath: runDir,
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
const deps = makeMockDeps({
|
|
382
|
+
stopAuto: async (_ctx, _pi, reason) => {
|
|
383
|
+
deps.callLog.push(`stopAuto:${reason ?? "no-reason"}`);
|
|
384
|
+
s.active = false;
|
|
385
|
+
},
|
|
386
|
+
postUnitPreVerification: async () => {
|
|
387
|
+
deps.callLog.push("postUnitPreVerification");
|
|
388
|
+
return "continue" as const;
|
|
389
|
+
},
|
|
390
|
+
postUnitPostVerification: async () => {
|
|
391
|
+
deps.callLog.push("postUnitPostVerification");
|
|
392
|
+
return "continue" as const;
|
|
393
|
+
},
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
const loopPromise = autoLoop(ctx, pi, s, deps);
|
|
397
|
+
|
|
398
|
+
await new Promise((r) => setTimeout(r, 80));
|
|
399
|
+
resolveAgentEnd({ messages: [{ role: "assistant" }] });
|
|
400
|
+
|
|
401
|
+
await loopPromise;
|
|
402
|
+
|
|
403
|
+
// Custom path should NOT call runFinalize's post-unit phases
|
|
404
|
+
assert.ok(
|
|
405
|
+
!deps.callLog.includes("postUnitPreVerification"),
|
|
406
|
+
"Custom path should skip postUnitPreVerification (runFinalize not called)",
|
|
407
|
+
);
|
|
408
|
+
assert.ok(
|
|
409
|
+
!deps.callLog.includes("postUnitPostVerification"),
|
|
410
|
+
"Custom path should skip postUnitPostVerification (runFinalize not called)",
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
// Should NOT have called resolveDispatch (dev dispatch)
|
|
414
|
+
assert.ok(
|
|
415
|
+
!deps.callLog.includes("resolveDispatch"),
|
|
416
|
+
"Custom path should skip resolveDispatch",
|
|
417
|
+
);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it("respects dependency ordering — step-b waits for step-a", async () => {
|
|
421
|
+
_resetPendingResolve();
|
|
422
|
+
|
|
423
|
+
const runDir = makeTmpDir();
|
|
424
|
+
// step-b depends on step-a, both pending
|
|
425
|
+
const graph = makeGraph([
|
|
426
|
+
makeStep({ id: "step-a" }),
|
|
427
|
+
makeStep({ id: "step-b", dependsOn: ["step-a"] }),
|
|
428
|
+
], "dep-order");
|
|
429
|
+
writeGraph(runDir, graph);
|
|
430
|
+
writeDefinition(runDir, graph.steps, "dep-order");
|
|
431
|
+
|
|
432
|
+
const ctx = makeMockCtx();
|
|
433
|
+
const pi = makeMockPi();
|
|
434
|
+
const dispatchedUnitIds: string[] = [];
|
|
435
|
+
|
|
436
|
+
const s = makeLoopSession({
|
|
437
|
+
activeEngineId: "custom",
|
|
438
|
+
activeRunDir: runDir,
|
|
439
|
+
basePath: runDir,
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
const originalSendMessage = pi.sendMessage;
|
|
443
|
+
pi.sendMessage = (...args: unknown[]) => {
|
|
444
|
+
// Track dispatched prompts to verify ordering
|
|
445
|
+
const promptArg = args[0] as { content?: string };
|
|
446
|
+
dispatchedUnitIds.push(promptArg?.content ?? "unknown");
|
|
447
|
+
return originalSendMessage(...args);
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
const deps = makeMockDeps({
|
|
451
|
+
stopAuto: async (_ctx, _pi, reason) => {
|
|
452
|
+
deps.callLog.push(`stopAuto:${reason ?? "no-reason"}`);
|
|
453
|
+
s.active = false;
|
|
454
|
+
},
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
const loopPromise = autoLoop(ctx, pi, s, deps);
|
|
458
|
+
|
|
459
|
+
// Resolve step-a
|
|
460
|
+
await new Promise((r) => setTimeout(r, 80));
|
|
461
|
+
resolveAgentEnd({ messages: [{ role: "assistant" }] });
|
|
462
|
+
|
|
463
|
+
// Resolve step-b
|
|
464
|
+
await new Promise((r) => setTimeout(r, 80));
|
|
465
|
+
resolveAgentEnd({ messages: [{ role: "assistant" }] });
|
|
466
|
+
|
|
467
|
+
await loopPromise;
|
|
468
|
+
|
|
469
|
+
// Verify step-a was dispatched before step-b
|
|
470
|
+
assert.equal(dispatchedUnitIds.length, 2, "Should have dispatched 2 steps");
|
|
471
|
+
assert.ok(
|
|
472
|
+
dispatchedUnitIds[0].includes("Do step-a"),
|
|
473
|
+
`First dispatch should be step-a, got: ${dispatchedUnitIds[0]}`,
|
|
474
|
+
);
|
|
475
|
+
assert.ok(
|
|
476
|
+
dispatchedUnitIds[1].includes("Do step-b"),
|
|
477
|
+
`Second dispatch should be step-b, got: ${dispatchedUnitIds[1]}`,
|
|
478
|
+
);
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
it("GRAPH.yaml step stays pending when session deactivates before reconcile", async () => {
|
|
482
|
+
_resetPendingResolve();
|
|
483
|
+
|
|
484
|
+
// Two-step workflow: a → b. We will complete step-a, then force a break
|
|
485
|
+
// during step-b's runUnitPhase (by returning cancelled status + deactivating).
|
|
486
|
+
const runDir = makeTmpDir();
|
|
487
|
+
const graph = makeGraph([
|
|
488
|
+
makeStep({ id: "step-a" }),
|
|
489
|
+
makeStep({ id: "step-b", dependsOn: ["step-a"] }),
|
|
490
|
+
], "failure-test");
|
|
491
|
+
writeGraph(runDir, graph);
|
|
492
|
+
writeDefinition(runDir, graph.steps, "failure-test");
|
|
493
|
+
|
|
494
|
+
const ctx = makeMockCtx();
|
|
495
|
+
const pi = makeMockPi();
|
|
496
|
+
|
|
497
|
+
const s = makeLoopSession({
|
|
498
|
+
activeEngineId: "custom",
|
|
499
|
+
activeRunDir: runDir,
|
|
500
|
+
basePath: runDir,
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
const deps = makeMockDeps({
|
|
504
|
+
stopAuto: async (_ctx, _pi, reason) => {
|
|
505
|
+
deps.callLog.push(`stopAuto:${reason ?? "no-reason"}`);
|
|
506
|
+
s.active = false;
|
|
507
|
+
},
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
const loopPromise = autoLoop(ctx, pi, s, deps);
|
|
511
|
+
|
|
512
|
+
// Resolve step-a successfully
|
|
513
|
+
await new Promise((r) => setTimeout(r, 80));
|
|
514
|
+
resolveAgentEnd({ messages: [{ role: "assistant" }] });
|
|
515
|
+
|
|
516
|
+
// Step-b enters runUnit — deactivate the session before resolving.
|
|
517
|
+
// runUnit checks s.active after newSession and returns cancelled if false.
|
|
518
|
+
// But since newSession resolves synchronously in our mock (before the
|
|
519
|
+
// active check), the unit still runs. Instead, let's just cancel it.
|
|
520
|
+
await new Promise((r) => setTimeout(r, 80));
|
|
521
|
+
// Resolve as cancelled to simulate a failed session
|
|
522
|
+
resolveAgentEnd({ messages: [{ role: "assistant" }] });
|
|
523
|
+
|
|
524
|
+
// The reconcile will still run for step-b in this flow since
|
|
525
|
+
// runUnitPhase returns "next" (not "break") for completed units.
|
|
526
|
+
// After both steps complete, the engine detects isComplete and stops.
|
|
527
|
+
await loopPromise;
|
|
528
|
+
|
|
529
|
+
// Verify step-a is complete
|
|
530
|
+
const finalGraph = readGraph(runDir);
|
|
531
|
+
const stepA = finalGraph.steps.find(s => s.id === "step-a");
|
|
532
|
+
assert.equal(stepA?.status, "complete", "Step-a should be complete");
|
|
533
|
+
|
|
534
|
+
// Verify the loop stopped appropriately
|
|
535
|
+
assert.ok(
|
|
536
|
+
deps.callLog.some((e: string) => e.startsWith("stopAuto:")),
|
|
537
|
+
"stopAuto should have been called",
|
|
538
|
+
);
|
|
539
|
+
});
|
|
540
|
+
});
|