@renseiai/agentfactory 0.8.12 → 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.
Files changed (101) hide show
  1. package/dist/src/config/repository-config.d.ts +24 -0
  2. package/dist/src/config/repository-config.d.ts.map +1 -1
  3. package/dist/src/config/repository-config.js +21 -0
  4. package/dist/src/config/repository-config.test.js +202 -0
  5. package/dist/src/governor/decision-engine.d.ts +2 -0
  6. package/dist/src/governor/decision-engine.d.ts.map +1 -1
  7. package/dist/src/governor/decision-engine.js +7 -0
  8. package/dist/src/governor/decision-engine.test.js +63 -0
  9. package/dist/src/governor/governor-types.d.ts +2 -1
  10. package/dist/src/governor/governor-types.d.ts.map +1 -1
  11. package/dist/src/index.d.ts +1 -0
  12. package/dist/src/index.d.ts.map +1 -1
  13. package/dist/src/index.js +1 -0
  14. package/dist/src/merge-queue/conflict-resolver.d.ts +62 -0
  15. package/dist/src/merge-queue/conflict-resolver.d.ts.map +1 -0
  16. package/dist/src/merge-queue/conflict-resolver.js +168 -0
  17. package/dist/src/merge-queue/conflict-resolver.test.d.ts +2 -0
  18. package/dist/src/merge-queue/conflict-resolver.test.d.ts.map +1 -0
  19. package/dist/src/merge-queue/conflict-resolver.test.js +405 -0
  20. package/dist/src/merge-queue/lock-file-regeneration.d.ts +14 -0
  21. package/dist/src/merge-queue/lock-file-regeneration.d.ts.map +1 -0
  22. package/dist/src/merge-queue/lock-file-regeneration.js +82 -0
  23. package/dist/src/merge-queue/lock-file-regeneration.test.d.ts +2 -0
  24. package/dist/src/merge-queue/lock-file-regeneration.test.d.ts.map +1 -0
  25. package/dist/src/merge-queue/lock-file-regeneration.test.js +236 -0
  26. package/dist/src/merge-queue/merge-worker.d.ts +79 -0
  27. package/dist/src/merge-queue/merge-worker.d.ts.map +1 -0
  28. package/dist/src/merge-queue/merge-worker.js +221 -0
  29. package/dist/src/merge-queue/merge-worker.test.d.ts +2 -0
  30. package/dist/src/merge-queue/merge-worker.test.d.ts.map +1 -0
  31. package/dist/src/merge-queue/merge-worker.test.js +883 -0
  32. package/dist/src/merge-queue/strategies/index.d.ts +19 -0
  33. package/dist/src/merge-queue/strategies/index.d.ts.map +1 -0
  34. package/dist/src/merge-queue/strategies/index.js +30 -0
  35. package/dist/src/merge-queue/strategies/merge-commit-strategy.d.ts +14 -0
  36. package/dist/src/merge-queue/strategies/merge-commit-strategy.d.ts.map +1 -0
  37. package/dist/src/merge-queue/strategies/merge-commit-strategy.js +58 -0
  38. package/dist/src/merge-queue/strategies/rebase-strategy.d.ts +14 -0
  39. package/dist/src/merge-queue/strategies/rebase-strategy.d.ts.map +1 -0
  40. package/dist/src/merge-queue/strategies/rebase-strategy.js +62 -0
  41. package/dist/src/merge-queue/strategies/squash-strategy.d.ts +14 -0
  42. package/dist/src/merge-queue/strategies/squash-strategy.d.ts.map +1 -0
  43. package/dist/src/merge-queue/strategies/squash-strategy.js +59 -0
  44. package/dist/src/merge-queue/strategies/strategies.test.d.ts +2 -0
  45. package/dist/src/merge-queue/strategies/strategies.test.d.ts.map +1 -0
  46. package/dist/src/merge-queue/strategies/strategies.test.js +354 -0
  47. package/dist/src/merge-queue/strategies/types.d.ts +62 -0
  48. package/dist/src/merge-queue/strategies/types.d.ts.map +1 -0
  49. package/dist/src/merge-queue/strategies/types.js +7 -0
  50. package/dist/src/orchestrator/parse-work-result.d.ts.map +1 -1
  51. package/dist/src/orchestrator/parse-work-result.js +22 -0
  52. package/dist/src/orchestrator/parse-work-result.test.js +49 -0
  53. package/dist/src/providers/index.d.ts +1 -0
  54. package/dist/src/providers/index.d.ts.map +1 -1
  55. package/dist/src/providers/plugin-types.d.ts +177 -0
  56. package/dist/src/providers/plugin-types.d.ts.map +1 -0
  57. package/dist/src/providers/plugin-types.js +10 -0
  58. package/dist/src/providers/plugin-types.test.d.ts +2 -0
  59. package/dist/src/providers/plugin-types.test.d.ts.map +1 -0
  60. package/dist/src/providers/plugin-types.test.js +810 -0
  61. package/dist/src/registry/index.d.ts +4 -0
  62. package/dist/src/registry/index.d.ts.map +1 -0
  63. package/dist/src/registry/index.js +2 -0
  64. package/dist/src/registry/loader.d.ts +25 -0
  65. package/dist/src/registry/loader.d.ts.map +1 -0
  66. package/dist/src/registry/loader.js +88 -0
  67. package/dist/src/registry/node-type-registry.d.ts +52 -0
  68. package/dist/src/registry/node-type-registry.d.ts.map +1 -0
  69. package/dist/src/registry/node-type-registry.js +130 -0
  70. package/dist/src/registry/types.d.ts +65 -0
  71. package/dist/src/registry/types.d.ts.map +1 -0
  72. package/dist/src/registry/types.js +10 -0
  73. package/dist/src/workflow/expression/ast.d.ts +1 -1
  74. package/dist/src/workflow/expression/ast.d.ts.map +1 -1
  75. package/dist/src/workflow/expression/context.d.ts +4 -0
  76. package/dist/src/workflow/expression/context.d.ts.map +1 -1
  77. package/dist/src/workflow/expression/context.js +5 -1
  78. package/dist/src/workflow/expression/evaluator.d.ts.map +1 -1
  79. package/dist/src/workflow/expression/evaluator.js +24 -1
  80. package/dist/src/workflow/expression/evaluator.test.js +174 -0
  81. package/dist/src/workflow/expression/expression.test.js +140 -1
  82. package/dist/src/workflow/expression/helpers.d.ts +4 -0
  83. package/dist/src/workflow/expression/helpers.d.ts.map +1 -1
  84. package/dist/src/workflow/expression/helpers.js +51 -0
  85. package/dist/src/workflow/expression/index.d.ts +14 -0
  86. package/dist/src/workflow/expression/index.d.ts.map +1 -1
  87. package/dist/src/workflow/expression/index.js +28 -1
  88. package/dist/src/workflow/expression/lexer.d.ts.map +1 -1
  89. package/dist/src/workflow/expression/lexer.js +43 -0
  90. package/dist/src/workflow/expression/parser.js +1 -1
  91. package/dist/src/workflow/index.d.ts +3 -3
  92. package/dist/src/workflow/index.d.ts.map +1 -1
  93. package/dist/src/workflow/index.js +4 -2
  94. package/dist/src/workflow/workflow-loader.d.ts +8 -2
  95. package/dist/src/workflow/workflow-loader.d.ts.map +1 -1
  96. package/dist/src/workflow/workflow-loader.js +21 -2
  97. package/dist/src/workflow/workflow-types.d.ts +781 -12
  98. package/dist/src/workflow/workflow-types.d.ts.map +1 -1
  99. package/dist/src/workflow/workflow-types.js +248 -3
  100. package/dist/src/workflow/workflow-types.test.js +621 -1
  101. 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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=strategies.test.d.ts.map
@@ -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
+ });