@cat-factory/app 0.296.6 → 0.297.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/inspector/EnvProbeReport.vue +176 -0
- package/app/components/panels/inspector/ServiceSelfTests.vue +371 -0
- package/app/components/panels/inspector/ServiceTestConfig.vue +6 -202
- package/app/composables/api/environments.ts +9 -4
- package/app/composables/usePipelineErrorToast.ts +19 -0
- package/app/stores/environmentTest.spec.ts +35 -8
- package/app/stores/environmentTest.ts +21 -10
- package/app/types/domain.ts +1 -0
- package/i18n/locales/de.json +43 -1
- package/i18n/locales/en.json +58 -1
- package/i18n/locales/es.json +43 -1
- package/i18n/locales/fr.json +43 -1
- package/i18n/locales/he.json +43 -1
- package/i18n/locales/it.json +43 -1
- package/i18n/locales/ja.json +43 -1
- package/i18n/locales/pl.json +43 -1
- package/i18n/locales/tr.json +43 -1
- package/i18n/locales/uk.json +43 -1
- package/package.json +2 -2
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import type {
|
|
4
|
+
EnvironmentProbeFailure,
|
|
5
|
+
EnvironmentProbeReport,
|
|
6
|
+
EnvironmentProbeVerdict,
|
|
7
|
+
} from '@cat-factory/contracts'
|
|
8
|
+
|
|
9
|
+
// What the AGENT DRY RUN found, as the inspector renders it.
|
|
10
|
+
//
|
|
11
|
+
// The order is deliberate and is the order a developer acts in: the VERDICT (can an agent work
|
|
12
|
+
// here at all), then what the platform failed to tell the agent (the thing to go and fix), then
|
|
13
|
+
// the operations as evidence, then the agent's own prose last. `missingContext` sits above the
|
|
14
|
+
// operation list rather than below it because it is the product of the diagnostic: an operator who
|
|
15
|
+
// reads only the first two lines has read the actionable part.
|
|
16
|
+
const props = defineProps<{ report: EnvironmentProbeReport }>()
|
|
17
|
+
|
|
18
|
+
const { t, te } = useI18n()
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Per-verdict label keys, exhaustive over the contracts union: a new verdict fails THIS
|
|
22
|
+
* typecheck until it has copy. (The lookup is dynamic, so the typed-message-keys check cannot see
|
|
23
|
+
* it; the map's exhaustiveness is the drift guard, as with the stage keys.)
|
|
24
|
+
*/
|
|
25
|
+
const VERDICT_KEYS: Record<EnvironmentProbeVerdict, string> = {
|
|
26
|
+
operable: 'inspector.testConfig.envProbe.verdict.operable',
|
|
27
|
+
partially_operable: 'inspector.testConfig.envProbe.verdict.partially_operable',
|
|
28
|
+
inoperable: 'inspector.testConfig.envProbe.verdict.inoperable',
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const VERDICT_CLASS: Record<EnvironmentProbeVerdict, string> = {
|
|
32
|
+
operable: 'text-emerald-300/90',
|
|
33
|
+
// Amber, not rose: the LIFECYCLE succeeded and the finding is partial. Colouring it as a
|
|
34
|
+
// failure would make it read as the run breaking rather than as the diagnostic working.
|
|
35
|
+
partially_operable: 'text-amber-300/90',
|
|
36
|
+
inoperable: 'text-rose-300/90',
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Per-failure-kind labels, exhaustive over the contracts vocabulary. Rendered through a `te`
|
|
41
|
+
* guard: the kinds are a CLOSED and PERSISTED vocabulary, so a report stored before a member was
|
|
42
|
+
* renamed must still render honestly (the raw value) rather than as an empty span.
|
|
43
|
+
*/
|
|
44
|
+
const FAILURE_KEYS: Record<EnvironmentProbeFailure, string> = {
|
|
45
|
+
auth_missing: 'inspector.testConfig.envProbe.failure.auth_missing',
|
|
46
|
+
auth_rejected: 'inspector.testConfig.envProbe.failure.auth_rejected',
|
|
47
|
+
access_unclear: 'inspector.testConfig.envProbe.failure.access_unclear',
|
|
48
|
+
endpoint_unknown: 'inspector.testConfig.envProbe.failure.endpoint_unknown',
|
|
49
|
+
unreachable: 'inspector.testConfig.envProbe.failure.unreachable',
|
|
50
|
+
timeout: 'inspector.testConfig.envProbe.failure.timeout',
|
|
51
|
+
server_error: 'inspector.testConfig.envProbe.failure.server_error',
|
|
52
|
+
bad_request: 'inspector.testConfig.envProbe.failure.bad_request',
|
|
53
|
+
tooling_missing: 'inspector.testConfig.envProbe.failure.tooling_missing',
|
|
54
|
+
other: 'inspector.testConfig.envProbe.failure.other',
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function failureLabel(kind: EnvironmentProbeFailure): string {
|
|
58
|
+
const key = FAILURE_KEYS[kind]
|
|
59
|
+
return key && te(key) ? t(key) : kind
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const verdictLabel = computed(() => {
|
|
63
|
+
const key = VERDICT_KEYS[props.report.verdict]
|
|
64
|
+
return key && te(key) ? t(key) : props.report.verdict
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
const verdictClass = computed(() => VERDICT_CLASS[props.report.verdict] ?? 'text-slate-300')
|
|
68
|
+
|
|
69
|
+
/** The icon per operation outcome. `not_attempted` is its own mark, never a failure's. */
|
|
70
|
+
function outcomeIcon(outcome: EnvironmentProbeReport['operations'][number]['outcome']): string {
|
|
71
|
+
if (outcome === 'succeeded') return 'i-lucide-check'
|
|
72
|
+
if (outcome === 'failed') return 'i-lucide-x'
|
|
73
|
+
return 'i-lucide-minus'
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function outcomeClass(outcome: EnvironmentProbeReport['operations'][number]['outcome']): string {
|
|
77
|
+
if (outcome === 'succeeded') return 'text-emerald-400'
|
|
78
|
+
if (outcome === 'failed') return 'text-rose-400'
|
|
79
|
+
return 'text-slate-500'
|
|
80
|
+
}
|
|
81
|
+
</script>
|
|
82
|
+
|
|
83
|
+
<template>
|
|
84
|
+
<div
|
|
85
|
+
class="space-y-2 rounded border border-white/5 bg-white/[0.02] p-2"
|
|
86
|
+
data-testid="env-probe-report"
|
|
87
|
+
>
|
|
88
|
+
<p class="text-[11px] font-medium" :class="verdictClass" data-testid="env-probe-verdict">
|
|
89
|
+
{{ verdictLabel }}
|
|
90
|
+
</p>
|
|
91
|
+
|
|
92
|
+
<!-- The tallies the PLATFORM computed from the agent's per-operation judgements, including
|
|
93
|
+
how many successes were behind authentication: the count the verdict turns on. -->
|
|
94
|
+
<p class="text-[11px] text-slate-400">
|
|
95
|
+
{{
|
|
96
|
+
t('inspector.testConfig.envProbe.counts', {
|
|
97
|
+
succeeded: report.succeeded,
|
|
98
|
+
attempted: report.attempted,
|
|
99
|
+
authenticated: report.authenticatedSucceeded,
|
|
100
|
+
})
|
|
101
|
+
}}
|
|
102
|
+
</p>
|
|
103
|
+
|
|
104
|
+
<!-- What the platform did not tell the agent: the actionable half of the report, so it comes
|
|
105
|
+
before the evidence. -->
|
|
106
|
+
<div v-if="report.missingContext.length" data-testid="env-probe-missing">
|
|
107
|
+
<p class="text-[11px] font-medium text-amber-300/90">
|
|
108
|
+
{{ t('inspector.testConfig.envProbe.missingContext') }}
|
|
109
|
+
</p>
|
|
110
|
+
<ul class="mt-0.5 list-disc space-y-0.5 pl-4 text-[11px] text-slate-300">
|
|
111
|
+
<li v-for="(line, i) in report.missingContext" :key="i">{{ line }}</li>
|
|
112
|
+
</ul>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<div v-if="report.blockers.length" data-testid="env-probe-blockers">
|
|
116
|
+
<p class="text-[11px] font-medium text-rose-300/90">
|
|
117
|
+
{{ t('inspector.testConfig.envProbe.blockers') }}
|
|
118
|
+
</p>
|
|
119
|
+
<ul class="mt-0.5 space-y-0.5 text-[11px] text-slate-300">
|
|
120
|
+
<li v-for="(blocker, i) in report.blockers" :key="i">
|
|
121
|
+
<span class="text-slate-400">{{ failureLabel(blocker.kind) }}:</span>
|
|
122
|
+
{{ blocker.detail }}
|
|
123
|
+
</li>
|
|
124
|
+
</ul>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<!-- The operations, as evidence for the verdict above. This is the "what was attempted" a dry
|
|
128
|
+
run exists to report: without it a verdict is an opinion. -->
|
|
129
|
+
<div v-if="report.operations.length" data-testid="env-probe-operations">
|
|
130
|
+
<p class="text-[11px] font-medium text-slate-300">
|
|
131
|
+
{{ t('inspector.testConfig.envProbe.operations') }}
|
|
132
|
+
</p>
|
|
133
|
+
<ul class="mt-0.5 space-y-1 text-[11px]">
|
|
134
|
+
<li v-for="(op, i) in report.operations" :key="i" class="flex items-start gap-1.5">
|
|
135
|
+
<UIcon
|
|
136
|
+
:name="outcomeIcon(op.outcome)"
|
|
137
|
+
class="mt-0.5 size-3 shrink-0"
|
|
138
|
+
:class="outcomeClass(op.outcome)"
|
|
139
|
+
/>
|
|
140
|
+
<span class="min-w-0">
|
|
141
|
+
<span class="text-slate-200">{{ op.name }}</span>
|
|
142
|
+
<span v-if="op.authenticated" class="ml-1 text-slate-500">
|
|
143
|
+
{{ t('inspector.testConfig.envProbe.authenticated') }}
|
|
144
|
+
</span>
|
|
145
|
+
<span v-if="op.target" class="ml-1 font-mono text-slate-500">{{ op.target }}</span>
|
|
146
|
+
<span v-if="op.failure" class="block text-rose-300/70">
|
|
147
|
+
{{ failureLabel(op.failure) }}<template v-if="op.detail">: {{ op.detail }}</template>
|
|
148
|
+
</span>
|
|
149
|
+
<span v-else-if="op.detail" class="block text-slate-400">{{ op.detail }}</span>
|
|
150
|
+
</span>
|
|
151
|
+
</li>
|
|
152
|
+
</ul>
|
|
153
|
+
<!-- Every cap records what it dropped: a reader must never take a truncated list for the
|
|
154
|
+
whole attempt. The two ways an operation fails to reach the list are rendered SEPARATELY
|
|
155
|
+
because they send a reader somewhere different: one says the agent reported more than is
|
|
156
|
+
shown, the other says its reply was malformed. -->
|
|
157
|
+
<p v-if="report.operationsOmitted" class="mt-1 text-[11px] text-slate-500">
|
|
158
|
+
{{
|
|
159
|
+
t('inspector.testConfig.envProbe.operationsOmitted', { count: report.operationsOmitted })
|
|
160
|
+
}}
|
|
161
|
+
</p>
|
|
162
|
+
<p v-if="report.operationsUnreadable" class="mt-1 text-[11px] text-slate-500">
|
|
163
|
+
{{
|
|
164
|
+
t('inspector.testConfig.envProbe.operationsUnreadable', {
|
|
165
|
+
count: report.operationsUnreadable,
|
|
166
|
+
})
|
|
167
|
+
}}
|
|
168
|
+
</p>
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
<p v-if="report.summary" class="text-[11px] text-slate-400">{{ report.summary }}</p>
|
|
172
|
+
<p v-if="report.model" class="text-[11px] text-slate-600">
|
|
173
|
+
{{ t('inspector.testConfig.envProbe.model', { model: report.model }) }}
|
|
174
|
+
</p>
|
|
175
|
+
</div>
|
|
176
|
+
</template>
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref } from 'vue'
|
|
3
|
+
import type { Block, EnvironmentTestMode, EnvironmentTestStage } from '~/types/domain'
|
|
4
|
+
import type { ConflictReason } from '@cat-factory/contracts'
|
|
5
|
+
import { environmentProbeSurfaceFor } from '@cat-factory/contracts'
|
|
6
|
+
import EnvProbeReport from '~/components/panels/inspector/EnvProbeReport.vue'
|
|
7
|
+
import { apiErrorEnvelope } from '~/composables/api/errors'
|
|
8
|
+
import { parseConflict } from '~/composables/usePipelineErrorToast'
|
|
9
|
+
|
|
10
|
+
// The two SELF-TESTS a service frame offers, side by side, because they answer consecutive
|
|
11
|
+
// questions about the same environment:
|
|
12
|
+
//
|
|
13
|
+
// 1. "Test environment creation": does the provisioning config stand an environment up and
|
|
14
|
+
// take it down again? (create branch → provision → tear down → delete branch)
|
|
15
|
+
// 2. "Test agent dry run": handed that environment, could an AGENT actually operate the
|
|
16
|
+
// service: work out what to call, authenticate, and get something real done? It adds one
|
|
17
|
+
// `probing` stage in the middle and comes back with a report.
|
|
18
|
+
//
|
|
19
|
+
// One component rather than two because they share every piece of state and every refusal: the
|
|
20
|
+
// same store, the same `infraless` precondition, the same 409 vocabulary, the same
|
|
21
|
+
// jump-to-the-handler remedy. Extracted out of `ServiceTestConfig.vue`, which owns the service's
|
|
22
|
+
// provisioning CONFIG and was the wrong home for a pair of run controls.
|
|
23
|
+
const props = defineProps<{ block: Block }>()
|
|
24
|
+
|
|
25
|
+
const envTest = useEnvironmentTestStore()
|
|
26
|
+
const ui = useUiStore()
|
|
27
|
+
const { t, te } = useI18n()
|
|
28
|
+
|
|
29
|
+
/** Nothing to provision for an `infraless` service, so neither self-test has a subject. */
|
|
30
|
+
const canTest = computed(() => (props.block.provisioning?.type ?? 'infraless') !== 'infraless')
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Which prober the dry run would use, through the SAME rule the backend picks it with. Rendered
|
|
34
|
+
* in the hint so a developer knows what they are about to spend a container on: a browser run on
|
|
35
|
+
* a frontend frame, HTTP calls on everything else.
|
|
36
|
+
*/
|
|
37
|
+
const probeSurface = computed(() => environmentProbeSurfaceFor(props.block.type))
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The self-test's start/stop error, per mode. Structured (not a bare string) so the
|
|
41
|
+
* not-provisionable case can render its remedy prose PLUS a one-click jump to the
|
|
42
|
+
* environment-handler config; every other case is plain text.
|
|
43
|
+
*/
|
|
44
|
+
interface SelfTestError {
|
|
45
|
+
text: string
|
|
46
|
+
/** Show the "Configure infrastructure" deep-link: only the not-provisionable handler case. */
|
|
47
|
+
configurable?: boolean
|
|
48
|
+
}
|
|
49
|
+
const errors = ref<Partial<Record<EnvironmentTestMode, SelfTestError>>>({})
|
|
50
|
+
const starting = ref<Partial<Record<EnvironmentTestMode, boolean>>>({})
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The newest run for this frame in one mode: re-attaches after a reconnect (running runs are
|
|
54
|
+
* carried in the snapshot), so the live stage keeps showing without a locally-held id. Scoped by
|
|
55
|
+
* mode so each control reports its OWN run rather than whichever ran last.
|
|
56
|
+
*/
|
|
57
|
+
const provisionRun = computed(() => envTest.runForBlock(props.block.id, 'provision'))
|
|
58
|
+
const probeRun = computed(() => envTest.runForBlock(props.block.id, 'agent-probe'))
|
|
59
|
+
function runFor(mode: EnvironmentTestMode) {
|
|
60
|
+
return mode === 'agent-probe' ? probeRun.value : provisionRun.value
|
|
61
|
+
}
|
|
62
|
+
function isRunning(mode: EnvironmentTestMode) {
|
|
63
|
+
return runFor(mode)?.status === 'running'
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Whether EITHER self-test is in flight for this frame, which disables both start buttons.
|
|
68
|
+
*
|
|
69
|
+
* Per FRAME, not per mode: each run provisions its own ephemeral environment under a synthetic
|
|
70
|
+
* per-run key that nothing supersedes, so two at once is two live environments for one service,
|
|
71
|
+
* billed twice and racing each other to create on any provider whose namespace is derived per
|
|
72
|
+
* service rather than per branch. The backend refuses the second outright
|
|
73
|
+
* (`env_test_already_running`); this is the affordance that keeps a developer from meeting that
|
|
74
|
+
* refusal by pressing the obvious thing.
|
|
75
|
+
*/
|
|
76
|
+
const busy = computed(() => isRunning('provision') || isRunning('agent-probe'))
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The prober's live todo counts, rendered while the dry run is probing.
|
|
80
|
+
*
|
|
81
|
+
* `probing` is the one stage of this run measured in minutes, so it is the one where a bare
|
|
82
|
+
* "probing with an agent" is indistinguishable from a wedged run for as long as the container
|
|
83
|
+
* works.
|
|
84
|
+
*/
|
|
85
|
+
const probeProgress = computed(() => {
|
|
86
|
+
const run = probeRun.value
|
|
87
|
+
if (!run || run.status !== 'running' || run.stage !== 'probing') return null
|
|
88
|
+
return run.probeProgress
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
// Per-stage label KEYS, exhaustive over the contracts `EnvironmentTestStage` union: a new
|
|
92
|
+
// backend stage fails THIS typecheck until mapped (the key is resolved at runtime, so the
|
|
93
|
+
// typed-message-keys check can't see the `t()` lookup: the map's exhaustiveness is the
|
|
94
|
+
// drift guard, same pattern as `CONFLICT_TITLE_KEYS`).
|
|
95
|
+
const STAGE_KEYS: Record<EnvironmentTestStage, string> = {
|
|
96
|
+
creating_branch: 'inspector.testConfig.envTest.stage.creating_branch',
|
|
97
|
+
provisioning: 'inspector.testConfig.envTest.stage.provisioning',
|
|
98
|
+
probing: 'inspector.testConfig.envTest.stage.probing',
|
|
99
|
+
tearing_down: 'inspector.testConfig.envTest.stage.tearing_down',
|
|
100
|
+
deleting_branch: 'inspector.testConfig.envTest.stage.deleting_branch',
|
|
101
|
+
done: 'inspector.testConfig.envTest.stage.done',
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function stageLabel(stage: EnvironmentTestStage): string {
|
|
105
|
+
const key = STAGE_KEYS[stage]
|
|
106
|
+
// `te`-guarded so a locale missing the key shows the raw stage id, never a raw message key.
|
|
107
|
+
return te(key) ? t(key) : stage
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// The start preflight's machine-readable 409 reasons, mapped to their localized titles,
|
|
111
|
+
// exhaustive over the contracts `env_test_*` conflict reasons (same drift guard as above).
|
|
112
|
+
// The raw backend `message` is only the last-resort fallback for unmapped/non-conflict errors.
|
|
113
|
+
const CONFLICT_KEYS: Record<Extract<ConflictReason, `env_test_${string}`>, string> = {
|
|
114
|
+
env_test_not_a_frame: 'errors.conflict.title.env_test_not_a_frame',
|
|
115
|
+
env_test_infraless: 'errors.conflict.title.env_test_infraless',
|
|
116
|
+
env_test_not_provisionable: 'errors.conflict.title.env_test_not_provisionable',
|
|
117
|
+
env_test_no_vcs: 'errors.conflict.title.env_test_no_vcs',
|
|
118
|
+
env_test_connection_failed: 'errors.conflict.title.env_test_connection_failed',
|
|
119
|
+
env_test_probe_unavailable: 'errors.conflict.title.env_test_probe_unavailable',
|
|
120
|
+
env_test_already_running: 'errors.conflict.title.env_test_already_running',
|
|
121
|
+
env_test_over_budget: 'errors.conflict.title.env_test_over_budget',
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function buildError(e: unknown): SelfTestError {
|
|
125
|
+
const parsed = parseConflict(e)
|
|
126
|
+
const reason = parsed?.reason
|
|
127
|
+
// No workspace handler resolves for the service's provision type. Word the SPECIFIC case,
|
|
128
|
+
// nothing configured vs. an ambiguous match (carried on `details.handlerIssue`, distinct from the
|
|
129
|
+
// `env_test_not_provisionable` code), and offer the one-click jump to the Infrastructure →
|
|
130
|
+
// Test-environments handler config.
|
|
131
|
+
if (reason === 'env_test_not_provisionable') {
|
|
132
|
+
const ambiguous = parsed?.details.handlerIssue === 'type-mismatch'
|
|
133
|
+
return {
|
|
134
|
+
text: t(
|
|
135
|
+
ambiguous
|
|
136
|
+
? 'errors.conflict.description.env_test_not_provisionable_type_mismatch'
|
|
137
|
+
: 'errors.conflict.description.env_test_not_provisionable_no_handler',
|
|
138
|
+
),
|
|
139
|
+
configurable: true,
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// The handler resolved but its live connection probe failed. The provider's OWN message is the
|
|
143
|
+
// actionable part ("project 'X' was not found"), so wrap it in localized prose rather than
|
|
144
|
+
// replacing it with a generic sentence, and offer the same jump, since the fix is in the
|
|
145
|
+
// handler's connection config.
|
|
146
|
+
if (reason === 'env_test_connection_failed' && parsed?.message) {
|
|
147
|
+
return {
|
|
148
|
+
text: t('errors.conflict.description.env_test_connection_failed_detail', {
|
|
149
|
+
detail: parsed.message,
|
|
150
|
+
}),
|
|
151
|
+
configurable: true,
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const key =
|
|
155
|
+
reason && reason in CONFLICT_KEYS
|
|
156
|
+
? CONFLICT_KEYS[reason as keyof typeof CONFLICT_KEYS]
|
|
157
|
+
: undefined
|
|
158
|
+
if (key && te(key)) return { text: t(key) }
|
|
159
|
+
return { text: apiErrorEnvelope(e)?.message ?? (e instanceof Error ? e.message : String(e)) }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function start(mode: EnvironmentTestMode) {
|
|
163
|
+
if (!canTest.value || starting.value[mode] || busy.value) return
|
|
164
|
+
starting.value = { ...starting.value, [mode]: true }
|
|
165
|
+
errors.value = { ...errors.value, [mode]: undefined }
|
|
166
|
+
try {
|
|
167
|
+
await envTest.start(props.block.id, mode)
|
|
168
|
+
} catch (e) {
|
|
169
|
+
errors.value = { ...errors.value, [mode]: buildError(e) }
|
|
170
|
+
} finally {
|
|
171
|
+
starting.value = { ...starting.value, [mode]: false }
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function stop(mode: EnvironmentTestMode) {
|
|
176
|
+
const run = runFor(mode)
|
|
177
|
+
if (!run || run.status !== 'running') return
|
|
178
|
+
try {
|
|
179
|
+
await envTest.stop(run.id)
|
|
180
|
+
} catch (e) {
|
|
181
|
+
errors.value = { ...errors.value, [mode]: buildError(e) }
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
</script>
|
|
185
|
+
|
|
186
|
+
<template>
|
|
187
|
+
<div class="mt-3 space-y-3 border-t border-white/5 pt-3" data-testid="env-test-section">
|
|
188
|
+
<!-- The precondition both self-tests share, stated once at the top: with no provision type
|
|
189
|
+
there is no environment to stand up and therefore nothing for an agent to drive either. -->
|
|
190
|
+
<p v-if="!canTest" class="text-[11px] text-slate-500">
|
|
191
|
+
{{ t('inspector.testConfig.envTest.infraless') }}
|
|
192
|
+
</p>
|
|
193
|
+
|
|
194
|
+
<!-- 1. The provisioning self-test: the whole lifecycle against a throwaway branch. -->
|
|
195
|
+
<div class="flex items-center justify-between gap-2">
|
|
196
|
+
<div class="min-w-0">
|
|
197
|
+
<p class="text-[11px] font-medium text-slate-300">
|
|
198
|
+
{{ t('inspector.testConfig.envTest.title') }}
|
|
199
|
+
</p>
|
|
200
|
+
<p class="text-[11px] text-slate-400">{{ t('inspector.testConfig.envTest.hint') }}</p>
|
|
201
|
+
</div>
|
|
202
|
+
<UButton
|
|
203
|
+
v-if="!isRunning('provision')"
|
|
204
|
+
icon="i-lucide-flask-conical"
|
|
205
|
+
size="xs"
|
|
206
|
+
color="primary"
|
|
207
|
+
variant="soft"
|
|
208
|
+
data-testid="env-test-start"
|
|
209
|
+
:loading="starting.provision"
|
|
210
|
+
:disabled="!canTest || busy"
|
|
211
|
+
@click="start('provision')"
|
|
212
|
+
>
|
|
213
|
+
{{ t('inspector.testConfig.envTest.start') }}
|
|
214
|
+
</UButton>
|
|
215
|
+
<UButton
|
|
216
|
+
v-else
|
|
217
|
+
icon="i-lucide-square"
|
|
218
|
+
size="xs"
|
|
219
|
+
color="neutral"
|
|
220
|
+
variant="ghost"
|
|
221
|
+
data-testid="env-test-stop"
|
|
222
|
+
@click="stop('provision')"
|
|
223
|
+
>
|
|
224
|
+
{{ t('inspector.testConfig.envTest.stop') }}
|
|
225
|
+
</UButton>
|
|
226
|
+
</div>
|
|
227
|
+
|
|
228
|
+
<p
|
|
229
|
+
v-if="provisionRun"
|
|
230
|
+
class="text-[11px]"
|
|
231
|
+
:class="{
|
|
232
|
+
'text-sky-300/80': provisionRun.status === 'running',
|
|
233
|
+
'text-emerald-300/80': provisionRun.status === 'succeeded',
|
|
234
|
+
'text-rose-300/80': provisionRun.status === 'failed',
|
|
235
|
+
}"
|
|
236
|
+
data-testid="env-test-status"
|
|
237
|
+
>
|
|
238
|
+
<template v-if="provisionRun.status === 'running'">
|
|
239
|
+
{{ t('inspector.testConfig.envTest.running', { stage: stageLabel(provisionRun.stage) }) }}
|
|
240
|
+
</template>
|
|
241
|
+
<template v-else-if="provisionRun.status === 'succeeded'">
|
|
242
|
+
{{ t('inspector.testConfig.envTest.succeeded') }}
|
|
243
|
+
</template>
|
|
244
|
+
<template v-else>
|
|
245
|
+
{{ t('inspector.testConfig.envTest.failed') }}
|
|
246
|
+
<template v-if="provisionRun.failedStage">
|
|
247
|
+
({{ stageLabel(provisionRun.failedStage) }})
|
|
248
|
+
</template>
|
|
249
|
+
<span v-if="provisionRun.error" class="block text-rose-300/70">
|
|
250
|
+
{{ provisionRun.error }}
|
|
251
|
+
</span>
|
|
252
|
+
</template>
|
|
253
|
+
</p>
|
|
254
|
+
|
|
255
|
+
<div v-if="errors.provision" class="text-[11px] text-rose-400" data-testid="env-test-error">
|
|
256
|
+
<p>{{ errors.provision.text }}</p>
|
|
257
|
+
<!-- Only the not-provisionable handler case is one-click fixable: jump to Infrastructure →
|
|
258
|
+
Test environments, where the workspace's per-type environment handler is registered. -->
|
|
259
|
+
<UButton
|
|
260
|
+
v-if="errors.provision.configurable"
|
|
261
|
+
class="mt-1.5"
|
|
262
|
+
icon="i-lucide-settings"
|
|
263
|
+
size="xs"
|
|
264
|
+
color="neutral"
|
|
265
|
+
variant="soft"
|
|
266
|
+
data-testid="env-test-configure-handler"
|
|
267
|
+
@click="ui.openProviderConnection('environment')"
|
|
268
|
+
>
|
|
269
|
+
{{ t('errors.conflict.action.configureInfrastructure') }}
|
|
270
|
+
</UButton>
|
|
271
|
+
</div>
|
|
272
|
+
|
|
273
|
+
<!-- 2. The agent dry run: the same lifecycle plus a prober, reporting what an agent could
|
|
274
|
+
and could not do with the environment it was handed. -->
|
|
275
|
+
<div class="flex items-center justify-between gap-2 border-t border-white/5 pt-3">
|
|
276
|
+
<div class="min-w-0">
|
|
277
|
+
<p class="text-[11px] font-medium text-slate-300">
|
|
278
|
+
{{ t('inspector.testConfig.envProbe.title') }}
|
|
279
|
+
</p>
|
|
280
|
+
<p class="text-[11px] text-slate-400">
|
|
281
|
+
{{ t('inspector.testConfig.envProbe.hint') }}
|
|
282
|
+
{{
|
|
283
|
+
probeSurface === 'ui'
|
|
284
|
+
? t('inspector.testConfig.envProbe.surfaceUi')
|
|
285
|
+
: t('inspector.testConfig.envProbe.surfaceApi')
|
|
286
|
+
}}
|
|
287
|
+
</p>
|
|
288
|
+
</div>
|
|
289
|
+
<UButton
|
|
290
|
+
v-if="!isRunning('agent-probe')"
|
|
291
|
+
icon="i-lucide-bot"
|
|
292
|
+
size="xs"
|
|
293
|
+
color="primary"
|
|
294
|
+
variant="soft"
|
|
295
|
+
data-testid="env-probe-start"
|
|
296
|
+
:loading="starting['agent-probe']"
|
|
297
|
+
:disabled="!canTest || busy"
|
|
298
|
+
@click="start('agent-probe')"
|
|
299
|
+
>
|
|
300
|
+
{{ t('inspector.testConfig.envProbe.start') }}
|
|
301
|
+
</UButton>
|
|
302
|
+
<UButton
|
|
303
|
+
v-else
|
|
304
|
+
icon="i-lucide-square"
|
|
305
|
+
size="xs"
|
|
306
|
+
color="neutral"
|
|
307
|
+
variant="ghost"
|
|
308
|
+
data-testid="env-probe-stop"
|
|
309
|
+
@click="stop('agent-probe')"
|
|
310
|
+
>
|
|
311
|
+
{{ t('inspector.testConfig.envTest.stop') }}
|
|
312
|
+
</UButton>
|
|
313
|
+
</div>
|
|
314
|
+
|
|
315
|
+
<p
|
|
316
|
+
v-if="probeRun"
|
|
317
|
+
class="text-[11px]"
|
|
318
|
+
:class="{
|
|
319
|
+
'text-sky-300/80': probeRun.status === 'running',
|
|
320
|
+
'text-emerald-300/80': probeRun.status === 'succeeded',
|
|
321
|
+
'text-rose-300/80': probeRun.status === 'failed',
|
|
322
|
+
}"
|
|
323
|
+
data-testid="env-probe-status"
|
|
324
|
+
>
|
|
325
|
+
<template v-if="probeRun.status === 'running'">
|
|
326
|
+
{{ t('inspector.testConfig.envTest.running', { stage: stageLabel(probeRun.stage) }) }}
|
|
327
|
+
<template v-if="probeProgress">
|
|
328
|
+
{{
|
|
329
|
+
t('inspector.testConfig.envProbe.progress', {
|
|
330
|
+
completed: probeProgress.completed,
|
|
331
|
+
total: probeProgress.total,
|
|
332
|
+
})
|
|
333
|
+
}}
|
|
334
|
+
</template>
|
|
335
|
+
</template>
|
|
336
|
+
<template v-else-if="probeRun.status === 'succeeded'">
|
|
337
|
+
{{ t('inspector.testConfig.envProbe.completed') }}
|
|
338
|
+
</template>
|
|
339
|
+
<template v-else>
|
|
340
|
+
{{ t('inspector.testConfig.envProbe.failed') }}
|
|
341
|
+
<template v-if="probeRun.failedStage">({{ stageLabel(probeRun.failedStage) }})</template>
|
|
342
|
+
<span v-if="probeRun.error" class="block text-rose-300/70">{{ probeRun.error }}</span>
|
|
343
|
+
</template>
|
|
344
|
+
</p>
|
|
345
|
+
|
|
346
|
+
<!-- The report renders whatever the dry run got as far as, on a succeeded run AND on one that
|
|
347
|
+
failed after the probe reported: the finding is the product, and withholding it because a
|
|
348
|
+
later stage broke would hide the only thing the run was for. -->
|
|
349
|
+
<EnvProbeReport v-if="probeRun?.probe" :report="probeRun.probe" />
|
|
350
|
+
|
|
351
|
+
<div
|
|
352
|
+
v-if="errors['agent-probe']"
|
|
353
|
+
class="text-[11px] text-rose-400"
|
|
354
|
+
data-testid="env-probe-error"
|
|
355
|
+
>
|
|
356
|
+
<p>{{ errors['agent-probe']!.text }}</p>
|
|
357
|
+
<UButton
|
|
358
|
+
v-if="errors['agent-probe']!.configurable"
|
|
359
|
+
class="mt-1.5"
|
|
360
|
+
icon="i-lucide-settings"
|
|
361
|
+
size="xs"
|
|
362
|
+
color="neutral"
|
|
363
|
+
variant="soft"
|
|
364
|
+
data-testid="env-probe-configure-handler"
|
|
365
|
+
@click="ui.openProviderConnection('environment')"
|
|
366
|
+
>
|
|
367
|
+
{{ t('errors.conflict.action.configureInfrastructure') }}
|
|
368
|
+
</UButton>
|
|
369
|
+
</div>
|
|
370
|
+
</div>
|
|
371
|
+
</template>
|