@nathapp/nax 0.18.6 → 0.19.0
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/nax/features/nax-compliance/prd.json +52 -0
- package/nax/features/nax-compliance/progress.txt +1 -0
- package/nax/features/v0.19.0-hardening/plan.md +7 -0
- package/nax/features/v0.19.0-hardening/prd.json +84 -0
- package/nax/features/v0.19.0-hardening/progress.txt +7 -0
- package/nax/features/v0.19.0-hardening/spec.md +18 -0
- package/nax/features/v0.19.0-hardening/tasks.md +8 -0
- package/nax/status.json +27 -0
- package/package.json +2 -2
- package/src/acceptance/fix-generator.ts +6 -2
- package/src/acceptance/generator.ts +3 -1
- package/src/acceptance/types.ts +3 -1
- package/src/agents/claude-plan.ts +6 -5
- package/src/cli/analyze.ts +1 -0
- package/src/cli/init.ts +7 -6
- package/src/config/defaults.ts +1 -0
- package/src/config/types.ts +2 -0
- package/src/context/injector.ts +18 -18
- package/src/execution/crash-recovery.ts +7 -10
- package/src/execution/lifecycle/acceptance-loop.ts +1 -0
- package/src/execution/lifecycle/index.ts +0 -1
- package/src/execution/lifecycle/precheck-runner.ts +1 -1
- package/src/execution/lifecycle/run-setup.ts +14 -14
- package/src/execution/parallel.ts +1 -1
- package/src/execution/runner.ts +1 -19
- package/src/execution/sequential-executor.ts +1 -1
- package/src/hooks/runner.ts +2 -2
- package/src/interaction/plugins/auto.ts +2 -2
- package/src/logger/logger.ts +3 -5
- package/src/plugins/loader.ts +36 -9
- package/src/routing/batch-route.ts +32 -0
- package/src/routing/index.ts +1 -0
- package/src/routing/loader.ts +7 -0
- package/src/utils/path-security.ts +56 -0
- package/src/verification/executor.ts +6 -13
- package/test/integration/plugins/config-resolution.test.ts +3 -3
- package/test/integration/plugins/loader.test.ts +3 -1
- package/test/integration/precheck-integration.test.ts +18 -11
- package/test/integration/security-loader.test.ts +83 -0
- package/test/unit/formatters.test.ts +2 -3
- package/test/unit/hooks/shell-security.test.ts +40 -0
- package/test/unit/utils/path-security.test.ts +47 -0
- package/src/execution/lifecycle/run-lifecycle.ts +0 -312
- package/test/unit/run-lifecycle.test.ts +0 -140
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { validateModulePath } from "../../../src/utils/path-security";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
describe("path-security utility", () => {
|
|
6
|
+
const projectRoot = "/home/project";
|
|
7
|
+
const globalRoot = "/home/global";
|
|
8
|
+
const roots = [projectRoot, globalRoot];
|
|
9
|
+
|
|
10
|
+
test("allows relative path within project root", () => {
|
|
11
|
+
// Relative paths are resolved relative to the first allowed root by our validator
|
|
12
|
+
const result = validateModulePath("./plugins/my-plugin.ts", roots);
|
|
13
|
+
expect(result.valid).toBe(true);
|
|
14
|
+
expect(result.absolutePath).toBe(resolve(projectRoot, "plugins/my-plugin.ts"));
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("allows absolute path within global root", () => {
|
|
18
|
+
const result = validateModulePath("/home/global/plugins/my-plugin.ts", roots);
|
|
19
|
+
expect(result.valid).toBe(true);
|
|
20
|
+
expect(result.absolutePath).toBe("/home/global/plugins/my-plugin.ts");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("blocks traversal out of root (../)", () => {
|
|
24
|
+
// resolve handles the ../ then our startsWith check fails
|
|
25
|
+
const result = validateModulePath("../../etc/passwd", roots);
|
|
26
|
+
expect(result.valid).toBe(false);
|
|
27
|
+
expect(result.error).toContain("outside allowed roots");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("blocks absolute path outside roots", () => {
|
|
31
|
+
const result = validateModulePath("/usr/bin/node", roots);
|
|
32
|
+
expect(result.valid).toBe(false);
|
|
33
|
+
expect(result.error).toContain("outside allowed roots");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("handles root itself", () => {
|
|
37
|
+
const result = validateModulePath("/home/project", roots);
|
|
38
|
+
expect(result.valid).toBe(true);
|
|
39
|
+
expect(result.absolutePath).toBe("/home/project");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("blocks empty path", () => {
|
|
43
|
+
const result = validateModulePath("", roots);
|
|
44
|
+
expect(result.valid).toBe(false);
|
|
45
|
+
expect(result.error).toContain("empty");
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -1,312 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Run Lifecycle — Setup & Teardown Logic
|
|
3
|
-
*
|
|
4
|
-
* Encapsulates the bookend operations for a nax execution run:
|
|
5
|
-
* - Setup: lock acquisition, PRD loading, plugin initialization, reporter setup
|
|
6
|
-
* - Teardown: metrics computation, final status write, lock release, plugin cleanup
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import * as os from "node:os";
|
|
10
|
-
import path from "node:path";
|
|
11
|
-
import { getAgent } from "../../agents";
|
|
12
|
-
import type { NaxConfig } from "../../config";
|
|
13
|
-
import {
|
|
14
|
-
AgentNotFoundError,
|
|
15
|
-
AgentNotInstalledError,
|
|
16
|
-
LockAcquisitionError,
|
|
17
|
-
StoryLimitExceededError,
|
|
18
|
-
} from "../../errors";
|
|
19
|
-
import { type LoadedHooksConfig, fireHook } from "../../hooks";
|
|
20
|
-
import { getSafeLogger } from "../../logger";
|
|
21
|
-
import { type StoryMetrics, saveRunMetrics } from "../../metrics";
|
|
22
|
-
import { loadPlugins } from "../../plugins/loader";
|
|
23
|
-
import type { PluginRegistry } from "../../plugins/registry";
|
|
24
|
-
import type { PRD } from "../../prd";
|
|
25
|
-
import { countStories, isComplete, isStalled, loadPRD } from "../../prd";
|
|
26
|
-
import { clearCache as clearLlmCache, routeBatch as llmRouteBatch } from "../../routing/strategies/llm";
|
|
27
|
-
import { type StoryBatch, precomputeBatchPlan } from "../batching";
|
|
28
|
-
import { acquireLock, getAllReadyStories, hookCtx, releaseLock } from "../helpers";
|
|
29
|
-
import type { StatusWriter } from "../status-writer";
|
|
30
|
-
|
|
31
|
-
/** Setup result containing initialized state */
|
|
32
|
-
export interface SetupResult {
|
|
33
|
-
prd: PRD;
|
|
34
|
-
pluginRegistry: PluginRegistry;
|
|
35
|
-
batchPlan: StoryBatch[];
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** Teardown options */
|
|
39
|
-
export interface TeardownOptions {
|
|
40
|
-
runId: string;
|
|
41
|
-
feature: string;
|
|
42
|
-
startedAt: string;
|
|
43
|
-
prd: PRD;
|
|
44
|
-
allStoryMetrics: StoryMetrics[];
|
|
45
|
-
totalCost: number;
|
|
46
|
-
storiesCompleted: number;
|
|
47
|
-
startTime: number;
|
|
48
|
-
workdir: string;
|
|
49
|
-
pluginRegistry: PluginRegistry;
|
|
50
|
-
statusWriter: StatusWriter;
|
|
51
|
-
iterations: number;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Run lifecycle manager — handles setup and teardown for nax execution
|
|
56
|
-
*/
|
|
57
|
-
export class RunLifecycle {
|
|
58
|
-
constructor(
|
|
59
|
-
private readonly prdPath: string,
|
|
60
|
-
private readonly workdir: string,
|
|
61
|
-
private readonly config: NaxConfig,
|
|
62
|
-
private readonly hooks: LoadedHooksConfig,
|
|
63
|
-
private readonly feature: string,
|
|
64
|
-
private readonly dryRun: boolean,
|
|
65
|
-
private readonly useBatch: boolean,
|
|
66
|
-
private readonly statusWriter: StatusWriter,
|
|
67
|
-
private readonly runId: string,
|
|
68
|
-
private readonly startedAt: string,
|
|
69
|
-
) {}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Setup: Acquire lock, load PRD, initialize plugins, setup reporters
|
|
73
|
-
*/
|
|
74
|
-
async setup(): Promise<SetupResult> {
|
|
75
|
-
const logger = getSafeLogger();
|
|
76
|
-
|
|
77
|
-
// Acquire lock to prevent concurrent execution
|
|
78
|
-
const lockAcquired = await acquireLock(this.workdir);
|
|
79
|
-
if (!lockAcquired) {
|
|
80
|
-
logger?.error("execution", "Another nax process is already running in this directory");
|
|
81
|
-
logger?.error("execution", "If you believe this is an error, remove nax.lock manually");
|
|
82
|
-
throw new LockAcquisitionError(this.workdir);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Load plugins
|
|
86
|
-
const globalPluginsDir = path.join(os.homedir(), ".nax", "plugins");
|
|
87
|
-
const projectPluginsDir = path.join(this.workdir, "nax", "plugins");
|
|
88
|
-
const configPlugins = this.config.plugins || [];
|
|
89
|
-
const pluginRegistry = await loadPlugins(globalPluginsDir, projectPluginsDir, configPlugins, this.workdir);
|
|
90
|
-
const reporters = pluginRegistry.getReporters();
|
|
91
|
-
|
|
92
|
-
logger?.info("plugins", `Loaded ${pluginRegistry.plugins.length} plugins`, {
|
|
93
|
-
plugins: pluginRegistry.plugins.map((p) => ({ name: p.name, version: p.version, provides: p.provides })),
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
// Log run start
|
|
97
|
-
const routingMode = this.config.routing.llm?.mode ?? "hybrid";
|
|
98
|
-
logger?.info("run.start", `Starting feature: ${this.feature}`, {
|
|
99
|
-
runId: this.runId,
|
|
100
|
-
feature: this.feature,
|
|
101
|
-
workdir: this.workdir,
|
|
102
|
-
dryRun: this.dryRun,
|
|
103
|
-
useBatch: this.useBatch,
|
|
104
|
-
routingMode,
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
// Fire on-start hook
|
|
108
|
-
await fireHook(this.hooks, "on-start", hookCtx(this.feature), this.workdir);
|
|
109
|
-
|
|
110
|
-
// Check agent installation before starting
|
|
111
|
-
const agent = getAgent(this.config.autoMode.defaultAgent);
|
|
112
|
-
if (!agent) {
|
|
113
|
-
logger?.error("execution", "Agent not found", {
|
|
114
|
-
agent: this.config.autoMode.defaultAgent,
|
|
115
|
-
});
|
|
116
|
-
throw new AgentNotFoundError(this.config.autoMode.defaultAgent);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const installed = await agent.isInstalled();
|
|
120
|
-
if (!installed) {
|
|
121
|
-
logger?.error("execution", "Agent is not installed or not in PATH", {
|
|
122
|
-
agent: this.config.autoMode.defaultAgent,
|
|
123
|
-
binary: agent.binary,
|
|
124
|
-
});
|
|
125
|
-
logger?.error("execution", "Please install the agent and try again");
|
|
126
|
-
throw new AgentNotInstalledError(this.config.autoMode.defaultAgent, agent.binary);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Load PRD
|
|
130
|
-
const prd = await loadPRD(this.prdPath);
|
|
131
|
-
const counts = countStories(prd);
|
|
132
|
-
|
|
133
|
-
// Status write point: run started
|
|
134
|
-
this.statusWriter.setPrd(prd);
|
|
135
|
-
this.statusWriter.setRunStatus("running");
|
|
136
|
-
this.statusWriter.setCurrentStory(null);
|
|
137
|
-
await this.statusWriter.update(0, 0);
|
|
138
|
-
|
|
139
|
-
// Update reporters with correct totalStories count
|
|
140
|
-
for (const reporter of reporters) {
|
|
141
|
-
if (reporter.onRunStart) {
|
|
142
|
-
try {
|
|
143
|
-
await reporter.onRunStart({
|
|
144
|
-
runId: this.runId,
|
|
145
|
-
feature: this.feature,
|
|
146
|
-
totalStories: counts.total,
|
|
147
|
-
startTime: this.startedAt,
|
|
148
|
-
});
|
|
149
|
-
} catch (error) {
|
|
150
|
-
logger?.warn("plugins", `Reporter '${reporter.name}' onRunStart failed`, { error });
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// MEM-1: Validate story count doesn't exceed limit
|
|
156
|
-
if (counts.total > this.config.execution.maxStoriesPerFeature) {
|
|
157
|
-
logger?.error("execution", "Feature exceeds story limit", {
|
|
158
|
-
totalStories: counts.total,
|
|
159
|
-
limit: this.config.execution.maxStoriesPerFeature,
|
|
160
|
-
});
|
|
161
|
-
logger?.error("execution", "Split this feature into smaller features or increase maxStoriesPerFeature in config");
|
|
162
|
-
throw new StoryLimitExceededError(counts.total, this.config.execution.maxStoriesPerFeature);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
logger?.info("execution", `Starting ${this.feature}`, {
|
|
166
|
-
totalStories: counts.total,
|
|
167
|
-
doneStories: counts.passed,
|
|
168
|
-
pendingStories: counts.pending,
|
|
169
|
-
batchingEnabled: this.useBatch,
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
// Clear LLM routing cache at start of new run
|
|
173
|
-
clearLlmCache();
|
|
174
|
-
|
|
175
|
-
// PERF-1: Precompute batch plan once from ready stories
|
|
176
|
-
let batchPlan: StoryBatch[] = [];
|
|
177
|
-
if (this.useBatch) {
|
|
178
|
-
const readyStories = getAllReadyStories(prd);
|
|
179
|
-
batchPlan = precomputeBatchPlan(readyStories, 4);
|
|
180
|
-
|
|
181
|
-
// Initial batch routing
|
|
182
|
-
const mode = this.config.routing.llm?.mode ?? "hybrid";
|
|
183
|
-
if (this.config.routing.strategy === "llm" && mode !== "per-story" && readyStories.length > 0) {
|
|
184
|
-
try {
|
|
185
|
-
logger?.debug("routing", "LLM batch routing: routing", { storyCount: readyStories.length, mode });
|
|
186
|
-
await llmRouteBatch(readyStories, { config: this.config });
|
|
187
|
-
logger?.debug("routing", "LLM batch routing complete", { label: "routing" });
|
|
188
|
-
} catch (err) {
|
|
189
|
-
logger?.warn("routing", "LLM batch routing failed, falling back to individual routing", {
|
|
190
|
-
error: (err as Error).message,
|
|
191
|
-
label: "routing",
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
return {
|
|
198
|
-
prd,
|
|
199
|
-
pluginRegistry,
|
|
200
|
-
batchPlan,
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Teardown: Compute final metrics, write final status, release lock, cleanup plugins
|
|
206
|
-
*/
|
|
207
|
-
async teardown(options: TeardownOptions): Promise<void> {
|
|
208
|
-
const logger = getSafeLogger();
|
|
209
|
-
const {
|
|
210
|
-
runId,
|
|
211
|
-
feature,
|
|
212
|
-
startedAt,
|
|
213
|
-
prd,
|
|
214
|
-
allStoryMetrics,
|
|
215
|
-
totalCost,
|
|
216
|
-
storiesCompleted,
|
|
217
|
-
startTime,
|
|
218
|
-
workdir,
|
|
219
|
-
pluginRegistry,
|
|
220
|
-
statusWriter,
|
|
221
|
-
iterations,
|
|
222
|
-
} = options;
|
|
223
|
-
|
|
224
|
-
const durationMs = Date.now() - startTime;
|
|
225
|
-
|
|
226
|
-
// Save run metrics
|
|
227
|
-
const runCompletedAt = new Date().toISOString();
|
|
228
|
-
const runMetrics = {
|
|
229
|
-
runId,
|
|
230
|
-
feature,
|
|
231
|
-
startedAt,
|
|
232
|
-
completedAt: runCompletedAt,
|
|
233
|
-
totalCost,
|
|
234
|
-
totalStories: allStoryMetrics.length,
|
|
235
|
-
storiesCompleted,
|
|
236
|
-
storiesFailed: countStories(prd).failed,
|
|
237
|
-
totalDurationMs: durationMs,
|
|
238
|
-
stories: allStoryMetrics,
|
|
239
|
-
};
|
|
240
|
-
|
|
241
|
-
await saveRunMetrics(workdir, runMetrics);
|
|
242
|
-
|
|
243
|
-
// Log run completion
|
|
244
|
-
const finalCounts = countStories(prd);
|
|
245
|
-
|
|
246
|
-
// Prepare per-story metrics summary
|
|
247
|
-
const storyMetricsSummary = allStoryMetrics.map((sm) => ({
|
|
248
|
-
storyId: sm.storyId,
|
|
249
|
-
complexity: sm.complexity,
|
|
250
|
-
modelTier: sm.modelTier,
|
|
251
|
-
modelUsed: sm.modelUsed,
|
|
252
|
-
attempts: sm.attempts,
|
|
253
|
-
finalTier: sm.finalTier,
|
|
254
|
-
success: sm.success,
|
|
255
|
-
cost: sm.cost,
|
|
256
|
-
durationMs: sm.durationMs,
|
|
257
|
-
firstPassSuccess: sm.firstPassSuccess,
|
|
258
|
-
}));
|
|
259
|
-
|
|
260
|
-
logger?.info("run.complete", "Feature execution completed", {
|
|
261
|
-
runId,
|
|
262
|
-
feature,
|
|
263
|
-
success: isComplete(prd),
|
|
264
|
-
iterations,
|
|
265
|
-
totalStories: finalCounts.total,
|
|
266
|
-
storiesCompleted,
|
|
267
|
-
storiesFailed: finalCounts.failed,
|
|
268
|
-
storiesPending: finalCounts.pending,
|
|
269
|
-
totalCost,
|
|
270
|
-
durationMs,
|
|
271
|
-
storyMetrics: storyMetricsSummary,
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
// Emit onRunEnd to reporters
|
|
275
|
-
const reporters = pluginRegistry.getReporters();
|
|
276
|
-
for (const reporter of reporters) {
|
|
277
|
-
if (reporter.onRunEnd) {
|
|
278
|
-
try {
|
|
279
|
-
await reporter.onRunEnd({
|
|
280
|
-
runId,
|
|
281
|
-
totalDurationMs: durationMs,
|
|
282
|
-
totalCost,
|
|
283
|
-
storySummary: {
|
|
284
|
-
completed: storiesCompleted,
|
|
285
|
-
failed: finalCounts.failed,
|
|
286
|
-
skipped: finalCounts.skipped,
|
|
287
|
-
paused: finalCounts.paused,
|
|
288
|
-
},
|
|
289
|
-
});
|
|
290
|
-
} catch (error) {
|
|
291
|
-
logger?.warn("plugins", `Reporter '${reporter.name}' onRunEnd failed`, { error });
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
// Status write point: run end
|
|
297
|
-
statusWriter.setPrd(prd);
|
|
298
|
-
statusWriter.setCurrentStory(null);
|
|
299
|
-
statusWriter.setRunStatus(isComplete(prd) ? "completed" : isStalled(prd) ? "stalled" : "running");
|
|
300
|
-
await statusWriter.update(totalCost, iterations);
|
|
301
|
-
|
|
302
|
-
// Teardown plugins
|
|
303
|
-
try {
|
|
304
|
-
await pluginRegistry.teardownAll();
|
|
305
|
-
} catch (error) {
|
|
306
|
-
logger?.warn("plugins", "Plugin teardown failed", { error });
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// Always release lock, even if execution fails
|
|
310
|
-
await releaseLock(workdir);
|
|
311
|
-
}
|
|
312
|
-
}
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for src/execution/run-lifecycle.ts
|
|
3
|
-
*
|
|
4
|
-
* Covers: RunLifecycle teardown critical paths
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { describe, expect, it, mock } from "bun:test";
|
|
8
|
-
import type { NaxConfig } from "../../src/config";
|
|
9
|
-
import { RunLifecycle } from "../../src/execution/lifecycle";
|
|
10
|
-
import type { LoadedHooksConfig } from "../../src/hooks";
|
|
11
|
-
|
|
12
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
13
|
-
// Test fixtures
|
|
14
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
15
|
-
|
|
16
|
-
const mockConfig: NaxConfig = {
|
|
17
|
-
autoMode: { defaultAgent: "claude" },
|
|
18
|
-
routing: { strategy: "complexity", llm: { mode: "hybrid" } },
|
|
19
|
-
execution: {
|
|
20
|
-
maxStoriesPerFeature: 100,
|
|
21
|
-
sessionTimeoutSeconds: 600,
|
|
22
|
-
maxIterations: 50,
|
|
23
|
-
costLimitUSD: 10.0,
|
|
24
|
-
},
|
|
25
|
-
models: {
|
|
26
|
-
fast: { provider: "anthropic", name: "claude-3-haiku-20240307" },
|
|
27
|
-
balanced: { provider: "anthropic", name: "claude-3-5-sonnet-20241022" },
|
|
28
|
-
powerful: { provider: "anthropic", name: "claude-opus-4-20250514" },
|
|
29
|
-
},
|
|
30
|
-
tierOrder: [
|
|
31
|
-
{ tier: "fast", attempts: 3 },
|
|
32
|
-
{ tier: "balanced", attempts: 2 },
|
|
33
|
-
{ tier: "powerful", attempts: 1 },
|
|
34
|
-
],
|
|
35
|
-
plugins: [],
|
|
36
|
-
tdd: { enabled: true },
|
|
37
|
-
} as NaxConfig;
|
|
38
|
-
|
|
39
|
-
const mockHooks: LoadedHooksConfig = {
|
|
40
|
-
"on-start": [],
|
|
41
|
-
"on-story-start": [],
|
|
42
|
-
"on-story-end": [],
|
|
43
|
-
"on-error": [],
|
|
44
|
-
"on-escalate": [],
|
|
45
|
-
"on-end": [],
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
const mockStatusWriter = {
|
|
49
|
-
setPrd: mock(() => {}),
|
|
50
|
-
setRunStatus: mock(() => {}),
|
|
51
|
-
setCurrentStory: mock(() => {}),
|
|
52
|
-
update: mock(async () => {}),
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
56
|
-
// RunLifecycle.teardown()
|
|
57
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
58
|
-
|
|
59
|
-
describe("RunLifecycle.teardown", () => {
|
|
60
|
-
it("calls plugin teardownAll during teardown", async () => {
|
|
61
|
-
const teardownAllMock = mock(async () => {});
|
|
62
|
-
const mockPluginRegistry = {
|
|
63
|
-
teardownAll: teardownAllMock,
|
|
64
|
-
plugins: [],
|
|
65
|
-
getReporters: () => [],
|
|
66
|
-
} as any;
|
|
67
|
-
|
|
68
|
-
const lifecycle = new RunLifecycle(
|
|
69
|
-
"/tmp/prd.json",
|
|
70
|
-
"/tmp",
|
|
71
|
-
mockConfig,
|
|
72
|
-
mockHooks,
|
|
73
|
-
"test-feature",
|
|
74
|
-
false,
|
|
75
|
-
false,
|
|
76
|
-
// @ts-expect-error - partial mock
|
|
77
|
-
mockStatusWriter,
|
|
78
|
-
"run-001",
|
|
79
|
-
new Date().toISOString(),
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
await lifecycle.teardown({
|
|
83
|
-
runId: "run-001",
|
|
84
|
-
feature: "test-feature",
|
|
85
|
-
startedAt: new Date().toISOString(),
|
|
86
|
-
prd: { feature: "test-feature", userStories: [] },
|
|
87
|
-
allStoryMetrics: [],
|
|
88
|
-
totalCost: 0,
|
|
89
|
-
storiesCompleted: 0,
|
|
90
|
-
startTime: Date.now(),
|
|
91
|
-
workdir: "/tmp",
|
|
92
|
-
pluginRegistry: mockPluginRegistry,
|
|
93
|
-
statusWriter: mockStatusWriter as any,
|
|
94
|
-
iterations: 0,
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
expect(teardownAllMock).toHaveBeenCalledTimes(1);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it("updates status writer during teardown", async () => {
|
|
101
|
-
const mockPluginRegistry = {
|
|
102
|
-
teardownAll: async () => {},
|
|
103
|
-
plugins: [],
|
|
104
|
-
getReporters: () => [],
|
|
105
|
-
} as any;
|
|
106
|
-
|
|
107
|
-
const lifecycle = new RunLifecycle(
|
|
108
|
-
"/tmp/prd.json",
|
|
109
|
-
"/tmp",
|
|
110
|
-
mockConfig,
|
|
111
|
-
mockHooks,
|
|
112
|
-
"test-feature",
|
|
113
|
-
false,
|
|
114
|
-
false,
|
|
115
|
-
// @ts-expect-error - partial mock
|
|
116
|
-
mockStatusWriter,
|
|
117
|
-
"run-001",
|
|
118
|
-
new Date().toISOString(),
|
|
119
|
-
);
|
|
120
|
-
|
|
121
|
-
await lifecycle.teardown({
|
|
122
|
-
runId: "run-001",
|
|
123
|
-
feature: "test-feature",
|
|
124
|
-
startedAt: new Date().toISOString(),
|
|
125
|
-
prd: { feature: "test-feature", userStories: [] },
|
|
126
|
-
allStoryMetrics: [],
|
|
127
|
-
totalCost: 1.5,
|
|
128
|
-
storiesCompleted: 5,
|
|
129
|
-
startTime: Date.now(),
|
|
130
|
-
workdir: "/tmp",
|
|
131
|
-
pluginRegistry: mockPluginRegistry,
|
|
132
|
-
statusWriter: mockStatusWriter as any,
|
|
133
|
-
iterations: 10,
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
expect(mockStatusWriter.setPrd).toHaveBeenCalled();
|
|
137
|
-
expect(mockStatusWriter.setRunStatus).toHaveBeenCalled();
|
|
138
|
-
expect(mockStatusWriter.update).toHaveBeenCalled();
|
|
139
|
-
});
|
|
140
|
-
});
|