@renseiai/agentfactory 0.8.11 → 0.8.13
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/src/config/repository-config.d.ts +24 -0
- package/dist/src/config/repository-config.d.ts.map +1 -1
- package/dist/src/config/repository-config.js +21 -0
- package/dist/src/config/repository-config.test.js +202 -0
- package/dist/src/governor/decision-engine.d.ts +2 -0
- package/dist/src/governor/decision-engine.d.ts.map +1 -1
- package/dist/src/governor/decision-engine.js +7 -0
- package/dist/src/governor/decision-engine.test.js +63 -0
- package/dist/src/governor/governor-types.d.ts +2 -1
- package/dist/src/governor/governor-types.d.ts.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/merge-queue/conflict-resolver.d.ts +62 -0
- package/dist/src/merge-queue/conflict-resolver.d.ts.map +1 -0
- package/dist/src/merge-queue/conflict-resolver.js +168 -0
- package/dist/src/merge-queue/conflict-resolver.test.d.ts +2 -0
- package/dist/src/merge-queue/conflict-resolver.test.d.ts.map +1 -0
- package/dist/src/merge-queue/conflict-resolver.test.js +405 -0
- package/dist/src/merge-queue/lock-file-regeneration.d.ts +14 -0
- package/dist/src/merge-queue/lock-file-regeneration.d.ts.map +1 -0
- package/dist/src/merge-queue/lock-file-regeneration.js +82 -0
- package/dist/src/merge-queue/lock-file-regeneration.test.d.ts +2 -0
- package/dist/src/merge-queue/lock-file-regeneration.test.d.ts.map +1 -0
- package/dist/src/merge-queue/lock-file-regeneration.test.js +236 -0
- package/dist/src/merge-queue/merge-worker.d.ts +79 -0
- package/dist/src/merge-queue/merge-worker.d.ts.map +1 -0
- package/dist/src/merge-queue/merge-worker.js +221 -0
- package/dist/src/merge-queue/merge-worker.test.d.ts +2 -0
- package/dist/src/merge-queue/merge-worker.test.d.ts.map +1 -0
- package/dist/src/merge-queue/merge-worker.test.js +883 -0
- package/dist/src/merge-queue/strategies/index.d.ts +19 -0
- package/dist/src/merge-queue/strategies/index.d.ts.map +1 -0
- package/dist/src/merge-queue/strategies/index.js +30 -0
- package/dist/src/merge-queue/strategies/merge-commit-strategy.d.ts +14 -0
- package/dist/src/merge-queue/strategies/merge-commit-strategy.d.ts.map +1 -0
- package/dist/src/merge-queue/strategies/merge-commit-strategy.js +58 -0
- package/dist/src/merge-queue/strategies/rebase-strategy.d.ts +14 -0
- package/dist/src/merge-queue/strategies/rebase-strategy.d.ts.map +1 -0
- package/dist/src/merge-queue/strategies/rebase-strategy.js +62 -0
- package/dist/src/merge-queue/strategies/squash-strategy.d.ts +14 -0
- package/dist/src/merge-queue/strategies/squash-strategy.d.ts.map +1 -0
- package/dist/src/merge-queue/strategies/squash-strategy.js +59 -0
- package/dist/src/merge-queue/strategies/strategies.test.d.ts +2 -0
- package/dist/src/merge-queue/strategies/strategies.test.d.ts.map +1 -0
- package/dist/src/merge-queue/strategies/strategies.test.js +354 -0
- package/dist/src/merge-queue/strategies/types.d.ts +62 -0
- package/dist/src/merge-queue/strategies/types.d.ts.map +1 -0
- package/dist/src/merge-queue/strategies/types.js +7 -0
- package/dist/src/orchestrator/parse-work-result.d.ts.map +1 -1
- package/dist/src/orchestrator/parse-work-result.js +22 -0
- package/dist/src/orchestrator/parse-work-result.test.js +49 -0
- package/dist/src/providers/index.d.ts +1 -0
- package/dist/src/providers/index.d.ts.map +1 -1
- package/dist/src/providers/plugin-types.d.ts +177 -0
- package/dist/src/providers/plugin-types.d.ts.map +1 -0
- package/dist/src/providers/plugin-types.js +10 -0
- package/dist/src/providers/plugin-types.test.d.ts +2 -0
- package/dist/src/providers/plugin-types.test.d.ts.map +1 -0
- package/dist/src/providers/plugin-types.test.js +810 -0
- package/dist/src/registry/index.d.ts +4 -0
- package/dist/src/registry/index.d.ts.map +1 -0
- package/dist/src/registry/index.js +2 -0
- package/dist/src/registry/loader.d.ts +25 -0
- package/dist/src/registry/loader.d.ts.map +1 -0
- package/dist/src/registry/loader.js +88 -0
- package/dist/src/registry/node-type-registry.d.ts +52 -0
- package/dist/src/registry/node-type-registry.d.ts.map +1 -0
- package/dist/src/registry/node-type-registry.js +130 -0
- package/dist/src/registry/types.d.ts +65 -0
- package/dist/src/registry/types.d.ts.map +1 -0
- package/dist/src/registry/types.js +10 -0
- package/dist/src/workflow/expression/ast.d.ts +1 -1
- package/dist/src/workflow/expression/ast.d.ts.map +1 -1
- package/dist/src/workflow/expression/context.d.ts +4 -0
- package/dist/src/workflow/expression/context.d.ts.map +1 -1
- package/dist/src/workflow/expression/context.js +5 -1
- package/dist/src/workflow/expression/evaluator.d.ts.map +1 -1
- package/dist/src/workflow/expression/evaluator.js +24 -1
- package/dist/src/workflow/expression/evaluator.test.js +174 -0
- package/dist/src/workflow/expression/expression.test.js +140 -1
- package/dist/src/workflow/expression/helpers.d.ts +4 -0
- package/dist/src/workflow/expression/helpers.d.ts.map +1 -1
- package/dist/src/workflow/expression/helpers.js +51 -0
- package/dist/src/workflow/expression/index.d.ts +14 -0
- package/dist/src/workflow/expression/index.d.ts.map +1 -1
- package/dist/src/workflow/expression/index.js +28 -1
- package/dist/src/workflow/expression/lexer.d.ts.map +1 -1
- package/dist/src/workflow/expression/lexer.js +43 -0
- package/dist/src/workflow/expression/parser.js +1 -1
- package/dist/src/workflow/index.d.ts +3 -3
- package/dist/src/workflow/index.d.ts.map +1 -1
- package/dist/src/workflow/index.js +4 -2
- package/dist/src/workflow/workflow-loader.d.ts +8 -2
- package/dist/src/workflow/workflow-loader.d.ts.map +1 -1
- package/dist/src/workflow/workflow-loader.js +21 -2
- package/dist/src/workflow/workflow-types.d.ts +781 -12
- package/dist/src/workflow/workflow-types.d.ts.map +1 -1
- package/dist/src/workflow/workflow-types.js +248 -3
- package/dist/src/workflow/workflow-types.test.js +621 -1
- package/package.json +3 -2
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merge Strategies Module
|
|
3
|
+
*
|
|
4
|
+
* Factory and exports for pluggable merge strategies (rebase, merge, squash).
|
|
5
|
+
*/
|
|
6
|
+
export type { MergeStrategy, MergeContext, PrepareResult, MergeResult } from './types.js';
|
|
7
|
+
export { RebaseStrategy } from './rebase-strategy.js';
|
|
8
|
+
export { MergeCommitStrategy } from './merge-commit-strategy.js';
|
|
9
|
+
export { SquashStrategy } from './squash-strategy.js';
|
|
10
|
+
import type { MergeStrategy } from './types.js';
|
|
11
|
+
/**
|
|
12
|
+
* Create a merge strategy by name.
|
|
13
|
+
*
|
|
14
|
+
* @param name - Strategy name: 'rebase', 'merge', or 'squash'
|
|
15
|
+
* @returns MergeStrategy instance
|
|
16
|
+
* @throws Error if strategy name is unknown
|
|
17
|
+
*/
|
|
18
|
+
export declare function createMergeStrategy(name: 'rebase' | 'merge' | 'squash'): MergeStrategy;
|
|
19
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/merge-queue/strategies/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AACzF,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AACrD,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAA;AAChE,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAErD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAK/C;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,GAAG,QAAQ,GAAG,aAAa,CAWtF"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merge Strategies Module
|
|
3
|
+
*
|
|
4
|
+
* Factory and exports for pluggable merge strategies (rebase, merge, squash).
|
|
5
|
+
*/
|
|
6
|
+
export { RebaseStrategy } from './rebase-strategy.js';
|
|
7
|
+
export { MergeCommitStrategy } from './merge-commit-strategy.js';
|
|
8
|
+
export { SquashStrategy } from './squash-strategy.js';
|
|
9
|
+
import { RebaseStrategy } from './rebase-strategy.js';
|
|
10
|
+
import { MergeCommitStrategy } from './merge-commit-strategy.js';
|
|
11
|
+
import { SquashStrategy } from './squash-strategy.js';
|
|
12
|
+
/**
|
|
13
|
+
* Create a merge strategy by name.
|
|
14
|
+
*
|
|
15
|
+
* @param name - Strategy name: 'rebase', 'merge', or 'squash'
|
|
16
|
+
* @returns MergeStrategy instance
|
|
17
|
+
* @throws Error if strategy name is unknown
|
|
18
|
+
*/
|
|
19
|
+
export function createMergeStrategy(name) {
|
|
20
|
+
switch (name) {
|
|
21
|
+
case 'rebase':
|
|
22
|
+
return new RebaseStrategy();
|
|
23
|
+
case 'merge':
|
|
24
|
+
return new MergeCommitStrategy();
|
|
25
|
+
case 'squash':
|
|
26
|
+
return new SquashStrategy();
|
|
27
|
+
default:
|
|
28
|
+
throw new Error(`Unknown merge strategy: ${name}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merge Commit Strategy
|
|
3
|
+
*
|
|
4
|
+
* Performs a standard merge commit (--no-ff) from the source branch into the target.
|
|
5
|
+
* Preserves full branch history with an explicit merge commit.
|
|
6
|
+
*/
|
|
7
|
+
import type { MergeStrategy, MergeContext, PrepareResult, MergeResult } from './types.js';
|
|
8
|
+
export declare class MergeCommitStrategy implements MergeStrategy {
|
|
9
|
+
readonly name: "merge";
|
|
10
|
+
prepare(ctx: MergeContext): Promise<PrepareResult>;
|
|
11
|
+
execute(ctx: MergeContext): Promise<MergeResult>;
|
|
12
|
+
finalize(ctx: MergeContext): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=merge-commit-strategy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"merge-commit-strategy.d.ts","sourceRoot":"","sources":["../../../../src/merge-queue/strategies/merge-commit-strategy.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAIzF,qBAAa,mBAAoB,YAAW,aAAa;IACvD,QAAQ,CAAC,IAAI,EAAG,OAAO,CAAS;IAE1B,OAAO,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,aAAa,CAAC;IAWlD,OAAO,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC;IAqChD,QAAQ,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;CAGjD"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merge Commit Strategy
|
|
3
|
+
*
|
|
4
|
+
* Performs a standard merge commit (--no-ff) from the source branch into the target.
|
|
5
|
+
* Preserves full branch history with an explicit merge commit.
|
|
6
|
+
*/
|
|
7
|
+
import { exec } from 'child_process';
|
|
8
|
+
import { promisify } from 'util';
|
|
9
|
+
const execAsync = promisify(exec);
|
|
10
|
+
export class MergeCommitStrategy {
|
|
11
|
+
name = 'merge';
|
|
12
|
+
async prepare(ctx) {
|
|
13
|
+
try {
|
|
14
|
+
await execAsync(`git fetch ${ctx.remote} ${ctx.targetBranch}`, { cwd: ctx.worktreePath });
|
|
15
|
+
await execAsync(`git checkout ${ctx.targetBranch}`, { cwd: ctx.worktreePath });
|
|
16
|
+
const { stdout } = await execAsync('git rev-parse HEAD', { cwd: ctx.worktreePath });
|
|
17
|
+
return { success: true, headSha: stdout.trim() };
|
|
18
|
+
}
|
|
19
|
+
catch (err) {
|
|
20
|
+
return { success: false, error: err instanceof Error ? err.message : String(err) };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async execute(ctx) {
|
|
24
|
+
try {
|
|
25
|
+
await execAsync(`git merge --no-ff ${ctx.remote}/${ctx.sourceBranch} -m "Merge PR #${ctx.prNumber} from ${ctx.sourceBranch}"`, { cwd: ctx.worktreePath });
|
|
26
|
+
const { stdout } = await execAsync('git rev-parse HEAD', { cwd: ctx.worktreePath });
|
|
27
|
+
return { status: 'success', mergedSha: stdout.trim() };
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
// Check for conflicts
|
|
31
|
+
try {
|
|
32
|
+
const { stdout: conflictOutput } = await execAsync('git diff --name-only --diff-filter=U', { cwd: ctx.worktreePath });
|
|
33
|
+
const conflictFiles = conflictOutput.trim().split('\n').filter(Boolean);
|
|
34
|
+
if (conflictFiles.length > 0) {
|
|
35
|
+
await execAsync('git merge --abort', { cwd: ctx.worktreePath });
|
|
36
|
+
return {
|
|
37
|
+
status: 'conflict',
|
|
38
|
+
conflictFiles,
|
|
39
|
+
conflictDetails: `Merge conflict in ${conflictFiles.length} file(s)`,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Could not detect conflicts; fall through to error
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
await execAsync('git merge --abort', { cwd: ctx.worktreePath });
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// Abort may fail if merge was not in progress
|
|
51
|
+
}
|
|
52
|
+
return { status: 'error', error: err instanceof Error ? err.message : String(err) };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async finalize(ctx) {
|
|
56
|
+
await execAsync(`git push ${ctx.remote} ${ctx.targetBranch}`, { cwd: ctx.worktreePath });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rebase Merge Strategy
|
|
3
|
+
*
|
|
4
|
+
* Rebases the source branch onto the target branch, then fast-forward merges.
|
|
5
|
+
* Produces a linear commit history without merge commits.
|
|
6
|
+
*/
|
|
7
|
+
import type { MergeStrategy, MergeContext, PrepareResult, MergeResult } from './types.js';
|
|
8
|
+
export declare class RebaseStrategy implements MergeStrategy {
|
|
9
|
+
readonly name: "rebase";
|
|
10
|
+
prepare(ctx: MergeContext): Promise<PrepareResult>;
|
|
11
|
+
execute(ctx: MergeContext): Promise<MergeResult>;
|
|
12
|
+
finalize(ctx: MergeContext): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=rebase-strategy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rebase-strategy.d.ts","sourceRoot":"","sources":["../../../../src/merge-queue/strategies/rebase-strategy.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAIzF,qBAAa,cAAe,YAAW,aAAa;IAClD,QAAQ,CAAC,IAAI,EAAG,QAAQ,CAAS;IAE3B,OAAO,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,aAAa,CAAC;IAWlD,OAAO,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC;IAmChD,QAAQ,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;CASjD"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rebase Merge Strategy
|
|
3
|
+
*
|
|
4
|
+
* Rebases the source branch onto the target branch, then fast-forward merges.
|
|
5
|
+
* Produces a linear commit history without merge commits.
|
|
6
|
+
*/
|
|
7
|
+
import { exec } from 'child_process';
|
|
8
|
+
import { promisify } from 'util';
|
|
9
|
+
const execAsync = promisify(exec);
|
|
10
|
+
export class RebaseStrategy {
|
|
11
|
+
name = 'rebase';
|
|
12
|
+
async prepare(ctx) {
|
|
13
|
+
try {
|
|
14
|
+
await execAsync(`git fetch ${ctx.remote} ${ctx.targetBranch}`, { cwd: ctx.worktreePath });
|
|
15
|
+
await execAsync(`git checkout ${ctx.sourceBranch}`, { cwd: ctx.worktreePath });
|
|
16
|
+
const { stdout } = await execAsync('git rev-parse HEAD', { cwd: ctx.worktreePath });
|
|
17
|
+
return { success: true, headSha: stdout.trim() };
|
|
18
|
+
}
|
|
19
|
+
catch (err) {
|
|
20
|
+
return { success: false, error: err instanceof Error ? err.message : String(err) };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async execute(ctx) {
|
|
24
|
+
try {
|
|
25
|
+
await execAsync(`git rebase ${ctx.remote}/${ctx.targetBranch}`, { cwd: ctx.worktreePath });
|
|
26
|
+
const { stdout } = await execAsync('git rev-parse HEAD', { cwd: ctx.worktreePath });
|
|
27
|
+
return { status: 'success', mergedSha: stdout.trim() };
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
// Check if this is a conflict
|
|
31
|
+
try {
|
|
32
|
+
const { stdout: conflictOutput } = await execAsync('git diff --name-only --diff-filter=U', { cwd: ctx.worktreePath });
|
|
33
|
+
const conflictFiles = conflictOutput.trim().split('\n').filter(Boolean);
|
|
34
|
+
if (conflictFiles.length > 0) {
|
|
35
|
+
await execAsync('git rebase --abort', { cwd: ctx.worktreePath });
|
|
36
|
+
return {
|
|
37
|
+
status: 'conflict',
|
|
38
|
+
conflictFiles,
|
|
39
|
+
conflictDetails: `Rebase conflict in ${conflictFiles.length} file(s)`,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Could not detect conflicts; fall through to error
|
|
45
|
+
}
|
|
46
|
+
// Not a conflict — abort rebase and return error
|
|
47
|
+
try {
|
|
48
|
+
await execAsync('git rebase --abort', { cwd: ctx.worktreePath });
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// Abort may fail if rebase was not in progress
|
|
52
|
+
}
|
|
53
|
+
return { status: 'error', error: err instanceof Error ? err.message : String(err) };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async finalize(ctx) {
|
|
57
|
+
await execAsync(`git push ${ctx.remote} ${ctx.sourceBranch} --force-with-lease`, { cwd: ctx.worktreePath });
|
|
58
|
+
await execAsync(`git checkout ${ctx.targetBranch}`, { cwd: ctx.worktreePath });
|
|
59
|
+
await execAsync(`git merge --ff-only ${ctx.sourceBranch}`, { cwd: ctx.worktreePath });
|
|
60
|
+
await execAsync(`git push ${ctx.remote} ${ctx.targetBranch}`, { cwd: ctx.worktreePath });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Squash Merge Strategy
|
|
3
|
+
*
|
|
4
|
+
* Squash-merges all commits from the source branch into a single commit on the target.
|
|
5
|
+
* Produces a clean, linear history with one commit per PR.
|
|
6
|
+
*/
|
|
7
|
+
import type { MergeStrategy, MergeContext, PrepareResult, MergeResult } from './types.js';
|
|
8
|
+
export declare class SquashStrategy implements MergeStrategy {
|
|
9
|
+
readonly name: "squash";
|
|
10
|
+
prepare(ctx: MergeContext): Promise<PrepareResult>;
|
|
11
|
+
execute(ctx: MergeContext): Promise<MergeResult>;
|
|
12
|
+
finalize(ctx: MergeContext): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=squash-strategy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"squash-strategy.d.ts","sourceRoot":"","sources":["../../../../src/merge-queue/strategies/squash-strategy.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAIzF,qBAAa,cAAe,YAAW,aAAa;IAClD,QAAQ,CAAC,IAAI,EAAG,QAAQ,CAAS;IAE3B,OAAO,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,aAAa,CAAC;IAWlD,OAAO,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC;IAyChD,QAAQ,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;CAGjD"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Squash Merge Strategy
|
|
3
|
+
*
|
|
4
|
+
* Squash-merges all commits from the source branch into a single commit on the target.
|
|
5
|
+
* Produces a clean, linear history with one commit per PR.
|
|
6
|
+
*/
|
|
7
|
+
import { exec } from 'child_process';
|
|
8
|
+
import { promisify } from 'util';
|
|
9
|
+
const execAsync = promisify(exec);
|
|
10
|
+
export class SquashStrategy {
|
|
11
|
+
name = 'squash';
|
|
12
|
+
async prepare(ctx) {
|
|
13
|
+
try {
|
|
14
|
+
await execAsync(`git fetch ${ctx.remote} ${ctx.targetBranch}`, { cwd: ctx.worktreePath });
|
|
15
|
+
await execAsync(`git checkout ${ctx.targetBranch}`, { cwd: ctx.worktreePath });
|
|
16
|
+
const { stdout } = await execAsync('git rev-parse HEAD', { cwd: ctx.worktreePath });
|
|
17
|
+
return { success: true, headSha: stdout.trim() };
|
|
18
|
+
}
|
|
19
|
+
catch (err) {
|
|
20
|
+
return { success: false, error: err instanceof Error ? err.message : String(err) };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async execute(ctx) {
|
|
24
|
+
try {
|
|
25
|
+
await execAsync(`git merge --squash ${ctx.remote}/${ctx.sourceBranch}`, { cwd: ctx.worktreePath });
|
|
26
|
+
await execAsync(`git commit -m "Squash merge PR #${ctx.prNumber} from ${ctx.sourceBranch}"`, { cwd: ctx.worktreePath });
|
|
27
|
+
const { stdout } = await execAsync('git rev-parse HEAD', { cwd: ctx.worktreePath });
|
|
28
|
+
return { status: 'success', mergedSha: stdout.trim() };
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
// Check for conflicts
|
|
32
|
+
try {
|
|
33
|
+
const { stdout: conflictOutput } = await execAsync('git diff --name-only --diff-filter=U', { cwd: ctx.worktreePath });
|
|
34
|
+
const conflictFiles = conflictOutput.trim().split('\n').filter(Boolean);
|
|
35
|
+
if (conflictFiles.length > 0) {
|
|
36
|
+
await execAsync('git merge --abort', { cwd: ctx.worktreePath });
|
|
37
|
+
return {
|
|
38
|
+
status: 'conflict',
|
|
39
|
+
conflictFiles,
|
|
40
|
+
conflictDetails: `Squash merge conflict in ${conflictFiles.length} file(s)`,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Could not detect conflicts; fall through to error
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
await execAsync('git merge --abort', { cwd: ctx.worktreePath });
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// Abort may fail if merge was not in progress
|
|
52
|
+
}
|
|
53
|
+
return { status: 'error', error: err instanceof Error ? err.message : String(err) };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async finalize(ctx) {
|
|
57
|
+
await execAsync(`git push ${ctx.remote} ${ctx.targetBranch}`, { cwd: ctx.worktreePath });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"strategies.test.d.ts","sourceRoot":"","sources":["../../../../src/merge-queue/strategies/strategies.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
vi.mock('child_process', () => ({
|
|
3
|
+
exec: vi.fn(),
|
|
4
|
+
}));
|
|
5
|
+
import { exec } from 'child_process';
|
|
6
|
+
import { createMergeStrategy } from './index.js';
|
|
7
|
+
import { RebaseStrategy } from './rebase-strategy.js';
|
|
8
|
+
import { MergeCommitStrategy } from './merge-commit-strategy.js';
|
|
9
|
+
import { SquashStrategy } from './squash-strategy.js';
|
|
10
|
+
const mockExec = vi.mocked(exec);
|
|
11
|
+
/** Default context for all tests */
|
|
12
|
+
const defaultCtx = {
|
|
13
|
+
repoPath: '/repo',
|
|
14
|
+
worktreePath: '/worktree',
|
|
15
|
+
sourceBranch: 'feature/test',
|
|
16
|
+
targetBranch: 'main',
|
|
17
|
+
prNumber: 42,
|
|
18
|
+
remote: 'origin',
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Mock exec to succeed with given stdout for every call.
|
|
22
|
+
* Matches the promisify(exec) pattern: exec(cmd, opts, callback).
|
|
23
|
+
*/
|
|
24
|
+
function mockExecSuccess(stdout = '') {
|
|
25
|
+
mockExec.mockImplementation((_cmd, _opts, callback) => {
|
|
26
|
+
const cb = typeof _opts === 'function' ? _opts : callback;
|
|
27
|
+
cb?.(null, { stdout, stderr: '' });
|
|
28
|
+
return {};
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Mock exec to fail with the given error message for every call.
|
|
33
|
+
*/
|
|
34
|
+
function mockExecFailure(message) {
|
|
35
|
+
mockExec.mockImplementation((_cmd, _opts, callback) => {
|
|
36
|
+
const cb = typeof _opts === 'function' ? _opts : callback;
|
|
37
|
+
cb?.(new Error(message), { stdout: '', stderr: '' });
|
|
38
|
+
return {};
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Queue sequential mock implementations.
|
|
43
|
+
* Each entry is either a string (success stdout) or an Error (failure).
|
|
44
|
+
*/
|
|
45
|
+
function mockExecSequence(results) {
|
|
46
|
+
const queue = [...results];
|
|
47
|
+
mockExec.mockImplementation((_cmd, _opts, callback) => {
|
|
48
|
+
const cb = typeof _opts === 'function' ? _opts : callback;
|
|
49
|
+
const next = queue.shift();
|
|
50
|
+
if (next instanceof Error) {
|
|
51
|
+
cb?.(next, { stdout: '', stderr: '' });
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
cb?.(null, { stdout: next ?? '', stderr: '' });
|
|
55
|
+
}
|
|
56
|
+
return {};
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
describe('createMergeStrategy factory', () => {
|
|
60
|
+
it('returns RebaseStrategy for "rebase"', () => {
|
|
61
|
+
const strategy = createMergeStrategy('rebase');
|
|
62
|
+
expect(strategy).toBeInstanceOf(RebaseStrategy);
|
|
63
|
+
expect(strategy.name).toBe('rebase');
|
|
64
|
+
});
|
|
65
|
+
it('returns MergeCommitStrategy for "merge"', () => {
|
|
66
|
+
const strategy = createMergeStrategy('merge');
|
|
67
|
+
expect(strategy).toBeInstanceOf(MergeCommitStrategy);
|
|
68
|
+
expect(strategy.name).toBe('merge');
|
|
69
|
+
});
|
|
70
|
+
it('returns SquashStrategy for "squash"', () => {
|
|
71
|
+
const strategy = createMergeStrategy('squash');
|
|
72
|
+
expect(strategy).toBeInstanceOf(SquashStrategy);
|
|
73
|
+
expect(strategy.name).toBe('squash');
|
|
74
|
+
});
|
|
75
|
+
it('throws on unknown strategy name', () => {
|
|
76
|
+
expect(() => createMergeStrategy('unknown')).toThrow('Unknown merge strategy: unknown');
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
describe('RebaseStrategy', () => {
|
|
80
|
+
let strategy;
|
|
81
|
+
beforeEach(() => {
|
|
82
|
+
vi.clearAllMocks();
|
|
83
|
+
strategy = new RebaseStrategy();
|
|
84
|
+
});
|
|
85
|
+
it('has name "rebase"', () => {
|
|
86
|
+
expect(strategy.name).toBe('rebase');
|
|
87
|
+
});
|
|
88
|
+
describe('prepare', () => {
|
|
89
|
+
it('fetches target branch and checks out source branch', async () => {
|
|
90
|
+
mockExecSequence([
|
|
91
|
+
'', // git fetch origin main
|
|
92
|
+
'', // git checkout feature/test
|
|
93
|
+
'abc123\n', // git rev-parse HEAD
|
|
94
|
+
]);
|
|
95
|
+
const result = await strategy.prepare(defaultCtx);
|
|
96
|
+
expect(result).toEqual({ success: true, headSha: 'abc123' });
|
|
97
|
+
expect(mockExec).toHaveBeenCalledTimes(3);
|
|
98
|
+
// Verify commands and cwd
|
|
99
|
+
const calls = mockExec.mock.calls;
|
|
100
|
+
expect(calls[0][0]).toBe('git fetch origin main');
|
|
101
|
+
expect(calls[0][1]).toEqual({ cwd: '/worktree' });
|
|
102
|
+
expect(calls[1][0]).toBe('git checkout feature/test');
|
|
103
|
+
expect(calls[1][1]).toEqual({ cwd: '/worktree' });
|
|
104
|
+
expect(calls[2][0]).toBe('git rev-parse HEAD');
|
|
105
|
+
expect(calls[2][1]).toEqual({ cwd: '/worktree' });
|
|
106
|
+
});
|
|
107
|
+
it('returns failure when fetch fails', async () => {
|
|
108
|
+
mockExecFailure('fatal: could not read from remote');
|
|
109
|
+
const result = await strategy.prepare(defaultCtx);
|
|
110
|
+
expect(result.success).toBe(false);
|
|
111
|
+
expect(result.error).toContain('could not read from remote');
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
describe('execute', () => {
|
|
115
|
+
it('runs rebase and returns success with merged SHA', async () => {
|
|
116
|
+
mockExecSequence([
|
|
117
|
+
'', // git rebase origin/main
|
|
118
|
+
'def456\n', // git rev-parse HEAD
|
|
119
|
+
]);
|
|
120
|
+
const result = await strategy.execute(defaultCtx);
|
|
121
|
+
expect(result).toEqual({ status: 'success', mergedSha: 'def456' });
|
|
122
|
+
expect(mockExec).toHaveBeenCalledTimes(2);
|
|
123
|
+
const calls = mockExec.mock.calls;
|
|
124
|
+
expect(calls[0][0]).toBe('git rebase origin/main');
|
|
125
|
+
expect(calls[0][1]).toEqual({ cwd: '/worktree' });
|
|
126
|
+
});
|
|
127
|
+
it('detects conflicts, aborts rebase, and returns conflict result', async () => {
|
|
128
|
+
mockExecSequence([
|
|
129
|
+
new Error('CONFLICT (content): Merge conflict in file.ts'), // git rebase fails
|
|
130
|
+
'src/file.ts\nsrc/other.ts\n', // git diff --name-only --diff-filter=U
|
|
131
|
+
'', // git rebase --abort
|
|
132
|
+
]);
|
|
133
|
+
const result = await strategy.execute(defaultCtx);
|
|
134
|
+
expect(result.status).toBe('conflict');
|
|
135
|
+
expect(result.conflictFiles).toEqual(['src/file.ts', 'src/other.ts']);
|
|
136
|
+
expect(result.conflictDetails).toBe('Rebase conflict in 2 file(s)');
|
|
137
|
+
});
|
|
138
|
+
it('aborts rebase and returns error on non-conflict failure', async () => {
|
|
139
|
+
mockExecSequence([
|
|
140
|
+
new Error('fatal: invalid upstream'), // git rebase fails
|
|
141
|
+
new Error('no conflicts'), // git diff --name-only fails (no conflict state)
|
|
142
|
+
'', // git rebase --abort
|
|
143
|
+
]);
|
|
144
|
+
const result = await strategy.execute(defaultCtx);
|
|
145
|
+
expect(result.status).toBe('error');
|
|
146
|
+
expect(result.error).toContain('invalid upstream');
|
|
147
|
+
});
|
|
148
|
+
it('handles abort failure gracefully after error', async () => {
|
|
149
|
+
mockExecSequence([
|
|
150
|
+
new Error('fatal: something went wrong'), // git rebase fails
|
|
151
|
+
'', // git diff returns empty (no conflicts)
|
|
152
|
+
new Error('no rebase in progress'), // git rebase --abort fails
|
|
153
|
+
]);
|
|
154
|
+
const result = await strategy.execute(defaultCtx);
|
|
155
|
+
expect(result.status).toBe('error');
|
|
156
|
+
expect(result.error).toContain('something went wrong');
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
describe('finalize', () => {
|
|
160
|
+
it('force-pushes source branch and fast-forward merges to target', async () => {
|
|
161
|
+
mockExecSequence([
|
|
162
|
+
'', // git push origin feature/test --force-with-lease
|
|
163
|
+
'', // git checkout main
|
|
164
|
+
'', // git merge --ff-only feature/test
|
|
165
|
+
'', // git push origin main
|
|
166
|
+
]);
|
|
167
|
+
await strategy.finalize(defaultCtx);
|
|
168
|
+
expect(mockExec).toHaveBeenCalledTimes(4);
|
|
169
|
+
const calls = mockExec.mock.calls;
|
|
170
|
+
expect(calls[0][0]).toBe('git push origin feature/test --force-with-lease');
|
|
171
|
+
expect(calls[0][1]).toEqual({ cwd: '/worktree' });
|
|
172
|
+
expect(calls[1][0]).toBe('git checkout main');
|
|
173
|
+
expect(calls[1][1]).toEqual({ cwd: '/worktree' });
|
|
174
|
+
expect(calls[2][0]).toBe('git merge --ff-only feature/test');
|
|
175
|
+
expect(calls[2][1]).toEqual({ cwd: '/worktree' });
|
|
176
|
+
expect(calls[3][0]).toBe('git push origin main');
|
|
177
|
+
expect(calls[3][1]).toEqual({ cwd: '/worktree' });
|
|
178
|
+
});
|
|
179
|
+
it('throws when push fails', async () => {
|
|
180
|
+
mockExecFailure('rejected: non-fast-forward');
|
|
181
|
+
await expect(strategy.finalize(defaultCtx)).rejects.toThrow('rejected: non-fast-forward');
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
describe('MergeCommitStrategy', () => {
|
|
186
|
+
let strategy;
|
|
187
|
+
beforeEach(() => {
|
|
188
|
+
vi.clearAllMocks();
|
|
189
|
+
strategy = new MergeCommitStrategy();
|
|
190
|
+
});
|
|
191
|
+
it('has name "merge"', () => {
|
|
192
|
+
expect(strategy.name).toBe('merge');
|
|
193
|
+
});
|
|
194
|
+
describe('prepare', () => {
|
|
195
|
+
it('fetches and checks out target branch', async () => {
|
|
196
|
+
mockExecSequence([
|
|
197
|
+
'', // git fetch origin main
|
|
198
|
+
'', // git checkout main
|
|
199
|
+
'aaa111\n', // git rev-parse HEAD
|
|
200
|
+
]);
|
|
201
|
+
const result = await strategy.prepare(defaultCtx);
|
|
202
|
+
expect(result).toEqual({ success: true, headSha: 'aaa111' });
|
|
203
|
+
const calls = mockExec.mock.calls;
|
|
204
|
+
expect(calls[0][0]).toBe('git fetch origin main');
|
|
205
|
+
expect(calls[1][0]).toBe('git checkout main');
|
|
206
|
+
expect(calls[1][1]).toEqual({ cwd: '/worktree' });
|
|
207
|
+
});
|
|
208
|
+
it('returns failure on error', async () => {
|
|
209
|
+
mockExecFailure('error: pathspec did not match');
|
|
210
|
+
const result = await strategy.prepare(defaultCtx);
|
|
211
|
+
expect(result.success).toBe(false);
|
|
212
|
+
expect(result.error).toContain('pathspec did not match');
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
describe('execute', () => {
|
|
216
|
+
it('runs merge --no-ff and returns success', async () => {
|
|
217
|
+
mockExecSequence([
|
|
218
|
+
'', // git merge --no-ff
|
|
219
|
+
'bbb222\n', // git rev-parse HEAD
|
|
220
|
+
]);
|
|
221
|
+
const result = await strategy.execute(defaultCtx);
|
|
222
|
+
expect(result).toEqual({ status: 'success', mergedSha: 'bbb222' });
|
|
223
|
+
const calls = mockExec.mock.calls;
|
|
224
|
+
expect(calls[0][0]).toBe('git merge --no-ff origin/feature/test -m "Merge PR #42 from feature/test"');
|
|
225
|
+
expect(calls[0][1]).toEqual({ cwd: '/worktree' });
|
|
226
|
+
});
|
|
227
|
+
it('detects conflicts, aborts merge, and returns conflict result', async () => {
|
|
228
|
+
mockExecSequence([
|
|
229
|
+
new Error('CONFLICT'), // git merge fails
|
|
230
|
+
'src/conflict.ts\n', // git diff --name-only --diff-filter=U
|
|
231
|
+
'', // git merge --abort
|
|
232
|
+
]);
|
|
233
|
+
const result = await strategy.execute(defaultCtx);
|
|
234
|
+
expect(result.status).toBe('conflict');
|
|
235
|
+
expect(result.conflictFiles).toEqual(['src/conflict.ts']);
|
|
236
|
+
expect(result.conflictDetails).toBe('Merge conflict in 1 file(s)');
|
|
237
|
+
});
|
|
238
|
+
it('returns error on non-conflict failure', async () => {
|
|
239
|
+
mockExecSequence([
|
|
240
|
+
new Error('fatal: refusing to merge unrelated histories'), // git merge fails
|
|
241
|
+
'', // git diff returns empty
|
|
242
|
+
'', // git merge --abort
|
|
243
|
+
]);
|
|
244
|
+
const result = await strategy.execute(defaultCtx);
|
|
245
|
+
expect(result.status).toBe('error');
|
|
246
|
+
expect(result.error).toContain('unrelated histories');
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
describe('finalize', () => {
|
|
250
|
+
it('pushes target branch', async () => {
|
|
251
|
+
mockExecSuccess();
|
|
252
|
+
await strategy.finalize(defaultCtx);
|
|
253
|
+
expect(mockExec).toHaveBeenCalledTimes(1);
|
|
254
|
+
const calls = mockExec.mock.calls;
|
|
255
|
+
expect(calls[0][0]).toBe('git push origin main');
|
|
256
|
+
expect(calls[0][1]).toEqual({ cwd: '/worktree' });
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
describe('SquashStrategy', () => {
|
|
261
|
+
let strategy;
|
|
262
|
+
beforeEach(() => {
|
|
263
|
+
vi.clearAllMocks();
|
|
264
|
+
strategy = new SquashStrategy();
|
|
265
|
+
});
|
|
266
|
+
it('has name "squash"', () => {
|
|
267
|
+
expect(strategy.name).toBe('squash');
|
|
268
|
+
});
|
|
269
|
+
describe('prepare', () => {
|
|
270
|
+
it('fetches and checks out target branch', async () => {
|
|
271
|
+
mockExecSequence([
|
|
272
|
+
'', // git fetch origin main
|
|
273
|
+
'', // git checkout main
|
|
274
|
+
'ccc333\n', // git rev-parse HEAD
|
|
275
|
+
]);
|
|
276
|
+
const result = await strategy.prepare(defaultCtx);
|
|
277
|
+
expect(result).toEqual({ success: true, headSha: 'ccc333' });
|
|
278
|
+
const calls = mockExec.mock.calls;
|
|
279
|
+
expect(calls[0][0]).toBe('git fetch origin main');
|
|
280
|
+
expect(calls[1][0]).toBe('git checkout main');
|
|
281
|
+
expect(calls[1][1]).toEqual({ cwd: '/worktree' });
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
describe('execute', () => {
|
|
285
|
+
it('runs merge --squash, commits, and returns success', async () => {
|
|
286
|
+
mockExecSequence([
|
|
287
|
+
'', // git merge --squash
|
|
288
|
+
'', // git commit
|
|
289
|
+
'ddd444\n', // git rev-parse HEAD
|
|
290
|
+
]);
|
|
291
|
+
const result = await strategy.execute(defaultCtx);
|
|
292
|
+
expect(result).toEqual({ status: 'success', mergedSha: 'ddd444' });
|
|
293
|
+
const calls = mockExec.mock.calls;
|
|
294
|
+
expect(calls[0][0]).toBe('git merge --squash origin/feature/test');
|
|
295
|
+
expect(calls[0][1]).toEqual({ cwd: '/worktree' });
|
|
296
|
+
expect(calls[1][0]).toBe('git commit -m "Squash merge PR #42 from feature/test"');
|
|
297
|
+
expect(calls[1][1]).toEqual({ cwd: '/worktree' });
|
|
298
|
+
});
|
|
299
|
+
it('detects conflicts, aborts, and returns conflict result', async () => {
|
|
300
|
+
mockExecSequence([
|
|
301
|
+
new Error('CONFLICT'), // git merge --squash fails
|
|
302
|
+
'src/a.ts\nsrc/b.ts\n', // git diff --name-only --diff-filter=U
|
|
303
|
+
'', // git merge --abort
|
|
304
|
+
]);
|
|
305
|
+
const result = await strategy.execute(defaultCtx);
|
|
306
|
+
expect(result.status).toBe('conflict');
|
|
307
|
+
expect(result.conflictFiles).toEqual(['src/a.ts', 'src/b.ts']);
|
|
308
|
+
expect(result.conflictDetails).toBe('Squash merge conflict in 2 file(s)');
|
|
309
|
+
});
|
|
310
|
+
it('returns error on non-conflict failure', async () => {
|
|
311
|
+
mockExecSequence([
|
|
312
|
+
new Error('fatal: not something we can merge'), // git merge --squash fails
|
|
313
|
+
'', // git diff returns empty
|
|
314
|
+
'', // git merge --abort
|
|
315
|
+
]);
|
|
316
|
+
const result = await strategy.execute(defaultCtx);
|
|
317
|
+
expect(result.status).toBe('error');
|
|
318
|
+
expect(result.error).toContain('not something we can merge');
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
describe('finalize', () => {
|
|
322
|
+
it('pushes target branch', async () => {
|
|
323
|
+
mockExecSuccess();
|
|
324
|
+
await strategy.finalize(defaultCtx);
|
|
325
|
+
expect(mockExec).toHaveBeenCalledTimes(1);
|
|
326
|
+
const calls = mockExec.mock.calls;
|
|
327
|
+
expect(calls[0][0]).toBe('git push origin main');
|
|
328
|
+
expect(calls[0][1]).toEqual({ cwd: '/worktree' });
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
describe('All strategies use ctx.worktreePath as cwd', () => {
|
|
333
|
+
const strategies = [
|
|
334
|
+
new RebaseStrategy(),
|
|
335
|
+
new MergeCommitStrategy(),
|
|
336
|
+
new SquashStrategy(),
|
|
337
|
+
];
|
|
338
|
+
beforeEach(() => {
|
|
339
|
+
vi.clearAllMocks();
|
|
340
|
+
});
|
|
341
|
+
for (const strategy of strategies) {
|
|
342
|
+
it(`${strategy.name} strategy passes worktreePath as cwd to all exec calls`, async () => {
|
|
343
|
+
const customCtx = {
|
|
344
|
+
...defaultCtx,
|
|
345
|
+
worktreePath: '/custom/worktree/path',
|
|
346
|
+
};
|
|
347
|
+
mockExecSuccess('abc123\n');
|
|
348
|
+
await strategy.prepare(customCtx);
|
|
349
|
+
for (const call of mockExec.mock.calls) {
|
|
350
|
+
expect(call[1]).toEqual({ cwd: '/custom/worktree/path' });
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
});
|