workflow-ai 1.0.65 → 1.0.66
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 +377 -371
- package/configs/agent-health-rules.yaml +12 -1
- package/configs/pipeline.yaml +6 -6
- package/package.json +1 -1
- package/src/lib/agent-spawner.mjs +47 -6
- package/src/lib/error-classifier.mjs +311 -274
- package/src/runner.mjs +215 -58
- package/src/skills/coach/tests/cases/TC-COACH-001/current/meta.json +93 -93
- package/src/skills/coach/tests/cases/TC-COACH-002/current/meta.json +93 -93
- package/src/skills/create-plan/SKILL.md +1 -0
- package/src/skills/create-plan/knowledge/test-hygiene.md +47 -0
- package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-005/current/meta.json +113 -113
- package/src/skills/execute-task/tests/cases/TC-EXECUTE-TASK-001/current/meta.json +87 -87
- package/src/skills/execute-task/tests/cases/TC-EXECUTE-TASK-005/current/meta.json +87 -87
- package/src/skills/review-result/SKILL.md +1 -0
- package/src/skills/review-result/knowledge/test-hygiene.md +44 -0
- package/src/skills/review-result/scripts/verify-artifacts.js +115 -2
- package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003/current/claude-sonnet/trial-1.md +7 -0
- package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003/current/claude-sonnet/trial-2.md +7 -0
- package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003/current/claude-sonnet/trial-3.md +7 -0
- package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003/current/judge.json +163 -0
- package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003/current/kilo-deepseek/trial-1.md +5 -0
- package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003/current/kilo-deepseek/trial-2.md +5 -0
- package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003/current/kilo-deepseek/trial-3.md +11 -0
- package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003/current/kilo-glm/trial-1.md +16 -0
- package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003/current/kilo-glm/trial-2.md +18 -0
- package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003/current/kilo-glm/trial-3.md +17 -0
- package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003/current/kilo-minimax/trial-1.md +17 -0
- package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003/current/kilo-minimax/trial-2.md +31 -0
- package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003/current/kilo-minimax/trial-3.md +5 -0
- package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003/current/meta.json +115 -0
- package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003-test-isolation.yaml +50 -0
- package/src/skills/review-result/tests/fixtures/QA-904-test-isolation-violation/QA-904.md +51 -0
- package/src/skills/review-result/tests/fixtures/QA-904-test-isolation-violation/example-test.mjs +36 -0
- package/src/skills/review-result/tests/index.yaml +5 -0
- package/src/skills/review-result/tests/rubrics/test-isolation.md +20 -0
|
@@ -1,88 +1,88 @@
|
|
|
1
|
-
{
|
|
2
|
-
"date": "2026-04-
|
|
3
|
-
"skill_sha": "1503ea1",
|
|
4
|
-
"status": "passed",
|
|
5
|
-
"duration_ms":
|
|
6
|
-
"per_model": {
|
|
7
|
-
"claude-haiku": {
|
|
8
|
-
"passed": true,
|
|
9
|
-
"errored": false,
|
|
10
|
-
"pass_count": 3,
|
|
11
|
-
"error_count": 0,
|
|
12
|
-
"total": 3,
|
|
13
|
-
"threshold": 2
|
|
14
|
-
},
|
|
15
|
-
"kilo-free": {
|
|
16
|
-
"passed": true,
|
|
17
|
-
"errored": false,
|
|
18
|
-
"pass_count": 3,
|
|
19
|
-
"error_count": 0,
|
|
20
|
-
"total": 3,
|
|
21
|
-
"threshold": 2
|
|
22
|
-
},
|
|
23
|
-
"kilo-glm-air": {
|
|
24
|
-
"passed": true,
|
|
25
|
-
"errored": false,
|
|
26
|
-
"pass_count": 3,
|
|
27
|
-
"error_count": 0,
|
|
28
|
-
"total": 3,
|
|
29
|
-
"threshold": 2
|
|
30
|
-
}
|
|
31
|
-
},
|
|
32
|
-
"rubric_scores": [
|
|
33
|
-
{
|
|
34
|
-
"agentId": "claude-haiku",
|
|
35
|
-
"trial": 1,
|
|
36
|
-
"score": 5,
|
|
37
|
-
"errored": false
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
"agentId": "claude-haiku",
|
|
41
|
-
"trial": 2,
|
|
42
|
-
"score": 5,
|
|
43
|
-
"errored": false
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
"agentId": "claude-haiku",
|
|
47
|
-
"trial": 3,
|
|
48
|
-
"score": 5,
|
|
49
|
-
"errored": false
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
"agentId": "kilo-free",
|
|
53
|
-
"trial": 1,
|
|
54
|
-
"score": 5,
|
|
55
|
-
"errored": false
|
|
56
|
-
},
|
|
57
|
-
{
|
|
58
|
-
"agentId": "kilo-free",
|
|
59
|
-
"trial": 2,
|
|
60
|
-
"score": 5,
|
|
61
|
-
"errored": false
|
|
62
|
-
},
|
|
63
|
-
{
|
|
64
|
-
"agentId": "kilo-free",
|
|
65
|
-
"trial": 3,
|
|
66
|
-
"score": 5,
|
|
67
|
-
"errored": false
|
|
68
|
-
},
|
|
69
|
-
{
|
|
70
|
-
"agentId": "kilo-glm-air",
|
|
71
|
-
"trial": 1,
|
|
72
|
-
"score": 5,
|
|
73
|
-
"errored": false
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
"agentId": "kilo-glm-air",
|
|
77
|
-
"trial": 2,
|
|
78
|
-
"score": 5,
|
|
79
|
-
"errored": false
|
|
80
|
-
},
|
|
81
|
-
{
|
|
82
|
-
"agentId": "kilo-glm-air",
|
|
83
|
-
"trial": 3,
|
|
84
|
-
"score": 5,
|
|
85
|
-
"errored": false
|
|
86
|
-
}
|
|
87
|
-
]
|
|
1
|
+
{
|
|
2
|
+
"date": "2026-04-21T16:43:17.772Z",
|
|
3
|
+
"skill_sha": "1503ea1",
|
|
4
|
+
"status": "passed",
|
|
5
|
+
"duration_ms": 2,
|
|
6
|
+
"per_model": {
|
|
7
|
+
"claude-haiku": {
|
|
8
|
+
"passed": true,
|
|
9
|
+
"errored": false,
|
|
10
|
+
"pass_count": 3,
|
|
11
|
+
"error_count": 0,
|
|
12
|
+
"total": 3,
|
|
13
|
+
"threshold": 2
|
|
14
|
+
},
|
|
15
|
+
"kilo-free": {
|
|
16
|
+
"passed": true,
|
|
17
|
+
"errored": false,
|
|
18
|
+
"pass_count": 3,
|
|
19
|
+
"error_count": 0,
|
|
20
|
+
"total": 3,
|
|
21
|
+
"threshold": 2
|
|
22
|
+
},
|
|
23
|
+
"kilo-glm-air": {
|
|
24
|
+
"passed": true,
|
|
25
|
+
"errored": false,
|
|
26
|
+
"pass_count": 3,
|
|
27
|
+
"error_count": 0,
|
|
28
|
+
"total": 3,
|
|
29
|
+
"threshold": 2
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"rubric_scores": [
|
|
33
|
+
{
|
|
34
|
+
"agentId": "claude-haiku",
|
|
35
|
+
"trial": 1,
|
|
36
|
+
"score": 5,
|
|
37
|
+
"errored": false
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"agentId": "claude-haiku",
|
|
41
|
+
"trial": 2,
|
|
42
|
+
"score": 5,
|
|
43
|
+
"errored": false
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"agentId": "claude-haiku",
|
|
47
|
+
"trial": 3,
|
|
48
|
+
"score": 5,
|
|
49
|
+
"errored": false
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"agentId": "kilo-free",
|
|
53
|
+
"trial": 1,
|
|
54
|
+
"score": 5,
|
|
55
|
+
"errored": false
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"agentId": "kilo-free",
|
|
59
|
+
"trial": 2,
|
|
60
|
+
"score": 5,
|
|
61
|
+
"errored": false
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"agentId": "kilo-free",
|
|
65
|
+
"trial": 3,
|
|
66
|
+
"score": 5,
|
|
67
|
+
"errored": false
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"agentId": "kilo-glm-air",
|
|
71
|
+
"trial": 1,
|
|
72
|
+
"score": 5,
|
|
73
|
+
"errored": false
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"agentId": "kilo-glm-air",
|
|
77
|
+
"trial": 2,
|
|
78
|
+
"score": 5,
|
|
79
|
+
"errored": false
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"agentId": "kilo-glm-air",
|
|
83
|
+
"trial": 3,
|
|
84
|
+
"score": 5,
|
|
85
|
+
"errored": false
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
88
|
}
|
|
@@ -35,6 +35,7 @@ description: >
|
|
|
35
35
|
| Модуль | Когда загружать |
|
|
36
36
|
|--------|----------------|
|
|
37
37
|
| `knowledge/dod-patterns.md` | При определении типа проверки для конкретного пункта DoD |
|
|
38
|
+
| `knowledge/test-hygiene.md` | Когда DoD требует создать/изменить автотест — красные и зелёные флаги изоляции, что считать нарушением и как оформить issue |
|
|
38
39
|
| `../shared/*` | **ВСЕГДА** перед началом работы — общие знания проекта |
|
|
39
40
|
|
|
40
41
|
## Загрузка шаблонов
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Проверка изоляции тестов на ревью
|
|
2
|
+
|
|
3
|
+
Когда DoD тикета требует создать/изменить автотест, ревьюер **отдельно** проверяет, что тест не пишет артефакты в рабочий репозиторий или в общие системные пути. Это не дублирует обычный «тесты проходят» — проверка изоляции ортогональна результату прогона (зелёный тест всё равно может быть токсичным).
|
|
4
|
+
|
|
5
|
+
## Зачем ревьюеру проверять это отдельно
|
|
6
|
+
|
|
7
|
+
Тест, который проходит в чистом прогоне, но пишет в рабочий репозиторий, создаёт отложенные инциденты:
|
|
8
|
+
- в CI / на второй машине / при параллельном запуске тесты ложно падают или ложно проходят;
|
|
9
|
+
- при прерывании (Ctrl+C, таймаут) оставленный state подхватывается следующим прогоном как валидный;
|
|
10
|
+
- затираются файлы, нужные параллельным агентам/пайплайну;
|
|
11
|
+
- ложно срабатывают механические проверки (verify-artifacts, git-snapshot).
|
|
12
|
+
|
|
13
|
+
Поэтому «зелёный прогон» — необходимый, но не достаточный сигнал. Нужна **отдельная** проверка изоляции.
|
|
14
|
+
|
|
15
|
+
## Что читать
|
|
16
|
+
|
|
17
|
+
1. Файл(ы) теста, затронутые тикетом.
|
|
18
|
+
2. Соседние test-файлы в той же директории — какой паттерн изоляции уже принят в проекте.
|
|
19
|
+
|
|
20
|
+
## Красные флаги при чтении теста
|
|
21
|
+
|
|
22
|
+
- Путь к тест-артефакту строится от корня репозитория или CWD (любая конструкция «директория исходника + подъём к корню», «текущий рабочий каталог», «заранее заданный projectRoot») **без** префикса стандартного системного tmp-корня языка.
|
|
23
|
+
- `cleanup`-функция удаляет конкретный файл, а не весь tmp-корень. При прерывании до её вызова файл останется в рабочем репозитории.
|
|
24
|
+
- Cleanup — в теле test-функции, а не в teardown/after-hook. Падение assertion до cleanup-вызова = утечка state.
|
|
25
|
+
- Тест модифицирует реальный файл в config-, data-, state-, cache- или иных рабочих каталогах проекта внутри корня репозитория.
|
|
26
|
+
- Глобальные константы теста указывают на рабочий репозиторий и используются более чем в одном тест-кейсе (shared mutable state).
|
|
27
|
+
|
|
28
|
+
## Зелёные сигналы
|
|
29
|
+
|
|
30
|
+
- Корень артефактов создаётся через стандартную tmp-функцию стандартной библиотеки языка (исполнитель выбирает конкретную по языку проекта).
|
|
31
|
+
- Cleanup удаляет **весь корень** рекурсивно и идемпотентен (можно вызвать повторно без ошибки).
|
|
32
|
+
- Cleanup в teardown/after-hook, гарантированно выполняется при любом исходе теста.
|
|
33
|
+
- Каждый тест получает свой свежий корень — нет разделяемого изменяемого состояния между тестами.
|
|
34
|
+
|
|
35
|
+
## Что делать при обнаружении нарушения
|
|
36
|
+
|
|
37
|
+
- Если нарушена изоляция — `failed` с конкретным issue: какая строка файла указывает на реальный каталог проекта, почему cleanup не защищает от прерывания.
|
|
38
|
+
- Не смягчай finding на основании того, что тест «проходит» — проверка изоляции ортогональна прогону.
|
|
39
|
+
- В issue — процитировать фрагмент теста дословно (путь до корня проекта, функция очистки), а не пересказать.
|
|
40
|
+
|
|
41
|
+
## Когда проверка не применяется
|
|
42
|
+
|
|
43
|
+
- Тикет явно просит тест-генератор, который коммитит файлы в репозиторий как артефакт. В этом случае в DoD должно быть явное обоснование.
|
|
44
|
+
- Тест read-only: не создаёт и не модифицирует файлы (только reads).
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
|
|
28
28
|
import fs from 'fs';
|
|
29
29
|
import path from 'path';
|
|
30
|
+
import { pathToFileURL } from 'url';
|
|
30
31
|
import { findProjectRoot } from 'workflow-ai/lib/find-root.mjs';
|
|
31
32
|
import { parseFrontmatter } from 'workflow-ai/lib/utils.mjs';
|
|
32
33
|
|
|
@@ -157,6 +158,98 @@ function checkResultSection(body) {
|
|
|
157
158
|
};
|
|
158
159
|
}
|
|
159
160
|
|
|
161
|
+
/**
|
|
162
|
+
* E2E assertion против ghost-execution: парсит секцию `### Implementation assertions`
|
|
163
|
+
* в теле тикета и возвращает список проверок вида
|
|
164
|
+
* { module, export, method?, type? }
|
|
165
|
+
*
|
|
166
|
+
* Формат каждой строки-bullet'а (значения в backticks, ключи через запятую):
|
|
167
|
+
* - module: `path/to/file.mjs`, export: `ClassName`, method: `methodName`
|
|
168
|
+
* - module: `path/to/file.mjs`, export: `namedExport`
|
|
169
|
+
* - module: `path/to/file.mjs`, export: `helper`, type: `function`
|
|
170
|
+
*
|
|
171
|
+
* Секция необязательна. Если её нет — возвращаем пустой массив
|
|
172
|
+
* (тикеты без исполняемых артефактов — например, DOCS/HUMAN — проходят без гейта).
|
|
173
|
+
*/
|
|
174
|
+
function parseImplementationAssertions(body) {
|
|
175
|
+
const headerRegex = /^###\s*(?:Implementation assertions|E2E assertions)\s*$/m;
|
|
176
|
+
const match = headerRegex.exec(body);
|
|
177
|
+
if (!match) return [];
|
|
178
|
+
|
|
179
|
+
const startIdx = match.index + match[0].length;
|
|
180
|
+
const nextH3 = body.indexOf('\n### ', startIdx);
|
|
181
|
+
const nextH2 = body.indexOf('\n## ', startIdx);
|
|
182
|
+
const candidates = [nextH3, nextH2].filter(i => i !== -1);
|
|
183
|
+
const sectionEnd = candidates.length > 0 ? Math.min(...candidates) : body.length;
|
|
184
|
+
const sectionContent = body.substring(startIdx, sectionEnd);
|
|
185
|
+
|
|
186
|
+
const assertions = [];
|
|
187
|
+
const bulletRegex = /^[-*]\s+(.+)$/gm;
|
|
188
|
+
let bulletMatch;
|
|
189
|
+
while ((bulletMatch = bulletRegex.exec(sectionContent)) !== null) {
|
|
190
|
+
const line = bulletMatch[1];
|
|
191
|
+
const kv = {};
|
|
192
|
+
const pairRegex = /(\w+)\s*:\s*`([^`]+)`/g;
|
|
193
|
+
let pair;
|
|
194
|
+
while ((pair = pairRegex.exec(line)) !== null) {
|
|
195
|
+
kv[pair[1]] = pair[2];
|
|
196
|
+
}
|
|
197
|
+
if (kv.module && kv.export) {
|
|
198
|
+
assertions.push({
|
|
199
|
+
module: kv.module,
|
|
200
|
+
export: kv.export,
|
|
201
|
+
method: kv.method || null,
|
|
202
|
+
type: kv.type || 'function',
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return assertions;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Выполняет список implementation assertions против живого кода.
|
|
211
|
+
* Для каждой: dynamic import модуля → достаём export → проверяем method/type.
|
|
212
|
+
* Возвращает массив { ...assertion, ok, reason? }.
|
|
213
|
+
*/
|
|
214
|
+
async function runImplementationAssertions(assertions) {
|
|
215
|
+
const results = [];
|
|
216
|
+
for (const a of assertions) {
|
|
217
|
+
const absPath = path.isAbsolute(a.module) ? a.module : path.join(PROJECT_DIR, a.module);
|
|
218
|
+
if (!fs.existsSync(absPath)) {
|
|
219
|
+
results.push({ ...a, ok: false, reason: `module_not_found: ${a.module}` });
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
let mod;
|
|
223
|
+
try {
|
|
224
|
+
mod = await import(pathToFileURL(absPath).href);
|
|
225
|
+
} catch (err) {
|
|
226
|
+
results.push({ ...a, ok: false, reason: `import_failed: ${err.message.replace(/\n/g, ' ')}` });
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
const exported = mod[a.export];
|
|
230
|
+
if (exported === undefined) {
|
|
231
|
+
results.push({ ...a, ok: false, reason: `export_not_found: ${a.export}` });
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (a.method) {
|
|
235
|
+
// Класс → метод на prototype; объект → метод как свойство
|
|
236
|
+
const target = typeof exported === 'function' ? exported.prototype : exported;
|
|
237
|
+
const fn = target ? target[a.method] : undefined;
|
|
238
|
+
if (typeof fn !== 'function') {
|
|
239
|
+
results.push({ ...a, ok: false, reason: `method_not_function: ${a.export}.${a.method}` });
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
} else {
|
|
243
|
+
if (typeof exported !== a.type) {
|
|
244
|
+
results.push({ ...a, ok: false, reason: `type_mismatch: expected ${a.type}, got ${typeof exported}` });
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
results.push({ ...a, ok: true });
|
|
249
|
+
}
|
|
250
|
+
return results;
|
|
251
|
+
}
|
|
252
|
+
|
|
160
253
|
function verifyTicket(ticketPath) {
|
|
161
254
|
if (!fs.existsSync(ticketPath)) {
|
|
162
255
|
throw new Error(`Ticket file not found: ${ticketPath}`);
|
|
@@ -177,6 +270,8 @@ function verifyTicket(ticketPath) {
|
|
|
177
270
|
|
|
178
271
|
const resultStats = checkResultSection(body);
|
|
179
272
|
|
|
273
|
+
const assertions = parseImplementationAssertions(body);
|
|
274
|
+
|
|
180
275
|
return {
|
|
181
276
|
ticket_id: frontmatter.id,
|
|
182
277
|
created_at: frontmatter.created_at,
|
|
@@ -185,7 +280,8 @@ function verifyTicket(ticketPath) {
|
|
|
185
280
|
dod_checked: dodStats.checked,
|
|
186
281
|
dod_completed: dodStats.completed,
|
|
187
282
|
result_exists: resultStats.exists,
|
|
188
|
-
result_filled: resultStats.summaryFilled
|
|
283
|
+
result_filled: resultStats.summaryFilled,
|
|
284
|
+
assertions,
|
|
189
285
|
};
|
|
190
286
|
}
|
|
191
287
|
|
|
@@ -252,6 +348,16 @@ function formatVerdict(result) {
|
|
|
252
348
|
);
|
|
253
349
|
}
|
|
254
350
|
|
|
351
|
+
const assertionFailures = (result.assertionResults || []).filter(r => !r.ok);
|
|
352
|
+
if (assertionFailures.length > 0) {
|
|
353
|
+
const descriptions = assertionFailures.map(r => {
|
|
354
|
+
const target = r.method ? `${r.export}.${r.method}` : r.export;
|
|
355
|
+
return `${target} в ${r.module} (${r.reason})`;
|
|
356
|
+
});
|
|
357
|
+
failReasons.push(`assertion_failed=${descriptions.length}`);
|
|
358
|
+
humanIssues.push(`не прошли E2E-assertions (ghost execution): ${descriptions.join('; ')}`);
|
|
359
|
+
}
|
|
360
|
+
|
|
255
361
|
const status = failReasons.length === 0 ? 'passed' : 'failed';
|
|
256
362
|
|
|
257
363
|
return { status, missingFiles, unchangedFiles, failReasons, humanIssues };
|
|
@@ -317,7 +423,7 @@ function appendReviewNote(ticketPath, humanIssues) {
|
|
|
317
423
|
return true;
|
|
318
424
|
}
|
|
319
425
|
|
|
320
|
-
function main() {
|
|
426
|
+
async function main() {
|
|
321
427
|
const args = process.argv.slice(2);
|
|
322
428
|
|
|
323
429
|
if (args.length === 0) {
|
|
@@ -349,6 +455,8 @@ function main() {
|
|
|
349
455
|
|
|
350
456
|
try {
|
|
351
457
|
const result = verifyTicket(ticketPath);
|
|
458
|
+
result.assertionResults = await runImplementationAssertions(result.assertions || []);
|
|
459
|
+
|
|
352
460
|
const verdict = formatVerdict(result);
|
|
353
461
|
|
|
354
462
|
let reviewNoteWritten = false;
|
|
@@ -356,6 +464,9 @@ function main() {
|
|
|
356
464
|
reviewNoteWritten = appendReviewNote(ticketPath, verdict.humanIssues);
|
|
357
465
|
}
|
|
358
466
|
|
|
467
|
+
const assertionsTotal = result.assertionResults.length;
|
|
468
|
+
const assertionsFailed = result.assertionResults.filter(r => !r.ok).length;
|
|
469
|
+
|
|
359
470
|
console.log('---RESULT---');
|
|
360
471
|
console.log(`status: ${verdict.status}`);
|
|
361
472
|
console.log(`ticket_id: ${result.ticket_id || ''}`);
|
|
@@ -365,6 +476,8 @@ function main() {
|
|
|
365
476
|
console.log(`result_filled: ${result.result_filled}`);
|
|
366
477
|
console.log(`missing_files: ${verdict.missingFiles.join(',')}`);
|
|
367
478
|
console.log(`unchanged_files: ${verdict.unchangedFiles.join(',')}`);
|
|
479
|
+
console.log(`assertions_total: ${assertionsTotal}`);
|
|
480
|
+
console.log(`assertions_failed: ${assertionsFailed}`);
|
|
368
481
|
if (verdict.failReasons.length > 0) {
|
|
369
482
|
console.log(`fail_reasons: ${verdict.failReasons.join('; ')}`);
|
|
370
483
|
console.log(`issues: ${verdict.humanIssues.join('; ')}`);
|
package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003/current/claude-sonnet/trial-1.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---RESULT---
|
|
2
|
+
status: failed
|
|
3
|
+
issues:
|
|
4
|
+
- "Пункт DoD #3 не выполнен: тест пишет в рабочий репозиторий — `const testProjectRoot = join(__dirname, '..', '..')` (example-test.mjs:17) строит корень артефактов от __dirname внутри репозитория, а не от системного tmp-корня"
|
|
5
|
+
- "Пункт DoD #3 не выполнен: cleanup удаляет конкретный файл `rmSync(testConfigPath)` (example-test.mjs:32), а не весь tmp-корень — при прерывании до cleanup файл `.workflow/config/sample.yaml` остаётся в рабочем репозитории"
|
|
6
|
+
- "Пункт DoD #3 не выполнен: cleanup вызывается в теле test-функции (example-test.mjs:35), а не в teardown/after-hook — падение assertion до cleanup = утечка state"
|
|
7
|
+
---RESULT---
|
package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003/current/claude-sonnet/trial-2.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---RESULT---
|
|
2
|
+
status: failed
|
|
3
|
+
issues:
|
|
4
|
+
- "DoD пункт 3 не выполнен: тест пишет в рабочий репозиторий, а не в системный tmp-корень. Строка `const testProjectRoot = join(__dirname, '..', '..')` привязывает корень артефактов к реальному корню проекта; запись идёт в `.workflow/config/sample.yaml` внутри рабочего репозитория. Ожидалось: использование `os.tmpdir()` / `fs.mkdtempSync` как корня."
|
|
5
|
+
- "DoD пункт 3 не выполнен: cleanup (`rmSync(testConfigPath)`) удаляет конкретный файл, а не весь tmp-корень рекурсивно. При прерывании до вызова cleanup файл останется в рабочем репозитории."
|
|
6
|
+
- "DoD пункт 3 не выполнен: `cleanupTestConfig()` вызывается в теле тест-функции, а не в teardown/after-hook. Падение `assert` до этой строки — cleanup не выполняется, state утекает в репозиторий."
|
|
7
|
+
---RESULT---
|
package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003/current/claude-sonnet/trial-3.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---RESULT---
|
|
2
|
+
status: failed
|
|
3
|
+
issues:
|
|
4
|
+
- "Пункт DoD 3 не выполнен: `const testProjectRoot = join(__dirname, '..', '..')` привязывает корень артефактов к реальному корню репозитория, а не к системному tmp. Тест пишет в `.workflow/config/sample.yaml` рабочего репозитория (example-test.mjs:18-21)."
|
|
5
|
+
- "Пункт DoD 3 не выполнен: cleanup (`rmSync(testConfigPath)`) удаляет конкретный файл, а не весь tmp-корень — при прерывании до вызова файл остаётся в рабочем репозитории (example-test.mjs:24-26)."
|
|
6
|
+
- "Пункт DoD 3 не выполнен: cleanup вызывается в теле test-функции, а не в teardown/after-hook — падение assertion до cleanup = утечка state в рабочем репозитории (example-test.mjs:29-34)."
|
|
7
|
+
---RESULT---
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
{
|
|
2
|
+
"per_model": {
|
|
3
|
+
"claude-sonnet": {
|
|
4
|
+
"pass_count": 3,
|
|
5
|
+
"total": 3,
|
|
6
|
+
"trials": [
|
|
7
|
+
{
|
|
8
|
+
"trial": 1,
|
|
9
|
+
"score": 5,
|
|
10
|
+
"passed": true
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"trial": 2,
|
|
14
|
+
"score": 5,
|
|
15
|
+
"passed": true
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"trial": 3,
|
|
19
|
+
"score": 4,
|
|
20
|
+
"passed": true
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
"kilo-glm": {
|
|
25
|
+
"pass_count": 3,
|
|
26
|
+
"total": 3,
|
|
27
|
+
"trials": [
|
|
28
|
+
{
|
|
29
|
+
"trial": 1,
|
|
30
|
+
"score": 4,
|
|
31
|
+
"passed": true
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"trial": 2,
|
|
35
|
+
"score": 4,
|
|
36
|
+
"passed": true
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"trial": 3,
|
|
40
|
+
"score": 4,
|
|
41
|
+
"passed": true
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
},
|
|
45
|
+
"kilo-minimax": {
|
|
46
|
+
"pass_count": 3,
|
|
47
|
+
"total": 3,
|
|
48
|
+
"trials": [
|
|
49
|
+
{
|
|
50
|
+
"trial": 1,
|
|
51
|
+
"score": 4,
|
|
52
|
+
"passed": true
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"trial": 2,
|
|
56
|
+
"score": 4,
|
|
57
|
+
"passed": true
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"trial": 3,
|
|
61
|
+
"score": 4,
|
|
62
|
+
"passed": true
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
},
|
|
66
|
+
"kilo-deepseek": {
|
|
67
|
+
"pass_count": 2,
|
|
68
|
+
"total": 3,
|
|
69
|
+
"trials": [
|
|
70
|
+
{
|
|
71
|
+
"trial": 1,
|
|
72
|
+
"score": 3,
|
|
73
|
+
"passed": false
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"trial": 2,
|
|
77
|
+
"score": 4,
|
|
78
|
+
"passed": true
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"trial": 3,
|
|
82
|
+
"score": 4,
|
|
83
|
+
"passed": true
|
|
84
|
+
}
|
|
85
|
+
]
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
"rubric_scores": [
|
|
89
|
+
{
|
|
90
|
+
"agentId": "claude-sonnet",
|
|
91
|
+
"trial": 1,
|
|
92
|
+
"score": 5,
|
|
93
|
+
"errored": false
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"agentId": "claude-sonnet",
|
|
97
|
+
"trial": 2,
|
|
98
|
+
"score": 5,
|
|
99
|
+
"errored": false
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"agentId": "claude-sonnet",
|
|
103
|
+
"trial": 3,
|
|
104
|
+
"score": 4,
|
|
105
|
+
"errored": false
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"agentId": "kilo-deepseek",
|
|
109
|
+
"trial": 1,
|
|
110
|
+
"score": 3,
|
|
111
|
+
"errored": false
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"agentId": "kilo-deepseek",
|
|
115
|
+
"trial": 2,
|
|
116
|
+
"score": 4,
|
|
117
|
+
"errored": false
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"agentId": "kilo-deepseek",
|
|
121
|
+
"trial": 3,
|
|
122
|
+
"score": 4,
|
|
123
|
+
"errored": false
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"agentId": "kilo-glm",
|
|
127
|
+
"trial": 1,
|
|
128
|
+
"score": 4,
|
|
129
|
+
"errored": false
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"agentId": "kilo-glm",
|
|
133
|
+
"trial": 2,
|
|
134
|
+
"score": 4,
|
|
135
|
+
"errored": false
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
"agentId": "kilo-glm",
|
|
139
|
+
"trial": 3,
|
|
140
|
+
"score": 4,
|
|
141
|
+
"errored": false
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"agentId": "kilo-minimax",
|
|
145
|
+
"trial": 1,
|
|
146
|
+
"score": 4,
|
|
147
|
+
"errored": false
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
"agentId": "kilo-minimax",
|
|
151
|
+
"trial": 2,
|
|
152
|
+
"score": 4,
|
|
153
|
+
"errored": false
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
"agentId": "kilo-minimax",
|
|
157
|
+
"trial": 3,
|
|
158
|
+
"score": 4,
|
|
159
|
+
"errored": false
|
|
160
|
+
}
|
|
161
|
+
],
|
|
162
|
+
"timestamp": "2026-04-21T14:36:41.299Z"
|
|
163
|
+
}
|
package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003/current/kilo-deepseek/trial-2.md
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
---RESULT---
|
|
2
|
+
status: failed
|
|
3
|
+
issues:
|
|
4
|
+
- "Пункт DoD 3 не выполнен: ожидалось создание tmp-корня и изоляция в teardown/after-hook, получено: корень артефактов привязан к реальному корню репозитория (строка `const testProjectRoot = join(__dirname, '..', '..');`), cleanup удаляет конкретный файл, cleanup вызывается в теле test-функции"
|
|
5
|
+
---RESULT---
|
package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003/current/kilo-deepseek/trial-3.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Проверка тикета QA-904 выявила следующие проблемы:
|
|
2
|
+
|
|
3
|
+
1. **Несоответствие путей** — в DoD указан файл `tests/fixtures/QA‑904‑test‑isolation‑violation/example‑test.mjs`, но фактический путь `.workflow/src/skills/review‑result/tests/fixtures/QA‑904‑test‑isolation‑violation/example‑test.mjs`.
|
|
4
|
+
2. **Нарушение изоляции теста** — тест пишет артефакты в рабочий репозиторий (строка `const testProjectRoot = join(__dirname, '..', '..');`), cleanup удаляет только конкретный файл, а не весь tmp‑корень, и находится в теле теста, а не в teardown/after‑hook.
|
|
5
|
+
|
|
6
|
+
---RESULT---
|
|
7
|
+
status: failed
|
|
8
|
+
issues:
|
|
9
|
+
- "Пункт DoD #1 не выполнен: ожидался файл tests/fixtures/QA-904-test-isolation-violation/example-test.mjs, фактический путь .workflow/src/skills/review-result/tests/fixtures/QA-904-test-isolation-violation/example-test.mjs"
|
|
10
|
+
- "Пункт DoD #3 не выполнен: тест нарушает изоляцию — корень артефактов привязан к рабочему репозиторию (строка 10), cleanup удаляет конкретный файл, а не tmp-корень, cleanup в теле теста, а не в teardown/after-hook"
|
|
11
|
+
---RESULT---
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Вердикт: **failed**.
|
|
2
|
+
|
|
3
|
+
Проверка DoD-критериев:
|
|
4
|
+
|
|
5
|
+
1. **Файл создан** — подтверждено.
|
|
6
|
+
2. **Тест проходит** — принято по условию.
|
|
7
|
+
3. **Изоляция** — **нарушена** по трём причинам:
|
|
8
|
+
- `testProjectRoot` указывает на корень репозитория (`join(__dirname, '..', '..')`), а не на системный tmp-каталог.
|
|
9
|
+
- `writeFileSync(testConfigPath, ...)` пишет в `.workflow/config/sample.yaml` внутри рабочего репозитория.
|
|
10
|
+
- `cleanupTestConfig()` вызывается в теле test-функции, а не в `after()`/`teardown`; при падении assertion файл остаётся в репозитории.
|
|
11
|
+
|
|
12
|
+
---RESULT---
|
|
13
|
+
status: failed
|
|
14
|
+
issues:
|
|
15
|
+
- "Пункт DoD 3 не выполнен: тест пишет в рабочий репозиторий (testProjectRoot = join(__dirname, '..', '..') → корень репозитория), а не в tmp-корень; cleanup вызывается в теле test-функции, а не в teardown/after-hook; при падении assertion артефакт остаётся в репозитории"
|
|
16
|
+
---RESULT---
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Тикет прочитан. Три критерия DoD. Критерий 3 (изоляция) нарушен — анализ ниже.
|
|
2
|
+
|
|
3
|
+
**DoD-проверка:**
|
|
4
|
+
|
|
5
|
+
1. **Файл создан** — подтверждено, файл существует.
|
|
6
|
+
2. **Тест проходит** — формально `node --test` success, принимается.
|
|
7
|
+
3. **Изоляция** — НАРУШЕНА:
|
|
8
|
+
- `testProjectRoot = join(__dirname, '..', '..')` — указывает на корень репозитория, а не на системный `tmp`.
|
|
9
|
+
- `createTestConfig` пишет в `.workflow/config/sample.yaml` внутри рабочего репозитория.
|
|
10
|
+
- `cleanupTestConfig` удаляет конкретный файл, не tmp-корень целиком.
|
|
11
|
+
- Cleanup вызывается в теле test-функции, а не в `teardown`/`after`-хуке — при падении assertion до строки cleanup файл остаётся в репозитории.
|
|
12
|
+
|
|
13
|
+
Вердикт: **failed**.
|
|
14
|
+
---RESULT---
|
|
15
|
+
status: failed
|
|
16
|
+
issues:
|
|
17
|
+
- "Пункт DoD 3 не выполнен: ожидалось что тест создаёт и уничтожает tmp-корень, не пишет в рабочий репозиторий (изоляция в teardown/after-hook); получено: testProjectRoot привязан к реальному корню репозитория (join(__dirname, '..', '..')), createTestConfig пишет в .workflow/config/sample.yaml рабочего репозитория, cleanupTestConfig удаляет конкретный файл а не tmp-корень, cleanup вызывается в теле test-функции а не в teardown — файл: src/skills/review-result/tests/fixtures/QA-904-test-isolation-violation/example-test.mjs:12-22"
|
|
18
|
+
---RESULT---
|