@cat-factory/app 0.154.0 → 0.155.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/panels/ResultWindowShell.vue +74 -3
- package/app/components/panels/StepValidationReport.vue +66 -0
- package/app/components/panels/inspector/ServiceTestConfig.vue +48 -10
- package/app/components/panels/inspector/ServiceValidationConfig.vue +208 -0
- package/app/components/settings/ApiTokensPanel.vue +8 -4
- package/app/composables/api/validationChecks.ts +39 -0
- package/app/composables/useApi.ts +2 -0
- package/app/modular/panels/inspector.logic.spec.ts +3 -0
- package/app/modular/panels/inspector.logic.ts +5 -0
- package/app/modular/panels/inspector.ts +2 -0
- package/app/stores/validationChecks.ts +111 -0
- package/app/types/validationChecks.ts +17 -0
- package/i18n/locales/de.json +32 -0
- package/i18n/locales/en.json +32 -0
- package/i18n/locales/es.json +32 -0
- package/i18n/locales/fr.json +32 -0
- package/i18n/locales/he.json +32 -0
- package/i18n/locales/it.json +32 -0
- package/i18n/locales/ja.json +32 -0
- package/i18n/locales/pl.json +32 -0
- package/i18n/locales/tr.json +32 -0
- package/i18n/locales/uk.json +32 -0
- package/package.json +2 -2
|
@@ -20,10 +20,11 @@
|
|
|
20
20
|
//
|
|
21
21
|
// It also owns the one trailing section every step-backed window shows: the agent's effort
|
|
22
22
|
// self-assessment (see the footer block below), so a window never renders it itself.
|
|
23
|
-
import { computed, ref } from 'vue'
|
|
23
|
+
import { computed, ref, watch } from 'vue'
|
|
24
24
|
import { useModalBehavior } from '@modular-vue/core'
|
|
25
25
|
import StepRestartControl from '~/components/panels/StepRestartControl.vue'
|
|
26
26
|
import StepEffortReport from '~/components/panels/StepEffortReport.vue'
|
|
27
|
+
import StepValidationReport from '~/components/panels/StepValidationReport.vue'
|
|
27
28
|
import { effortBand, effortHint } from '~/utils/effort'
|
|
28
29
|
|
|
29
30
|
/** A pipeline step reference — passed by step-result windows to surface the shared
|
|
@@ -85,11 +86,26 @@ const { dialogRef } = useModalBehavior({
|
|
|
85
86
|
// step whose agent wrote no report both resolve to null, and the footer disappears.
|
|
86
87
|
const ui = useUiStore()
|
|
87
88
|
const execution = useExecutionStore()
|
|
88
|
-
const
|
|
89
|
+
const activeStep = computed(() => {
|
|
89
90
|
const view = ui.resultView
|
|
90
91
|
if (!view || view.instanceId === null || view.stepIndex === null) return null
|
|
91
|
-
return execution.getInstance(view.instanceId)?.steps[view.stepIndex]
|
|
92
|
+
return execution.getInstance(view.instanceId)?.steps[view.stepIndex] ?? null
|
|
92
93
|
})
|
|
94
|
+
const effortReport = computed(() => activeStep.value?.effortReport ?? null)
|
|
95
|
+
// The step's PRE-PR VALIDATION report — the second universal trailing section, resolved the same
|
|
96
|
+
// way and for the same reason: every window whose step ran a coding job can show whether the
|
|
97
|
+
// checkout actually passed the service's checks before the PR opened (and, on a red run, exactly
|
|
98
|
+
// what failed). Absent for a service that configured no checks, and the footer disappears.
|
|
99
|
+
const validationReport = computed(() => activeStep.value?.validation ?? null)
|
|
100
|
+
const validationOpen = ref(false)
|
|
101
|
+
// A failing report is the interesting one, so it opens expanded; a green one stays a one-line row.
|
|
102
|
+
watch(
|
|
103
|
+
validationReport,
|
|
104
|
+
(report) => {
|
|
105
|
+
if (report && !report.passed) validationOpen.value = true
|
|
106
|
+
},
|
|
107
|
+
{ immediate: true },
|
|
108
|
+
)
|
|
93
109
|
// Collapsed by default — the windows own the vertical space, and the row already carries the
|
|
94
110
|
// difficulty plus the gist of what held the agent back.
|
|
95
111
|
const effortOpen = ref(false)
|
|
@@ -204,6 +220,61 @@ const panelClass = computed(() => [
|
|
|
204
220
|
<StepEffortReport :report="effortReport" variant="flat" />
|
|
205
221
|
</div>
|
|
206
222
|
</section>
|
|
223
|
+
|
|
224
|
+
<!-- Shared trailing section: the pre-PR validation report (the service's check commands
|
|
225
|
+
run against the checkout BEFORE the PR opened). Collapsed when green, expanded when
|
|
226
|
+
red — a failed checkout is the one an operator opened the window to read. -->
|
|
227
|
+
<section
|
|
228
|
+
v-if="validationReport"
|
|
229
|
+
class="shrink-0 border-t border-slate-800 bg-slate-900/60"
|
|
230
|
+
data-testid="result-window-validation"
|
|
231
|
+
>
|
|
232
|
+
<button
|
|
233
|
+
type="button"
|
|
234
|
+
class="flex w-full items-center gap-2 px-5 py-2 text-start hover:bg-slate-800/40"
|
|
235
|
+
:aria-expanded="validationOpen"
|
|
236
|
+
data-testid="result-window-validation-toggle"
|
|
237
|
+
@click="validationOpen = !validationOpen"
|
|
238
|
+
>
|
|
239
|
+
<UIcon
|
|
240
|
+
:name="validationReport.passed ? 'i-lucide-shield-check' : 'i-lucide-shield-alert'"
|
|
241
|
+
class="h-3.5 w-3.5 shrink-0"
|
|
242
|
+
:class="validationReport.passed ? 'text-emerald-400' : 'text-rose-400'"
|
|
243
|
+
/>
|
|
244
|
+
<span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
245
|
+
{{ t('panels.stepDetail.validation.heading') }}
|
|
246
|
+
</span>
|
|
247
|
+
<span
|
|
248
|
+
class="shrink-0 rounded px-1.5 py-0.5 text-[11px] font-medium tabular-nums"
|
|
249
|
+
:class="
|
|
250
|
+
validationReport.passed
|
|
251
|
+
? 'bg-emerald-500/15 text-emerald-300'
|
|
252
|
+
: 'bg-rose-500/15 text-rose-300'
|
|
253
|
+
"
|
|
254
|
+
>
|
|
255
|
+
{{
|
|
256
|
+
validationReport.passed
|
|
257
|
+
? t('panels.stepDetail.validation.passed')
|
|
258
|
+
: t('panels.stepDetail.validation.failed')
|
|
259
|
+
}}
|
|
260
|
+
</span>
|
|
261
|
+
<span class="min-w-0 flex-1 truncate text-[12px] text-slate-400">
|
|
262
|
+
{{
|
|
263
|
+
t('panels.stepDetail.validation.attempts', {
|
|
264
|
+
attempts: validationReport.attempts,
|
|
265
|
+
maxAttempts: validationReport.maxAttempts,
|
|
266
|
+
})
|
|
267
|
+
}}
|
|
268
|
+
</span>
|
|
269
|
+
<UIcon
|
|
270
|
+
:name="validationOpen ? 'i-lucide-chevron-down' : 'i-lucide-chevron-up'"
|
|
271
|
+
class="ms-auto h-3.5 w-3.5 shrink-0 text-slate-500"
|
|
272
|
+
/>
|
|
273
|
+
</button>
|
|
274
|
+
<div v-if="validationOpen" class="max-h-72 overflow-y-auto px-5 pb-3">
|
|
275
|
+
<StepValidationReport :report="validationReport" />
|
|
276
|
+
</div>
|
|
277
|
+
</section>
|
|
207
278
|
</div>
|
|
208
279
|
</div>
|
|
209
280
|
</Teleport>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import type { ValidationReport } from '~/types/validationChecks'
|
|
4
|
+
|
|
5
|
+
// The PRE-PR VALIDATION report for a step: which of the service's configured check commands
|
|
6
|
+
// passed against the checkout before the PR was allowed to open, their exit codes, and a
|
|
7
|
+
// bounded, secret-scrubbed tail of each command's output. Produced by the executor-harness
|
|
8
|
+
// running the commands — never a model self-report — so this is the run's captured PROOF on a
|
|
9
|
+
// green run and its EVIDENCE on a failed one. See docs/initiatives/pre-pr-validation.md.
|
|
10
|
+
const props = defineProps<{ report: ValidationReport }>()
|
|
11
|
+
const { t } = useI18n()
|
|
12
|
+
|
|
13
|
+
const failed = computed(() => props.report.outcomes.filter((o) => !o.passed))
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<template>
|
|
17
|
+
<div class="space-y-2" data-testid="step-validation-report">
|
|
18
|
+
<p class="text-[11px] text-slate-400">
|
|
19
|
+
{{
|
|
20
|
+
report.passed
|
|
21
|
+
? t('panels.stepDetail.validation.passedSummary', {
|
|
22
|
+
total: report.outcomes.length,
|
|
23
|
+
attempts: report.attempts,
|
|
24
|
+
})
|
|
25
|
+
: t('panels.stepDetail.validation.failedSummary', {
|
|
26
|
+
failed: failed.length,
|
|
27
|
+
total: report.outcomes.length,
|
|
28
|
+
attempts: report.attempts,
|
|
29
|
+
maxAttempts: report.maxAttempts,
|
|
30
|
+
})
|
|
31
|
+
}}
|
|
32
|
+
</p>
|
|
33
|
+
|
|
34
|
+
<div
|
|
35
|
+
v-for="outcome in report.outcomes"
|
|
36
|
+
:key="outcome.label"
|
|
37
|
+
class="rounded-md border border-slate-800 bg-slate-950/40 p-2"
|
|
38
|
+
data-testid="validation-outcome"
|
|
39
|
+
>
|
|
40
|
+
<div class="flex items-center gap-2">
|
|
41
|
+
<UIcon
|
|
42
|
+
:name="outcome.passed ? 'i-lucide-check' : 'i-lucide-x'"
|
|
43
|
+
class="h-3.5 w-3.5 shrink-0"
|
|
44
|
+
:class="outcome.passed ? 'text-emerald-400' : 'text-rose-400'"
|
|
45
|
+
/>
|
|
46
|
+
<span class="text-[12px] font-medium text-slate-200">{{ outcome.label }}</span>
|
|
47
|
+
<span class="truncate font-mono text-[11px] text-slate-500">{{ outcome.command }}</span>
|
|
48
|
+
<span
|
|
49
|
+
v-if="!outcome.passed"
|
|
50
|
+
class="ms-auto shrink-0 rounded bg-rose-500/15 px-1.5 py-0.5 text-[11px] tabular-nums text-rose-300"
|
|
51
|
+
>
|
|
52
|
+
{{
|
|
53
|
+
outcome.timedOut
|
|
54
|
+
? t('panels.stepDetail.validation.timedOut')
|
|
55
|
+
: t('panels.stepDetail.validation.exitCode', { code: outcome.exitCode })
|
|
56
|
+
}}
|
|
57
|
+
</span>
|
|
58
|
+
</div>
|
|
59
|
+
<pre
|
|
60
|
+
v-if="outcome.outputTail"
|
|
61
|
+
class="mt-1.5 max-h-48 overflow-auto whitespace-pre-wrap break-words rounded bg-slate-950 p-2 font-mono text-[11px] text-slate-400"
|
|
62
|
+
data-testid="validation-output"
|
|
63
|
+
>{{ outcome.outputTail }}</pre>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
</template>
|
|
@@ -255,7 +255,15 @@ async function generateOrFixManifest() {
|
|
|
255
255
|
// it failed at. The returned run is tracked live (by frame id) via the workspace stream store.
|
|
256
256
|
const envTest = useEnvironmentTestStore()
|
|
257
257
|
const envTestStarting = ref(false)
|
|
258
|
-
|
|
258
|
+
// The self-test's start/stop error. Structured (not a bare string) so the not-provisionable case
|
|
259
|
+
// can render its remedy prose PLUS a one-click jump to the environment-handler config; every other
|
|
260
|
+
// case is plain text.
|
|
261
|
+
interface EnvTestError {
|
|
262
|
+
text: string
|
|
263
|
+
/** Show the "Configure infrastructure" deep-link — only the not-provisionable handler case. */
|
|
264
|
+
configurable?: boolean
|
|
265
|
+
}
|
|
266
|
+
const envTestError = ref<EnvTestError | null>(null)
|
|
259
267
|
// The newest self-test run for this frame — re-attaches after a reconnect (the run is carried in
|
|
260
268
|
// the snapshot while running), so the live stage keeps showing without a locally-held id.
|
|
261
269
|
const envTestRun = computed(() => envTest.runForBlock(props.block.id))
|
|
@@ -291,14 +299,30 @@ const ENV_TEST_CONFLICT_KEYS: Record<Extract<ConflictReason, `env_test_${string}
|
|
|
291
299
|
env_test_no_vcs: 'errors.conflict.title.env_test_no_vcs',
|
|
292
300
|
}
|
|
293
301
|
|
|
294
|
-
function
|
|
295
|
-
const
|
|
302
|
+
function buildEnvTestError(e: unknown): EnvTestError {
|
|
303
|
+
const parsed = parseConflict(e)
|
|
304
|
+
const reason = parsed?.reason
|
|
305
|
+
// No workspace handler resolves for the service's provision type. Word the SPECIFIC case —
|
|
306
|
+
// nothing configured vs. an ambiguous match (carried on `details.handlerIssue`, distinct from the
|
|
307
|
+
// `env_test_not_provisionable` code) — and offer the one-click jump to the Infrastructure →
|
|
308
|
+
// Test-environments handler config.
|
|
309
|
+
if (reason === 'env_test_not_provisionable') {
|
|
310
|
+
const ambiguous = parsed?.details.handlerIssue === 'type-mismatch'
|
|
311
|
+
return {
|
|
312
|
+
text: t(
|
|
313
|
+
ambiguous
|
|
314
|
+
? 'errors.conflict.description.env_test_not_provisionable_type_mismatch'
|
|
315
|
+
: 'errors.conflict.description.env_test_not_provisionable_no_handler',
|
|
316
|
+
),
|
|
317
|
+
configurable: true,
|
|
318
|
+
}
|
|
319
|
+
}
|
|
296
320
|
const key =
|
|
297
321
|
reason && reason in ENV_TEST_CONFLICT_KEYS
|
|
298
322
|
? ENV_TEST_CONFLICT_KEYS[reason as keyof typeof ENV_TEST_CONFLICT_KEYS]
|
|
299
323
|
: undefined
|
|
300
|
-
if (key && te(key)) return t(key)
|
|
301
|
-
return apiErrorEnvelope(e)?.message ?? (e instanceof Error ? e.message : String(e))
|
|
324
|
+
if (key && te(key)) return { text: t(key) }
|
|
325
|
+
return { text: apiErrorEnvelope(e)?.message ?? (e instanceof Error ? e.message : String(e)) }
|
|
302
326
|
}
|
|
303
327
|
|
|
304
328
|
async function startEnvTest() {
|
|
@@ -308,7 +332,7 @@ async function startEnvTest() {
|
|
|
308
332
|
try {
|
|
309
333
|
await envTest.start(props.block.id)
|
|
310
334
|
} catch (e) {
|
|
311
|
-
envTestError.value =
|
|
335
|
+
envTestError.value = buildEnvTestError(e)
|
|
312
336
|
} finally {
|
|
313
337
|
envTestStarting.value = false
|
|
314
338
|
}
|
|
@@ -320,7 +344,7 @@ async function stopEnvTest() {
|
|
|
320
344
|
try {
|
|
321
345
|
await envTest.stop(run.id)
|
|
322
346
|
} catch (e) {
|
|
323
|
-
envTestError.value =
|
|
347
|
+
envTestError.value = buildEnvTestError(e)
|
|
324
348
|
}
|
|
325
349
|
}
|
|
326
350
|
|
|
@@ -1072,9 +1096,23 @@ function setSize(value: InstanceSize) {
|
|
|
1072
1096
|
</template>
|
|
1073
1097
|
</p>
|
|
1074
1098
|
|
|
1075
|
-
<
|
|
1076
|
-
{{ envTestError }}
|
|
1077
|
-
|
|
1099
|
+
<div v-if="envTestError" class="text-[11px] text-rose-400" data-testid="env-test-error">
|
|
1100
|
+
<p>{{ envTestError.text }}</p>
|
|
1101
|
+
<!-- Only the not-provisionable handler case is one-click fixable: jump to Infrastructure →
|
|
1102
|
+
Test environments, where the workspace's per-type environment handler is registered. -->
|
|
1103
|
+
<UButton
|
|
1104
|
+
v-if="envTestError.configurable"
|
|
1105
|
+
class="mt-1.5"
|
|
1106
|
+
icon="i-lucide-settings"
|
|
1107
|
+
size="xs"
|
|
1108
|
+
color="neutral"
|
|
1109
|
+
variant="soft"
|
|
1110
|
+
data-testid="env-test-configure-handler"
|
|
1111
|
+
@click="ui.openProviderConnection('environment')"
|
|
1112
|
+
>
|
|
1113
|
+
{{ t('errors.conflict.action.configureInfrastructure') }}
|
|
1114
|
+
</UButton>
|
|
1115
|
+
</div>
|
|
1078
1116
|
</div>
|
|
1079
1117
|
</InspectorSection>
|
|
1080
1118
|
</template>
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref, watch } from 'vue'
|
|
3
|
+
import type { Block } from '~/types/domain'
|
|
4
|
+
import InspectorSection from '~/components/panels/inspector/InspectorSection.vue'
|
|
5
|
+
import {
|
|
6
|
+
VALIDATION_DEFAULT_MAX_ATTEMPTS,
|
|
7
|
+
VALIDATION_MAX_ATTEMPTS_CEILING,
|
|
8
|
+
VALIDATION_MAX_CHECKS,
|
|
9
|
+
type ValidationCheck,
|
|
10
|
+
} from '~/types/validationChecks'
|
|
11
|
+
|
|
12
|
+
// Per-service (frame) PRE-PR VALIDATION CHECKS: the shell commands the executor-harness runs
|
|
13
|
+
// against the checkout after the coder settles and BEFORE the PR opens. A failing command's
|
|
14
|
+
// output is handed back to the agent, which retries under the attempt budget; only a green
|
|
15
|
+
// checkout opens a PR, and an exhausted budget fails the step with the captured output.
|
|
16
|
+
// Keyed by THIS block's id (service frames only — a task resolves its checks up the frame chain).
|
|
17
|
+
const props = defineProps<{ block: Block }>()
|
|
18
|
+
|
|
19
|
+
const store = useValidationChecksStore()
|
|
20
|
+
const toast = useToast()
|
|
21
|
+
const { t } = useI18n()
|
|
22
|
+
const { confirmAction, toastDone } = useConfirmAction()
|
|
23
|
+
|
|
24
|
+
const busy = ref(false)
|
|
25
|
+
const rows = ref<ValidationCheck[]>([])
|
|
26
|
+
const maxAttempts = ref(VALIDATION_DEFAULT_MAX_ATTEMPTS)
|
|
27
|
+
|
|
28
|
+
const saved = computed(() => store.forBlock(props.block.id))
|
|
29
|
+
const configured = computed(() => saved.value.checks.length > 0)
|
|
30
|
+
const canAdd = computed(() => rows.value.length < VALIDATION_MAX_CHECKS)
|
|
31
|
+
/** A row is only submittable once it has a command; the label falls back to the command. */
|
|
32
|
+
const submittable = computed(() =>
|
|
33
|
+
rows.value
|
|
34
|
+
.map((r) => ({ label: r.label.trim() || r.command.trim(), command: r.command.trim() }))
|
|
35
|
+
.filter((r) => r.command.length > 0),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
onMounted(() => {
|
|
39
|
+
store.ensureLoaded().catch(() => {})
|
|
40
|
+
})
|
|
41
|
+
watch(
|
|
42
|
+
saved,
|
|
43
|
+
(config) => {
|
|
44
|
+
rows.value = config.checks.map((c) => ({ ...c }))
|
|
45
|
+
maxAttempts.value = config.maxAttempts
|
|
46
|
+
},
|
|
47
|
+
{ immediate: true },
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
function addRow() {
|
|
51
|
+
if (canAdd.value) rows.value.push({ label: '', command: '' })
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function removeRow(index: number) {
|
|
55
|
+
rows.value.splice(index, 1)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function notifyError(title: string, e: unknown) {
|
|
59
|
+
toast.add({
|
|
60
|
+
title,
|
|
61
|
+
description: e instanceof Error ? e.message : String(e),
|
|
62
|
+
icon: 'i-lucide-triangle-alert',
|
|
63
|
+
color: 'error',
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function save() {
|
|
68
|
+
busy.value = true
|
|
69
|
+
try {
|
|
70
|
+
await store.save(props.block.id, submittable.value, maxAttempts.value)
|
|
71
|
+
toast.add({
|
|
72
|
+
title: t('inspector.validationChecks.savedToast'),
|
|
73
|
+
icon: 'i-lucide-check',
|
|
74
|
+
color: 'success',
|
|
75
|
+
})
|
|
76
|
+
} catch (e) {
|
|
77
|
+
notifyError(t('inspector.validationChecks.saveFailed'), e)
|
|
78
|
+
} finally {
|
|
79
|
+
busy.value = false
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function clear() {
|
|
84
|
+
const noun = t('inspector.validationChecks.configNoun')
|
|
85
|
+
if (!(await confirmAction('clear', noun))) return
|
|
86
|
+
busy.value = true
|
|
87
|
+
try {
|
|
88
|
+
await store.remove(props.block.id)
|
|
89
|
+
rows.value = []
|
|
90
|
+
toastDone('clear', noun)
|
|
91
|
+
} catch (e) {
|
|
92
|
+
notifyError(t('inspector.validationChecks.clearFailed'), e)
|
|
93
|
+
} finally {
|
|
94
|
+
busy.value = false
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
</script>
|
|
98
|
+
|
|
99
|
+
<template>
|
|
100
|
+
<InspectorSection
|
|
101
|
+
v-if="store.available !== false"
|
|
102
|
+
:title="t('inspector.validationChecks.title')"
|
|
103
|
+
:hint="t('inspector.validationChecks.sectionHint')"
|
|
104
|
+
data-testid="service-validation-config"
|
|
105
|
+
>
|
|
106
|
+
<template #actions>
|
|
107
|
+
<UButton
|
|
108
|
+
v-if="configured"
|
|
109
|
+
color="error"
|
|
110
|
+
variant="ghost"
|
|
111
|
+
size="xs"
|
|
112
|
+
icon="i-lucide-trash-2"
|
|
113
|
+
:loading="busy"
|
|
114
|
+
data-testid="validation-clear"
|
|
115
|
+
@click="clear"
|
|
116
|
+
>
|
|
117
|
+
{{ t('inspector.validationChecks.clear') }}
|
|
118
|
+
</UButton>
|
|
119
|
+
</template>
|
|
120
|
+
|
|
121
|
+
<div class="space-y-2">
|
|
122
|
+
<p class="text-[11px] text-slate-500">
|
|
123
|
+
{{ t('inspector.validationChecks.hint') }}
|
|
124
|
+
</p>
|
|
125
|
+
|
|
126
|
+
<div
|
|
127
|
+
v-for="(row, index) in rows"
|
|
128
|
+
:key="index"
|
|
129
|
+
class="flex items-end gap-2"
|
|
130
|
+
data-testid="validation-check-row"
|
|
131
|
+
>
|
|
132
|
+
<UFormField :label="t('inspector.validationChecks.label')" class="w-1/3">
|
|
133
|
+
<UInput
|
|
134
|
+
v-model="row.label"
|
|
135
|
+
:placeholder="t('inspector.validationChecks.labelPlaceholder')"
|
|
136
|
+
size="sm"
|
|
137
|
+
class="w-full"
|
|
138
|
+
data-testid="validation-check-label"
|
|
139
|
+
/>
|
|
140
|
+
</UFormField>
|
|
141
|
+
<UFormField :label="t('inspector.validationChecks.command')" class="flex-1">
|
|
142
|
+
<UInput
|
|
143
|
+
v-model="row.command"
|
|
144
|
+
placeholder="pnpm lint"
|
|
145
|
+
size="sm"
|
|
146
|
+
class="w-full"
|
|
147
|
+
data-testid="validation-check-command"
|
|
148
|
+
/>
|
|
149
|
+
</UFormField>
|
|
150
|
+
<UButton
|
|
151
|
+
color="neutral"
|
|
152
|
+
variant="ghost"
|
|
153
|
+
size="xs"
|
|
154
|
+
icon="i-lucide-x"
|
|
155
|
+
:aria-label="t('inspector.validationChecks.removeCheck')"
|
|
156
|
+
data-testid="validation-check-remove"
|
|
157
|
+
@click="removeRow(index)"
|
|
158
|
+
/>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
<p v-if="rows.length === 0" class="text-[11px] text-slate-500" data-testid="validation-empty">
|
|
162
|
+
{{ t('inspector.validationChecks.empty') }}
|
|
163
|
+
</p>
|
|
164
|
+
|
|
165
|
+
<div class="flex items-end justify-between gap-2">
|
|
166
|
+
<UFormField
|
|
167
|
+
:label="t('inspector.validationChecks.maxAttempts')"
|
|
168
|
+
:hint="t('inspector.validationChecks.maxAttemptsHint')"
|
|
169
|
+
class="w-40"
|
|
170
|
+
>
|
|
171
|
+
<UInput
|
|
172
|
+
v-model.number="maxAttempts"
|
|
173
|
+
type="number"
|
|
174
|
+
:min="1"
|
|
175
|
+
:max="VALIDATION_MAX_ATTEMPTS_CEILING"
|
|
176
|
+
size="sm"
|
|
177
|
+
class="w-full"
|
|
178
|
+
data-testid="validation-max-attempts"
|
|
179
|
+
/>
|
|
180
|
+
</UFormField>
|
|
181
|
+
<div class="flex gap-2">
|
|
182
|
+
<UButton
|
|
183
|
+
color="neutral"
|
|
184
|
+
variant="soft"
|
|
185
|
+
size="xs"
|
|
186
|
+
icon="i-lucide-plus"
|
|
187
|
+
:disabled="!canAdd"
|
|
188
|
+
data-testid="validation-add-check"
|
|
189
|
+
@click="addRow"
|
|
190
|
+
>
|
|
191
|
+
{{ t('inspector.validationChecks.addCheck') }}
|
|
192
|
+
</UButton>
|
|
193
|
+
<UButton
|
|
194
|
+
color="primary"
|
|
195
|
+
variant="soft"
|
|
196
|
+
size="xs"
|
|
197
|
+
icon="i-lucide-save"
|
|
198
|
+
:loading="busy"
|
|
199
|
+
data-testid="validation-save"
|
|
200
|
+
@click="save"
|
|
201
|
+
>
|
|
202
|
+
{{ t('inspector.validationChecks.save') }}
|
|
203
|
+
</UButton>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
</InspectorSection>
|
|
208
|
+
</template>
|
|
@@ -11,10 +11,12 @@ import CopyButton from '~/components/common/CopyButton.vue'
|
|
|
11
11
|
|
|
12
12
|
const { t, d } = useI18n()
|
|
13
13
|
|
|
14
|
-
// The permission ladder a minted key can carry (read ⊂ write ⊂ admin), mirroring the
|
|
15
|
-
// contract. A `read` key can only observe; `write` adds create/start/manage; `
|
|
16
|
-
//
|
|
17
|
-
|
|
14
|
+
// The permission ladder a minted key can carry (read ⊂ write ⊂ decide ⊂ admin), mirroring the
|
|
15
|
+
// backend contract. A `read` key can only observe; `write` adds create/start/manage; `decide` adds
|
|
16
|
+
// answering a run's PARKED human decisions (and, because of that, starting a headless run on a
|
|
17
|
+
// pipeline that can park at all); `admin` adds the destructive/merge-adjacent operations (e.g.
|
|
18
|
+
// deleting a task).
|
|
19
|
+
const SCOPES: PublicApiScope[] = ['read', 'write', 'decide', 'admin']
|
|
18
20
|
|
|
19
21
|
/** Localized label for a scope — an exhaustive switch, so a new scope is a compile error here. */
|
|
20
22
|
function scopeLabel(scope: PublicApiScope): string {
|
|
@@ -23,6 +25,8 @@ function scopeLabel(scope: PublicApiScope): string {
|
|
|
23
25
|
return t('settings.apiTokens.scopes.read')
|
|
24
26
|
case 'write':
|
|
25
27
|
return t('settings.apiTokens.scopes.write')
|
|
28
|
+
case 'decide':
|
|
29
|
+
return t('settings.apiTokens.scopes.decide')
|
|
26
30
|
case 'admin':
|
|
27
31
|
return t('settings.apiTokens.scopes.admin')
|
|
28
32
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {
|
|
2
|
+
deleteServiceValidationConfigContract,
|
|
3
|
+
getServiceValidationConfigContract,
|
|
4
|
+
listServiceValidationConfigsContract,
|
|
5
|
+
setServiceValidationConfigContract,
|
|
6
|
+
} from '@cat-factory/contracts'
|
|
7
|
+
import type { UpsertServiceValidationConfigInput } from '~/types/validationChecks'
|
|
8
|
+
import type { ApiContext } from './context'
|
|
9
|
+
|
|
10
|
+
/** Pre-PR validation checks: the per-service-frame commands the harness runs before opening a PR. */
|
|
11
|
+
export function validationChecksApi({ send, ws }: ApiContext) {
|
|
12
|
+
return {
|
|
13
|
+
listServiceValidationConfigs: (workspaceId: string) =>
|
|
14
|
+
send(listServiceValidationConfigsContract, { pathPrefix: ws(workspaceId) }),
|
|
15
|
+
|
|
16
|
+
getServiceValidationConfig: (workspaceId: string, blockId: string) =>
|
|
17
|
+
send(getServiceValidationConfigContract, {
|
|
18
|
+
pathPrefix: ws(workspaceId),
|
|
19
|
+
pathParams: { blockId },
|
|
20
|
+
}),
|
|
21
|
+
|
|
22
|
+
setServiceValidationConfig: (
|
|
23
|
+
workspaceId: string,
|
|
24
|
+
blockId: string,
|
|
25
|
+
body: UpsertServiceValidationConfigInput,
|
|
26
|
+
) =>
|
|
27
|
+
send(setServiceValidationConfigContract, {
|
|
28
|
+
pathPrefix: ws(workspaceId),
|
|
29
|
+
pathParams: { blockId },
|
|
30
|
+
body,
|
|
31
|
+
}),
|
|
32
|
+
|
|
33
|
+
deleteServiceValidationConfig: (workspaceId: string, blockId: string) =>
|
|
34
|
+
send(deleteServiceValidationConfigContract, {
|
|
35
|
+
pathPrefix: ws(workspaceId),
|
|
36
|
+
pathParams: { blockId },
|
|
37
|
+
}),
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -35,6 +35,7 @@ import { recurringApi } from './api/recurring'
|
|
|
35
35
|
import { previewApi } from './api/preview'
|
|
36
36
|
import { environmentsApi } from './api/environments'
|
|
37
37
|
import { releaseHealthApi } from './api/releaseHealth'
|
|
38
|
+
import { validationChecksApi } from './api/validationChecks'
|
|
38
39
|
import { sandboxApi } from './api/sandbox'
|
|
39
40
|
import { reviewsApi } from './api/reviews'
|
|
40
41
|
import { slackApi } from './api/slack'
|
|
@@ -135,6 +136,7 @@ export function useApi() {
|
|
|
135
136
|
...docInterviewApi(ctx),
|
|
136
137
|
...provisioningLogsApi(ctx),
|
|
137
138
|
...releaseHealthApi(ctx),
|
|
139
|
+
...validationChecksApi(ctx),
|
|
138
140
|
...testSecretsApi(ctx),
|
|
139
141
|
...packageRegistriesApi(ctx),
|
|
140
142
|
...previewApi(ctx),
|
|
@@ -41,6 +41,7 @@ describe('inspector panel group', () => {
|
|
|
41
41
|
'service-test-secrets',
|
|
42
42
|
'service-fragments',
|
|
43
43
|
'service-release-health',
|
|
44
|
+
'service-validation-checks',
|
|
44
45
|
])
|
|
45
46
|
})
|
|
46
47
|
|
|
@@ -59,6 +60,7 @@ describe('inspector panel group', () => {
|
|
|
59
60
|
'service-test-secrets',
|
|
60
61
|
'service-fragments',
|
|
61
62
|
'service-release-health',
|
|
63
|
+
'service-validation-checks',
|
|
62
64
|
])
|
|
63
65
|
})
|
|
64
66
|
|
|
@@ -70,6 +72,7 @@ describe('inspector panel group', () => {
|
|
|
70
72
|
'service-test-secrets',
|
|
71
73
|
'service-fragments',
|
|
72
74
|
'service-release-health',
|
|
75
|
+
'service-validation-checks',
|
|
73
76
|
])
|
|
74
77
|
})
|
|
75
78
|
|
|
@@ -53,6 +53,7 @@ export const INSPECTOR_PANEL_IDS = [
|
|
|
53
53
|
'service-test-secrets',
|
|
54
54
|
'service-fragments',
|
|
55
55
|
'service-release-health',
|
|
56
|
+
'service-validation-checks',
|
|
56
57
|
// epic / initiative body
|
|
57
58
|
'epic-children',
|
|
58
59
|
'initiative-inspector',
|
|
@@ -118,6 +119,10 @@ export const INSPECTOR_PANEL_SPECS: readonly InspectorPanelSpec[] = [
|
|
|
118
119
|
{ id: 'service-test-secrets', order: 150, when: isDeployableFrame },
|
|
119
120
|
{ id: 'service-fragments', order: 160, when: isFrame },
|
|
120
121
|
{ id: 'service-release-health', order: 170, when: isDeployableFrame },
|
|
122
|
+
// Pre-PR validation checks: the commands the harness runs before opening this service's PRs.
|
|
123
|
+
// Deployable frames only — a doc repo ships no build to install/lint/test, exactly like the
|
|
124
|
+
// test-infra and release-health panels above it.
|
|
125
|
+
{ id: 'service-validation-checks', order: 180, when: isDeployableFrame },
|
|
121
126
|
{ id: 'epic-children', order: 200, when: (b) => b.level === 'epic' },
|
|
122
127
|
{ id: 'initiative-inspector', order: 210, when: (b) => b.level === 'initiative' },
|
|
123
128
|
]
|
|
@@ -28,6 +28,7 @@ import ServiceTestConfig from '~/components/panels/inspector/ServiceTestConfig.v
|
|
|
28
28
|
import ServiceTestSecrets from '~/components/panels/inspector/ServiceTestSecrets.vue'
|
|
29
29
|
import ServiceFragments from '~/components/panels/inspector/ServiceFragments.vue'
|
|
30
30
|
import ServiceReleaseHealthConfig from '~/components/panels/inspector/ServiceReleaseHealthConfig.vue'
|
|
31
|
+
import ServiceValidationConfig from '~/components/panels/inspector/ServiceValidationConfig.vue'
|
|
31
32
|
import EpicChildren from '~/components/panels/inspector/EpicChildren.vue'
|
|
32
33
|
import InitiativeInspector from '~/components/panels/inspector/InitiativeInspector.vue'
|
|
33
34
|
|
|
@@ -80,6 +81,7 @@ const COMPONENTS: Record<InspectorPanelId, Component> = {
|
|
|
80
81
|
'service-test-secrets': ServiceTestSecrets,
|
|
81
82
|
'service-fragments': ServiceFragments,
|
|
82
83
|
'service-release-health': ServiceReleaseHealthConfig,
|
|
84
|
+
'service-validation-checks': ServiceValidationConfig,
|
|
83
85
|
'epic-children': EpicChildren,
|
|
84
86
|
'initiative-inspector': InitiativeInspector,
|
|
85
87
|
}
|