viberadar 0.3.59 → 0.3.61
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/server/index.d.ts.map +1 -1
- package/dist/server/index.js +502 -108
- package/dist/server/index.js.map +1 -1
- package/dist/ui/dashboard.html +1060 -411
- package/package.json +1 -1
package/dist/server/index.js
CHANGED
|
@@ -145,6 +145,9 @@ function randomInt(min, max) {
|
|
|
145
145
|
return min;
|
|
146
146
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
147
147
|
}
|
|
148
|
+
function newRunId() {
|
|
149
|
+
return `run_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
150
|
+
}
|
|
148
151
|
function detectQueueBlockSignal(line) {
|
|
149
152
|
const s = line.toLowerCase();
|
|
150
153
|
const is403 = ((s.includes('403') && (s.includes('forbidden') || s.includes('unexpected status') || s.includes('status'))) ||
|
|
@@ -848,6 +851,10 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
848
851
|
let testsRunning = false;
|
|
849
852
|
const agentQueue = [];
|
|
850
853
|
let queueCooldownTimer = null;
|
|
854
|
+
let activeAgentProcess = null;
|
|
855
|
+
const runs = [];
|
|
856
|
+
const MAX_RUN_HISTORY = 120;
|
|
857
|
+
let activeRunId = null;
|
|
851
858
|
// Keyed by absolute file path → per-file failure details from last test run
|
|
852
859
|
const lastTestResults = new Map();
|
|
853
860
|
// ── SSE clients ────────────────────────────────────────────────────────────
|
|
@@ -863,6 +870,194 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
863
870
|
}
|
|
864
871
|
}
|
|
865
872
|
}
|
|
873
|
+
function compactRunHistory() {
|
|
874
|
+
if (runs.length <= MAX_RUN_HISTORY)
|
|
875
|
+
return;
|
|
876
|
+
runs.splice(0, runs.length - MAX_RUN_HISTORY);
|
|
877
|
+
}
|
|
878
|
+
function refreshQueuePositions() {
|
|
879
|
+
const pos = new Map();
|
|
880
|
+
agentQueue.forEach((item, index) => pos.set(item.runId, index + 1));
|
|
881
|
+
runs.forEach((run) => {
|
|
882
|
+
run.queuePosition = pos.get(run.runId) ?? null;
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
function queueSnapshotItem(item, index) {
|
|
886
|
+
return {
|
|
887
|
+
runId: item.runId,
|
|
888
|
+
title: item.title,
|
|
889
|
+
task: item.task,
|
|
890
|
+
featureKey: item.featureKey || null,
|
|
891
|
+
filePath: item.filePath || null,
|
|
892
|
+
selectedFilePaths: item.selectedFilePaths || null,
|
|
893
|
+
selectedFileCount: item.selectedFilePaths?.length || 0,
|
|
894
|
+
position: index + 1,
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
function emitQueueUpdated() {
|
|
898
|
+
refreshQueuePositions();
|
|
899
|
+
const queue = agentQueue.map((item, index) => queueSnapshotItem(item, index));
|
|
900
|
+
broadcast('agent-queue-updated', { queue });
|
|
901
|
+
}
|
|
902
|
+
function findRun(runId) {
|
|
903
|
+
return runs.find((r) => r.runId === runId);
|
|
904
|
+
}
|
|
905
|
+
function createRun(item, phase) {
|
|
906
|
+
const now = new Date().toISOString();
|
|
907
|
+
const run = {
|
|
908
|
+
runId: item.runId,
|
|
909
|
+
task: item.task,
|
|
910
|
+
title: item.title,
|
|
911
|
+
agent: item.agent,
|
|
912
|
+
featureKey: item.featureKey,
|
|
913
|
+
filePath: item.filePath,
|
|
914
|
+
selectedFilePaths: item.selectedFilePaths,
|
|
915
|
+
phase,
|
|
916
|
+
queuePosition: null,
|
|
917
|
+
createdAt: now,
|
|
918
|
+
};
|
|
919
|
+
runs.push(run);
|
|
920
|
+
compactRunHistory();
|
|
921
|
+
refreshQueuePositions();
|
|
922
|
+
broadcast('agent-run-created', { run });
|
|
923
|
+
return run;
|
|
924
|
+
}
|
|
925
|
+
function updateRun(runId, patch) {
|
|
926
|
+
const run = findRun(runId);
|
|
927
|
+
if (!run)
|
|
928
|
+
return;
|
|
929
|
+
Object.assign(run, patch);
|
|
930
|
+
refreshQueuePositions();
|
|
931
|
+
broadcast('agent-run-updated', { run });
|
|
932
|
+
}
|
|
933
|
+
function setRunPhase(runId, phase, patch = {}) {
|
|
934
|
+
const nowIso = new Date().toISOString();
|
|
935
|
+
const autoPatch = {};
|
|
936
|
+
if (phase === 'starting' && !findRun(runId)?.startedAt)
|
|
937
|
+
autoPatch.startedAt = nowIso;
|
|
938
|
+
if (phase === 'completed' || phase === 'failed' || phase === 'canceled') {
|
|
939
|
+
autoPatch.finishedAt = nowIso;
|
|
940
|
+
}
|
|
941
|
+
updateRun(runId, { phase, ...autoPatch, ...patch });
|
|
942
|
+
if (phase === 'running' || phase === 'starting' || phase === 'validating') {
|
|
943
|
+
activeRunId = runId;
|
|
944
|
+
}
|
|
945
|
+
if (phase === 'completed' || phase === 'failed' || phase === 'canceled') {
|
|
946
|
+
if (activeRunId === runId)
|
|
947
|
+
activeRunId = null;
|
|
948
|
+
const run = findRun(runId);
|
|
949
|
+
if (run)
|
|
950
|
+
broadcast('agent-run-finished', { run });
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
function buildValidationStats(fileOutcomes) {
|
|
954
|
+
let covered = 0;
|
|
955
|
+
let notCovered = 0;
|
|
956
|
+
let blocked = 0;
|
|
957
|
+
let infra = 0;
|
|
958
|
+
for (const outcome of fileOutcomes) {
|
|
959
|
+
if (outcome.status === 'covered')
|
|
960
|
+
covered += 1;
|
|
961
|
+
else if (outcome.status === 'not-covered')
|
|
962
|
+
notCovered += 1;
|
|
963
|
+
else if (outcome.status === 'blocked')
|
|
964
|
+
blocked += 1;
|
|
965
|
+
else if (outcome.status === 'infra')
|
|
966
|
+
infra += 1;
|
|
967
|
+
}
|
|
968
|
+
return {
|
|
969
|
+
total: fileOutcomes.length,
|
|
970
|
+
covered,
|
|
971
|
+
notCovered,
|
|
972
|
+
blocked,
|
|
973
|
+
infra,
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
function buildFileOutcomes(targetSourcePaths) {
|
|
977
|
+
const normalizeRelPath = (p) => p.replace(/\\/g, '/');
|
|
978
|
+
const uniqueTargets = Array.from(new Set(targetSourcePaths.map(normalizeRelPath)));
|
|
979
|
+
const srcByPath = new Map(currentData.modules
|
|
980
|
+
.filter((m) => m.type !== 'test')
|
|
981
|
+
.map((m) => [normalizeRelPath(m.relativePath), m]));
|
|
982
|
+
const fileOutcomes = uniqueTargets.map((relPath) => {
|
|
983
|
+
const mod = srcByPath.get(relPath);
|
|
984
|
+
if (!mod) {
|
|
985
|
+
return {
|
|
986
|
+
sourcePath: relPath,
|
|
987
|
+
status: 'blocked',
|
|
988
|
+
reason: 'source-file-not-found-after-rescan',
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
if (mod.isInfra) {
|
|
992
|
+
return {
|
|
993
|
+
sourcePath: relPath,
|
|
994
|
+
status: 'infra',
|
|
995
|
+
reason: 'matched-infra-ignore-pattern',
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
if (mod.hasTests) {
|
|
999
|
+
return {
|
|
1000
|
+
sourcePath: relPath,
|
|
1001
|
+
status: 'covered',
|
|
1002
|
+
testFile: mod.testFile ? normalizeRelPath(mod.testFile) : undefined,
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
return {
|
|
1006
|
+
sourcePath: relPath,
|
|
1007
|
+
status: 'not-covered',
|
|
1008
|
+
reason: 'scanner-did-not-link-test-file',
|
|
1009
|
+
};
|
|
1010
|
+
});
|
|
1011
|
+
return { fileOutcomes, validationStats: buildValidationStats(fileOutcomes) };
|
|
1012
|
+
}
|
|
1013
|
+
function moveQueueItem(runId, direction) {
|
|
1014
|
+
const index = agentQueue.findIndex((item) => item.runId === runId);
|
|
1015
|
+
if (index === -1)
|
|
1016
|
+
return { ok: false, message: `Run ${runId} не найден в очереди` };
|
|
1017
|
+
const targetIndex = direction === 'up' ? index - 1 : index + 1;
|
|
1018
|
+
if (targetIndex < 0 || targetIndex >= agentQueue.length) {
|
|
1019
|
+
return { ok: false, message: `Run ${runId} нельзя сдвинуть ${direction === 'up' ? 'вверх' : 'вниз'}` };
|
|
1020
|
+
}
|
|
1021
|
+
const [item] = agentQueue.splice(index, 1);
|
|
1022
|
+
agentQueue.splice(targetIndex, 0, item);
|
|
1023
|
+
emitQueueUpdated();
|
|
1024
|
+
return { ok: true };
|
|
1025
|
+
}
|
|
1026
|
+
function cancelQueuedRun(runId, reason = 'canceled-by-user') {
|
|
1027
|
+
const index = agentQueue.findIndex((item) => item.runId === runId);
|
|
1028
|
+
if (index === -1)
|
|
1029
|
+
return { ok: false, message: `Run ${runId} не найден в очереди` };
|
|
1030
|
+
const [item] = agentQueue.splice(index, 1);
|
|
1031
|
+
setRunPhase(item.runId, 'canceled', { error: reason });
|
|
1032
|
+
emitQueueUpdated();
|
|
1033
|
+
return { ok: true };
|
|
1034
|
+
}
|
|
1035
|
+
function enqueueItem(item) {
|
|
1036
|
+
agentQueue.push(item);
|
|
1037
|
+
refreshQueuePositions();
|
|
1038
|
+
emitQueueUpdated();
|
|
1039
|
+
}
|
|
1040
|
+
function buildAgentStateSnapshot() {
|
|
1041
|
+
refreshQueuePositions();
|
|
1042
|
+
const queue = agentQueue.map((item, index) => queueSnapshotItem(item, index));
|
|
1043
|
+
const activeRun = activeRunId ? (findRun(activeRunId) || null) : null;
|
|
1044
|
+
return {
|
|
1045
|
+
activeRun,
|
|
1046
|
+
queue,
|
|
1047
|
+
runs: runs.slice(-50),
|
|
1048
|
+
runtimeFlags: {
|
|
1049
|
+
agentRunning,
|
|
1050
|
+
testsRunning,
|
|
1051
|
+
queueCooldownActive: !!queueCooldownTimer,
|
|
1052
|
+
queueMax: runtimeEnv.agentQueueMax,
|
|
1053
|
+
cooldownMinMs: runtimeEnv.agentCooldownMinMs,
|
|
1054
|
+
cooldownMaxMs: runtimeEnv.agentCooldownMaxMs,
|
|
1055
|
+
codexSandboxMode: runtimeEnv.codexSandboxMode,
|
|
1056
|
+
autoFixFailedTests: runtimeEnv.autoFixFailedTests,
|
|
1057
|
+
autoFixMaxRetries: runtimeEnv.autoFixMaxRetries,
|
|
1058
|
+
},
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
866
1061
|
function clearQueueCooldownTimer() {
|
|
867
1062
|
if (!queueCooldownTimer)
|
|
868
1063
|
return;
|
|
@@ -875,9 +1070,19 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
875
1070
|
agentQueue.length = 0;
|
|
876
1071
|
process.stdout.write(` ⛔ Queue stopped: ${reason} (dropped: ${dropped})\n`);
|
|
877
1072
|
broadcast('agent-output', { line: `⛔ Очередь остановлена: ${reason}`, isError: true });
|
|
1073
|
+
const nowIso = new Date().toISOString();
|
|
1074
|
+
runs.forEach((run) => {
|
|
1075
|
+
if (run.phase === 'queued') {
|
|
1076
|
+
run.phase = 'canceled';
|
|
1077
|
+
run.finishedAt = nowIso;
|
|
1078
|
+
run.error = reason;
|
|
1079
|
+
broadcast('agent-run-finished', { run });
|
|
1080
|
+
}
|
|
1081
|
+
});
|
|
878
1082
|
if (dropped > 0) {
|
|
879
1083
|
broadcast('agent-output', { line: `🗑 Отменено задач из очереди: ${dropped}` });
|
|
880
1084
|
}
|
|
1085
|
+
emitQueueUpdated();
|
|
881
1086
|
broadcast('agent-queue-stopped', { reason, dropped });
|
|
882
1087
|
}
|
|
883
1088
|
// ── File watcher + re-scan ─────────────────────────────────────────────────
|
|
@@ -919,9 +1124,10 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
919
1124
|
return;
|
|
920
1125
|
}
|
|
921
1126
|
const next = agentQueue.shift();
|
|
1127
|
+
emitQueueUpdated();
|
|
922
1128
|
process.stdout.write(` 📋 Starting next from queue: "${next.title}" (remaining: ${agentQueue.length})\n`);
|
|
923
|
-
broadcast('agent-output', { line: `📋 Следующая задача из очереди: ${next.title}` });
|
|
924
|
-
broadcast('agent-output', { line: ` В очереди осталось: ${agentQueue.length}` });
|
|
1129
|
+
broadcast('agent-output', { runId: next.runId, line: `📋 Следующая задача из очереди: ${next.title}` });
|
|
1130
|
+
broadcast('agent-output', { runId: next.runId, line: ` В очереди осталось: ${agentQueue.length}` });
|
|
925
1131
|
executeAgentItem(next);
|
|
926
1132
|
}
|
|
927
1133
|
else {
|
|
@@ -931,8 +1137,11 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
931
1137
|
}
|
|
932
1138
|
/** Actually spawn the agent process for a queue item */
|
|
933
1139
|
function executeAgentItem(item) {
|
|
934
|
-
const { task, featureKey, filePath, selectedFilePaths, title, agent, savedErrors, savedFailedFiles, savedTestType, autoFixAttempt = 0, autoFixSourceTask, } = item;
|
|
1140
|
+
const { runId, task, featureKey, filePath, selectedFilePaths, title, agent, savedErrors, savedFailedFiles, savedTestType, autoFixAttempt = 0, autoFixSourceTask, } = item;
|
|
935
1141
|
const normalizeRelPath = (p) => p.replace(/\\/g, '/');
|
|
1142
|
+
const emitOutput = (line, isError = false, isDim = false) => {
|
|
1143
|
+
broadcast('agent-output', { runId, line, isError, isDim });
|
|
1144
|
+
};
|
|
936
1145
|
const targetSourcePaths = (() => {
|
|
937
1146
|
if (task === 'write-tests-file' && filePath) {
|
|
938
1147
|
return [normalizeRelPath(filePath)];
|
|
@@ -947,14 +1156,24 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
947
1156
|
}
|
|
948
1157
|
return [];
|
|
949
1158
|
})();
|
|
1159
|
+
setRunPhase(runId, 'starting', {
|
|
1160
|
+
targetSourcePaths,
|
|
1161
|
+
error: undefined,
|
|
1162
|
+
fileOutcomes: undefined,
|
|
1163
|
+
validationStats: undefined,
|
|
1164
|
+
});
|
|
1165
|
+
function failBeforeStart(message) {
|
|
1166
|
+
setRunPhase(runId, 'failed', { error: message, targetSourcePaths });
|
|
1167
|
+
broadcast('agent-error', { runId, message });
|
|
1168
|
+
agentRunning = false;
|
|
1169
|
+
processNextInQueue();
|
|
1170
|
+
}
|
|
950
1171
|
// Build prompt lazily at execution time
|
|
951
1172
|
let prompt;
|
|
952
1173
|
if (task === 'write-tests') {
|
|
953
1174
|
const feat = currentData.features?.find(f => f.key === featureKey);
|
|
954
1175
|
if (!feat) {
|
|
955
|
-
|
|
956
|
-
agentRunning = false;
|
|
957
|
-
processNextInQueue();
|
|
1176
|
+
failBeforeStart(`Фича не найдена: ${featureKey}`);
|
|
958
1177
|
return;
|
|
959
1178
|
}
|
|
960
1179
|
prompt = buildWriteTestsPrompt(feat, currentData.modules, currentData.testRunner || 'vitest');
|
|
@@ -962,9 +1181,7 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
962
1181
|
else if (task === 'write-tests-file') {
|
|
963
1182
|
const feat = currentData.features?.find(f => f.key === featureKey);
|
|
964
1183
|
if (!feat || !filePath) {
|
|
965
|
-
|
|
966
|
-
agentRunning = false;
|
|
967
|
-
processNextInQueue();
|
|
1184
|
+
failBeforeStart('Не указана фича или файл');
|
|
968
1185
|
return;
|
|
969
1186
|
}
|
|
970
1187
|
prompt = buildWriteTestsForFilePrompt(filePath, feat, currentData.modules, currentData.testRunner || 'vitest');
|
|
@@ -972,9 +1189,7 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
972
1189
|
else if (task === 'write-tests-selected') {
|
|
973
1190
|
const feat = currentData.features?.find(f => f.key === featureKey);
|
|
974
1191
|
if (!feat || !selectedFilePaths || selectedFilePaths.length === 0) {
|
|
975
|
-
|
|
976
|
-
agentRunning = false;
|
|
977
|
-
processNextInQueue();
|
|
1192
|
+
failBeforeStart('Не указана фича или выбранные файлы');
|
|
978
1193
|
return;
|
|
979
1194
|
}
|
|
980
1195
|
prompt = buildWriteTestsForSelectedPrompt(selectedFilePaths, feat, currentData.modules, currentData.testRunner || 'vitest');
|
|
@@ -982,27 +1197,21 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
982
1197
|
else if (task === 'refresh-tests-selected') {
|
|
983
1198
|
const feat = currentData.features?.find(f => f.key === featureKey);
|
|
984
1199
|
if (!feat || !selectedFilePaths || selectedFilePaths.length === 0) {
|
|
985
|
-
|
|
986
|
-
agentRunning = false;
|
|
987
|
-
processNextInQueue();
|
|
1200
|
+
failBeforeStart('Не указана фича или выбранные файлы');
|
|
988
1201
|
return;
|
|
989
1202
|
}
|
|
990
1203
|
prompt = buildRefreshTestsForSelectedPrompt(selectedFilePaths, feat, currentData.modules, currentData.testRunner || 'vitest');
|
|
991
1204
|
}
|
|
992
1205
|
else if (task === 'fix-tests') {
|
|
993
1206
|
if (!filePath || !savedErrors || savedErrors.length === 0) {
|
|
994
|
-
|
|
995
|
-
agentRunning = false;
|
|
996
|
-
processNextInQueue();
|
|
1207
|
+
failBeforeStart(`Нет сохранённых ошибок для ${filePath}`);
|
|
997
1208
|
return;
|
|
998
1209
|
}
|
|
999
1210
|
prompt = buildFixTestsPrompt(filePath, savedErrors);
|
|
1000
1211
|
}
|
|
1001
1212
|
else if (task === 'fix-tests-all') {
|
|
1002
1213
|
if (!savedFailedFiles || savedFailedFiles.length === 0) {
|
|
1003
|
-
|
|
1004
|
-
agentRunning = false;
|
|
1005
|
-
processNextInQueue();
|
|
1214
|
+
failBeforeStart('Нет упавших тестов для исправления');
|
|
1006
1215
|
return;
|
|
1007
1216
|
}
|
|
1008
1217
|
prompt = buildFixAllTestsPrompt(savedFailedFiles, savedTestType || 'unit');
|
|
@@ -1010,9 +1219,7 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
1010
1219
|
else if (task === 'generate-e2e-plan') {
|
|
1011
1220
|
const feat = currentData.features?.find(f => f.key === featureKey);
|
|
1012
1221
|
if (!feat) {
|
|
1013
|
-
|
|
1014
|
-
agentRunning = false;
|
|
1015
|
-
processNextInQueue();
|
|
1222
|
+
failBeforeStart(`Фича не найдена: ${featureKey}`);
|
|
1016
1223
|
return;
|
|
1017
1224
|
}
|
|
1018
1225
|
prompt = buildE2ePlanPrompt(feat, currentData.modules);
|
|
@@ -1021,9 +1228,7 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
1021
1228
|
const feat = currentData.features?.find(f => f.key === featureKey);
|
|
1022
1229
|
const plan = featureKey ? loadE2ePlan(projectRoot, featureKey) : null;
|
|
1023
1230
|
if (!feat || !plan) {
|
|
1024
|
-
|
|
1025
|
-
agentRunning = false;
|
|
1026
|
-
processNextInQueue();
|
|
1231
|
+
failBeforeStart(`Фича или план не найдены: ${featureKey}`);
|
|
1027
1232
|
return;
|
|
1028
1233
|
}
|
|
1029
1234
|
prompt = buildWriteE2eTestPrompt(feat, plan, currentData.modules);
|
|
@@ -1032,7 +1237,9 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
1032
1237
|
prompt = buildMapUnmappedPrompt(currentData.modules, currentData.features || []);
|
|
1033
1238
|
}
|
|
1034
1239
|
agentRunning = true;
|
|
1240
|
+
setRunPhase(runId, 'running', { targetSourcePaths });
|
|
1035
1241
|
broadcast('agent-started', {
|
|
1242
|
+
runId,
|
|
1036
1243
|
title,
|
|
1037
1244
|
task,
|
|
1038
1245
|
featureKey,
|
|
@@ -1057,27 +1264,30 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
1057
1264
|
shell: true,
|
|
1058
1265
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1059
1266
|
});
|
|
1060
|
-
|
|
1267
|
+
activeAgentProcess = proc;
|
|
1268
|
+
emitOutput(`🚀 Запускаю: ${agent === 'claude' ? 'Claude Code' : 'Codex'}`);
|
|
1061
1269
|
if (agent === 'codex') {
|
|
1062
|
-
|
|
1270
|
+
emitOutput(`🔐 Codex sandbox: ${runtimeEnv.codexSandboxMode}`);
|
|
1063
1271
|
}
|
|
1064
|
-
|
|
1272
|
+
emitOutput('📄 Задача записана в .viberadar/task.md');
|
|
1065
1273
|
// Track test files written/edited by agent (for auto-run after)
|
|
1066
1274
|
const createdTestFiles = [];
|
|
1067
1275
|
// Accumulate full result text for E2E plan parsing
|
|
1068
1276
|
let agentResultText = '';
|
|
1069
1277
|
let agentRawOutputText = '';
|
|
1070
1278
|
let queueBlockSignal = null;
|
|
1279
|
+
let finalFileOutcomes = [];
|
|
1280
|
+
let finalValidationStats = buildValidationStats([]);
|
|
1281
|
+
let validationError;
|
|
1282
|
+
let lastTestResult = null;
|
|
1283
|
+
let lastTestSummary = null;
|
|
1071
1284
|
function inspectQueueBlockSignal(line) {
|
|
1072
1285
|
if (queueBlockSignal !== null)
|
|
1073
1286
|
return;
|
|
1074
1287
|
const signal = detectQueueBlockSignal(line);
|
|
1075
1288
|
if (signal !== null) {
|
|
1076
1289
|
queueBlockSignal = signal;
|
|
1077
|
-
|
|
1078
|
-
line: `⚠️ Обнаружен блокирующий сигнал ${signal}. После завершения текущей задачи очередь будет остановлена.`,
|
|
1079
|
-
isError: true,
|
|
1080
|
-
});
|
|
1290
|
+
emitOutput(`⚠️ Обнаружен блокирующий сигнал ${signal}. После завершения текущей задачи очередь будет остановлена.`, true);
|
|
1081
1291
|
}
|
|
1082
1292
|
}
|
|
1083
1293
|
function trackWrittenFiles(raw) {
|
|
@@ -1109,21 +1319,21 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
1109
1319
|
trackWrittenFiles(raw);
|
|
1110
1320
|
const parsed = agent === 'claude' ? parseClaudeEvent(raw) : raw;
|
|
1111
1321
|
if (!parsed) {
|
|
1112
|
-
|
|
1322
|
+
emitOutput(raw.slice(0, 120), false, true);
|
|
1113
1323
|
return;
|
|
1114
1324
|
}
|
|
1115
1325
|
if (parsed.startsWith('§RESULT§')) {
|
|
1116
1326
|
agentResultText = parsed.slice('§RESULT§'.length).trim();
|
|
1117
|
-
|
|
1327
|
+
emitOutput('─────────────────────────────');
|
|
1118
1328
|
for (const l of agentResultText.split('\n')) {
|
|
1119
1329
|
if (l.trim())
|
|
1120
|
-
|
|
1330
|
+
emitOutput(' ' + l);
|
|
1121
1331
|
}
|
|
1122
1332
|
}
|
|
1123
1333
|
else {
|
|
1124
1334
|
for (const l of parsed.split('\n')) {
|
|
1125
1335
|
if (l.trim())
|
|
1126
|
-
|
|
1336
|
+
emitOutput(l);
|
|
1127
1337
|
}
|
|
1128
1338
|
}
|
|
1129
1339
|
});
|
|
@@ -1135,11 +1345,21 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
1135
1345
|
agentRawOutputText += line + '\n';
|
|
1136
1346
|
}
|
|
1137
1347
|
inspectQueueBlockSignal(line);
|
|
1138
|
-
|
|
1348
|
+
emitOutput(line, true);
|
|
1139
1349
|
}
|
|
1140
1350
|
});
|
|
1141
1351
|
proc.on('close', async (code) => {
|
|
1142
1352
|
agentRunning = false;
|
|
1353
|
+
activeAgentProcess = null;
|
|
1354
|
+
const alreadyCanceled = findRun(runId)?.phase === 'canceled';
|
|
1355
|
+
if (alreadyCanceled) {
|
|
1356
|
+
process.stdout.write(` ⏹ Agent process closed for canceled run ${runId}\n`);
|
|
1357
|
+
processNextInQueue(true);
|
|
1358
|
+
return;
|
|
1359
|
+
}
|
|
1360
|
+
let finalPhase = 'completed';
|
|
1361
|
+
let finalError;
|
|
1362
|
+
let autoFixQueued = false;
|
|
1143
1363
|
if (code === 0) {
|
|
1144
1364
|
// Auto-run created/fixed test files and show results
|
|
1145
1365
|
let testFilesToRun;
|
|
@@ -1153,9 +1373,11 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
1153
1373
|
testFilesToRun = createdTestFiles;
|
|
1154
1374
|
}
|
|
1155
1375
|
if ((task === 'write-tests' || task === 'write-tests-file' || task === 'write-tests-selected' || task === 'refresh-tests-selected' || task === 'fix-tests' || task === 'fix-tests-all') && testFilesToRun.length > 0) {
|
|
1156
|
-
|
|
1157
|
-
|
|
1376
|
+
setRunPhase(runId, 'validating', { targetSourcePaths });
|
|
1377
|
+
emitOutput('─────────────────────────────');
|
|
1378
|
+
emitOutput(`🧪 Запускаю тесты (${testFilesToRun.length} файлов)...`);
|
|
1158
1379
|
const result = await runTestFiles(testFilesToRun, projectRoot);
|
|
1380
|
+
lastTestResult = result;
|
|
1159
1381
|
lastTestResults.clear();
|
|
1160
1382
|
for (const [fp, detail] of Object.entries(result.fileDetails)) {
|
|
1161
1383
|
if (detail.failed > 0)
|
|
@@ -1171,26 +1393,27 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
1171
1393
|
const testedFileCount = testFilesToRun.length;
|
|
1172
1394
|
const failedFileCount = failedFiles.length;
|
|
1173
1395
|
const passedFileCount = Math.max(0, testedFileCount - failedFileCount);
|
|
1174
|
-
|
|
1396
|
+
emitOutput('┌──────────────── Тест-отчёт ────────────────');
|
|
1175
1397
|
if (result.runError) {
|
|
1176
|
-
|
|
1398
|
+
emitOutput(`│ ❌ Ошибка запуска тестов: ${result.runError}`);
|
|
1399
|
+
finalPhase = 'failed';
|
|
1400
|
+
finalError = `Ошибка запуска тестов: ${result.runError}`;
|
|
1177
1401
|
}
|
|
1178
1402
|
else {
|
|
1179
1403
|
const status = result.failed === 0 ? '✅ OK' : '❌ FAILED';
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1404
|
+
emitOutput(`│ Статус: ${status}`);
|
|
1405
|
+
emitOutput(`│ Файлы: ${testedFileCount} • passed: ${passedFileCount} • failed: ${failedFileCount}`);
|
|
1406
|
+
emitOutput(`│ Тест-кейсы: passed ${result.passed} • failed ${result.failed}`);
|
|
1183
1407
|
}
|
|
1184
|
-
|
|
1408
|
+
emitOutput('└─────────────────────────────────────────────');
|
|
1185
1409
|
if (result.failed > 0) {
|
|
1186
1410
|
for (const f of failedFiles) {
|
|
1187
|
-
|
|
1411
|
+
emitOutput(` ❌ ${f.rel} — ${f.detail.failed} упало`);
|
|
1188
1412
|
for (const e of f.detail.errors.slice(0, 3)) {
|
|
1189
|
-
|
|
1413
|
+
emitOutput(` • ${e.testName}`, false, true);
|
|
1190
1414
|
}
|
|
1191
1415
|
}
|
|
1192
1416
|
}
|
|
1193
|
-
let autoFixQueued = false;
|
|
1194
1417
|
if (!result.runError && result.failed > 0) {
|
|
1195
1418
|
const nextAttempt = autoFixAttempt + 1;
|
|
1196
1419
|
const sourceTask = autoFixSourceTask || task;
|
|
@@ -1202,6 +1425,7 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
1202
1425
|
const only = failedFiles[0];
|
|
1203
1426
|
const fileName = only.rel.split('/').pop() || only.rel;
|
|
1204
1427
|
fixItem = {
|
|
1428
|
+
runId: newRunId(),
|
|
1205
1429
|
task: 'fix-tests',
|
|
1206
1430
|
featureKey,
|
|
1207
1431
|
filePath: only.rel,
|
|
@@ -1214,6 +1438,7 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
1214
1438
|
}
|
|
1215
1439
|
else if (failedFiles.length > 1) {
|
|
1216
1440
|
fixItem = {
|
|
1441
|
+
runId: newRunId(),
|
|
1217
1442
|
task: 'fix-tests-all',
|
|
1218
1443
|
featureKey,
|
|
1219
1444
|
title: `${agent === 'claude' ? 'Claude Code' : 'Codex'} — автоисправление ${failedFiles.length} тестов ${attemptSuffix}`,
|
|
@@ -1226,22 +1451,23 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
1226
1451
|
}
|
|
1227
1452
|
if (fixItem) {
|
|
1228
1453
|
if (agentQueue.length < runtimeEnv.agentQueueMax) {
|
|
1454
|
+
createRun(fixItem, 'queued');
|
|
1229
1455
|
agentQueue.unshift(fixItem);
|
|
1456
|
+
emitQueueUpdated();
|
|
1230
1457
|
autoFixQueued = true;
|
|
1231
|
-
|
|
1458
|
+
emitOutput(`🛠️ Обнаружены падения. Запускаю автоисправление ${attemptSuffix}...`);
|
|
1232
1459
|
broadcast('agent-queued', {
|
|
1460
|
+
runId: fixItem.runId,
|
|
1233
1461
|
queueLength: agentQueue.length,
|
|
1234
1462
|
title: fixItem.title,
|
|
1235
1463
|
task: fixItem.task,
|
|
1236
1464
|
featureKey: fixItem.featureKey || null,
|
|
1237
1465
|
filePath: fixItem.filePath || null,
|
|
1466
|
+
selectedFilePaths: fixItem.selectedFilePaths || null,
|
|
1238
1467
|
});
|
|
1239
1468
|
}
|
|
1240
1469
|
else {
|
|
1241
|
-
|
|
1242
|
-
line: `⚠️ Автоисправление не поставлено: очередь заполнена (${runtimeEnv.agentQueueMax})`,
|
|
1243
|
-
isError: true,
|
|
1244
|
-
});
|
|
1470
|
+
emitOutput(`⚠️ Автоисправление не поставлено: очередь заполнена (${runtimeEnv.agentQueueMax})`, true);
|
|
1245
1471
|
}
|
|
1246
1472
|
}
|
|
1247
1473
|
}
|
|
@@ -1249,13 +1475,19 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
1249
1475
|
const reason = !runtimeEnv.autoFixFailedTests
|
|
1250
1476
|
? 'автоисправление выключено'
|
|
1251
1477
|
: `достигнут лимит попыток (${runtimeEnv.autoFixMaxRetries})`;
|
|
1252
|
-
|
|
1478
|
+
emitOutput(`⚠️ Автоисправление не запущено: ${reason}`);
|
|
1253
1479
|
}
|
|
1254
1480
|
}
|
|
1255
1481
|
if (result.failed > 0 && !autoFixQueued) {
|
|
1256
|
-
|
|
1482
|
+
emitOutput(' → Нажми 🔧 исправить в дашборде чтобы агент починил');
|
|
1483
|
+
finalPhase = 'failed';
|
|
1484
|
+
if (!finalError)
|
|
1485
|
+
finalError = 'Есть упавшие тесты';
|
|
1257
1486
|
}
|
|
1258
|
-
|
|
1487
|
+
if (result.failed > 0 && autoFixQueued && finalPhase !== 'failed') {
|
|
1488
|
+
finalPhase = 'completed';
|
|
1489
|
+
}
|
|
1490
|
+
lastTestSummary = { testedFileCount, passedFileCount, failedFileCount, autoFixQueued };
|
|
1259
1491
|
}
|
|
1260
1492
|
// E2E plan post-processing
|
|
1261
1493
|
if (task === 'generate-e2e-plan' && featureKey) {
|
|
@@ -1279,9 +1511,12 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
1279
1511
|
? ` Возможная причина: блокировка/лимит агента (${queueBlockSignal}).`
|
|
1280
1512
|
: '';
|
|
1281
1513
|
broadcast('e2e-plan-error', {
|
|
1514
|
+
runId,
|
|
1282
1515
|
featureKey,
|
|
1283
1516
|
message: `Не удалось распарсить план: ${err.message}.${blockHint}`.trim(),
|
|
1284
1517
|
});
|
|
1518
|
+
finalPhase = 'failed';
|
|
1519
|
+
finalError = `Не удалось распарсить E2E план: ${err.message}`;
|
|
1285
1520
|
}
|
|
1286
1521
|
}
|
|
1287
1522
|
if (task === 'write-e2e-tests' && featureKey) {
|
|
@@ -1297,45 +1532,69 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
1297
1532
|
}
|
|
1298
1533
|
process.stdout.write(' ✅ Agent done, rescanning...\n');
|
|
1299
1534
|
try {
|
|
1535
|
+
if (targetSourcePaths.length > 0) {
|
|
1536
|
+
setRunPhase(runId, 'validating', { targetSourcePaths });
|
|
1537
|
+
}
|
|
1300
1538
|
currentData = await (0, scanner_1.scanProject)(projectRoot);
|
|
1301
1539
|
if (targetSourcePaths.length > 0) {
|
|
1302
|
-
const
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
const unresolved =
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
line: `⚠️ После задачи осталось без тестов: ${unresolved.length}/${targetSourcePaths.length} файлов`,
|
|
1312
|
-
isError: true,
|
|
1540
|
+
const matrix = buildFileOutcomes(targetSourcePaths);
|
|
1541
|
+
finalFileOutcomes = matrix.fileOutcomes;
|
|
1542
|
+
finalValidationStats = matrix.validationStats;
|
|
1543
|
+
const unresolved = finalFileOutcomes.filter((o) => o.status === 'not-covered');
|
|
1544
|
+
const blocked = finalFileOutcomes.filter((o) => o.status === 'blocked');
|
|
1545
|
+
if (unresolved.length > 0 || blocked.length > 0) {
|
|
1546
|
+
emitOutput(`⚠️ После задачи осталось без тестов: ${unresolved.length}/${targetSourcePaths.length} файлов`, true);
|
|
1547
|
+
unresolved.slice(0, 20).forEach((entry) => {
|
|
1548
|
+
emitOutput(` • ${entry.sourcePath}${entry.reason ? ` — ${entry.reason}` : ''}`, true);
|
|
1313
1549
|
});
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
broadcast('agent-output', {
|
|
1319
|
-
line: ` ... и еще ${unresolved.length - 20} файлов`,
|
|
1320
|
-
isError: true,
|
|
1550
|
+
if (blocked.length > 0) {
|
|
1551
|
+
emitOutput(`⚠️ Заблокированные/несопоставленные файлы: ${blocked.length}`, true);
|
|
1552
|
+
blocked.slice(0, 10).forEach((entry) => {
|
|
1553
|
+
emitOutput(` • ${entry.sourcePath}${entry.reason ? ` — ${entry.reason}` : ''}`, true);
|
|
1321
1554
|
});
|
|
1322
1555
|
}
|
|
1556
|
+
finalPhase = 'failed';
|
|
1557
|
+
if (!finalError) {
|
|
1558
|
+
finalError = `Валидация покрытия: covered=${finalValidationStats.covered}, not-covered=${finalValidationStats.notCovered}, blocked=${finalValidationStats.blocked}, infra=${finalValidationStats.infra}`;
|
|
1559
|
+
}
|
|
1323
1560
|
}
|
|
1324
1561
|
else {
|
|
1325
|
-
|
|
1326
|
-
line: `✅ Проверка: все ${targetSourcePaths.length} целевых файлов теперь отмечены как с тестами`,
|
|
1327
|
-
});
|
|
1562
|
+
emitOutput(`✅ Проверка: все ${targetSourcePaths.length} целевых файлов теперь отмечены как с тестами`);
|
|
1328
1563
|
}
|
|
1329
1564
|
}
|
|
1330
1565
|
broadcast('data-updated');
|
|
1331
1566
|
}
|
|
1332
|
-
catch {
|
|
1567
|
+
catch (err) {
|
|
1568
|
+
validationError = err?.message || String(err);
|
|
1569
|
+
finalPhase = 'failed';
|
|
1570
|
+
finalError = `Ошибка валидации после rescan: ${validationError}`;
|
|
1571
|
+
emitOutput(`❌ Не удалось выполнить валидацию после задачи: ${validationError}`, true);
|
|
1572
|
+
}
|
|
1573
|
+
if (targetSourcePaths.length > 0) {
|
|
1574
|
+
broadcast('agent-summary', {
|
|
1575
|
+
runId,
|
|
1576
|
+
targetSourcePaths,
|
|
1577
|
+
fileOutcomes: finalFileOutcomes,
|
|
1578
|
+
validationStats: finalValidationStats,
|
|
1579
|
+
...(lastTestResult || {}),
|
|
1580
|
+
...(lastTestSummary || {}),
|
|
1581
|
+
});
|
|
1582
|
+
}
|
|
1583
|
+
setRunPhase(runId, finalPhase, {
|
|
1584
|
+
targetSourcePaths,
|
|
1585
|
+
fileOutcomes: finalFileOutcomes,
|
|
1586
|
+
validationStats: finalValidationStats,
|
|
1587
|
+
error: finalError,
|
|
1588
|
+
});
|
|
1333
1589
|
processNextInQueue(true);
|
|
1334
1590
|
}
|
|
1335
1591
|
else if (code === 255) {
|
|
1336
1592
|
process.stdout.write(` ❌ Agent auth error (exit code 255)\n`);
|
|
1593
|
+
const message = `${agent === 'claude' ? 'Claude Code' : 'Codex'} не авторизован. Нажми 🔑 Перелогиниться в меню агента.`;
|
|
1594
|
+
setRunPhase(runId, 'failed', { targetSourcePaths, error: message });
|
|
1337
1595
|
broadcast('agent-error', {
|
|
1338
|
-
|
|
1596
|
+
runId,
|
|
1597
|
+
message,
|
|
1339
1598
|
authRequired: true,
|
|
1340
1599
|
agent,
|
|
1341
1600
|
});
|
|
@@ -1343,7 +1602,9 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
1343
1602
|
}
|
|
1344
1603
|
else {
|
|
1345
1604
|
process.stdout.write(` ❌ Agent failed (exit code ${code})\n`);
|
|
1346
|
-
|
|
1605
|
+
const message = `Агент завершился с кодом ${code}`;
|
|
1606
|
+
setRunPhase(runId, 'failed', { targetSourcePaths, error: message });
|
|
1607
|
+
broadcast('agent-error', { runId, message });
|
|
1347
1608
|
if (queueBlockSignal === 403 || queueBlockSignal === 429) {
|
|
1348
1609
|
stopQueuedTasks(`пойман ${queueBlockSignal} от ${agent === 'claude' ? 'Claude Code' : 'Codex'}`);
|
|
1349
1610
|
}
|
|
@@ -1352,13 +1613,19 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
1352
1613
|
});
|
|
1353
1614
|
proc.on('error', (err) => {
|
|
1354
1615
|
agentRunning = false;
|
|
1616
|
+
activeAgentProcess = null;
|
|
1617
|
+
if (findRun(runId)?.phase === 'canceled') {
|
|
1618
|
+
processNextInQueue(true);
|
|
1619
|
+
return;
|
|
1620
|
+
}
|
|
1355
1621
|
const isNotFound = err.code === 'ENOENT' || err.message.includes('ENOENT');
|
|
1356
1622
|
const agentName = agent === 'claude' ? 'Claude Code' : 'Codex';
|
|
1357
1623
|
const msg = isNotFound
|
|
1358
1624
|
? `${agentName} не установлен. Скачай с ${agent === 'claude' ? 'claude.ai/download' : 'github.com/openai/codex'}`
|
|
1359
1625
|
: `Не удалось запустить ${agent}: ${err.message}`;
|
|
1360
1626
|
process.stdout.write(' ❌ Agent spawn error: ' + err.message + '\n');
|
|
1361
|
-
|
|
1627
|
+
setRunPhase(runId, 'failed', { targetSourcePaths, error: msg });
|
|
1628
|
+
broadcast('agent-error', { runId, message: msg, notInstalled: isNotFound, agent });
|
|
1362
1629
|
processNextInQueue(true);
|
|
1363
1630
|
});
|
|
1364
1631
|
}
|
|
@@ -1367,7 +1634,7 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
1367
1634
|
const agent = currentData.agent;
|
|
1368
1635
|
if (!agent) {
|
|
1369
1636
|
broadcast('agent-error', { message: 'Агент не выбран. Укажи agent в viberadar.config.json' });
|
|
1370
|
-
return;
|
|
1637
|
+
return null;
|
|
1371
1638
|
}
|
|
1372
1639
|
const agentLabel = agent === 'claude' ? 'Claude Code' : 'Codex';
|
|
1373
1640
|
let title;
|
|
@@ -1378,7 +1645,7 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
1378
1645
|
const feat = currentData.features?.find(f => f.key === featureKey);
|
|
1379
1646
|
if (!feat) {
|
|
1380
1647
|
broadcast('agent-error', { message: `Фича не найдена: ${featureKey}` });
|
|
1381
|
-
return;
|
|
1648
|
+
return null;
|
|
1382
1649
|
}
|
|
1383
1650
|
title = `${agentLabel} — тесты для "${feat.label}"`;
|
|
1384
1651
|
}
|
|
@@ -1386,7 +1653,7 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
1386
1653
|
const feat = currentData.features?.find(f => f.key === featureKey);
|
|
1387
1654
|
if (!feat || !filePath) {
|
|
1388
1655
|
broadcast('agent-error', { message: 'Не указана фича или файл' });
|
|
1389
|
-
return;
|
|
1656
|
+
return null;
|
|
1390
1657
|
}
|
|
1391
1658
|
const fileName = filePath.replace(/\\/g, '/').split('/').pop() || filePath;
|
|
1392
1659
|
title = `${agentLabel} — тест для "${fileName}"`;
|
|
@@ -1396,7 +1663,7 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
1396
1663
|
const count = selectedFilePaths?.length ?? 0;
|
|
1397
1664
|
if (!feat || count === 0) {
|
|
1398
1665
|
broadcast('agent-error', { message: 'Не указана фича или выбранные файлы' });
|
|
1399
|
-
return;
|
|
1666
|
+
return null;
|
|
1400
1667
|
}
|
|
1401
1668
|
title = `${agentLabel} — тесты для выбранных файлов (${count})`;
|
|
1402
1669
|
}
|
|
@@ -1405,14 +1672,14 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
1405
1672
|
const count = selectedFilePaths?.length ?? 0;
|
|
1406
1673
|
if (!feat || count === 0) {
|
|
1407
1674
|
broadcast('agent-error', { message: 'Не указана фича или выбранные файлы' });
|
|
1408
|
-
return;
|
|
1675
|
+
return null;
|
|
1409
1676
|
}
|
|
1410
1677
|
title = `${agentLabel} — актуализировать тесты (${count})`;
|
|
1411
1678
|
}
|
|
1412
1679
|
else if (task === 'fix-tests') {
|
|
1413
1680
|
if (!filePath) {
|
|
1414
1681
|
broadcast('agent-error', { message: 'Не указан файл для исправления' });
|
|
1415
|
-
return;
|
|
1682
|
+
return null;
|
|
1416
1683
|
}
|
|
1417
1684
|
for (const [fp, detail] of lastTestResults) {
|
|
1418
1685
|
const rel = path.relative(projectRoot, fp).replace(/\\/g, '/');
|
|
@@ -1423,7 +1690,7 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
1423
1690
|
}
|
|
1424
1691
|
if (!savedErrors || savedErrors.length === 0) {
|
|
1425
1692
|
broadcast('agent-error', { message: `Нет сохранённых ошибок для ${filePath}. Сначала запусти тесты.` });
|
|
1426
|
-
return;
|
|
1693
|
+
return null;
|
|
1427
1694
|
}
|
|
1428
1695
|
const fileName = filePath.replace(/\\/g, '/').split('/').pop() || filePath;
|
|
1429
1696
|
title = `${agentLabel} — исправить тесты в "${fileName}"`;
|
|
@@ -1440,7 +1707,7 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
1440
1707
|
}
|
|
1441
1708
|
if (savedFailedFiles.length === 0) {
|
|
1442
1709
|
broadcast('agent-error', { message: `Нет упавших ${savedTestType} тестов. Сначала запусти тесты.` });
|
|
1443
|
-
return;
|
|
1710
|
+
return null;
|
|
1444
1711
|
}
|
|
1445
1712
|
title = `${agentLabel} — починить все ${savedTestType} тесты (${savedFailedFiles.length} файлов)`;
|
|
1446
1713
|
}
|
|
@@ -1448,7 +1715,7 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
1448
1715
|
const feat = currentData.features?.find(f => f.key === featureKey);
|
|
1449
1716
|
if (!feat) {
|
|
1450
1717
|
broadcast('agent-error', { message: `Фича не найдена: ${featureKey}` });
|
|
1451
|
-
return;
|
|
1718
|
+
return null;
|
|
1452
1719
|
}
|
|
1453
1720
|
title = `${agentLabel} — E2E план для "${feat.label}"`;
|
|
1454
1721
|
}
|
|
@@ -1456,25 +1723,39 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
1456
1723
|
const feat = currentData.features?.find(f => f.key === featureKey);
|
|
1457
1724
|
if (!feat) {
|
|
1458
1725
|
broadcast('agent-error', { message: `Фича не найдена: ${featureKey}` });
|
|
1459
|
-
return;
|
|
1726
|
+
return null;
|
|
1460
1727
|
}
|
|
1461
1728
|
title = `${agentLabel} — пишу E2E тесты для "${feat.label}"`;
|
|
1462
1729
|
}
|
|
1463
1730
|
else {
|
|
1464
1731
|
title = `${agentLabel} — разобрать unmapped`;
|
|
1465
1732
|
}
|
|
1466
|
-
const item = {
|
|
1733
|
+
const item = {
|
|
1734
|
+
runId: newRunId(),
|
|
1735
|
+
task,
|
|
1736
|
+
featureKey,
|
|
1737
|
+
filePath,
|
|
1738
|
+
selectedFilePaths,
|
|
1739
|
+
title,
|
|
1740
|
+
agent,
|
|
1741
|
+
savedErrors,
|
|
1742
|
+
savedFailedFiles,
|
|
1743
|
+
savedTestType,
|
|
1744
|
+
};
|
|
1745
|
+
createRun(item, 'queued');
|
|
1467
1746
|
if (agentRunning || queueCooldownTimer) {
|
|
1468
1747
|
if (agentQueue.length >= runtimeEnv.agentQueueMax) {
|
|
1748
|
+
setRunPhase(item.runId, 'failed', { error: `Очередь переполнена (${runtimeEnv.agentQueueMax})` });
|
|
1469
1749
|
const msg = `Очередь агента ограничена (${runtimeEnv.agentQueueMax}). Дождись завершения текущих задач.`;
|
|
1470
|
-
broadcast('agent-error', { message: msg });
|
|
1750
|
+
broadcast('agent-error', { runId: item.runId, message: msg });
|
|
1471
1751
|
process.stdout.write(` ⚠️ Queue limit reached (${runtimeEnv.agentQueueMax}), rejected: "${title}"\n`);
|
|
1472
|
-
return;
|
|
1752
|
+
return item.runId;
|
|
1473
1753
|
}
|
|
1474
|
-
|
|
1754
|
+
enqueueItem(item);
|
|
1475
1755
|
const ql = agentQueue.length;
|
|
1476
1756
|
process.stdout.write(` 📋 Agent busy, queued: "${title}" (queue size: ${ql})\n`);
|
|
1477
1757
|
broadcast('agent-queued', {
|
|
1758
|
+
runId: item.runId,
|
|
1478
1759
|
queueLength: ql,
|
|
1479
1760
|
title,
|
|
1480
1761
|
task,
|
|
@@ -1482,9 +1763,10 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
1482
1763
|
filePath: filePath || null,
|
|
1483
1764
|
selectedFilePaths: selectedFilePaths || null,
|
|
1484
1765
|
});
|
|
1485
|
-
return;
|
|
1766
|
+
return item.runId;
|
|
1486
1767
|
}
|
|
1487
1768
|
executeAgentItem(item);
|
|
1769
|
+
return item.runId;
|
|
1488
1770
|
}
|
|
1489
1771
|
// ── Chokidar watcher ───────────────────────────────────────────────────────
|
|
1490
1772
|
chokidar_1.default.watch([
|
|
@@ -1504,7 +1786,9 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
1504
1786
|
.on('unlink', f => scheduleRescan(path.join(projectRoot, f)));
|
|
1505
1787
|
// ── HTTP server ────────────────────────────────────────────────────────────
|
|
1506
1788
|
const server = http.createServer((req, res) => {
|
|
1507
|
-
const
|
|
1789
|
+
const rawUrl = req.url ?? '/';
|
|
1790
|
+
const parsedUrl = new URL(rawUrl, 'http://127.0.0.1');
|
|
1791
|
+
const url = parsedUrl.pathname;
|
|
1508
1792
|
if (url === '/' || url === '/radar/qa' || url === '/radar/observability') {
|
|
1509
1793
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
1510
1794
|
res.end(DASHBOARD_HTML);
|
|
@@ -1544,7 +1828,98 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
1544
1828
|
}
|
|
1545
1829
|
if (url === '/api/status') {
|
|
1546
1830
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1547
|
-
res.end(JSON.stringify({
|
|
1831
|
+
res.end(JSON.stringify({
|
|
1832
|
+
agentRunning,
|
|
1833
|
+
queueLength: agentQueue.length,
|
|
1834
|
+
activeRunId,
|
|
1835
|
+
queueCooldownActive: !!queueCooldownTimer,
|
|
1836
|
+
}));
|
|
1837
|
+
return;
|
|
1838
|
+
}
|
|
1839
|
+
if (url === '/api/agent/state' && req.method === 'GET') {
|
|
1840
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1841
|
+
res.end(JSON.stringify(buildAgentStateSnapshot()));
|
|
1842
|
+
return;
|
|
1843
|
+
}
|
|
1844
|
+
const queueCancelMatch = url.match(/^\/api\/queue\/([^/]+)\/cancel$/);
|
|
1845
|
+
if (queueCancelMatch && req.method === 'POST') {
|
|
1846
|
+
const runId = decodeURIComponent(queueCancelMatch[1]);
|
|
1847
|
+
const result = cancelQueuedRun(runId, 'canceled-by-user');
|
|
1848
|
+
if (!result.ok) {
|
|
1849
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
1850
|
+
res.end(JSON.stringify({ ok: false, error: result.message }));
|
|
1851
|
+
return;
|
|
1852
|
+
}
|
|
1853
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1854
|
+
res.end(JSON.stringify({ ok: true, state: buildAgentStateSnapshot() }));
|
|
1855
|
+
return;
|
|
1856
|
+
}
|
|
1857
|
+
const queueRetryMatch = url.match(/^\/api\/queue\/([^/]+)\/retry$/);
|
|
1858
|
+
if (queueRetryMatch && req.method === 'POST') {
|
|
1859
|
+
const runId = decodeURIComponent(queueRetryMatch[1]);
|
|
1860
|
+
const existing = findRun(runId);
|
|
1861
|
+
if (!existing) {
|
|
1862
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
1863
|
+
res.end(JSON.stringify({ ok: false, error: `Run ${runId} не найден` }));
|
|
1864
|
+
return;
|
|
1865
|
+
}
|
|
1866
|
+
if (existing.task === 'fix-tests' || existing.task === 'fix-tests-all') {
|
|
1867
|
+
res.writeHead(409, { 'Content-Type': 'application/json' });
|
|
1868
|
+
res.end(JSON.stringify({
|
|
1869
|
+
ok: false,
|
|
1870
|
+
error: `Retry для ${existing.task} недоступен: нет сохраненного контекста ошибок`,
|
|
1871
|
+
}));
|
|
1872
|
+
return;
|
|
1873
|
+
}
|
|
1874
|
+
const retriedItem = {
|
|
1875
|
+
runId: newRunId(),
|
|
1876
|
+
task: existing.task,
|
|
1877
|
+
featureKey: existing.featureKey,
|
|
1878
|
+
filePath: existing.filePath,
|
|
1879
|
+
selectedFilePaths: existing.selectedFilePaths,
|
|
1880
|
+
title: `${existing.title} (retry)`,
|
|
1881
|
+
agent: existing.agent,
|
|
1882
|
+
};
|
|
1883
|
+
createRun(retriedItem, 'queued');
|
|
1884
|
+
enqueueItem(retriedItem);
|
|
1885
|
+
broadcast('agent-queued', {
|
|
1886
|
+
runId: retriedItem.runId,
|
|
1887
|
+
queueLength: agentQueue.length,
|
|
1888
|
+
title: retriedItem.title,
|
|
1889
|
+
task: retriedItem.task,
|
|
1890
|
+
featureKey: retriedItem.featureKey || null,
|
|
1891
|
+
filePath: retriedItem.filePath || null,
|
|
1892
|
+
selectedFilePaths: retriedItem.selectedFilePaths || null,
|
|
1893
|
+
});
|
|
1894
|
+
if (!agentRunning && !queueCooldownTimer) {
|
|
1895
|
+
processNextInQueue(false);
|
|
1896
|
+
}
|
|
1897
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1898
|
+
res.end(JSON.stringify({ ok: true, runId: retriedItem.runId, state: buildAgentStateSnapshot() }));
|
|
1899
|
+
return;
|
|
1900
|
+
}
|
|
1901
|
+
const queueReorderMatch = url.match(/^\/api\/queue\/([^/]+)\/reorder$/);
|
|
1902
|
+
if (queueReorderMatch && req.method === 'POST') {
|
|
1903
|
+
const runId = decodeURIComponent(queueReorderMatch[1]);
|
|
1904
|
+
let body = '';
|
|
1905
|
+
req.on('data', d => body += d);
|
|
1906
|
+
req.on('end', () => {
|
|
1907
|
+
let direction = 'up';
|
|
1908
|
+
try {
|
|
1909
|
+
const parsed = JSON.parse(body || '{}');
|
|
1910
|
+
if (parsed?.direction === 'down')
|
|
1911
|
+
direction = 'down';
|
|
1912
|
+
}
|
|
1913
|
+
catch { }
|
|
1914
|
+
const result = moveQueueItem(runId, direction);
|
|
1915
|
+
if (!result.ok) {
|
|
1916
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1917
|
+
res.end(JSON.stringify({ ok: false, error: result.message }));
|
|
1918
|
+
return;
|
|
1919
|
+
}
|
|
1920
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1921
|
+
res.end(JSON.stringify({ ok: true, state: buildAgentStateSnapshot() }));
|
|
1922
|
+
});
|
|
1548
1923
|
return;
|
|
1549
1924
|
}
|
|
1550
1925
|
if (url === '/api/run-all-tests' && req.method === 'POST') {
|
|
@@ -1679,9 +2054,9 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
1679
2054
|
try {
|
|
1680
2055
|
const { task, featureKey, filePath, selectedFilePaths } = JSON.parse(body);
|
|
1681
2056
|
process.stdout.write(` 📥 run-agent: task=${task} featureKey=${featureKey} filePath=${filePath} selected=${Array.isArray(selectedFilePaths) ? selectedFilePaths.length : 0}\n`);
|
|
1682
|
-
runAgent(task, featureKey, filePath, selectedFilePaths);
|
|
2057
|
+
const runId = runAgent(task, featureKey, filePath, selectedFilePaths);
|
|
1683
2058
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1684
|
-
res.end(JSON.stringify({ ok: true }));
|
|
2059
|
+
res.end(JSON.stringify({ ok: true, runId }));
|
|
1685
2060
|
}
|
|
1686
2061
|
catch (err) {
|
|
1687
2062
|
process.stdout.write(` ❌ run-agent parse error: ${err.message}\n`);
|
|
@@ -1692,21 +2067,40 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
1692
2067
|
return;
|
|
1693
2068
|
}
|
|
1694
2069
|
if (url === '/api/cancel-agent' && req.method === 'POST') {
|
|
2070
|
+
const nowReason = 'canceled-by-user';
|
|
2071
|
+
if (activeRunId) {
|
|
2072
|
+
setRunPhase(activeRunId, 'canceled', { error: nowReason });
|
|
2073
|
+
}
|
|
2074
|
+
for (const q of agentQueue) {
|
|
2075
|
+
setRunPhase(q.runId, 'canceled', { error: nowReason });
|
|
2076
|
+
}
|
|
1695
2077
|
agentRunning = false;
|
|
2078
|
+
if (activeAgentProcess) {
|
|
2079
|
+
try {
|
|
2080
|
+
activeAgentProcess.kill('SIGTERM');
|
|
2081
|
+
}
|
|
2082
|
+
catch { }
|
|
2083
|
+
activeAgentProcess = null;
|
|
2084
|
+
}
|
|
1696
2085
|
agentQueue.length = 0; // clear queue too
|
|
1697
2086
|
clearQueueCooldownTimer();
|
|
2087
|
+
emitQueueUpdated();
|
|
1698
2088
|
process.stdout.write(' ⏹ Agent state reset by user (queue cleared)\n');
|
|
1699
2089
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1700
|
-
res.end(JSON.stringify({ ok: true }));
|
|
2090
|
+
res.end(JSON.stringify({ ok: true, state: buildAgentStateSnapshot() }));
|
|
1701
2091
|
return;
|
|
1702
2092
|
}
|
|
1703
2093
|
if (url === '/api/clear-queue' && req.method === 'POST') {
|
|
1704
2094
|
const cleared = agentQueue.length;
|
|
2095
|
+
for (const q of agentQueue) {
|
|
2096
|
+
setRunPhase(q.runId, 'canceled', { error: 'queue-cleared-by-user' });
|
|
2097
|
+
}
|
|
1705
2098
|
agentQueue.length = 0;
|
|
1706
2099
|
clearQueueCooldownTimer();
|
|
2100
|
+
emitQueueUpdated();
|
|
1707
2101
|
process.stdout.write(` 🗑 Queue cleared (${cleared} items)\n`);
|
|
1708
2102
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1709
|
-
res.end(JSON.stringify({ ok: true, cleared }));
|
|
2103
|
+
res.end(JSON.stringify({ ok: true, cleared, state: buildAgentStateSnapshot() }));
|
|
1710
2104
|
return;
|
|
1711
2105
|
}
|
|
1712
2106
|
if (url === '/api/set-agent' && req.method === 'POST') {
|
|
@@ -1750,9 +2144,9 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
1750
2144
|
try {
|
|
1751
2145
|
const { featureKey } = JSON.parse(body);
|
|
1752
2146
|
broadcast('e2e-plan-generating', { featureKey });
|
|
1753
|
-
runAgent('generate-e2e-plan', featureKey);
|
|
2147
|
+
const runId = runAgent('generate-e2e-plan', featureKey);
|
|
1754
2148
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1755
|
-
res.end(JSON.stringify({ ok: true }));
|
|
2149
|
+
res.end(JSON.stringify({ ok: true, runId }));
|
|
1756
2150
|
}
|
|
1757
2151
|
catch (err) {
|
|
1758
2152
|
res.writeHead(400);
|
|
@@ -1832,9 +2226,9 @@ function startServer({ data: initialData, port, projectRoot }) {
|
|
|
1832
2226
|
try {
|
|
1833
2227
|
const { featureKey } = JSON.parse(body);
|
|
1834
2228
|
broadcast('e2e-tests-writing', { featureKey });
|
|
1835
|
-
runAgent('write-e2e-tests', featureKey);
|
|
2229
|
+
const runId = runAgent('write-e2e-tests', featureKey);
|
|
1836
2230
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1837
|
-
res.end(JSON.stringify({ ok: true }));
|
|
2231
|
+
res.end(JSON.stringify({ ok: true, runId }));
|
|
1838
2232
|
}
|
|
1839
2233
|
catch (err) {
|
|
1840
2234
|
res.writeHead(400);
|