@cat-factory/app 0.241.2 → 0.242.0
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/app/components/outcome/OutcomeSummaryWindow.vue +59 -45
- package/app/composables/api/spec.ts +14 -4
- package/app/docs/consumer-extensions.md +8 -0
- package/app/stores/serviceSpec.spec.ts +81 -0
- package/app/stores/serviceSpec.ts +55 -25
- package/app/utils/catalog.ts +7 -6
- package/app/utils/runOutcome.ts +38 -507
- package/i18n/locales/de.json +5 -6
- package/i18n/locales/en.json +5 -6
- package/i18n/locales/es.json +5 -6
- package/i18n/locales/fr.json +5 -6
- package/i18n/locales/he.json +5 -6
- package/i18n/locales/it.json +5 -6
- package/i18n/locales/ja.json +5 -6
- package/i18n/locales/pl.json +5 -6
- package/i18n/locales/tr.json +5 -6
- package/i18n/locales/uk.json +5 -6
- package/package.json +2 -2
- package/app/utils/runOutcome.spec.ts +0 -492
package/i18n/locales/he.json
CHANGED
|
@@ -5575,11 +5575,8 @@
|
|
|
5575
5575
|
"notCovered": "לא נבדקו: {count}",
|
|
5576
5576
|
"regressions": "נסיגות: {count}",
|
|
5577
5577
|
"regressionTag": "נסיגה",
|
|
5578
|
-
"idOnly": "מזהה בלבד",
|
|
5579
|
-
"idOnlyHint": "המזהה הזה לא נמצא במפרט השירות, ולכן אין כותרת להציג עבורו.",
|
|
5580
5578
|
"spec": {
|
|
5581
|
-
"not_read": "מפרט השירות לא נקרא, ולכן
|
|
5582
|
-
"unmatched": "אף אחד ממזהי הדרישות האלה לא מופיע במפרט השירות שנקרא, ולכן מוצגים רק המזהים שהבודק דיווח עליהם."
|
|
5579
|
+
"not_read": "מפרט השירות לא נקרא, ולכן הספירות האלה מכסות רק את מה שהבודק הכריע לגביו, לפי מזהה."
|
|
5583
5580
|
},
|
|
5584
5581
|
"verdict": {
|
|
5585
5582
|
"met": "אומת",
|
|
@@ -5589,8 +5586,10 @@
|
|
|
5589
5586
|
"gap": {
|
|
5590
5587
|
"no_tester_step": "בצינור הזה אין שלב בדיקות, ולכן שום דרישה לא נבדקה.",
|
|
5591
5588
|
"tester_not_reported": "הבודק עדיין לא דיווח, ולכן לא הוכרעה שום דרישה.",
|
|
5592
|
-
"no_verdicts": "הבודק דיווח בלי להכריע לגבי אף דרישה במפרט."
|
|
5593
|
-
|
|
5589
|
+
"no_verdicts": "הבודק דיווח בלי להכריע לגבי אף דרישה במפרט.",
|
|
5590
|
+
"no_requirements": "במפרט השירות לא רשומות דרישות, ולכן לא היה לבודק על מה להכריע."
|
|
5591
|
+
},
|
|
5592
|
+
"unmatchedVerdicts": "הבודק הכריע גם לגבי {count} מזהים שאינם קיימים במפרט הזה, ולכן הספירה שלו גבוהה מהמספרים שלמעלה."
|
|
5594
5593
|
},
|
|
5595
5594
|
"tests": {
|
|
5596
5595
|
"title": "איך זה נבדק",
|
package/i18n/locales/it.json
CHANGED
|
@@ -6131,11 +6131,8 @@
|
|
|
6131
6131
|
"notCovered": "Non verificati: {count}",
|
|
6132
6132
|
"regressions": "Regressioni: {count}",
|
|
6133
6133
|
"regressionTag": "Regressione",
|
|
6134
|
-
"idOnly": "solo id",
|
|
6135
|
-
"idOnlyHint": "Questo identificatore non è nella specifica del servizio, quindi non c'è alcun titolo da mostrare.",
|
|
6136
6134
|
"spec": {
|
|
6137
|
-
"not_read": "La specifica del servizio non è stata letta, quindi questi
|
|
6138
|
-
"unmatched": "Nessuno di questi identificatori di requisito compare nella specifica del servizio che è stata letta, quindi vengono mostrati solo gli identificatori riportati dal tester."
|
|
6135
|
+
"not_read": "La specifica del servizio non è stata letta, quindi questi conteggi coprono solo ciò su cui il tester si è pronunciato, per identificatore."
|
|
6139
6136
|
},
|
|
6140
6137
|
"verdict": {
|
|
6141
6138
|
"met": "Verificato",
|
|
@@ -6145,8 +6142,10 @@
|
|
|
6145
6142
|
"gap": {
|
|
6146
6143
|
"no_tester_step": "Questa pipeline non ha un passo di test, quindi non è stato verificato alcun requisito.",
|
|
6147
6144
|
"tester_not_reported": "Il tester non ha ancora riportato nulla, quindi nessun requisito è stato deciso.",
|
|
6148
|
-
"no_verdicts": "Il tester ha riportato senza pronunciarsi su alcun requisito della specifica."
|
|
6149
|
-
|
|
6145
|
+
"no_verdicts": "Il tester ha riportato senza pronunciarsi su alcun requisito della specifica.",
|
|
6146
|
+
"no_requirements": "La specifica del servizio non registra alcun requisito, quindi non c'era nulla su cui il tester potesse pronunciarsi."
|
|
6147
|
+
},
|
|
6148
|
+
"unmatchedVerdicts": "Il tester si è pronunciato anche su {count} identificatore/i che questa specifica non contiene, quindi il suo conteggio è più alto di quelli sopra."
|
|
6150
6149
|
},
|
|
6151
6150
|
"tests": {
|
|
6152
6151
|
"title": "Come è stato testato",
|
package/i18n/locales/ja.json
CHANGED
|
@@ -5575,11 +5575,8 @@
|
|
|
5575
5575
|
"notCovered": "未確認: {count}",
|
|
5576
5576
|
"regressions": "リグレッション: {count}",
|
|
5577
5577
|
"regressionTag": "リグレッション",
|
|
5578
|
-
"idOnly": "ID のみ",
|
|
5579
|
-
"idOnlyHint": "この ID はサービス仕様に含まれていないため、表示できる名称がありません。",
|
|
5580
5578
|
"spec": {
|
|
5581
|
-
"not_read": "
|
|
5582
|
-
"unmatched": "読み取ったサービス仕様には、これらの要件 ID がひとつも含まれていません。そのため、テスターが報告した ID のみを表示しています。"
|
|
5579
|
+
"not_read": "サービス仕様を読み取れなかったため、これらの集計はテスターが判定した分だけを ID 単位で数えています。"
|
|
5583
5580
|
},
|
|
5584
5581
|
"verdict": {
|
|
5585
5582
|
"met": "確認済み",
|
|
@@ -5589,8 +5586,10 @@
|
|
|
5589
5586
|
"gap": {
|
|
5590
5587
|
"no_tester_step": "このパイプラインにはテスト工程がないため、要件は確認されていません。",
|
|
5591
5588
|
"tester_not_reported": "テスターがまだ報告していないため、どの要件も判定されていません。",
|
|
5592
|
-
"no_verdicts": "テスターは報告しましたが、仕様のいずれの要件についても判定していません。"
|
|
5593
|
-
|
|
5589
|
+
"no_verdicts": "テスターは報告しましたが、仕様のいずれの要件についても判定していません。",
|
|
5590
|
+
"no_requirements": "サービス仕様に要件が記録されていないため、テスターが判定するものはありませんでした。"
|
|
5591
|
+
},
|
|
5592
|
+
"unmatchedVerdicts": "テスターはこの仕様に存在しない ID を {count} 件も判定しているため、テスター自身の集計は上の件数より多くなります。"
|
|
5594
5593
|
},
|
|
5595
5594
|
"tests": {
|
|
5596
5595
|
"title": "テストの内容",
|
package/i18n/locales/pl.json
CHANGED
|
@@ -5575,11 +5575,8 @@
|
|
|
5575
5575
|
"notCovered": "Niesprawdzone: {count}",
|
|
5576
5576
|
"regressions": "Regresje: {count}",
|
|
5577
5577
|
"regressionTag": "Regresja",
|
|
5578
|
-
"idOnly": "sam identyfikator",
|
|
5579
|
-
"idOnlyHint": "Tego identyfikatora nie ma w specyfikacji usługi, więc nie ma nazwy do pokazania.",
|
|
5580
5578
|
"spec": {
|
|
5581
|
-
"not_read": "Specyfikacja usługi nie została odczytana, więc
|
|
5582
|
-
"unmatched": "Żaden z tych identyfikatorów wymagań nie występuje w odczytanej specyfikacji usługi, więc pokazane są tylko identyfikatory zgłoszone przez testera."
|
|
5579
|
+
"not_read": "Specyfikacja usługi nie została odczytana, więc te liczby obejmują tylko to, co tester rozstrzygnął, według identyfikatora."
|
|
5583
5580
|
},
|
|
5584
5581
|
"verdict": {
|
|
5585
5582
|
"met": "Potwierdzone",
|
|
@@ -5589,8 +5586,10 @@
|
|
|
5589
5586
|
"gap": {
|
|
5590
5587
|
"no_tester_step": "Ten potok nie ma kroku testowego, więc nie sprawdzono żadnego wymagania.",
|
|
5591
5588
|
"tester_not_reported": "Tester jeszcze nie zgłosił raportu, więc żadne wymaganie nie zostało rozstrzygnięte.",
|
|
5592
|
-
"no_verdicts": "Tester zgłosił raport, nie rozstrzygając żadnego wymagania ze specyfikacji."
|
|
5593
|
-
|
|
5589
|
+
"no_verdicts": "Tester zgłosił raport, nie rozstrzygając żadnego wymagania ze specyfikacji.",
|
|
5590
|
+
"no_requirements": "Specyfikacja usługi nie zawiera żadnych wymagań, więc tester nie miał czego rozstrzygać."
|
|
5591
|
+
},
|
|
5592
|
+
"unmatchedVerdicts": "Tester rozstrzygnął dodatkowo {count} identyfikator(ów), których nie ma w tej specyfikacji, więc jego własne zliczenie jest wyższe niż powyższe."
|
|
5594
5593
|
},
|
|
5595
5594
|
"tests": {
|
|
5596
5595
|
"title": "Jak to przetestowano",
|
package/i18n/locales/tr.json
CHANGED
|
@@ -5575,11 +5575,8 @@
|
|
|
5575
5575
|
"notCovered": "Denetlenmeyen: {count}",
|
|
5576
5576
|
"regressions": "Gerilemeler: {count}",
|
|
5577
5577
|
"regressionTag": "Gerileme",
|
|
5578
|
-
"idOnly": "yalnızca kimlik",
|
|
5579
|
-
"idOnlyHint": "Bu kimlik servis şartnamesinde yer almadığı için gösterilecek bir başlık yok.",
|
|
5580
5578
|
"spec": {
|
|
5581
|
-
"not_read": "Servis şartnamesi
|
|
5582
|
-
"unmatched": "Okunan servis şartnamesinde bu gereksinim kimliklerinin hiçbiri yok; bu nedenle yalnızca test edicinin bildirdiği kimlikler gösteriliyor."
|
|
5579
|
+
"not_read": "Servis şartnamesi okunamadı; bu sayılar yalnızca testçinin karara bağladıklarını, kimlik bazında kapsıyor."
|
|
5583
5580
|
},
|
|
5584
5581
|
"verdict": {
|
|
5585
5582
|
"met": "Doğrulandı",
|
|
@@ -5589,8 +5586,10 @@
|
|
|
5589
5586
|
"gap": {
|
|
5590
5587
|
"no_tester_step": "Bu hatta test adımı yok, bu yüzden hiçbir gereksinim denetlenmedi.",
|
|
5591
5588
|
"tester_not_reported": "Testçi henüz rapor vermedi, bu yüzden hiçbir gereksinim karara bağlanmadı.",
|
|
5592
|
-
"no_verdicts": "Testçi rapor verdi ancak belirtimdeki hiçbir gereksinim hakkında karar vermedi."
|
|
5593
|
-
|
|
5589
|
+
"no_verdicts": "Testçi rapor verdi ancak belirtimdeki hiçbir gereksinim hakkında karar vermedi.",
|
|
5590
|
+
"no_requirements": "Servis şartnamesinde hiçbir gereksinim kayıtlı değil, dolayısıyla testçinin karara bağlayacağı bir şey yoktu."
|
|
5591
|
+
},
|
|
5592
|
+
"unmatchedVerdicts": "Testçi ayrıca bu şartnamede bulunmayan {count} kimlik hakkında karar verdi; bu yüzden kendi sayımı yukarıdakilerden yüksek."
|
|
5594
5593
|
},
|
|
5595
5594
|
"tests": {
|
|
5596
5595
|
"title": "Nasıl test edildi",
|
package/i18n/locales/uk.json
CHANGED
|
@@ -5575,11 +5575,8 @@
|
|
|
5575
5575
|
"notCovered": "Не перевірено: {count}",
|
|
5576
5576
|
"regressions": "Регресії: {count}",
|
|
5577
5577
|
"regressionTag": "Регресія",
|
|
5578
|
-
"idOnly": "лише ідентифікатор",
|
|
5579
|
-
"idOnlyHint": "Цього ідентифікатора немає у специфікації сервісу, тому назви для показу немає.",
|
|
5580
5578
|
"spec": {
|
|
5581
|
-
"not_read": "Специфікацію сервісу не прочитано,
|
|
5582
|
-
"unmatched": "Жодного з цих ідентифікаторів вимог немає у прочитаній специфікації сервісу, тому показано лише ті ідентифікатори, які повідомив тестувальник."
|
|
5579
|
+
"not_read": "Специфікацію сервісу не прочитано, тож ці підрахунки охоплюють лише те, щодо чого тестувальник виніс вердикт, за ідентифікатором."
|
|
5583
5580
|
},
|
|
5584
5581
|
"verdict": {
|
|
5585
5582
|
"met": "Підтверджено",
|
|
@@ -5589,8 +5586,10 @@
|
|
|
5589
5586
|
"gap": {
|
|
5590
5587
|
"no_tester_step": "У цьому конвеєрі немає кроку тестування, тож жодну вимогу не перевіряли.",
|
|
5591
5588
|
"tester_not_reported": "Тестувальник ще не звітував, тож жодну вимогу не вирішено.",
|
|
5592
|
-
"no_verdicts": "Тестувальник прозвітував, не висловившись про жодну вимогу зі специфікації."
|
|
5593
|
-
|
|
5589
|
+
"no_verdicts": "Тестувальник прозвітував, не висловившись про жодну вимогу зі специфікації.",
|
|
5590
|
+
"no_requirements": "Специфікація сервісу не містить жодної вимоги, тож тестувальнику не було чого оцінювати."
|
|
5591
|
+
},
|
|
5592
|
+
"unmatchedVerdicts": "Тестувальник виніс вердикт ще щодо {count} ідентифікатор(ів), яких немає в цій специфікації, тож його власний підрахунок вищий за наведений вище."
|
|
5594
5593
|
},
|
|
5595
5594
|
"tests": {
|
|
5596
5595
|
"title": "Як це тестували",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.242.0",
|
|
4
4
|
"description": "Reusable Nuxt layer for the Agent Architecture Board SPA (components, stores, composables, pages). Consume it from a thin deployment app via `extends: ['@cat-factory/app']` and point it at your backend with NUXT_PUBLIC_API_BASE. See deploy/frontend for an example.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"valibot": "^1.4.2",
|
|
41
41
|
"vue": "3.5.40",
|
|
42
42
|
"wretch": "^3.0.9",
|
|
43
|
-
"@cat-factory/contracts": "0.
|
|
43
|
+
"@cat-factory/contracts": "0.263.0"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@toad-contracts/testing": "0.3.2",
|
|
@@ -1,492 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import type { Block } from '~/types/domain'
|
|
3
|
-
import type { ExecutionInstance, PipelineStep } from '~/types/execution'
|
|
4
|
-
import type { ServiceSpecView } from '~/types/spec'
|
|
5
|
-
import { composeRunOutcome, hasOutcomeToShow } from '~/utils/runOutcome'
|
|
6
|
-
|
|
7
|
-
// The composer's whole job is to keep facts that mean different things from rendering the same,
|
|
8
|
-
// so the cases worth pinning are the COLLAPSES: an absent producer vs a producer that found
|
|
9
|
-
// nothing, an aspirational failure vs a regression, a capture nobody reviewed vs a pair a human
|
|
10
|
-
// approved, and a tester that could not run vs one that ran and raised concerns.
|
|
11
|
-
|
|
12
|
-
function block(overrides: Partial<Block> = {}): Block {
|
|
13
|
-
return {
|
|
14
|
-
id: 'blk_1',
|
|
15
|
-
title: 'Password reset',
|
|
16
|
-
description: ' Let a signed-out user reset their password by email. ',
|
|
17
|
-
status: 'pr_ready',
|
|
18
|
-
level: 'task',
|
|
19
|
-
type: 'task',
|
|
20
|
-
parentId: 'frm_1',
|
|
21
|
-
position: { x: 0, y: 0 },
|
|
22
|
-
progress: 0,
|
|
23
|
-
dependsOn: [],
|
|
24
|
-
taskType: 'feature',
|
|
25
|
-
...overrides,
|
|
26
|
-
} as Block
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function step(overrides: Partial<PipelineStep> = {}): PipelineStep {
|
|
30
|
-
return {
|
|
31
|
-
agentKind: 'coder',
|
|
32
|
-
state: 'done',
|
|
33
|
-
progress: 1,
|
|
34
|
-
decision: null,
|
|
35
|
-
...overrides,
|
|
36
|
-
} as PipelineStep
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function run(steps: PipelineStep[], overrides: Partial<ExecutionInstance> = {}): ExecutionInstance {
|
|
40
|
-
return {
|
|
41
|
-
id: 'exe_1',
|
|
42
|
-
blockId: 'blk_1',
|
|
43
|
-
pipelineId: 'pl_1',
|
|
44
|
-
pipelineName: 'Build',
|
|
45
|
-
steps,
|
|
46
|
-
currentStep: 0,
|
|
47
|
-
status: 'running',
|
|
48
|
-
...overrides,
|
|
49
|
-
} as ExecutionInstance
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function testerStep(report: Record<string, unknown>, kind = 'tester-ui'): PipelineStep {
|
|
53
|
-
return step({
|
|
54
|
-
agentKind: kind,
|
|
55
|
-
test: {
|
|
56
|
-
phase: 'testing',
|
|
57
|
-
attempts: 0,
|
|
58
|
-
maxAttempts: 3,
|
|
59
|
-
lastReport: {
|
|
60
|
-
greenlight: true,
|
|
61
|
-
summary: 'Exercised the reset flow end to end.',
|
|
62
|
-
tested: ['Password reset'],
|
|
63
|
-
outcomes: [],
|
|
64
|
-
concerns: [],
|
|
65
|
-
...report,
|
|
66
|
-
},
|
|
67
|
-
},
|
|
68
|
-
} as Partial<PipelineStep>)
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const spec: ServiceSpecView = {
|
|
72
|
-
present: true,
|
|
73
|
-
spec: {
|
|
74
|
-
service: 'accounts',
|
|
75
|
-
summary: '',
|
|
76
|
-
modules: [
|
|
77
|
-
{
|
|
78
|
-
name: 'auth',
|
|
79
|
-
summary: '',
|
|
80
|
-
groups: [
|
|
81
|
-
{
|
|
82
|
-
name: 'login',
|
|
83
|
-
summary: '',
|
|
84
|
-
rules: [],
|
|
85
|
-
requirements: [
|
|
86
|
-
{
|
|
87
|
-
id: 'req-reset',
|
|
88
|
-
title: 'A user can reset their password',
|
|
89
|
-
statement: 'The system SHALL send a reset link.',
|
|
90
|
-
kind: 'functional',
|
|
91
|
-
priority: 'must',
|
|
92
|
-
state: 'aspirational',
|
|
93
|
-
acceptance: [],
|
|
94
|
-
sourceBlockIds: [],
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
id: 'req-login',
|
|
98
|
-
title: 'A user can sign in',
|
|
99
|
-
statement: 'The system SHALL authenticate a user.',
|
|
100
|
-
kind: 'functional',
|
|
101
|
-
priority: 'must',
|
|
102
|
-
state: 'established',
|
|
103
|
-
acceptance: [],
|
|
104
|
-
sourceBlockIds: [],
|
|
105
|
-
},
|
|
106
|
-
],
|
|
107
|
-
},
|
|
108
|
-
],
|
|
109
|
-
},
|
|
110
|
-
],
|
|
111
|
-
},
|
|
112
|
-
features: [],
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
describe('composeRunOutcome', () => {
|
|
116
|
-
it('carries the ask and every pull request so the diff stays one click away', () => {
|
|
117
|
-
const outcome = composeRunOutcome({
|
|
118
|
-
block: block({
|
|
119
|
-
pullRequest: { url: 'https://host/pr/7', number: 7, branch: 'cat-factory/blk_1' },
|
|
120
|
-
peerPullRequests: [{ repo: 'acme/api', ref: { url: 'https://host/api/pr/3', number: 3 } }],
|
|
121
|
-
}),
|
|
122
|
-
instance: null,
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
expect(outcome.title).toBe('Password reset')
|
|
126
|
-
expect(outcome.ask).toBe('Let a signed-out user reset their password by email.')
|
|
127
|
-
expect(outcome.disposition).toBe('awaiting_merge')
|
|
128
|
-
expect(outcome.pullRequests).toEqual([
|
|
129
|
-
{ url: 'https://host/pr/7', number: 7, branch: 'cat-factory/blk_1', repo: null },
|
|
130
|
-
{ url: 'https://host/api/pr/3', number: 3, branch: null, repo: 'acme/api' },
|
|
131
|
-
])
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
it('distinguishes no tester step from a tester that has not reported', () => {
|
|
135
|
-
const none = composeRunOutcome({ block: block(), instance: run([step()]) })
|
|
136
|
-
expect(none.tests).toEqual({ status: 'absent', gap: 'no_tester_step' })
|
|
137
|
-
expect(none.requirements).toEqual({ status: 'absent', gap: 'no_tester_step' })
|
|
138
|
-
|
|
139
|
-
const silent = composeRunOutcome({
|
|
140
|
-
block: block(),
|
|
141
|
-
instance: run([step({ agentKind: 'tester-api' })]),
|
|
142
|
-
})
|
|
143
|
-
expect(silent.tests).toEqual({ status: 'absent', gap: 'tester_not_reported' })
|
|
144
|
-
})
|
|
145
|
-
|
|
146
|
-
it('keeps a tester that could not run apart from one that raised concerns', () => {
|
|
147
|
-
const aborted = composeRunOutcome({
|
|
148
|
-
block: block(),
|
|
149
|
-
instance: run([
|
|
150
|
-
testerStep({
|
|
151
|
-
greenlight: false,
|
|
152
|
-
abort: { reason: 'The preview environment never came up.' },
|
|
153
|
-
}),
|
|
154
|
-
]),
|
|
155
|
-
})
|
|
156
|
-
expect(aborted.tests).toMatchObject({
|
|
157
|
-
status: 'reported',
|
|
158
|
-
verdict: 'could_not_run',
|
|
159
|
-
abortReason: 'The preview environment never came up.',
|
|
160
|
-
concerns: [],
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
const buggy = composeRunOutcome({
|
|
164
|
-
block: block(),
|
|
165
|
-
instance: run([
|
|
166
|
-
testerStep({
|
|
167
|
-
greenlight: false,
|
|
168
|
-
concerns: [{ title: 'Reset link expires immediately', detail: '…', severity: 'high' }],
|
|
169
|
-
outcomes: [
|
|
170
|
-
{ name: 'Reset', status: 'failed' },
|
|
171
|
-
{ name: 'Login', status: 'passed' },
|
|
172
|
-
{ name: 'Rate limit', status: 'skipped' },
|
|
173
|
-
],
|
|
174
|
-
}),
|
|
175
|
-
]),
|
|
176
|
-
})
|
|
177
|
-
expect(buggy.tests).toMatchObject({
|
|
178
|
-
verdict: 'concerns',
|
|
179
|
-
passed: 1,
|
|
180
|
-
failed: 1,
|
|
181
|
-
skipped: 1,
|
|
182
|
-
concerns: [{ title: 'Reset link expires immediately', severity: 'high' }],
|
|
183
|
-
})
|
|
184
|
-
})
|
|
185
|
-
|
|
186
|
-
it('reports a failing ESTABLISHED requirement as a regression and an aspirational one as not', () => {
|
|
187
|
-
const outcome = composeRunOutcome({
|
|
188
|
-
block: block(),
|
|
189
|
-
instance: run([
|
|
190
|
-
testerStep({
|
|
191
|
-
requirementVerdicts: [
|
|
192
|
-
{ requirementId: 'req-reset', status: 'not_met', detail: 'Not built yet.' },
|
|
193
|
-
{ requirementId: 'req-login', status: 'not_met', detail: 'Sign-in now 500s.' },
|
|
194
|
-
],
|
|
195
|
-
}),
|
|
196
|
-
]),
|
|
197
|
-
spec,
|
|
198
|
-
})
|
|
199
|
-
|
|
200
|
-
expect(outcome.requirements).toMatchObject({
|
|
201
|
-
status: 'reported',
|
|
202
|
-
spec: 'joined',
|
|
203
|
-
regressions: 1,
|
|
204
|
-
})
|
|
205
|
-
if (outcome.requirements.status !== 'reported') throw new Error('expected a reported section')
|
|
206
|
-
// The regression leads, and both are still counted as failures.
|
|
207
|
-
expect(outcome.requirements.entries.map((e) => [e.id, e.regression])).toEqual([
|
|
208
|
-
['req-login', true],
|
|
209
|
-
['req-reset', false],
|
|
210
|
-
])
|
|
211
|
-
expect(outcome.requirements.notMet).toBe(2)
|
|
212
|
-
expect(outcome.requirements.entries[0]?.title).toBe('A user can sign in')
|
|
213
|
-
})
|
|
214
|
-
|
|
215
|
-
it('says the spec was never read rather than rendering ids as titles', () => {
|
|
216
|
-
const outcome = composeRunOutcome({
|
|
217
|
-
block: block(),
|
|
218
|
-
instance: run([
|
|
219
|
-
testerStep({ requirementVerdicts: [{ requirementId: 'req-reset', status: 'met' }] }),
|
|
220
|
-
]),
|
|
221
|
-
})
|
|
222
|
-
expect(outcome.requirements).toMatchObject({ status: 'reported', spec: 'not_read' })
|
|
223
|
-
if (outcome.requirements.status !== 'reported') throw new Error('expected a reported section')
|
|
224
|
-
expect(outcome.requirements.entries[0]).toMatchObject({
|
|
225
|
-
id: 'req-reset',
|
|
226
|
-
title: null,
|
|
227
|
-
state: null,
|
|
228
|
-
regression: false,
|
|
229
|
-
})
|
|
230
|
-
})
|
|
231
|
-
|
|
232
|
-
// A spec that WAS read and names none of the reported ids leaves identical rows behind, and
|
|
233
|
-
// sends the reader to a different fix: the spec moved on, or the tester keyed its verdicts by
|
|
234
|
-
// something else. Reporting it as a failed read would send them to fix a read that worked.
|
|
235
|
-
it('keeps a spec that was never read apart from one that named none of these ids', () => {
|
|
236
|
-
const outcome = composeRunOutcome({
|
|
237
|
-
block: block(),
|
|
238
|
-
instance: run([
|
|
239
|
-
testerStep({ requirementVerdicts: [{ requirementId: 'req-gone', status: 'met' }] }),
|
|
240
|
-
]),
|
|
241
|
-
spec,
|
|
242
|
-
})
|
|
243
|
-
expect(outcome.requirements).toMatchObject({ status: 'reported', spec: 'unmatched' })
|
|
244
|
-
})
|
|
245
|
-
|
|
246
|
-
// The partial join is the case a section-level note cannot state: some rows carry titles, and
|
|
247
|
-
// an id sitting unmarked between them reads as a requirement named after a slug.
|
|
248
|
-
it('marks the rows the spec did not name while the section as a whole joined', () => {
|
|
249
|
-
const outcome = composeRunOutcome({
|
|
250
|
-
block: block(),
|
|
251
|
-
instance: run([
|
|
252
|
-
testerStep({
|
|
253
|
-
requirementVerdicts: [
|
|
254
|
-
{ requirementId: 'req-login', status: 'met' },
|
|
255
|
-
{ requirementId: 'req-gone', status: 'met' },
|
|
256
|
-
],
|
|
257
|
-
}),
|
|
258
|
-
]),
|
|
259
|
-
spec,
|
|
260
|
-
})
|
|
261
|
-
expect(outcome.requirements).toMatchObject({ status: 'reported', spec: 'joined' })
|
|
262
|
-
if (outcome.requirements.status !== 'reported') throw new Error('expected a reported section')
|
|
263
|
-
const rows = outcome.requirements.entries
|
|
264
|
-
expect(rows.find((e) => e.id === 'req-login')?.title).toBe('A user can sign in')
|
|
265
|
-
expect(rows.find((e) => e.id === 'req-gone')?.title).toBeNull()
|
|
266
|
-
})
|
|
267
|
-
|
|
268
|
-
it('separates a tester report with no verdicts from a tester that never reported', () => {
|
|
269
|
-
const outcome = composeRunOutcome({
|
|
270
|
-
block: block(),
|
|
271
|
-
instance: run([testerStep({ requirementVerdicts: [] })]),
|
|
272
|
-
})
|
|
273
|
-
expect(outcome.requirements).toEqual({ status: 'absent', gap: 'no_verdicts' })
|
|
274
|
-
})
|
|
275
|
-
|
|
276
|
-
it('prefers the reviewed visual-confirmation pairs over the tester’s raw captures', () => {
|
|
277
|
-
const outcome = composeRunOutcome({
|
|
278
|
-
block: block(),
|
|
279
|
-
instance: run([
|
|
280
|
-
testerStep({ screenshots: [{ view: 'reset', artifactId: 'art_shot' }] }),
|
|
281
|
-
step({
|
|
282
|
-
agentKind: 'visual-confirmation',
|
|
283
|
-
visualConfirm: {
|
|
284
|
-
phase: 'approved',
|
|
285
|
-
attempts: 0,
|
|
286
|
-
maxAttempts: 2,
|
|
287
|
-
pairs: [{ view: 'reset', actualArtifactId: 'art_a', referenceArtifactId: 'art_ref' }],
|
|
288
|
-
},
|
|
289
|
-
} as Partial<PipelineStep>),
|
|
290
|
-
]),
|
|
291
|
-
})
|
|
292
|
-
expect(outcome.visuals).toEqual({
|
|
293
|
-
status: 'reported',
|
|
294
|
-
source: 'visual_confirm',
|
|
295
|
-
phase: 'approved',
|
|
296
|
-
views: [{ view: 'reset', artifactId: 'art_a', referenceArtifactId: 'art_ref' }],
|
|
297
|
-
})
|
|
298
|
-
})
|
|
299
|
-
|
|
300
|
-
it('falls back to the tester’s captures, and says which gap it hit when there are none', () => {
|
|
301
|
-
const captured = composeRunOutcome({
|
|
302
|
-
block: block(),
|
|
303
|
-
instance: run([testerStep({ screenshots: [{ view: 'reset', artifactId: 'art_shot' }] })]),
|
|
304
|
-
})
|
|
305
|
-
expect(captured.visuals).toMatchObject({ status: 'reported', source: 'tester', phase: null })
|
|
306
|
-
|
|
307
|
-
// An API tester captures nothing by design: no producer, so nothing was ever meant to be seen.
|
|
308
|
-
const apiOnly = composeRunOutcome({
|
|
309
|
-
block: block(),
|
|
310
|
-
instance: run([testerStep({}, 'tester-api')]),
|
|
311
|
-
})
|
|
312
|
-
expect(apiOnly.visuals).toEqual({ status: 'absent', gap: 'no_visual_step', detail: null })
|
|
313
|
-
|
|
314
|
-
// A gate that ran and gathered nothing is a different fact, and it recorded why.
|
|
315
|
-
const degraded = composeRunOutcome({
|
|
316
|
-
block: block(),
|
|
317
|
-
instance: run([
|
|
318
|
-
step({
|
|
319
|
-
agentKind: 'visual-confirmation',
|
|
320
|
-
visualConfirm: {
|
|
321
|
-
phase: 'awaiting_human',
|
|
322
|
-
attempts: 0,
|
|
323
|
-
maxAttempts: 2,
|
|
324
|
-
pairs: [],
|
|
325
|
-
degradedReason: 'No artifact storage configured.',
|
|
326
|
-
},
|
|
327
|
-
} as Partial<PipelineStep>),
|
|
328
|
-
]),
|
|
329
|
-
})
|
|
330
|
-
expect(degraded.visuals).toEqual({
|
|
331
|
-
status: 'absent',
|
|
332
|
-
gap: 'none_captured',
|
|
333
|
-
detail: 'No artifact storage configured.',
|
|
334
|
-
})
|
|
335
|
-
})
|
|
336
|
-
|
|
337
|
-
it('lists only the checks that actually recorded a verdict', () => {
|
|
338
|
-
const outcome = composeRunOutcome({
|
|
339
|
-
block: block(),
|
|
340
|
-
instance: run([
|
|
341
|
-
step({
|
|
342
|
-
agentKind: 'ci',
|
|
343
|
-
gate: { phase: 'checking', attempts: 0, maxAttempts: 3 },
|
|
344
|
-
} as Partial<PipelineStep>),
|
|
345
|
-
step({
|
|
346
|
-
validation: { passed: false, attempts: 2, maxAttempts: 3, outcomes: [] },
|
|
347
|
-
} as Partial<PipelineStep>),
|
|
348
|
-
step({
|
|
349
|
-
reproduction: {
|
|
350
|
-
status: 'inconclusive',
|
|
351
|
-
command: 'pnpm test',
|
|
352
|
-
testPaths: [],
|
|
353
|
-
attempts: 1,
|
|
354
|
-
maxAttempts: 2,
|
|
355
|
-
at: 1,
|
|
356
|
-
},
|
|
357
|
-
} as Partial<PipelineStep>),
|
|
358
|
-
]),
|
|
359
|
-
})
|
|
360
|
-
|
|
361
|
-
// The CI gate has not probed yet, so it contributes NOTHING rather than a green row.
|
|
362
|
-
expect(outcome.checks).toEqual([
|
|
363
|
-
{ kind: 'validation', state: 'fail', reproduction: null },
|
|
364
|
-
{ kind: 'reproduction', state: 'inconclusive', reproduction: 'inconclusive' },
|
|
365
|
-
])
|
|
366
|
-
})
|
|
367
|
-
|
|
368
|
-
it('reads the CI gate’s recorded verdict once it has probed', () => {
|
|
369
|
-
const outcome = composeRunOutcome({
|
|
370
|
-
block: block(),
|
|
371
|
-
instance: run([
|
|
372
|
-
step({
|
|
373
|
-
agentKind: 'ci',
|
|
374
|
-
gate: { phase: 'checking', attempts: 0, maxAttempts: 3, lastVerdict: 'pass' },
|
|
375
|
-
} as Partial<PipelineStep>),
|
|
376
|
-
]),
|
|
377
|
-
})
|
|
378
|
-
expect(outcome.checks).toEqual([{ kind: 'ci', state: 'pass', reproduction: null }])
|
|
379
|
-
})
|
|
380
|
-
|
|
381
|
-
// A block that NAMES a run the caller could not resolve is the trap this whole module is
|
|
382
|
-
// about: composed from the empty step list it would report a pipeline that ran and produced
|
|
383
|
-
// nothing, which is the opposite of "nobody could read what it produced".
|
|
384
|
-
it('says the run could not be read rather than blaming the pipeline for the missing steps', () => {
|
|
385
|
-
const outcome = composeRunOutcome({
|
|
386
|
-
block: block({ status: 'done', executionId: 'exe_gone' }),
|
|
387
|
-
instance: null,
|
|
388
|
-
})
|
|
389
|
-
|
|
390
|
-
expect(outcome.requirements).toEqual({ status: 'absent', gap: 'run_unavailable' })
|
|
391
|
-
expect(outcome.tests).toEqual({ status: 'absent', gap: 'run_unavailable' })
|
|
392
|
-
expect(outcome.visuals).toEqual({ status: 'absent', gap: 'run_unavailable', detail: null })
|
|
393
|
-
expect(outcome.checks).toEqual([])
|
|
394
|
-
// The block still carries what the block knows.
|
|
395
|
-
expect(outcome.disposition).toBe('merged')
|
|
396
|
-
expect(outcome.title).toBe('Password reset')
|
|
397
|
-
})
|
|
398
|
-
|
|
399
|
-
it('keeps a task that never ran apart from one whose run could not be read', () => {
|
|
400
|
-
const never = composeRunOutcome({ block: block({ status: 'ready' }), instance: null })
|
|
401
|
-
const unread = composeRunOutcome({
|
|
402
|
-
block: block({ status: 'ready', executionId: 'exe_gone' }),
|
|
403
|
-
instance: null,
|
|
404
|
-
})
|
|
405
|
-
|
|
406
|
-
expect(never.disposition).toBe('not_run')
|
|
407
|
-
expect(never.tests).toEqual({ status: 'absent', gap: 'no_tester_step' })
|
|
408
|
-
expect(unread.disposition).toBe('unknown')
|
|
409
|
-
expect(unread.tests).toEqual({ status: 'absent', gap: 'run_unavailable' })
|
|
410
|
-
})
|
|
411
|
-
|
|
412
|
-
// The selected tester is the one that REPORTED, which can be the api half of a pipeline whose
|
|
413
|
-
// ui half has not. Reading the producer off it would tell a reader looking at a UI pipeline
|
|
414
|
-
// that nothing in it captures the interface.
|
|
415
|
-
it('finds the interface producer anywhere in the pipeline, not only on the reporting tester', () => {
|
|
416
|
-
const outcome = composeRunOutcome({
|
|
417
|
-
block: block(),
|
|
418
|
-
instance: run([
|
|
419
|
-
step({ agentKind: 'tester-ui' }),
|
|
420
|
-
testerStep({ screenshots: [] }, 'tester-api'),
|
|
421
|
-
]),
|
|
422
|
-
})
|
|
423
|
-
expect(outcome.visuals).toEqual({ status: 'absent', gap: 'none_captured', detail: null })
|
|
424
|
-
})
|
|
425
|
-
|
|
426
|
-
it('derives the disposition from the block, and from the run only where the block cannot', () => {
|
|
427
|
-
const dispositions = [
|
|
428
|
-
composeRunOutcome({ block: block({ status: 'done' }), instance: null }).disposition,
|
|
429
|
-
composeRunOutcome({ block: block({ status: 'pr_ready' }), instance: null }).disposition,
|
|
430
|
-
composeRunOutcome({ block: block({ status: 'planned' }), instance: null }).disposition,
|
|
431
|
-
composeRunOutcome({ block: block({ status: 'in_progress' }), instance: run([step()]) })
|
|
432
|
-
.disposition,
|
|
433
|
-
composeRunOutcome({
|
|
434
|
-
block: block({ status: 'in_progress' }),
|
|
435
|
-
instance: run([step()], { status: 'failed' }),
|
|
436
|
-
}).disposition,
|
|
437
|
-
// `in_progress` is the block's own word for a live run, so it stands with no instance.
|
|
438
|
-
composeRunOutcome({
|
|
439
|
-
block: block({ status: 'in_progress', executionId: 'exe_gone' }),
|
|
440
|
-
instance: null,
|
|
441
|
-
}).disposition,
|
|
442
|
-
]
|
|
443
|
-
expect(dispositions).toEqual([
|
|
444
|
-
'merged',
|
|
445
|
-
'awaiting_merge',
|
|
446
|
-
'not_run',
|
|
447
|
-
'in_flight',
|
|
448
|
-
'needs_attention',
|
|
449
|
-
'in_flight',
|
|
450
|
-
])
|
|
451
|
-
})
|
|
452
|
-
})
|
|
453
|
-
|
|
454
|
-
describe('hasOutcomeToShow', () => {
|
|
455
|
-
it('is false for a run that has produced nothing to read yet', () => {
|
|
456
|
-
expect(
|
|
457
|
-
hasOutcomeToShow(composeRunOutcome({ block: block({ status: 'planned' }), instance: null })),
|
|
458
|
-
).toBe(false)
|
|
459
|
-
})
|
|
460
|
-
|
|
461
|
-
// The affordance the board card and the inspector both gate on: a task marked done by hand,
|
|
462
|
-
// carrying no pull request and no readable run, has nothing an outcome card could show.
|
|
463
|
-
it('is false for a task whose run cannot be read and which carries no pull request', () => {
|
|
464
|
-
expect(
|
|
465
|
-
hasOutcomeToShow(
|
|
466
|
-
composeRunOutcome({
|
|
467
|
-
block: block({ status: 'done', executionId: 'exe_gone' }),
|
|
468
|
-
instance: null,
|
|
469
|
-
}),
|
|
470
|
-
),
|
|
471
|
-
).toBe(false)
|
|
472
|
-
})
|
|
473
|
-
|
|
474
|
-
it('is true as soon as there is a PR or any recorded evidence', () => {
|
|
475
|
-
expect(
|
|
476
|
-
hasOutcomeToShow(
|
|
477
|
-
composeRunOutcome({
|
|
478
|
-
block: block({ pullRequest: { url: 'https://host/pr/7', number: 7 } }),
|
|
479
|
-
instance: null,
|
|
480
|
-
}),
|
|
481
|
-
),
|
|
482
|
-
).toBe(true)
|
|
483
|
-
expect(
|
|
484
|
-
hasOutcomeToShow(
|
|
485
|
-
composeRunOutcome({
|
|
486
|
-
block: block({ status: 'in_progress' }),
|
|
487
|
-
instance: run([testerStep({})]),
|
|
488
|
-
}),
|
|
489
|
-
),
|
|
490
|
-
).toBe(true)
|
|
491
|
-
})
|
|
492
|
-
})
|