@ronkovic/aad 0.4.0 → 0.5.1
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/README.md +42 -4
- package/package.json +1 -1
- package/src/__tests__/e2e/cleanup-e2e.test.ts +186 -0
- package/src/__tests__/e2e/dashboard-api-e2e.test.ts +87 -0
- package/src/__tests__/e2e/pipeline-e2e.test.ts +10 -69
- package/src/__tests__/e2e/resume-e2e.test.ts +7 -11
- package/src/__tests__/e2e/retry-e2e.test.ts +285 -0
- package/src/__tests__/e2e/status-e2e.test.ts +227 -0
- package/src/__tests__/e2e/tdd-pipeline-e2e.test.ts +360 -0
- package/src/__tests__/helpers/index.ts +6 -0
- package/src/__tests__/helpers/mock-claude-provider.ts +53 -0
- package/src/__tests__/helpers/mock-logger.ts +36 -0
- package/src/__tests__/helpers/wait-helpers.ts +34 -0
- package/src/__tests__/integration/pipeline.test.ts +2 -0
- package/src/modules/claude-provider/__tests__/claude-sdk.adapter.test.ts +79 -0
- package/src/modules/claude-provider/__tests__/provider-registry.test.ts +2 -0
- package/src/modules/cli/__tests__/cleanup.test.ts +1 -0
- package/src/modules/cli/__tests__/resume.test.ts +3 -0
- package/src/modules/cli/__tests__/run.test.ts +36 -0
- package/src/modules/cli/__tests__/status.test.ts +1 -0
- package/src/modules/cli/app.ts +2 -0
- package/src/modules/cli/commands/resume.ts +11 -6
- package/src/modules/cli/commands/run.ts +14 -2
- package/src/modules/dashboard/ui/dashboard.html +640 -474
- package/src/modules/planning/__tests__/planning-service.test.ts +2 -0
- package/src/modules/process-manager/__tests__/process-manager.test.ts +2 -0
- package/src/modules/process-manager/process-manager.ts +2 -1
- package/src/modules/task-execution/__tests__/executor.test.ts +420 -10
- package/src/modules/task-execution/executor.ts +76 -0
- package/src/modules/task-queue/dispatcher.ts +46 -2
- package/src/shared/__tests__/config.test.ts +30 -0
- package/src/shared/__tests__/events.test.ts +42 -16
- package/src/shared/__tests__/shutdown-handler.test.ts +96 -0
- package/src/shared/config.ts +4 -0
- package/src/shared/events.ts +5 -0
- package/src/shared/memory-check.ts +2 -2
- package/src/shared/shutdown-handler.ts +12 -5
- package/src/shared/types.ts +12 -0
- package/src/modules/claude-provider/__tests__/claude-sdk-real-env.test.ts +0 -127
|
@@ -29,7 +29,7 @@ export class ProcessManager {
|
|
|
29
29
|
/**
|
|
30
30
|
* Initialize worker pool
|
|
31
31
|
*/
|
|
32
|
-
|
|
32
|
+
initializePool(numWorkers: number): Promise<void> {
|
|
33
33
|
if (this.initialized) {
|
|
34
34
|
throw new ProcessManagerError("ProcessManager already initialized");
|
|
35
35
|
}
|
|
@@ -49,6 +49,7 @@ export class ProcessManager {
|
|
|
49
49
|
|
|
50
50
|
this.initialized = true;
|
|
51
51
|
this.deps.logger.info({ numWorkers }, "Worker pool initialized");
|
|
52
|
+
return Promise.resolve();
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
/**
|
|
@@ -43,7 +43,9 @@ describe("executeTddPipeline", () => {
|
|
|
43
43
|
teams: { splitter: false, reviewer: false },
|
|
44
44
|
memorySync: true,
|
|
45
45
|
dashboard: { enabled: true, port: 7333, host: "localhost" },
|
|
46
|
-
|
|
46
|
+
git: { autoPush: false },
|
|
47
|
+
skipCompleted: true,
|
|
48
|
+
strictTdd: true,
|
|
47
49
|
};
|
|
48
50
|
|
|
49
51
|
const mockProvider: ClaudeProvider = {
|
|
@@ -140,7 +142,9 @@ describe("executeTddPipeline", () => {
|
|
|
140
142
|
teams: { splitter: false, reviewer: false },
|
|
141
143
|
memorySync: true,
|
|
142
144
|
dashboard: { enabled: true, port: 7333, host: "localhost" },
|
|
143
|
-
|
|
145
|
+
git: { autoPush: false },
|
|
146
|
+
skipCompleted: true,
|
|
147
|
+
strictTdd: true,
|
|
144
148
|
};
|
|
145
149
|
|
|
146
150
|
let callCount = 0;
|
|
@@ -227,7 +231,9 @@ describe("executeTddPipeline", () => {
|
|
|
227
231
|
teams: { splitter: false, reviewer: false },
|
|
228
232
|
memorySync: true,
|
|
229
233
|
dashboard: { enabled: true, port: 7333, host: "localhost" },
|
|
230
|
-
|
|
234
|
+
git: { autoPush: false },
|
|
235
|
+
skipCompleted: true,
|
|
236
|
+
strictTdd: true,
|
|
231
237
|
};
|
|
232
238
|
|
|
233
239
|
const mockProvider: ClaudeProvider = {
|
|
@@ -318,7 +324,9 @@ describe("executeTddPipeline", () => {
|
|
|
318
324
|
teams: { splitter: false, reviewer: false },
|
|
319
325
|
memorySync: true,
|
|
320
326
|
dashboard: { enabled: true, port: 7333, host: "localhost" },
|
|
321
|
-
|
|
327
|
+
git: { autoPush: false },
|
|
328
|
+
skipCompleted: true,
|
|
329
|
+
strictTdd: true,
|
|
322
330
|
};
|
|
323
331
|
|
|
324
332
|
let phaseCount = 0;
|
|
@@ -420,7 +428,9 @@ describe("executeTddPipeline", () => {
|
|
|
420
428
|
teams: { splitter: false, reviewer: false },
|
|
421
429
|
memorySync: true,
|
|
422
430
|
dashboard: { enabled: true, port: 7333, host: "localhost" },
|
|
423
|
-
|
|
431
|
+
git: { autoPush: false },
|
|
432
|
+
skipCompleted: true,
|
|
433
|
+
strictTdd: true,
|
|
424
434
|
};
|
|
425
435
|
|
|
426
436
|
const mockProvider: ClaudeProvider = {
|
|
@@ -517,7 +527,9 @@ describe("executeTddPipeline", () => {
|
|
|
517
527
|
teams: { splitter: false, reviewer: false },
|
|
518
528
|
memorySync: true,
|
|
519
529
|
dashboard: { enabled: true, port: 7333, host: "localhost" },
|
|
520
|
-
|
|
530
|
+
git: { autoPush: false },
|
|
531
|
+
skipCompleted: true,
|
|
532
|
+
strictTdd: true,
|
|
521
533
|
};
|
|
522
534
|
|
|
523
535
|
let callCount = 0;
|
|
@@ -578,7 +590,9 @@ describe("executeTddPipeline", () => {
|
|
|
578
590
|
teams: { splitter: false, reviewer: false },
|
|
579
591
|
memorySync: true,
|
|
580
592
|
dashboard: { enabled: true, port: 7333, host: "localhost" },
|
|
581
|
-
|
|
593
|
+
git: { autoPush: false },
|
|
594
|
+
skipCompleted: true,
|
|
595
|
+
strictTdd: true,
|
|
582
596
|
};
|
|
583
597
|
|
|
584
598
|
const mockProvider: ClaudeProvider = {
|
|
@@ -636,7 +650,9 @@ describe("executeTddPipeline", () => {
|
|
|
636
650
|
teams: { splitter: false, reviewer: false },
|
|
637
651
|
memorySync: true,
|
|
638
652
|
dashboard: { enabled: true, port: 7333, host: "localhost" },
|
|
639
|
-
|
|
653
|
+
git: { autoPush: false },
|
|
654
|
+
skipCompleted: true,
|
|
655
|
+
strictTdd: true,
|
|
640
656
|
};
|
|
641
657
|
|
|
642
658
|
const mockProvider: ClaudeProvider = {
|
|
@@ -695,7 +711,9 @@ describe("executeTddPipeline", () => {
|
|
|
695
711
|
teams: { splitter: false, reviewer: false },
|
|
696
712
|
memorySync: true,
|
|
697
713
|
dashboard: { enabled: true, port: 7333, host: "localhost" },
|
|
698
|
-
|
|
714
|
+
git: { autoPush: false },
|
|
715
|
+
skipCompleted: true,
|
|
716
|
+
strictTdd: true,
|
|
699
717
|
};
|
|
700
718
|
|
|
701
719
|
const mockProvider: ClaudeProvider = {
|
|
@@ -747,7 +765,9 @@ describe("executeTddPipeline", () => {
|
|
|
747
765
|
teams: { splitter: false, reviewer: false },
|
|
748
766
|
memorySync: true,
|
|
749
767
|
dashboard: { enabled: true, port: 7333, host: "localhost" },
|
|
750
|
-
|
|
768
|
+
git: { autoPush: false },
|
|
769
|
+
skipCompleted: true,
|
|
770
|
+
strictTdd: true,
|
|
751
771
|
};
|
|
752
772
|
|
|
753
773
|
const mockProvider: ClaudeProvider = {
|
|
@@ -800,6 +820,8 @@ describe("executeTddPipeline", () => {
|
|
|
800
820
|
memorySync: true,
|
|
801
821
|
dashboard: { enabled: true, port: 7333, host: "localhost" },
|
|
802
822
|
git: { autoPush: false },
|
|
823
|
+
skipCompleted: true,
|
|
824
|
+
strictTdd: true,
|
|
803
825
|
};
|
|
804
826
|
|
|
805
827
|
const mockProvider: ClaudeProvider = {
|
|
@@ -843,4 +865,392 @@ describe("executeTddPipeline", () => {
|
|
|
843
865
|
expect(warnEvents.some((e) => e.entry?.message?.includes("Red phase"))).toBe(true);
|
|
844
866
|
expect(warnEvents.some((e) => e.entry?.message?.includes("Green phase"))).toBe(true);
|
|
845
867
|
});
|
|
868
|
+
|
|
869
|
+
test("skips pipeline when tests already pass (pre-check succeeds)", async () => {
|
|
870
|
+
const task: Task = {
|
|
871
|
+
taskId: createTaskId("task-skip-001"),
|
|
872
|
+
title: "Already passing task",
|
|
873
|
+
description: "Tests already pass",
|
|
874
|
+
filesToModify: ["src/feature.ts"],
|
|
875
|
+
dependsOn: [],
|
|
876
|
+
priority: 1,
|
|
877
|
+
status: "running",
|
|
878
|
+
retryCount: 0,
|
|
879
|
+
};
|
|
880
|
+
|
|
881
|
+
const workspace: WorkspaceInfo = {
|
|
882
|
+
path: "/workspace",
|
|
883
|
+
language: "typescript",
|
|
884
|
+
packageManager: "bun",
|
|
885
|
+
framework: "hono",
|
|
886
|
+
testFramework: "bun-test",
|
|
887
|
+
};
|
|
888
|
+
|
|
889
|
+
const config: Config = {
|
|
890
|
+
workers: { num: 2, max: 8 },
|
|
891
|
+
models: {},
|
|
892
|
+
timeouts: { claude: 1200, test: 600, staleTask: 5400 },
|
|
893
|
+
retry: { maxRetries: 2 },
|
|
894
|
+
debug: false,
|
|
895
|
+
adaptiveEffort: false,
|
|
896
|
+
teams: { splitter: false, reviewer: false },
|
|
897
|
+
memorySync: true,
|
|
898
|
+
dashboard: { enabled: true, port: 7333, host: "localhost" },
|
|
899
|
+
git: { autoPush: false },
|
|
900
|
+
skipCompleted: true,
|
|
901
|
+
strictTdd: false,
|
|
902
|
+
};
|
|
903
|
+
|
|
904
|
+
let claudeCallCount = 0;
|
|
905
|
+
const mockProvider: ClaudeProvider = {
|
|
906
|
+
async call(): Promise<ClaudeResponse> {
|
|
907
|
+
claudeCallCount++;
|
|
908
|
+
return {
|
|
909
|
+
result: "Should not be called",
|
|
910
|
+
exitCode: 0,
|
|
911
|
+
model: "claude-sonnet-4-5",
|
|
912
|
+
effortLevel: "medium",
|
|
913
|
+
duration: 1000,
|
|
914
|
+
};
|
|
915
|
+
},
|
|
916
|
+
};
|
|
917
|
+
|
|
918
|
+
const mockMergeService = {
|
|
919
|
+
async mergeToParent(): Promise<MergeResult> {
|
|
920
|
+
throw new Error("Should not reach merge");
|
|
921
|
+
},
|
|
922
|
+
} as unknown as MergeService;
|
|
923
|
+
|
|
924
|
+
const events: string[] = [];
|
|
925
|
+
const mockEventBus = {
|
|
926
|
+
on() {},
|
|
927
|
+
off() {},
|
|
928
|
+
emit(event: any) {
|
|
929
|
+
events.push(event.type);
|
|
930
|
+
},
|
|
931
|
+
} as unknown as EventBus;
|
|
932
|
+
|
|
933
|
+
// Pre-check always passes
|
|
934
|
+
const mockSpawner = {
|
|
935
|
+
async spawn() {
|
|
936
|
+
return { exitCode: 0, stdout: "All tests passed", stderr: "" };
|
|
937
|
+
},
|
|
938
|
+
};
|
|
939
|
+
|
|
940
|
+
const result = await executeTddPipeline(
|
|
941
|
+
task, workspace, "branch", "main", "/parent", createRunId("run-skip"), config,
|
|
942
|
+
mockProvider, mockMergeService, mockEventBus, mockSpawner
|
|
943
|
+
);
|
|
944
|
+
|
|
945
|
+
expect(result.status).toBe("completed");
|
|
946
|
+
expect(result.skipped).toBe(true);
|
|
947
|
+
expect(result.phasesExecuted).toEqual(["pre-check"]);
|
|
948
|
+
expect(claudeCallCount).toBe(0); // No Claude calls made
|
|
949
|
+
expect(events).toContain("execution:skipped");
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
test("runs full pipeline when tests fail pre-check", async () => {
|
|
953
|
+
const task: Task = {
|
|
954
|
+
taskId: createTaskId("task-run-full"),
|
|
955
|
+
title: "Tests need work",
|
|
956
|
+
description: "Pre-check fails, run full pipeline",
|
|
957
|
+
filesToModify: [],
|
|
958
|
+
dependsOn: [],
|
|
959
|
+
priority: 1,
|
|
960
|
+
status: "running",
|
|
961
|
+
retryCount: 0,
|
|
962
|
+
};
|
|
963
|
+
|
|
964
|
+
const workspace: WorkspaceInfo = {
|
|
965
|
+
path: "/workspace",
|
|
966
|
+
language: "typescript",
|
|
967
|
+
packageManager: "bun",
|
|
968
|
+
framework: "hono",
|
|
969
|
+
testFramework: "bun-test",
|
|
970
|
+
};
|
|
971
|
+
|
|
972
|
+
const config: Config = {
|
|
973
|
+
workers: { num: 2, max: 8 },
|
|
974
|
+
models: {},
|
|
975
|
+
timeouts: { claude: 1200, test: 600, staleTask: 5400 },
|
|
976
|
+
retry: { maxRetries: 2 },
|
|
977
|
+
debug: false,
|
|
978
|
+
adaptiveEffort: false,
|
|
979
|
+
teams: { splitter: false, reviewer: false },
|
|
980
|
+
memorySync: true,
|
|
981
|
+
dashboard: { enabled: true, port: 7333, host: "localhost" },
|
|
982
|
+
git: { autoPush: false },
|
|
983
|
+
skipCompleted: true,
|
|
984
|
+
strictTdd: false,
|
|
985
|
+
};
|
|
986
|
+
|
|
987
|
+
const mockProvider: ClaudeProvider = {
|
|
988
|
+
async call(): Promise<ClaudeResponse> {
|
|
989
|
+
return {
|
|
990
|
+
result: "OK",
|
|
991
|
+
exitCode: 0,
|
|
992
|
+
model: "claude-sonnet-4-5",
|
|
993
|
+
effortLevel: "medium",
|
|
994
|
+
duration: 1000,
|
|
995
|
+
};
|
|
996
|
+
},
|
|
997
|
+
};
|
|
998
|
+
|
|
999
|
+
const mockMergeService = {
|
|
1000
|
+
async mergeToParent(): Promise<MergeResult> {
|
|
1001
|
+
return { success: true };
|
|
1002
|
+
},
|
|
1003
|
+
} as unknown as MergeService;
|
|
1004
|
+
|
|
1005
|
+
const events: string[] = [];
|
|
1006
|
+
const mockEventBus = {
|
|
1007
|
+
on() {},
|
|
1008
|
+
off() {},
|
|
1009
|
+
emit(event: any) {
|
|
1010
|
+
events.push(event.type);
|
|
1011
|
+
},
|
|
1012
|
+
} as unknown as EventBus;
|
|
1013
|
+
|
|
1014
|
+
let precheckDone = false;
|
|
1015
|
+
const mockSpawner = {
|
|
1016
|
+
async spawn() {
|
|
1017
|
+
if (!precheckDone) {
|
|
1018
|
+
precheckDone = true;
|
|
1019
|
+
return { exitCode: 1, stdout: "", stderr: "Tests failed" };
|
|
1020
|
+
}
|
|
1021
|
+
return { exitCode: 0, stdout: "OK", stderr: "" };
|
|
1022
|
+
},
|
|
1023
|
+
};
|
|
1024
|
+
|
|
1025
|
+
const result = await executeTddPipeline(
|
|
1026
|
+
task, workspace, "branch", "main", "/parent", createRunId("run-full"), config,
|
|
1027
|
+
mockProvider, mockMergeService, mockEventBus, mockSpawner
|
|
1028
|
+
);
|
|
1029
|
+
|
|
1030
|
+
expect(result.status).toBe("completed");
|
|
1031
|
+
expect(result.skipped).toBeUndefined();
|
|
1032
|
+
expect(events).toContain("execution:phase:started");
|
|
1033
|
+
expect(events).not.toContain("execution:skipped");
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
test("respects strictTdd flag to skip pre-check", async () => {
|
|
1037
|
+
const task: Task = {
|
|
1038
|
+
taskId: createTaskId("task-strict"),
|
|
1039
|
+
title: "Strict TDD mode",
|
|
1040
|
+
description: "Always run full pipeline",
|
|
1041
|
+
filesToModify: [],
|
|
1042
|
+
dependsOn: [],
|
|
1043
|
+
priority: 1,
|
|
1044
|
+
status: "running",
|
|
1045
|
+
retryCount: 0,
|
|
1046
|
+
};
|
|
1047
|
+
|
|
1048
|
+
const workspace: WorkspaceInfo = {
|
|
1049
|
+
path: "/workspace",
|
|
1050
|
+
language: "typescript",
|
|
1051
|
+
packageManager: "bun",
|
|
1052
|
+
framework: "hono",
|
|
1053
|
+
testFramework: "bun-test",
|
|
1054
|
+
};
|
|
1055
|
+
|
|
1056
|
+
const config: Config = {
|
|
1057
|
+
workers: { num: 2, max: 8 },
|
|
1058
|
+
models: {},
|
|
1059
|
+
timeouts: { claude: 1200, test: 600, staleTask: 5400 },
|
|
1060
|
+
retry: { maxRetries: 2 },
|
|
1061
|
+
debug: false,
|
|
1062
|
+
adaptiveEffort: false,
|
|
1063
|
+
teams: { splitter: false, reviewer: false },
|
|
1064
|
+
memorySync: true,
|
|
1065
|
+
dashboard: { enabled: true, port: 7333, host: "localhost" },
|
|
1066
|
+
git: { autoPush: false },
|
|
1067
|
+
skipCompleted: true,
|
|
1068
|
+
strictTdd: true,
|
|
1069
|
+
};
|
|
1070
|
+
|
|
1071
|
+
const mockProvider: ClaudeProvider = {
|
|
1072
|
+
async call(): Promise<ClaudeResponse> {
|
|
1073
|
+
return {
|
|
1074
|
+
result: "OK",
|
|
1075
|
+
exitCode: 0,
|
|
1076
|
+
model: "claude-sonnet-4-5",
|
|
1077
|
+
effortLevel: "medium",
|
|
1078
|
+
duration: 1000,
|
|
1079
|
+
};
|
|
1080
|
+
},
|
|
1081
|
+
};
|
|
1082
|
+
|
|
1083
|
+
const mockMergeService = {
|
|
1084
|
+
async mergeToParent(): Promise<MergeResult> {
|
|
1085
|
+
return { success: true };
|
|
1086
|
+
},
|
|
1087
|
+
} as unknown as MergeService;
|
|
1088
|
+
|
|
1089
|
+
const events: string[] = [];
|
|
1090
|
+
const mockEventBus = {
|
|
1091
|
+
on() {},
|
|
1092
|
+
off() {},
|
|
1093
|
+
emit(event: any) {
|
|
1094
|
+
events.push(event.type);
|
|
1095
|
+
},
|
|
1096
|
+
} as unknown as EventBus;
|
|
1097
|
+
|
|
1098
|
+
const mockSpawner = {
|
|
1099
|
+
async spawn() {
|
|
1100
|
+
return { exitCode: 0, stdout: "OK", stderr: "" };
|
|
1101
|
+
},
|
|
1102
|
+
};
|
|
1103
|
+
|
|
1104
|
+
const result = await executeTddPipeline(
|
|
1105
|
+
task, workspace, "branch", "main", "/parent", createRunId("run-strict"), config,
|
|
1106
|
+
mockProvider, mockMergeService, mockEventBus, mockSpawner
|
|
1107
|
+
);
|
|
1108
|
+
|
|
1109
|
+
expect(result.status).toBe("completed");
|
|
1110
|
+
expect(result.skipped).toBeUndefined();
|
|
1111
|
+
expect(events).toContain("execution:phase:started");
|
|
1112
|
+
expect(events).not.toContain("execution:skipped");
|
|
1113
|
+
});
|
|
1114
|
+
|
|
1115
|
+
test("emits execution:skipped event when skipping", async () => {
|
|
1116
|
+
const task: Task = {
|
|
1117
|
+
taskId: createTaskId("task-event-skip"),
|
|
1118
|
+
title: "Event test",
|
|
1119
|
+
description: "Test skip event",
|
|
1120
|
+
filesToModify: [],
|
|
1121
|
+
dependsOn: [],
|
|
1122
|
+
priority: 1,
|
|
1123
|
+
status: "running",
|
|
1124
|
+
retryCount: 0,
|
|
1125
|
+
};
|
|
1126
|
+
|
|
1127
|
+
const workspace: WorkspaceInfo = {
|
|
1128
|
+
path: "/workspace",
|
|
1129
|
+
language: "typescript",
|
|
1130
|
+
packageManager: "bun",
|
|
1131
|
+
framework: "hono",
|
|
1132
|
+
testFramework: "bun-test",
|
|
1133
|
+
};
|
|
1134
|
+
|
|
1135
|
+
const config: Config = {
|
|
1136
|
+
workers: { num: 2, max: 8 },
|
|
1137
|
+
models: {},
|
|
1138
|
+
timeouts: { claude: 1200, test: 600, staleTask: 5400 },
|
|
1139
|
+
retry: { maxRetries: 2 },
|
|
1140
|
+
debug: false,
|
|
1141
|
+
adaptiveEffort: false,
|
|
1142
|
+
teams: { splitter: false, reviewer: false },
|
|
1143
|
+
memorySync: true,
|
|
1144
|
+
dashboard: { enabled: true, port: 7333, host: "localhost" },
|
|
1145
|
+
git: { autoPush: false },
|
|
1146
|
+
skipCompleted: true,
|
|
1147
|
+
strictTdd: false,
|
|
1148
|
+
};
|
|
1149
|
+
|
|
1150
|
+
const mockProvider: ClaudeProvider = {
|
|
1151
|
+
async call(): Promise<ClaudeResponse> {
|
|
1152
|
+
throw new Error("Should not be called");
|
|
1153
|
+
},
|
|
1154
|
+
};
|
|
1155
|
+
|
|
1156
|
+
const mockMergeService = {
|
|
1157
|
+
async mergeToParent(): Promise<MergeResult> {
|
|
1158
|
+
throw new Error("Should not reach merge");
|
|
1159
|
+
},
|
|
1160
|
+
} as unknown as MergeService;
|
|
1161
|
+
|
|
1162
|
+
const events: Array<{ type: string; taskId?: string }> = [];
|
|
1163
|
+
const mockEventBus = {
|
|
1164
|
+
on() {},
|
|
1165
|
+
off() {},
|
|
1166
|
+
emit(event: any) {
|
|
1167
|
+
events.push(event);
|
|
1168
|
+
},
|
|
1169
|
+
} as unknown as EventBus;
|
|
1170
|
+
|
|
1171
|
+
const mockSpawner = {
|
|
1172
|
+
async spawn() {
|
|
1173
|
+
return { exitCode: 0, stdout: "Tests pass", stderr: "" };
|
|
1174
|
+
},
|
|
1175
|
+
};
|
|
1176
|
+
|
|
1177
|
+
await executeTddPipeline(
|
|
1178
|
+
task, workspace, "branch", "main", "/parent", createRunId("run-evt"), config,
|
|
1179
|
+
mockProvider, mockMergeService, mockEventBus, mockSpawner
|
|
1180
|
+
);
|
|
1181
|
+
|
|
1182
|
+
const skipEvent = events.find((e) => e.type === "execution:skipped");
|
|
1183
|
+
expect(skipEvent).toBeDefined();
|
|
1184
|
+
expect(skipEvent?.taskId).toBe(task.taskId);
|
|
1185
|
+
});
|
|
1186
|
+
|
|
1187
|
+
test("includes skipped flag in result when skipped", async () => {
|
|
1188
|
+
const task: Task = {
|
|
1189
|
+
taskId: createTaskId("task-result-flag"),
|
|
1190
|
+
title: "Result flag test",
|
|
1191
|
+
description: "Test result structure",
|
|
1192
|
+
filesToModify: [],
|
|
1193
|
+
dependsOn: [],
|
|
1194
|
+
priority: 1,
|
|
1195
|
+
status: "running",
|
|
1196
|
+
retryCount: 0,
|
|
1197
|
+
};
|
|
1198
|
+
|
|
1199
|
+
const workspace: WorkspaceInfo = {
|
|
1200
|
+
path: "/workspace",
|
|
1201
|
+
language: "typescript",
|
|
1202
|
+
packageManager: "bun",
|
|
1203
|
+
framework: "hono",
|
|
1204
|
+
testFramework: "bun-test",
|
|
1205
|
+
};
|
|
1206
|
+
|
|
1207
|
+
const config: Config = {
|
|
1208
|
+
workers: { num: 2, max: 8 },
|
|
1209
|
+
models: {},
|
|
1210
|
+
timeouts: { claude: 1200, test: 600, staleTask: 5400 },
|
|
1211
|
+
retry: { maxRetries: 2 },
|
|
1212
|
+
debug: false,
|
|
1213
|
+
adaptiveEffort: false,
|
|
1214
|
+
teams: { splitter: false, reviewer: false },
|
|
1215
|
+
memorySync: true,
|
|
1216
|
+
dashboard: { enabled: true, port: 7333, host: "localhost" },
|
|
1217
|
+
git: { autoPush: false },
|
|
1218
|
+
skipCompleted: true,
|
|
1219
|
+
strictTdd: false,
|
|
1220
|
+
};
|
|
1221
|
+
|
|
1222
|
+
const mockProvider: ClaudeProvider = {
|
|
1223
|
+
async call(): Promise<ClaudeResponse> {
|
|
1224
|
+
throw new Error("Should not be called");
|
|
1225
|
+
},
|
|
1226
|
+
};
|
|
1227
|
+
|
|
1228
|
+
const mockMergeService = {
|
|
1229
|
+
async mergeToParent(): Promise<MergeResult> {
|
|
1230
|
+
throw new Error("Should not reach merge");
|
|
1231
|
+
},
|
|
1232
|
+
} as unknown as MergeService;
|
|
1233
|
+
|
|
1234
|
+
const mockEventBus = {
|
|
1235
|
+
on() {},
|
|
1236
|
+
off() {},
|
|
1237
|
+
emit() {},
|
|
1238
|
+
} as unknown as EventBus;
|
|
1239
|
+
|
|
1240
|
+
const mockSpawner = {
|
|
1241
|
+
async spawn() {
|
|
1242
|
+
return { exitCode: 0, stdout: "Pass", stderr: "" };
|
|
1243
|
+
},
|
|
1244
|
+
};
|
|
1245
|
+
|
|
1246
|
+
const result = await executeTddPipeline(
|
|
1247
|
+
task, workspace, "branch", "main", "/parent", createRunId("run-flag"), config,
|
|
1248
|
+
mockProvider, mockMergeService, mockEventBus, mockSpawner
|
|
1249
|
+
);
|
|
1250
|
+
|
|
1251
|
+
expect(result.status).toBe("completed");
|
|
1252
|
+
expect(result.skipped).toBe(true);
|
|
1253
|
+
expect(result.phasesExecuted).toEqual(["pre-check"]);
|
|
1254
|
+
expect(result.duration).toBeGreaterThanOrEqual(0);
|
|
1255
|
+
});
|
|
846
1256
|
});
|
|
@@ -28,9 +28,22 @@ export interface RetryContext {
|
|
|
28
28
|
previousFailure?: PreviousFailure;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Run pre-check to see if tests already pass
|
|
33
|
+
*/
|
|
34
|
+
async function runPreCheck(
|
|
35
|
+
workspace: WorkspaceInfo,
|
|
36
|
+
testSpawner: ProcessSpawner | undefined,
|
|
37
|
+
timeout: number
|
|
38
|
+
): Promise<boolean> {
|
|
39
|
+
const result = await runTests(workspace, testSpawner, timeout);
|
|
40
|
+
return result.success;
|
|
41
|
+
}
|
|
42
|
+
|
|
31
43
|
/**
|
|
32
44
|
* Execute full TDD pipeline for a task
|
|
33
45
|
* Phases: Red → Green → Verify → Review → Merge
|
|
46
|
+
* Optionally skips pipeline if tests already pass (pre-check)
|
|
34
47
|
*/
|
|
35
48
|
export async function executeTddPipeline(
|
|
36
49
|
task: Task,
|
|
@@ -49,6 +62,68 @@ export async function executeTddPipeline(
|
|
|
49
62
|
const startTime = Date.now();
|
|
50
63
|
|
|
51
64
|
try {
|
|
65
|
+
// ===== Pre-check: Skip if tests already pass (unless strictTdd) =====
|
|
66
|
+
if (config.skipCompleted && !config.strictTdd) {
|
|
67
|
+
eventBus.emit({
|
|
68
|
+
type: "log:entry",
|
|
69
|
+
entry: {
|
|
70
|
+
level: "info",
|
|
71
|
+
service: "task-execution",
|
|
72
|
+
message: "Running pre-check to see if tests already pass",
|
|
73
|
+
timestamp: Date.now(),
|
|
74
|
+
taskId: task.taskId as string,
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const testsAlreadyPass = await runPreCheck(
|
|
79
|
+
workspace,
|
|
80
|
+
testSpawner,
|
|
81
|
+
config.timeouts.test * 1000
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
if (testsAlreadyPass) {
|
|
85
|
+
const duration = Date.now() - startTime;
|
|
86
|
+
|
|
87
|
+
eventBus.emit({
|
|
88
|
+
type: "execution:skipped",
|
|
89
|
+
taskId: task.taskId,
|
|
90
|
+
reason: "Tests already pass",
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
eventBus.emit({
|
|
94
|
+
type: "log:entry",
|
|
95
|
+
entry: {
|
|
96
|
+
level: "info",
|
|
97
|
+
service: "task-execution",
|
|
98
|
+
message: "Tests already pass, skipping TDD pipeline",
|
|
99
|
+
timestamp: Date.now(),
|
|
100
|
+
taskId: task.taskId as string,
|
|
101
|
+
duration,
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
taskId: task.taskId,
|
|
107
|
+
status: "completed",
|
|
108
|
+
duration,
|
|
109
|
+
output: "Skipped: tests already pass",
|
|
110
|
+
skipped: true,
|
|
111
|
+
phasesExecuted: ["pre-check"],
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
eventBus.emit({
|
|
116
|
+
type: "log:entry",
|
|
117
|
+
entry: {
|
|
118
|
+
level: "info",
|
|
119
|
+
service: "task-execution",
|
|
120
|
+
message: "Pre-check failed, running full TDD pipeline",
|
|
121
|
+
timestamp: Date.now(),
|
|
122
|
+
taskId: task.taskId as string,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
52
127
|
// Estimate task complexity for adaptive effort
|
|
53
128
|
const complexity = estimateTaskComplexity(task);
|
|
54
129
|
const testerEffort = getAdaptiveEffortLevel("tester", complexity, config);
|
|
@@ -365,6 +440,7 @@ export async function executeTddPipeline(
|
|
|
365
440
|
status: "completed",
|
|
366
441
|
duration,
|
|
367
442
|
output: "TDD pipeline completed successfully",
|
|
443
|
+
phasesExecuted: ["red", "green", "verify", "review", "merge"],
|
|
368
444
|
};
|
|
369
445
|
} catch (error) {
|
|
370
446
|
const duration = Date.now() - startTime;
|