steroids-cli 0.8.22 → 0.8.24
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/commands/loop-phases.d.ts +1 -1
- package/dist/commands/loop-phases.d.ts.map +1 -1
- package/dist/commands/loop-phases.js +3 -3
- package/dist/commands/loop-phases.js.map +1 -1
- package/dist/commands/runners.js +9 -0
- package/dist/commands/runners.js.map +1 -1
- package/dist/database/schema.d.ts +2 -2
- package/dist/database/schema.d.ts.map +1 -1
- package/dist/database/schema.js +30 -0
- package/dist/database/schema.js.map +1 -1
- package/dist/orchestrator/task-selector.d.ts +7 -2
- package/dist/orchestrator/task-selector.d.ts.map +1 -1
- package/dist/orchestrator/task-selector.js +91 -14
- package/dist/orchestrator/task-selector.js.map +1 -1
- package/dist/parallel/clone.d.ts +41 -0
- package/dist/parallel/clone.d.ts.map +1 -0
- package/dist/parallel/clone.js +176 -0
- package/dist/parallel/clone.js.map +1 -0
- package/dist/parallel/clone.test.d.ts +2 -0
- package/dist/parallel/clone.test.d.ts.map +1 -0
- package/dist/parallel/clone.test.js +252 -0
- package/dist/parallel/clone.test.js.map +1 -0
- package/dist/parallel/merge-conflict.d.ts +22 -0
- package/dist/parallel/merge-conflict.d.ts.map +1 -0
- package/dist/parallel/merge-conflict.js +227 -0
- package/dist/parallel/merge-conflict.js.map +1 -0
- package/dist/parallel/merge-errors.d.ts +10 -0
- package/dist/parallel/merge-errors.d.ts.map +1 -0
- package/dist/parallel/merge-errors.js +16 -0
- package/dist/parallel/merge-errors.js.map +1 -0
- package/dist/parallel/merge-git.d.ts +25 -0
- package/dist/parallel/merge-git.d.ts.map +1 -0
- package/dist/parallel/merge-git.js +134 -0
- package/dist/parallel/merge-git.js.map +1 -0
- package/dist/parallel/merge-lock.d.ts +26 -0
- package/dist/parallel/merge-lock.d.ts.map +1 -0
- package/dist/parallel/merge-lock.js +58 -0
- package/dist/parallel/merge-lock.js.map +1 -0
- package/dist/parallel/merge-progress.d.ts +20 -0
- package/dist/parallel/merge-progress.d.ts.map +1 -0
- package/dist/parallel/merge-progress.js +36 -0
- package/dist/parallel/merge-progress.js.map +1 -0
- package/dist/parallel/merge.d.ts +39 -0
- package/dist/parallel/merge.d.ts.map +1 -0
- package/dist/parallel/merge.js +231 -0
- package/dist/parallel/merge.js.map +1 -0
- package/dist/parallel/merge.test.d.ts +5 -0
- package/dist/parallel/merge.test.d.ts.map +1 -0
- package/dist/parallel/merge.test.js +322 -0
- package/dist/parallel/merge.test.js.map +1 -0
- package/dist/parallel/scheduler.d.ts +43 -0
- package/dist/parallel/scheduler.d.ts.map +1 -0
- package/dist/parallel/scheduler.js +281 -0
- package/dist/parallel/scheduler.js.map +1 -0
- package/dist/runners/daemon.d.ts +11 -1
- package/dist/runners/daemon.d.ts.map +1 -1
- package/dist/runners/daemon.js +18 -4
- package/dist/runners/daemon.js.map +1 -1
- package/dist/runners/global-db.d.ts.map +1 -1
- package/dist/runners/global-db.js +42 -1
- package/dist/runners/global-db.js.map +1 -1
- package/dist/runners/orchestrator-loop.d.ts +3 -0
- package/dist/runners/orchestrator-loop.d.ts.map +1 -1
- package/dist/runners/orchestrator-loop.js +19 -9
- package/dist/runners/orchestrator-loop.js.map +1 -1
- package/dist/runners/wakeup.d.ts.map +1 -1
- package/dist/runners/wakeup.js +2 -1
- package/dist/runners/wakeup.js.map +1 -1
- package/migrations/011_add_merge_locks_and_progress.sql +39 -0
- package/migrations/manifest.json +9 -1
- package/package.json +1 -1
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parallel merge orchestration
|
|
3
|
+
* Cherry-picks completed workstream branches into main with crash-safe progress tracking.
|
|
4
|
+
*/
|
|
5
|
+
import { MergeLockRecord } from './merge-lock.js';
|
|
6
|
+
import { ParallelMergeError } from './merge-errors.js';
|
|
7
|
+
export interface MergeWorkstreamSpec {
|
|
8
|
+
id: string;
|
|
9
|
+
branchName: string;
|
|
10
|
+
}
|
|
11
|
+
export interface MergeOptions {
|
|
12
|
+
projectPath: string;
|
|
13
|
+
sessionId: string;
|
|
14
|
+
runnerId: string;
|
|
15
|
+
workstreams: MergeWorkstreamSpec[];
|
|
16
|
+
remote?: string;
|
|
17
|
+
mainBranch?: string;
|
|
18
|
+
lockTimeoutMinutes?: number;
|
|
19
|
+
heartbeatIntervalMs?: number;
|
|
20
|
+
remoteWorkspaceRoot?: string;
|
|
21
|
+
cleanupOnSuccess?: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface MergeResult {
|
|
24
|
+
success: boolean;
|
|
25
|
+
completedCommits: number;
|
|
26
|
+
conflicts: number;
|
|
27
|
+
skipped: number;
|
|
28
|
+
errors: string[];
|
|
29
|
+
}
|
|
30
|
+
declare function getNowISOString(): string;
|
|
31
|
+
declare function cleanupWorkspaceState(projectPath: string, workspaceRoot: string, workstreamIds: string[], options: {
|
|
32
|
+
cleanupOnSuccess: boolean;
|
|
33
|
+
}): void;
|
|
34
|
+
/**
|
|
35
|
+
* Run cherry-pick merge loop across completed workstreams.
|
|
36
|
+
*/
|
|
37
|
+
export declare function runParallelMerge(options: MergeOptions): Promise<MergeResult>;
|
|
38
|
+
export { MergeLockRecord, ParallelMergeError, getNowISOString, cleanupWorkspaceState, };
|
|
39
|
+
//# sourceMappingURL=merge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"merge.d.ts","sourceRoot":"","sources":["../../src/parallel/merge.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAgBH,OAAO,EACL,eAAe,EAIhB,MAAM,iBAAiB,CAAC;AASzB,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAEvD,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,mBAAmB,EAAE,CAAC;IACnC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,gBAAgB,EAAE,MAAM,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAOD,iBAAS,eAAe,IAAI,MAAM,CAEjC;AAcD,iBAAS,qBAAqB,CAC5B,WAAW,EAAE,MAAM,EACnB,aAAa,EAAE,MAAM,EACrB,aAAa,EAAE,MAAM,EAAE,EACvB,OAAO,EAAE;IAAE,gBAAgB,EAAE,OAAO,CAAA;CAAE,GACrC,IAAI,CAqBN;AAqGD;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CA+HlF;AAED,OAAO,EACL,eAAe,EACf,kBAAkB,EAClB,eAAe,EACf,qBAAqB,GACtB,CAAC"}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Parallel merge orchestration
|
|
4
|
+
* Cherry-picks completed workstream branches into main with crash-safe progress tracking.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.ParallelMergeError = void 0;
|
|
8
|
+
exports.runParallelMerge = runParallelMerge;
|
|
9
|
+
exports.getNowISOString = getNowISOString;
|
|
10
|
+
exports.cleanupWorkspaceState = cleanupWorkspaceState;
|
|
11
|
+
const node_path_1 = require("node:path");
|
|
12
|
+
const node_fs_1 = require("node:fs");
|
|
13
|
+
const clone_js_1 = require("./clone.js");
|
|
14
|
+
const clone_js_2 = require("./clone.js");
|
|
15
|
+
const connection_js_1 = require("../database/connection.js");
|
|
16
|
+
const merge_git_js_1 = require("./merge-git.js");
|
|
17
|
+
const merge_lock_js_1 = require("./merge-lock.js");
|
|
18
|
+
const merge_progress_js_1 = require("./merge-progress.js");
|
|
19
|
+
const merge_conflict_js_1 = require("./merge-conflict.js");
|
|
20
|
+
const merge_errors_js_1 = require("./merge-errors.js");
|
|
21
|
+
Object.defineProperty(exports, "ParallelMergeError", { enumerable: true, get: function () { return merge_errors_js_1.ParallelMergeError; } });
|
|
22
|
+
const DEFAULT_REMOTE = 'origin';
|
|
23
|
+
const DEFAULT_MAIN_BRANCH = 'main';
|
|
24
|
+
const DEFAULT_LOCK_TIMEOUT_MINUTES = 120;
|
|
25
|
+
const DEFAULT_HEARTBEAT_INTERVAL_MS = 30_000;
|
|
26
|
+
function getNowISOString() {
|
|
27
|
+
return new Date().toISOString();
|
|
28
|
+
}
|
|
29
|
+
function ensureMergeWorkingTree(projectPath) {
|
|
30
|
+
const lines = (0, merge_git_js_1.gitStatusLines)(projectPath);
|
|
31
|
+
if (lines.length === 0)
|
|
32
|
+
return;
|
|
33
|
+
if (!(0, merge_git_js_1.hasCherryPickInProgress)(projectPath)) {
|
|
34
|
+
throw new merge_errors_js_1.ParallelMergeError('Working tree is dirty. Commit or stash changes before merging.', 'DIRTY_WORKTREE');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function cleanupWorkspaceState(projectPath, workspaceRoot, workstreamIds, options) {
|
|
38
|
+
if (!options.cleanupOnSuccess)
|
|
39
|
+
return;
|
|
40
|
+
const baseRoot = (0, node_path_1.resolve)(workspaceRoot);
|
|
41
|
+
const hash = (0, clone_js_2.getProjectHash)(projectPath);
|
|
42
|
+
const projectWorkspaceRoot = (0, node_path_1.resolve)(baseRoot, hash);
|
|
43
|
+
if (!projectWorkspaceRoot.startsWith(baseRoot)) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
for (const workstreamId of workstreamIds) {
|
|
47
|
+
const folder = (0, node_path_1.resolve)(projectWorkspaceRoot, workstreamId.startsWith('ws-') ? workstreamId : `ws-${workstreamId}`);
|
|
48
|
+
if ((0, node_path_1.resolve)(folder).startsWith(projectWorkspaceRoot)) {
|
|
49
|
+
(0, node_fs_1.rmSync)(folder, { recursive: true, force: true });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async function processWorkstream(db, projectPath, sessionId, workstream, mainBranch, remote, progressRows, heartbeat) {
|
|
54
|
+
const summary = { applied: 0, skipped: 0, conflicts: 0 };
|
|
55
|
+
const commits = (0, merge_git_js_1.getWorkstreamCommitList)(projectPath, remote, workstream.branchName, mainBranch);
|
|
56
|
+
if (commits.length === 0) {
|
|
57
|
+
return summary;
|
|
58
|
+
}
|
|
59
|
+
const workstreamProgress = (0, merge_progress_js_1.getMergeProgressForWorkstream)(progressRows, workstream.id);
|
|
60
|
+
const workstreamLookup = new Map();
|
|
61
|
+
for (const row of workstreamProgress) {
|
|
62
|
+
workstreamLookup.set(row.position, row);
|
|
63
|
+
}
|
|
64
|
+
for (let position = 0; position < commits.length; position += 1) {
|
|
65
|
+
const commitSha = commits[position];
|
|
66
|
+
const shortSha = (0, merge_git_js_1.getCommitShortSha)(commitSha);
|
|
67
|
+
const prior = workstreamLookup.get(position);
|
|
68
|
+
if (prior?.status === 'applied' && prior.commit_sha === commitSha) {
|
|
69
|
+
summary.applied += 1;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (prior?.status === 'skipped' && prior.commit_sha === commitSha) {
|
|
73
|
+
summary.skipped += 1;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (prior?.status === 'conflict' && prior.commit_sha === commitSha) {
|
|
77
|
+
if ((0, merge_git_js_1.hasCherryPickInProgress)(projectPath)) {
|
|
78
|
+
const outcome = await (0, merge_conflict_js_1.runConflictResolutionCycle)({
|
|
79
|
+
db,
|
|
80
|
+
projectPath,
|
|
81
|
+
sessionId,
|
|
82
|
+
workstreamId: workstream.id,
|
|
83
|
+
branchName: workstream.branchName,
|
|
84
|
+
position,
|
|
85
|
+
commitSha,
|
|
86
|
+
existingTaskId: prior.conflict_task_id ?? undefined,
|
|
87
|
+
});
|
|
88
|
+
if (outcome === 'skipped')
|
|
89
|
+
summary.skipped += 1;
|
|
90
|
+
else
|
|
91
|
+
summary.applied += 1;
|
|
92
|
+
summary.conflicts += 1;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
(0, merge_progress_js_1.clearProgressEntry)(db, sessionId, workstream.id, position);
|
|
96
|
+
}
|
|
97
|
+
if (prior && prior.commit_sha !== commitSha) {
|
|
98
|
+
(0, merge_progress_js_1.clearProgressEntry)(db, sessionId, workstream.id, position);
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
(0, merge_git_js_1.runGitCommand)(projectPath, ['cherry-pick', commitSha]);
|
|
102
|
+
(0, merge_progress_js_1.upsertProgressEntry)(db, sessionId, workstream.id, position, commitSha, 'applied');
|
|
103
|
+
summary.applied += 1;
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
107
|
+
if (!/CONFLICT|merge conflict|could not apply|needs merge/i.test(message)) {
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
const outcome = await (0, merge_conflict_js_1.runConflictResolutionCycle)({
|
|
111
|
+
db,
|
|
112
|
+
projectPath,
|
|
113
|
+
sessionId,
|
|
114
|
+
workstreamId: workstream.id,
|
|
115
|
+
branchName: workstream.branchName,
|
|
116
|
+
position,
|
|
117
|
+
commitSha,
|
|
118
|
+
});
|
|
119
|
+
if (outcome === 'skipped') {
|
|
120
|
+
summary.skipped += 1;
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
summary.applied += 1;
|
|
124
|
+
}
|
|
125
|
+
summary.conflicts += 1;
|
|
126
|
+
}
|
|
127
|
+
(0, merge_lock_js_1.refreshMergeLock)(db, heartbeat.sessionId, heartbeat.runnerId, heartbeat.timeoutMinutes);
|
|
128
|
+
}
|
|
129
|
+
return summary;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Run cherry-pick merge loop across completed workstreams.
|
|
133
|
+
*/
|
|
134
|
+
async function runParallelMerge(options) {
|
|
135
|
+
const projectPath = (0, node_path_1.resolve)(options.projectPath);
|
|
136
|
+
const sessionId = options.sessionId;
|
|
137
|
+
const runnerId = options.runnerId;
|
|
138
|
+
const workstreams = options.workstreams;
|
|
139
|
+
const remote = options.remote ?? DEFAULT_REMOTE;
|
|
140
|
+
const mainBranch = options.mainBranch ?? DEFAULT_MAIN_BRANCH;
|
|
141
|
+
const lockTimeoutMinutes = options.lockTimeoutMinutes ?? DEFAULT_LOCK_TIMEOUT_MINUTES;
|
|
142
|
+
const heartbeatIntervalMs = options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
|
|
143
|
+
const workspaceRoot = options.remoteWorkspaceRoot ?? (0, clone_js_1.getDefaultWorkspaceRoot)();
|
|
144
|
+
const cleanupOnSuccess = options.cleanupOnSuccess ?? true;
|
|
145
|
+
const { db, close } = (0, connection_js_1.openDatabase)(projectPath);
|
|
146
|
+
const summary = {
|
|
147
|
+
success: false,
|
|
148
|
+
completedCommits: 0,
|
|
149
|
+
conflicts: 0,
|
|
150
|
+
skipped: 0,
|
|
151
|
+
errors: [],
|
|
152
|
+
};
|
|
153
|
+
let heartbeatTimer = null;
|
|
154
|
+
try {
|
|
155
|
+
const lock = (0, merge_lock_js_1.acquireMergeLock)(db, {
|
|
156
|
+
sessionId,
|
|
157
|
+
runnerId,
|
|
158
|
+
timeoutMinutes: lockTimeoutMinutes,
|
|
159
|
+
});
|
|
160
|
+
if (!lock.acquired) {
|
|
161
|
+
summary.errors.push(`Could not acquire merge lock (held by ${lock.lock?.runner_id ?? 'another process'})`);
|
|
162
|
+
return summary;
|
|
163
|
+
}
|
|
164
|
+
const recoveringFromCherryPick = (0, merge_git_js_1.hasCherryPickInProgress)(projectPath);
|
|
165
|
+
heartbeatTimer = setInterval(() => {
|
|
166
|
+
try {
|
|
167
|
+
(0, merge_lock_js_1.refreshMergeLock)(db, sessionId, runnerId, lockTimeoutMinutes);
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
// If heartbeat fails, merge keeps running until lock-dependent operations fail.
|
|
171
|
+
}
|
|
172
|
+
}, heartbeatIntervalMs);
|
|
173
|
+
ensureMergeWorkingTree(projectPath);
|
|
174
|
+
for (const stream of workstreams) {
|
|
175
|
+
(0, merge_git_js_1.safeRunMergeCommand)(projectPath, remote, stream.branchName);
|
|
176
|
+
}
|
|
177
|
+
if (!recoveringFromCherryPick) {
|
|
178
|
+
const pullOutput = (0, merge_git_js_1.runGitCommand)(projectPath, ['pull', '--ff-only', remote, mainBranch], { allowFailure: true });
|
|
179
|
+
const pullOutputLower = pullOutput.toLowerCase();
|
|
180
|
+
if (pullOutputLower.includes('fatal:') || pullOutputLower.includes('error:') || pullOutputLower.includes('error ')) {
|
|
181
|
+
if (pullOutputLower.includes('could not apply') ||
|
|
182
|
+
pullOutputLower.includes('not possible to fast-forward')) {
|
|
183
|
+
throw new merge_errors_js_1.ParallelMergeError('main is behind; local commits detected. Run "git pull --rebase" before merge.', 'NON_FAST_FORWARD');
|
|
184
|
+
}
|
|
185
|
+
throw new merge_errors_js_1.ParallelMergeError(`Failed to refresh main from ${remote}/${mainBranch}: ${pullOutput}`, 'PULL_FAILED');
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const progressRows = (0, merge_progress_js_1.listMergeProgress)(db, sessionId);
|
|
189
|
+
for (const workstream of workstreams) {
|
|
190
|
+
const stats = await processWorkstream(db, projectPath, sessionId, workstream, mainBranch, remote, progressRows, { sessionId, runnerId, timeoutMinutes: lockTimeoutMinutes });
|
|
191
|
+
summary.completedCommits += stats.applied;
|
|
192
|
+
summary.skipped += stats.skipped;
|
|
193
|
+
summary.conflicts += stats.conflicts;
|
|
194
|
+
}
|
|
195
|
+
const pushResult = (0, merge_git_js_1.runGitCommand)(projectPath, ['push', remote, mainBranch], { allowFailure: true });
|
|
196
|
+
if ((0, merge_git_js_1.isNoPushError)(pushResult)) {
|
|
197
|
+
summary.errors.push('Push to main failed.');
|
|
198
|
+
throw new merge_errors_js_1.ParallelMergeError(pushResult, 'PUSH_FAILED');
|
|
199
|
+
}
|
|
200
|
+
for (const stream of workstreams) {
|
|
201
|
+
try {
|
|
202
|
+
(0, merge_git_js_1.runGitCommand)(projectPath, ['push', remote, '--delete', stream.branchName]);
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
// Ignore branch delete failures; branch may already be deleted.
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
(0, merge_git_js_1.runGitCommand)(projectPath, ['remote', 'prune', remote]);
|
|
209
|
+
cleanupWorkspaceState(projectPath, workspaceRoot, workstreams.map((stream) => stream.id), {
|
|
210
|
+
cleanupOnSuccess,
|
|
211
|
+
});
|
|
212
|
+
summary.success = true;
|
|
213
|
+
return summary;
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
217
|
+
if (!summary.errors.includes(message)) {
|
|
218
|
+
summary.errors.push(message);
|
|
219
|
+
}
|
|
220
|
+
return summary;
|
|
221
|
+
}
|
|
222
|
+
finally {
|
|
223
|
+
if (heartbeatTimer) {
|
|
224
|
+
clearInterval(heartbeatTimer);
|
|
225
|
+
heartbeatTimer = null;
|
|
226
|
+
}
|
|
227
|
+
(0, merge_lock_js_1.releaseMergeLock)(db, sessionId, runnerId);
|
|
228
|
+
close();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
//# sourceMappingURL=merge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"merge.js","sourceRoot":"","sources":["../../src/parallel/merge.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"merge.test.d.ts","sourceRoot":"","sources":["../../src/parallel/merge.test.ts"],"names":[],"mappings":"AAAA;;GAEG"}
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Tests for parallel merge orchestration and conflict recovery.
|
|
4
|
+
*/
|
|
5
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
6
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
7
|
+
};
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
|
|
10
|
+
const globals_1 = require("@jest/globals");
|
|
11
|
+
const node_fs_1 = require("node:fs");
|
|
12
|
+
const node_fs_2 = require("node:fs");
|
|
13
|
+
const node_os_1 = require("node:os");
|
|
14
|
+
const node_path_1 = require("node:path");
|
|
15
|
+
const schema_js_1 = require("../database/schema.js");
|
|
16
|
+
const clone_js_1 = require("./clone.js");
|
|
17
|
+
const mockExecFileSync = globals_1.jest.fn();
|
|
18
|
+
globals_1.jest.unstable_mockModule('node:child_process', () => ({
|
|
19
|
+
execFileSync: mockExecFileSync,
|
|
20
|
+
}));
|
|
21
|
+
const mockOpenDatabase = globals_1.jest.fn();
|
|
22
|
+
const mockClose = globals_1.jest.fn();
|
|
23
|
+
let db;
|
|
24
|
+
globals_1.jest.unstable_mockModule('../database/connection.js', () => ({
|
|
25
|
+
openDatabase: mockOpenDatabase,
|
|
26
|
+
getDbPath: globals_1.jest.fn().mockReturnValue('/tmp/steroids.db'),
|
|
27
|
+
}));
|
|
28
|
+
const mockLoadConfig = globals_1.jest.fn();
|
|
29
|
+
globals_1.jest.unstable_mockModule('../config/loader.js', () => ({
|
|
30
|
+
loadConfig: mockLoadConfig,
|
|
31
|
+
}));
|
|
32
|
+
const mockProviderInvoke = globals_1.jest.fn();
|
|
33
|
+
globals_1.jest.unstable_mockModule('../providers/registry.js', () => ({
|
|
34
|
+
getProviderRegistry: () => ({
|
|
35
|
+
get: () => ({ invoke: mockProviderInvoke }),
|
|
36
|
+
}),
|
|
37
|
+
}));
|
|
38
|
+
const mockLogInvocation = globals_1.jest.fn();
|
|
39
|
+
globals_1.jest.unstable_mockModule('../providers/invocation-logger.js', () => ({
|
|
40
|
+
logInvocation: mockLogInvocation,
|
|
41
|
+
}));
|
|
42
|
+
let mergeModule;
|
|
43
|
+
let lockModule;
|
|
44
|
+
let progressModule;
|
|
45
|
+
let conflictModule;
|
|
46
|
+
let gitPlan = [];
|
|
47
|
+
let invocationOutputs = [];
|
|
48
|
+
function createDb() {
|
|
49
|
+
const next = new better_sqlite3_1.default(':memory:');
|
|
50
|
+
next.exec(schema_js_1.SCHEMA_SQL);
|
|
51
|
+
return next;
|
|
52
|
+
}
|
|
53
|
+
function setGitPlan(steps) {
|
|
54
|
+
gitPlan = steps.map((step) => ({ ...step }));
|
|
55
|
+
}
|
|
56
|
+
function queueInvocationOutputs(outputs) {
|
|
57
|
+
invocationOutputs = [...outputs];
|
|
58
|
+
}
|
|
59
|
+
function takeGitOutput(args) {
|
|
60
|
+
const step = gitPlan.shift();
|
|
61
|
+
if (!step) {
|
|
62
|
+
throw new Error(`Unexpected git command: git ${args.join(' ')}`);
|
|
63
|
+
}
|
|
64
|
+
(0, globals_1.expect)(step.args).toEqual(globals_1.expect.arrayContaining(args));
|
|
65
|
+
if (step.error) {
|
|
66
|
+
throw new Error(step.error);
|
|
67
|
+
}
|
|
68
|
+
return step.output ?? '';
|
|
69
|
+
}
|
|
70
|
+
(0, globals_1.beforeEach)(async () => {
|
|
71
|
+
db = createDb();
|
|
72
|
+
mockOpenDatabase.mockReturnValue({ db, close: mockClose });
|
|
73
|
+
globals_1.jest.clearAllMocks();
|
|
74
|
+
mockClose.mockClear();
|
|
75
|
+
gitPlan = [];
|
|
76
|
+
invocationOutputs = [];
|
|
77
|
+
mockExecFileSync.mockImplementation(((command, args) => {
|
|
78
|
+
if (command !== 'git') {
|
|
79
|
+
throw new Error(`Unexpected command ${command}`);
|
|
80
|
+
}
|
|
81
|
+
if (!Array.isArray(args)) {
|
|
82
|
+
throw new Error('Invalid git args');
|
|
83
|
+
}
|
|
84
|
+
return takeGitOutput(args);
|
|
85
|
+
}));
|
|
86
|
+
mockLoadConfig.mockReturnValue({
|
|
87
|
+
ai: {
|
|
88
|
+
coder: { provider: 'mock', model: 'mock-model' },
|
|
89
|
+
reviewer: { provider: 'mock', model: 'mock-model' },
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
mockLogInvocation.mockImplementation(async () => ({
|
|
93
|
+
success: true,
|
|
94
|
+
exitCode: 0,
|
|
95
|
+
stdout: invocationOutputs.shift() ?? 'APPROVE',
|
|
96
|
+
stderr: '',
|
|
97
|
+
duration: 1,
|
|
98
|
+
timedOut: false,
|
|
99
|
+
}));
|
|
100
|
+
[mergeModule, lockModule, progressModule, conflictModule] = await Promise.all([
|
|
101
|
+
import('./merge.js'),
|
|
102
|
+
import('./merge-lock.js'),
|
|
103
|
+
import('./merge-progress.js'),
|
|
104
|
+
import('./merge-conflict.js'),
|
|
105
|
+
]);
|
|
106
|
+
});
|
|
107
|
+
(0, globals_1.afterEach)(() => {
|
|
108
|
+
if (db) {
|
|
109
|
+
db.close();
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
(0, globals_1.describe)('parseReviewDecision', () => {
|
|
113
|
+
(0, globals_1.it)('parses explicit APPROVE', () => {
|
|
114
|
+
const result = conflictModule.parseReviewDecision('APPROVE - looks good');
|
|
115
|
+
(0, globals_1.expect)(result.decision).toBe('approve');
|
|
116
|
+
});
|
|
117
|
+
(0, globals_1.it)('parses explicit REJECT', () => {
|
|
118
|
+
const result = conflictModule.parseReviewDecision('REJECT - this is incorrect');
|
|
119
|
+
(0, globals_1.expect)(result.decision).toBe('reject');
|
|
120
|
+
});
|
|
121
|
+
(0, globals_1.it)('treats ambiguous responses as reject', () => {
|
|
122
|
+
const result = conflictModule.parseReviewDecision('This is partly APPROVE, but also REJECT for conflicts');
|
|
123
|
+
(0, globals_1.expect)(result.decision).toBe('reject');
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
(0, globals_1.describe)('merge lock behavior', () => {
|
|
127
|
+
(0, globals_1.it)('acquires lock when none exists', () => {
|
|
128
|
+
const result = lockModule.acquireMergeLock(db, {
|
|
129
|
+
sessionId: 'session-1',
|
|
130
|
+
runnerId: 'runner-a',
|
|
131
|
+
timeoutMinutes: 120,
|
|
132
|
+
});
|
|
133
|
+
(0, globals_1.expect)(result.acquired).toBe(true);
|
|
134
|
+
(0, globals_1.expect)(result.lock?.runner_id).toBe('runner-a');
|
|
135
|
+
});
|
|
136
|
+
(0, globals_1.it)('rejects lock when held by another active runner', () => {
|
|
137
|
+
const lockRow = new Date(Date.now() + 60 * 60 * 1000).toISOString();
|
|
138
|
+
db.prepare('INSERT INTO merge_locks (session_id, runner_id, acquired_at, expires_at, heartbeat_at) VALUES (?, ?, datetime("now"), ?, datetime("now"))').run('session-2', 'runner-current', lockRow);
|
|
139
|
+
const result = lockModule.acquireMergeLock(db, {
|
|
140
|
+
sessionId: 'session-2',
|
|
141
|
+
runnerId: 'runner-other',
|
|
142
|
+
timeoutMinutes: 120,
|
|
143
|
+
});
|
|
144
|
+
(0, globals_1.expect)(result.acquired).toBe(false);
|
|
145
|
+
(0, globals_1.expect)(result.lock?.runner_id).toBe('runner-current');
|
|
146
|
+
});
|
|
147
|
+
(0, globals_1.it)('replaces lock when expired', () => {
|
|
148
|
+
const stale = new Date(Date.now() - 60_000).toISOString();
|
|
149
|
+
db.prepare('INSERT INTO merge_locks (session_id, runner_id, acquired_at, expires_at, heartbeat_at) VALUES (?, ?, datetime("now"), ?, datetime("now"))').run('session-3', 'runner-stale', stale);
|
|
150
|
+
const result = lockModule.acquireMergeLock(db, {
|
|
151
|
+
sessionId: 'session-3',
|
|
152
|
+
runnerId: 'runner-fresh',
|
|
153
|
+
timeoutMinutes: 120,
|
|
154
|
+
});
|
|
155
|
+
(0, globals_1.expect)(result.acquired).toBe(true);
|
|
156
|
+
(0, globals_1.expect)(result.lock?.runner_id).toBe('runner-fresh');
|
|
157
|
+
});
|
|
158
|
+
(0, globals_1.it)('refreshes existing lock heartbeat', () => {
|
|
159
|
+
lockModule.acquireMergeLock(db, {
|
|
160
|
+
sessionId: 'session-4',
|
|
161
|
+
runnerId: 'runner-refresh',
|
|
162
|
+
timeoutMinutes: 120,
|
|
163
|
+
});
|
|
164
|
+
const before = lockModule.getLatestMergeLock(db, 'session-4');
|
|
165
|
+
(0, globals_1.expect)(before).toBeTruthy();
|
|
166
|
+
const after = lockModule.refreshMergeLock(db, 'session-4', 'runner-refresh', 120);
|
|
167
|
+
(0, globals_1.expect)(after.id).toBe(before.id);
|
|
168
|
+
(0, globals_1.expect)(new Date(after.heartbeat_at).getTime()).toBeGreaterThanOrEqual(new Date(before.heartbeat_at).getTime());
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
(0, globals_1.describe)('merge progress tracking', () => {
|
|
172
|
+
(0, globals_1.it)('stores and clears progress entries', () => {
|
|
173
|
+
progressModule.upsertProgressEntry(db, 'session-progress', 'ws-1', 0, 'abc123', 'applied');
|
|
174
|
+
progressModule.upsertProgressEntry(db, 'session-progress', 'ws-1', 1, 'def456', 'conflict', 'task-1');
|
|
175
|
+
const rows = progressModule.listMergeProgress(db, 'session-progress');
|
|
176
|
+
(0, globals_1.expect)(rows).toHaveLength(2);
|
|
177
|
+
(0, globals_1.expect)(progressModule.getMergeProgressForWorkstream(rows, 'ws-1').map((row) => row.position)).toEqual([0, 1]);
|
|
178
|
+
progressModule.clearProgressEntry(db, 'session-progress', 'ws-1', 0);
|
|
179
|
+
const remaining = progressModule.listMergeProgress(db, 'session-progress');
|
|
180
|
+
(0, globals_1.expect)(remaining.map((row) => row.position)).toEqual([1]);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
(0, globals_1.describe)('runParallelMerge integration', () => {
|
|
184
|
+
const createProjectAndWorkspace = () => {
|
|
185
|
+
const projectPath = (0, node_fs_2.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'steroids-merge-XXXXXX'));
|
|
186
|
+
const workspaceRoot = (0, node_fs_2.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'steroids-merge-workspace-XXXXXX'));
|
|
187
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.resolve)(projectPath, '.git'), { recursive: true });
|
|
188
|
+
return { projectPath, workspaceRoot };
|
|
189
|
+
};
|
|
190
|
+
(0, globals_1.it)('merges successfully with clean cherry-pick path', async () => {
|
|
191
|
+
const { projectPath, workspaceRoot } = createProjectAndWorkspace();
|
|
192
|
+
setGitPlan([
|
|
193
|
+
{ args: ['status', '--porcelain'], output: '' },
|
|
194
|
+
{ args: ['fetch', '--prune', 'origin', 'steroids/ws-alpha'], output: '' },
|
|
195
|
+
{ args: ['pull', '--ff-only', 'origin', 'main'], output: '' },
|
|
196
|
+
{ args: ['log', 'main..origin/steroids/ws-alpha', '--format=%H', '--reverse'], output: 'commit-a\ncommit-b' },
|
|
197
|
+
{ args: ['cherry-pick', 'commit-a'], output: '' },
|
|
198
|
+
{ args: ['cherry-pick', 'commit-b'], output: '' },
|
|
199
|
+
{ args: ['push', 'origin', 'main'], output: 'ok' },
|
|
200
|
+
{ args: ['push', 'origin', '--delete', 'steroids/ws-alpha'], output: '' },
|
|
201
|
+
{ args: ['remote', 'prune', 'origin'], output: '' },
|
|
202
|
+
]);
|
|
203
|
+
const result = await mergeModule.runParallelMerge({
|
|
204
|
+
projectPath,
|
|
205
|
+
sessionId: 'merge-session',
|
|
206
|
+
runnerId: 'runner-1',
|
|
207
|
+
workstreams: [{ id: 'alpha', branchName: 'steroids/ws-alpha' }],
|
|
208
|
+
remoteWorkspaceRoot: workspaceRoot,
|
|
209
|
+
});
|
|
210
|
+
(0, globals_1.expect)(result.success).toBe(true);
|
|
211
|
+
(0, globals_1.expect)(result.completedCommits).toBe(2);
|
|
212
|
+
(0, globals_1.expect)(result.errors).toHaveLength(0);
|
|
213
|
+
(0, globals_1.expect)(mockOpenDatabase).toHaveBeenCalledWith(projectPath);
|
|
214
|
+
});
|
|
215
|
+
(0, globals_1.it)('resumes from prior progress rows', async () => {
|
|
216
|
+
progressModule.upsertProgressEntry(db, 'resume-session', 'alpha', 0, 'commit-a', 'applied');
|
|
217
|
+
const { projectPath } = createProjectAndWorkspace();
|
|
218
|
+
setGitPlan([
|
|
219
|
+
{ args: ['status', '--porcelain'], output: '' },
|
|
220
|
+
{ args: ['fetch', '--prune', 'origin', 'steroids/ws-alpha'], output: '' },
|
|
221
|
+
{ args: ['pull', '--ff-only', 'origin', 'main'], output: '' },
|
|
222
|
+
{ args: ['log', 'main..origin/steroids/ws-alpha', '--format=%H', '--reverse'], output: 'commit-a\ncommit-b' },
|
|
223
|
+
{ args: ['cherry-pick', 'commit-b'], output: '' },
|
|
224
|
+
{ args: ['push', 'origin', 'main'], output: 'ok' },
|
|
225
|
+
{ args: ['push', 'origin', '--delete', 'steroids/ws-alpha'], output: '' },
|
|
226
|
+
{ args: ['remote', 'prune', 'origin'], output: '' },
|
|
227
|
+
]);
|
|
228
|
+
const result = await mergeModule.runParallelMerge({
|
|
229
|
+
projectPath,
|
|
230
|
+
sessionId: 'resume-session',
|
|
231
|
+
runnerId: 'runner-1',
|
|
232
|
+
workstreams: [{ id: 'alpha', branchName: 'steroids/ws-alpha' }],
|
|
233
|
+
});
|
|
234
|
+
(0, globals_1.expect)(result.success).toBe(true);
|
|
235
|
+
(0, globals_1.expect)(result.completedCommits).toBe(2);
|
|
236
|
+
const rows = progressModule.listMergeProgress(db, 'resume-session');
|
|
237
|
+
(0, globals_1.expect)(rows).toHaveLength(2);
|
|
238
|
+
});
|
|
239
|
+
(0, globals_1.it)('handles merge conflict with coder/reviewer loop', async () => {
|
|
240
|
+
const { projectPath } = createProjectAndWorkspace();
|
|
241
|
+
queueInvocationOutputs([
|
|
242
|
+
'coder resolved',
|
|
243
|
+
'APPROVE - conflict resolved',
|
|
244
|
+
]);
|
|
245
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.resolve)(projectPath, '.git'), { recursive: true });
|
|
246
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(projectPath, '.git', 'CHERRY_PICK_HEAD'), '0000000000000000000000000000000000000000');
|
|
247
|
+
setGitPlan([
|
|
248
|
+
{ args: ['status', '--porcelain'], output: '' },
|
|
249
|
+
{ args: ['fetch', '--prune', 'origin', 'steroids/ws-alpha'], output: '' },
|
|
250
|
+
{ args: ['pull', '--ff-only', 'origin', 'main'], output: '' },
|
|
251
|
+
{ args: ['log', 'main..origin/steroids/ws-alpha', '--format=%H', '--reverse'], output: 'commit-conflict' },
|
|
252
|
+
{ args: ['cherry-pick', 'commit-conflict'], error: 'CONFLICT: could not apply commit-conflict' },
|
|
253
|
+
{ args: ['show', 'commit-conflict', '--'], output: 'patch' },
|
|
254
|
+
{ args: ['log', '-1', '--format=%s%n%b', 'commit-conflict'], output: 'Conflicting commit' },
|
|
255
|
+
{ args: ['diff', '--name-only', '--diff-filter=U'], output: 'src/file.ts' },
|
|
256
|
+
{ args: ['diff', '--name-only', '--diff-filter=U'], output: '' },
|
|
257
|
+
{ args: ['diff', '--cached', '--name-only'], output: 'src/file.ts' },
|
|
258
|
+
{ args: ['diff', '--cached'], output: 'staged diff' },
|
|
259
|
+
{ args: ['diff', '--name-only', '--diff-filter=U'], output: '' },
|
|
260
|
+
{ args: ['-c', 'core.editor=true', 'cherry-pick', '--continue'], output: '' },
|
|
261
|
+
{ args: ['push', 'origin', 'main'], output: 'ok' },
|
|
262
|
+
{ args: ['push', 'origin', '--delete', 'steroids/ws-alpha'], output: '' },
|
|
263
|
+
{ args: ['remote', 'prune', 'origin'], output: '' },
|
|
264
|
+
]);
|
|
265
|
+
const result = await mergeModule.runParallelMerge({
|
|
266
|
+
projectPath,
|
|
267
|
+
sessionId: 'conflict-session',
|
|
268
|
+
runnerId: 'runner-1',
|
|
269
|
+
workstreams: [{ id: 'alpha', branchName: 'steroids/ws-alpha' }],
|
|
270
|
+
});
|
|
271
|
+
(0, globals_1.expect)(result.success).toBe(true);
|
|
272
|
+
(0, globals_1.expect)(result.completedCommits).toBe(1);
|
|
273
|
+
(0, globals_1.expect)(result.conflicts).toBe(1);
|
|
274
|
+
});
|
|
275
|
+
(0, globals_1.it)('cleans workspace directory after successful merge', async () => {
|
|
276
|
+
const { projectPath, workspaceRoot } = createProjectAndWorkspace();
|
|
277
|
+
const projectHash = (0, clone_js_1.getProjectHash)(projectPath);
|
|
278
|
+
const sessionPath = (0, node_path_1.resolve)(workspaceRoot, projectHash, 'ws-alpha');
|
|
279
|
+
(0, node_fs_1.mkdirSync)(sessionPath, { recursive: true });
|
|
280
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(sessionPath, '.keep'), 'cleanup target');
|
|
281
|
+
setGitPlan([
|
|
282
|
+
{ args: ['status', '--porcelain'], output: '' },
|
|
283
|
+
{ args: ['fetch', '--prune', 'origin', 'steroids/ws-alpha'], output: '' },
|
|
284
|
+
{ args: ['pull', '--ff-only', 'origin', 'main'], output: '' },
|
|
285
|
+
{ args: ['log', 'main..origin/steroids/ws-alpha', '--format=%H', '--reverse'], output: 'commit-a' },
|
|
286
|
+
{ args: ['cherry-pick', 'commit-a'], output: '' },
|
|
287
|
+
{ args: ['push', 'origin', 'main'], output: 'ok' },
|
|
288
|
+
{ args: ['push', 'origin', '--delete', 'steroids/ws-alpha'], output: '' },
|
|
289
|
+
{ args: ['remote', 'prune', 'origin'], output: '' },
|
|
290
|
+
]);
|
|
291
|
+
const result = await mergeModule.runParallelMerge({
|
|
292
|
+
projectPath,
|
|
293
|
+
sessionId: 'cleanup-session',
|
|
294
|
+
runnerId: 'runner-1',
|
|
295
|
+
workstreams: [{ id: 'alpha', branchName: 'steroids/ws-alpha' }],
|
|
296
|
+
remoteWorkspaceRoot: workspaceRoot,
|
|
297
|
+
cleanupOnSuccess: true,
|
|
298
|
+
});
|
|
299
|
+
(0, globals_1.expect)(result.success).toBe(true);
|
|
300
|
+
(0, globals_1.expect)((0, node_fs_1.existsSync)(sessionPath)).toBe(false);
|
|
301
|
+
});
|
|
302
|
+
(0, globals_1.it)('reports push failures as errors', async () => {
|
|
303
|
+
const { projectPath } = createProjectAndWorkspace();
|
|
304
|
+
setGitPlan([
|
|
305
|
+
{ args: ['status', '--porcelain'], output: '' },
|
|
306
|
+
{ args: ['fetch', '--prune', 'origin', 'steroids/ws-alpha'], output: '' },
|
|
307
|
+
{ args: ['pull', '--ff-only', 'origin', 'main'], output: '' },
|
|
308
|
+
{ args: ['log', 'main..origin/steroids/ws-alpha', '--format=%H', '--reverse'], output: 'commit-a' },
|
|
309
|
+
{ args: ['cherry-pick', 'commit-a'], output: '' },
|
|
310
|
+
{ args: ['push', 'origin', 'main'], output: 'error: failed to push' },
|
|
311
|
+
]);
|
|
312
|
+
const result = await mergeModule.runParallelMerge({
|
|
313
|
+
projectPath,
|
|
314
|
+
sessionId: 'push-fail-session',
|
|
315
|
+
runnerId: 'runner-1',
|
|
316
|
+
workstreams: [{ id: 'alpha', branchName: 'steroids/ws-alpha' }],
|
|
317
|
+
});
|
|
318
|
+
(0, globals_1.expect)(result.success).toBe(false);
|
|
319
|
+
(0, globals_1.expect)(result.errors.some((error) => error.includes('Push to main failed.'))).toBe(true);
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
//# sourceMappingURL=merge.test.js.map
|