@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
|
@@ -3,13 +3,11 @@ import { computed, onMounted, ref, watch } from 'vue'
|
|
|
3
3
|
import type {
|
|
4
4
|
Block,
|
|
5
5
|
CloudProvider,
|
|
6
|
-
EnvironmentTestStage,
|
|
7
6
|
InstanceSize,
|
|
8
7
|
ProvisionType,
|
|
9
8
|
ServiceProvisioning,
|
|
10
9
|
} from '~/types/domain'
|
|
11
10
|
import type {
|
|
12
|
-
ConflictReason,
|
|
13
11
|
KubernetesManifestSource,
|
|
14
12
|
KubernetesRenderer,
|
|
15
13
|
ProvisioningComposeServiceCandidate,
|
|
@@ -20,8 +18,8 @@ import type {
|
|
|
20
18
|
} from '@cat-factory/contracts'
|
|
21
19
|
import RepoTreeBrowser from '~/components/github/RepoTreeBrowser.vue'
|
|
22
20
|
import InspectorSection from '~/components/panels/inspector/InspectorSection.vue'
|
|
21
|
+
import ServiceSelfTests from '~/components/panels/inspector/ServiceSelfTests.vue'
|
|
23
22
|
import { apiErrorEnvelope } from '~/composables/api/errors'
|
|
24
|
-
import { parseConflict } from '~/composables/usePipelineErrorToast'
|
|
25
23
|
|
|
26
24
|
// Service-level (frame) configuration: the service-owned PROVISIONING — the provision
|
|
27
25
|
// TYPE this service produces (`infraless` / `docker-compose` / `kubernetes` / `custom`)
|
|
@@ -263,117 +261,6 @@ async function generateOrFixManifest() {
|
|
|
263
261
|
}
|
|
264
262
|
}
|
|
265
263
|
|
|
266
|
-
// Ephemeral-environment self-test: run the whole create-branch → provision → tear-down →
|
|
267
|
-
// delete-branch cycle against this service's provisioning config and report success / the stage
|
|
268
|
-
// it failed at. The returned run is tracked live (by frame id) via the workspace stream store.
|
|
269
|
-
const envTest = useEnvironmentTestStore()
|
|
270
|
-
const envTestStarting = ref(false)
|
|
271
|
-
// The self-test's start/stop error. Structured (not a bare string) so the not-provisionable case
|
|
272
|
-
// can render its remedy prose PLUS a one-click jump to the environment-handler config; every other
|
|
273
|
-
// case is plain text.
|
|
274
|
-
interface EnvTestError {
|
|
275
|
-
text: string
|
|
276
|
-
/** Show the "Configure infrastructure" deep-link — only the not-provisionable handler case. */
|
|
277
|
-
configurable?: boolean
|
|
278
|
-
}
|
|
279
|
-
const envTestError = ref<EnvTestError | null>(null)
|
|
280
|
-
// The newest self-test run for this frame — re-attaches after a reconnect (the run is carried in
|
|
281
|
-
// the snapshot while running), so the live stage keeps showing without a locally-held id.
|
|
282
|
-
const envTestRun = computed(() => envTest.runForBlock(props.block.id))
|
|
283
|
-
const envTestRunning = computed(() => envTestRun.value?.status === 'running')
|
|
284
|
-
// Nothing to provision for an `infraless` service, so there is nothing to test.
|
|
285
|
-
const canTestEnv = computed(() => provisionType.value !== 'infraless')
|
|
286
|
-
|
|
287
|
-
// Per-stage label KEYS, exhaustive over the contracts `EnvironmentTestStage` union: a new
|
|
288
|
-
// backend stage fails THIS typecheck until mapped (the key is resolved at runtime, so the
|
|
289
|
-
// typed-message-keys check can't see the `t()` lookup — the map's exhaustiveness is the
|
|
290
|
-
// drift guard, same pattern as `CONFLICT_TITLE_KEYS`).
|
|
291
|
-
const ENV_TEST_STAGE_KEYS: Record<EnvironmentTestStage, string> = {
|
|
292
|
-
creating_branch: 'inspector.testConfig.envTest.stage.creating_branch',
|
|
293
|
-
provisioning: 'inspector.testConfig.envTest.stage.provisioning',
|
|
294
|
-
tearing_down: 'inspector.testConfig.envTest.stage.tearing_down',
|
|
295
|
-
deleting_branch: 'inspector.testConfig.envTest.stage.deleting_branch',
|
|
296
|
-
done: 'inspector.testConfig.envTest.stage.done',
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
function envTestStageLabel(stage: EnvironmentTestStage): string {
|
|
300
|
-
const key = ENV_TEST_STAGE_KEYS[stage]
|
|
301
|
-
// `te`-guarded so a locale missing the key shows the raw stage id, never a raw message key.
|
|
302
|
-
return te(key) ? t(key) : stage
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// The start preflight's machine-readable 409 reasons, mapped to their localized titles —
|
|
306
|
-
// exhaustive over the contracts `env_test_*` conflict reasons (same drift guard as above).
|
|
307
|
-
// The raw backend `message` is only the last-resort fallback for unmapped/non-conflict errors.
|
|
308
|
-
const ENV_TEST_CONFLICT_KEYS: Record<Extract<ConflictReason, `env_test_${string}`>, string> = {
|
|
309
|
-
env_test_not_a_frame: 'errors.conflict.title.env_test_not_a_frame',
|
|
310
|
-
env_test_infraless: 'errors.conflict.title.env_test_infraless',
|
|
311
|
-
env_test_not_provisionable: 'errors.conflict.title.env_test_not_provisionable',
|
|
312
|
-
env_test_no_vcs: 'errors.conflict.title.env_test_no_vcs',
|
|
313
|
-
env_test_connection_failed: 'errors.conflict.title.env_test_connection_failed',
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
function buildEnvTestError(e: unknown): EnvTestError {
|
|
317
|
-
const parsed = parseConflict(e)
|
|
318
|
-
const reason = parsed?.reason
|
|
319
|
-
// No workspace handler resolves for the service's provision type. Word the SPECIFIC case —
|
|
320
|
-
// nothing configured vs. an ambiguous match (carried on `details.handlerIssue`, distinct from the
|
|
321
|
-
// `env_test_not_provisionable` code) — and offer the one-click jump to the Infrastructure →
|
|
322
|
-
// Test-environments handler config.
|
|
323
|
-
if (reason === 'env_test_not_provisionable') {
|
|
324
|
-
const ambiguous = parsed?.details.handlerIssue === 'type-mismatch'
|
|
325
|
-
return {
|
|
326
|
-
text: t(
|
|
327
|
-
ambiguous
|
|
328
|
-
? 'errors.conflict.description.env_test_not_provisionable_type_mismatch'
|
|
329
|
-
: 'errors.conflict.description.env_test_not_provisionable_no_handler',
|
|
330
|
-
),
|
|
331
|
-
configurable: true,
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
// The handler resolved but its live connection probe failed. The provider's OWN message is the
|
|
335
|
-
// actionable part ("project 'X' was not found"), so wrap it in localized prose rather than
|
|
336
|
-
// replacing it with a generic sentence — and offer the same jump, since the fix is in the
|
|
337
|
-
// handler's connection config.
|
|
338
|
-
if (reason === 'env_test_connection_failed' && parsed?.message) {
|
|
339
|
-
return {
|
|
340
|
-
text: t('errors.conflict.description.env_test_connection_failed_detail', {
|
|
341
|
-
detail: parsed.message,
|
|
342
|
-
}),
|
|
343
|
-
configurable: true,
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
const key =
|
|
347
|
-
reason && reason in ENV_TEST_CONFLICT_KEYS
|
|
348
|
-
? ENV_TEST_CONFLICT_KEYS[reason as keyof typeof ENV_TEST_CONFLICT_KEYS]
|
|
349
|
-
: undefined
|
|
350
|
-
if (key && te(key)) return { text: t(key) }
|
|
351
|
-
return { text: apiErrorEnvelope(e)?.message ?? (e instanceof Error ? e.message : String(e)) }
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
async function startEnvTest() {
|
|
355
|
-
if (!canTestEnv.value || envTestStarting.value || envTestRunning.value) return
|
|
356
|
-
envTestStarting.value = true
|
|
357
|
-
envTestError.value = null
|
|
358
|
-
try {
|
|
359
|
-
await envTest.start(props.block.id)
|
|
360
|
-
} catch (e) {
|
|
361
|
-
envTestError.value = buildEnvTestError(e)
|
|
362
|
-
} finally {
|
|
363
|
-
envTestStarting.value = false
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
async function stopEnvTest() {
|
|
368
|
-
const run = envTestRun.value
|
|
369
|
-
if (!run || run.status !== 'running') return
|
|
370
|
-
try {
|
|
371
|
-
await envTest.stop(run.id)
|
|
372
|
-
} catch (e) {
|
|
373
|
-
envTestError.value = buildEnvTestError(e)
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
|
|
377
264
|
// The provisioning hints (cloud provider + instance size) are advisory inputs to the
|
|
378
265
|
// ephemeral-environment provisioner, not commonly tuned — keep them collapsed by default.
|
|
379
266
|
const showProvisioning = ref(false)
|
|
@@ -1049,93 +936,10 @@ function setSize(value: InstanceSize) {
|
|
|
1049
936
|
</div>
|
|
1050
937
|
</InspectorSection>
|
|
1051
938
|
|
|
1052
|
-
<!--
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
<p class="text-[11px] font-medium text-slate-300">
|
|
1058
|
-
{{ t('inspector.testConfig.envTest.title') }}
|
|
1059
|
-
</p>
|
|
1060
|
-
<p class="text-[11px] text-slate-400">{{ t('inspector.testConfig.envTest.hint') }}</p>
|
|
1061
|
-
</div>
|
|
1062
|
-
<UButton
|
|
1063
|
-
v-if="!envTestRunning"
|
|
1064
|
-
icon="i-lucide-flask-conical"
|
|
1065
|
-
size="xs"
|
|
1066
|
-
color="primary"
|
|
1067
|
-
variant="soft"
|
|
1068
|
-
data-testid="env-test-start"
|
|
1069
|
-
:loading="envTestStarting"
|
|
1070
|
-
:disabled="!canTestEnv"
|
|
1071
|
-
@click="startEnvTest"
|
|
1072
|
-
>
|
|
1073
|
-
{{ t('inspector.testConfig.envTest.start') }}
|
|
1074
|
-
</UButton>
|
|
1075
|
-
<UButton
|
|
1076
|
-
v-else
|
|
1077
|
-
icon="i-lucide-square"
|
|
1078
|
-
size="xs"
|
|
1079
|
-
color="neutral"
|
|
1080
|
-
variant="ghost"
|
|
1081
|
-
data-testid="env-test-stop"
|
|
1082
|
-
@click="stopEnvTest"
|
|
1083
|
-
>
|
|
1084
|
-
{{ t('inspector.testConfig.envTest.stop') }}
|
|
1085
|
-
</UButton>
|
|
1086
|
-
</div>
|
|
1087
|
-
|
|
1088
|
-
<p v-if="!canTestEnv" class="text-[11px] text-slate-500">
|
|
1089
|
-
{{ t('inspector.testConfig.envTest.infraless') }}
|
|
1090
|
-
</p>
|
|
1091
|
-
|
|
1092
|
-
<!-- Live stage + terminal outcome of the tracked run (pushed via the workspace stream). -->
|
|
1093
|
-
<p
|
|
1094
|
-
v-if="envTestRun"
|
|
1095
|
-
class="text-[11px]"
|
|
1096
|
-
:class="{
|
|
1097
|
-
'text-sky-300/80': envTestRun.status === 'running',
|
|
1098
|
-
'text-emerald-300/80': envTestRun.status === 'succeeded',
|
|
1099
|
-
'text-rose-300/80': envTestRun.status === 'failed',
|
|
1100
|
-
}"
|
|
1101
|
-
data-testid="env-test-status"
|
|
1102
|
-
>
|
|
1103
|
-
<template v-if="envTestRun.status === 'running'">
|
|
1104
|
-
{{
|
|
1105
|
-
t('inspector.testConfig.envTest.running', {
|
|
1106
|
-
stage: envTestStageLabel(envTestRun.stage),
|
|
1107
|
-
})
|
|
1108
|
-
}}
|
|
1109
|
-
</template>
|
|
1110
|
-
<template v-else-if="envTestRun.status === 'succeeded'">
|
|
1111
|
-
{{ t('inspector.testConfig.envTest.succeeded') }}
|
|
1112
|
-
</template>
|
|
1113
|
-
<template v-else>
|
|
1114
|
-
{{ t('inspector.testConfig.envTest.failed') }}
|
|
1115
|
-
<template v-if="envTestRun.failedStage">
|
|
1116
|
-
({{ envTestStageLabel(envTestRun.failedStage) }})
|
|
1117
|
-
</template>
|
|
1118
|
-
<span v-if="envTestRun.error" class="block text-rose-300/70">{{ envTestRun.error }}</span>
|
|
1119
|
-
</template>
|
|
1120
|
-
</p>
|
|
1121
|
-
|
|
1122
|
-
<div v-if="envTestError" class="text-[11px] text-rose-400" data-testid="env-test-error">
|
|
1123
|
-
<p>{{ envTestError.text }}</p>
|
|
1124
|
-
<!-- Only the not-provisionable handler case is one-click fixable: jump to Infrastructure →
|
|
1125
|
-
Test environments, where the workspace's per-type environment handler is registered. -->
|
|
1126
|
-
<UButton
|
|
1127
|
-
v-if="envTestError.configurable"
|
|
1128
|
-
class="mt-1.5"
|
|
1129
|
-
icon="i-lucide-settings"
|
|
1130
|
-
size="xs"
|
|
1131
|
-
color="neutral"
|
|
1132
|
-
variant="soft"
|
|
1133
|
-
data-testid="env-test-configure-handler"
|
|
1134
|
-
@click="ui.openProviderConnection('environment')"
|
|
1135
|
-
>
|
|
1136
|
-
{{ t('errors.conflict.action.configureInfrastructure') }}
|
|
1137
|
-
</UButton>
|
|
1138
|
-
</div>
|
|
1139
|
-
</div>
|
|
939
|
+
<!-- The two self-tests this service offers: does its provisioning stand an environment up,
|
|
940
|
+
and could an agent handed that environment actually operate the service. Their own
|
|
941
|
+
collaborator (they share every piece of state and every refusal), so this view stays
|
|
942
|
+
about the provisioning CONFIG. -->
|
|
943
|
+
<ServiceSelfTests :block="block" />
|
|
1140
944
|
</InspectorSection>
|
|
1141
945
|
</template>
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
startEnvironmentTestContract,
|
|
6
6
|
stopEnvironmentTestContract,
|
|
7
7
|
} from '@cat-factory/contracts'
|
|
8
|
-
import type { ProvisionEnvironmentInput } from '@cat-factory/contracts'
|
|
8
|
+
import type { EnvironmentTestMode, ProvisionEnvironmentInput } from '@cat-factory/contracts'
|
|
9
9
|
import type { ApiContext } from './context'
|
|
10
10
|
|
|
11
11
|
/** Ephemeral environments: the workspace's live env handles (used to resolve frontend bindings). */
|
|
@@ -20,9 +20,14 @@ export function environmentsApi({ send, ws }: ApiContext) {
|
|
|
20
20
|
send(provisionEnvironmentContract, { pathPrefix: ws(workspaceId), body }),
|
|
21
21
|
|
|
22
22
|
// Ephemeral-environment self-test: start a full create-branch → provision → tear-down →
|
|
23
|
-
// delete-branch cycle against a service frame, then read / stop its run.
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
// delete-branch cycle against a service frame, then read / stop its run. `mode` picks what it
|
|
24
|
+
// exercises: the provisioning alone, or that plus an agent dry run against the environment.
|
|
25
|
+
startEnvironmentTest: (workspaceId: string, blockId: string, mode: EnvironmentTestMode) =>
|
|
26
|
+
send(startEnvironmentTestContract, {
|
|
27
|
+
pathPrefix: ws(workspaceId),
|
|
28
|
+
pathParams: { blockId },
|
|
29
|
+
body: { mode },
|
|
30
|
+
}),
|
|
26
31
|
getEnvironmentTest: (workspaceId: string, id: string) =>
|
|
27
32
|
send(getEnvironmentTestContract, { pathPrefix: ws(workspaceId), pathParams: { id } }),
|
|
28
33
|
stopEnvironmentTest: (workspaceId: string, id: string) =>
|
|
@@ -270,6 +270,25 @@ const CONFLICT_INFO: Record<Exclude<ConflictReason, BespokeConflictReason>, Conf
|
|
|
270
270
|
run: (ui) => ui.openProviderConnection('environment'),
|
|
271
271
|
},
|
|
272
272
|
},
|
|
273
|
+
// An agent dry run on a deployment that cannot drive one. No ACTION: the gap is a container
|
|
274
|
+
// runner, a proxyable model or a repository seam, none of which a user can wire from the SPA.
|
|
275
|
+
// Offering a settings jump here would send them somewhere that cannot fix it.
|
|
276
|
+
env_test_probe_unavailable: {
|
|
277
|
+
titleKey: 'errors.conflict.title.env_test_probe_unavailable',
|
|
278
|
+
descriptionKey: 'errors.conflict.description.env_test_probe_unavailable',
|
|
279
|
+
},
|
|
280
|
+
// A second self-test on a frame that already has one running. No ACTION: the remedy is to wait
|
|
281
|
+
// for the run showing in the same panel, or to stop it with the button beside it.
|
|
282
|
+
env_test_already_running: {
|
|
283
|
+
titleKey: 'errors.conflict.title.env_test_already_running',
|
|
284
|
+
descriptionKey: 'errors.conflict.description.env_test_already_running',
|
|
285
|
+
},
|
|
286
|
+
// The workspace is over a spend budget and a dry run is a billable call. No ACTION here either:
|
|
287
|
+
// budgets are an account-level setting, and the provisioning self-test beside it still runs.
|
|
288
|
+
env_test_over_budget: {
|
|
289
|
+
titleKey: 'errors.conflict.title.env_test_over_budget',
|
|
290
|
+
descriptionKey: 'errors.conflict.description.env_test_over_budget',
|
|
291
|
+
},
|
|
273
292
|
// Opt-in review-debt friction. In the normal task-create flow AddTaskModal intercepts these
|
|
274
293
|
// 409s and opens the friction dialog (which can retry with an acknowledgement), so these entries
|
|
275
294
|
// are the last-resort toast fallback for any OTHER caller — a generic, param-free title +
|
|
@@ -13,12 +13,15 @@ function run(id: string, over: Partial<EnvironmentTestRun> = {}): EnvironmentTes
|
|
|
13
13
|
id,
|
|
14
14
|
workspaceId: 'ws_test',
|
|
15
15
|
blockId: `blk_${id}`,
|
|
16
|
+
mode: 'provision',
|
|
16
17
|
status: 'running',
|
|
17
18
|
stage: 'provisioning',
|
|
18
19
|
branch: null,
|
|
19
20
|
envUrl: null,
|
|
20
21
|
error: null,
|
|
21
22
|
failedStage: null,
|
|
23
|
+
probe: null,
|
|
24
|
+
probeProgress: null,
|
|
22
25
|
createdAt: 1,
|
|
23
26
|
updatedAt: 1,
|
|
24
27
|
...over,
|
|
@@ -41,7 +44,7 @@ describe('environmentTest store — monotonic run reconcile', () => {
|
|
|
41
44
|
// as `running` (older updatedAt) — it must NOT clobber the terminal state (terminal runs
|
|
42
45
|
// emit nothing further, so the inspector would be stuck on "testing" forever).
|
|
43
46
|
store.hydrate([run('r1', { status: 'running', updatedAt: 2 })], 'ws_test')
|
|
44
|
-
expect(store.runForBlock('blk_r1')!.status).toBe('failed')
|
|
47
|
+
expect(store.runForBlock('blk_r1', 'provision')!.status).toBe('failed')
|
|
45
48
|
})
|
|
46
49
|
|
|
47
50
|
it('hydrate does NOT drop a live-added run the stale snapshot never saw', () => {
|
|
@@ -49,7 +52,7 @@ describe('environmentTest store — monotonic run reconcile', () => {
|
|
|
49
52
|
// inspector still shows must survive a full refresh.
|
|
50
53
|
store.upsert(run('r1', { status: 'succeeded', stage: 'done', updatedAt: 5 }))
|
|
51
54
|
store.hydrate([], 'ws_test')
|
|
52
|
-
expect(store.runForBlock('blk_r1')!.status).toBe('succeeded')
|
|
55
|
+
expect(store.runForBlock('blk_r1', 'provision')!.status).toBe('succeeded')
|
|
53
56
|
})
|
|
54
57
|
|
|
55
58
|
it('hydrate point-reads a preserved RUNNING run the snapshot omitted (finished offline)', async () => {
|
|
@@ -62,7 +65,9 @@ describe('environmentTest store — monotonic run reconcile', () => {
|
|
|
62
65
|
)
|
|
63
66
|
store.hydrate([], 'ws_test')
|
|
64
67
|
expect(apiMock.getEnvironmentTest).toHaveBeenCalledWith('ws_test', 'r1')
|
|
65
|
-
await vi.waitFor(() =>
|
|
68
|
+
await vi.waitFor(() =>
|
|
69
|
+
expect(store.runForBlock('blk_r1', 'provision')!.status).toBe('succeeded'),
|
|
70
|
+
)
|
|
66
71
|
})
|
|
67
72
|
|
|
68
73
|
it('a STALE point-read cannot regress a run a live event advanced meanwhile', async () => {
|
|
@@ -74,7 +79,7 @@ describe('environmentTest store — monotonic run reconcile', () => {
|
|
|
74
79
|
store.upsert(run('r1', { status: 'failed', failedStage: 'tearing_down', updatedAt: 8 }))
|
|
75
80
|
await vi.waitFor(() => expect(apiMock.getEnvironmentTest).toHaveBeenCalled())
|
|
76
81
|
await Promise.resolve()
|
|
77
|
-
expect(store.runForBlock('blk_r1')!.status).toBe('failed')
|
|
82
|
+
expect(store.runForBlock('blk_r1', 'provision')!.status).toBe('failed')
|
|
78
83
|
})
|
|
79
84
|
|
|
80
85
|
/**
|
|
@@ -106,7 +111,9 @@ describe('environmentTest store — monotonic run reconcile', () => {
|
|
|
106
111
|
expect(apiMock.getEnvironmentTest).toHaveBeenCalledTimes(1)
|
|
107
112
|
|
|
108
113
|
releaseFirst()
|
|
109
|
-
await vi.waitFor(() =>
|
|
114
|
+
await vi.waitFor(() =>
|
|
115
|
+
expect(store.runForBlock('blk_r1', 'provision')!.status).toBe('succeeded'),
|
|
116
|
+
)
|
|
110
117
|
expect(apiMock.getEnvironmentTest).toHaveBeenCalledTimes(2)
|
|
111
118
|
})
|
|
112
119
|
|
|
@@ -122,15 +129,35 @@ describe('environmentTest store — monotonic run reconcile', () => {
|
|
|
122
129
|
[run('r1', { status: 'running', stage: 'tearing_down', updatedAt: 9 })],
|
|
123
130
|
'ws_test',
|
|
124
131
|
)
|
|
125
|
-
expect(store.runForBlock('blk_r1')!.stage).toBe('tearing_down')
|
|
132
|
+
expect(store.runForBlock('blk_r1', 'provision')!.stage).toBe('tearing_down')
|
|
126
133
|
})
|
|
127
134
|
|
|
128
135
|
it('upsert ignores an older/out-of-order write but applies newer/equal', () => {
|
|
129
136
|
store.upsert(run('r1', { status: 'failed', updatedAt: 5 }))
|
|
130
137
|
// e.g. a `start()` response resolving AFTER the fast-failing run's terminal event landed.
|
|
131
138
|
store.upsert(run('r1', { status: 'running', stage: 'creating_branch', updatedAt: 3 }))
|
|
132
|
-
expect(store.runForBlock('blk_r1')!.status).toBe('failed')
|
|
139
|
+
expect(store.runForBlock('blk_r1', 'provision')!.status).toBe('failed')
|
|
133
140
|
store.upsert(run('r1', { status: 'succeeded', stage: 'done', updatedAt: 5 }))
|
|
134
|
-
expect(store.runForBlock('blk_r1')!.status).toBe('succeeded')
|
|
141
|
+
expect(store.runForBlock('blk_r1', 'provision')!.status).toBe('succeeded')
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The two self-tests render side by side in the inspector, each with its own status line, so a
|
|
146
|
+
* per-block read has to be per MODE too. Unscoped, whichever ran more recently would report
|
|
147
|
+
* under both controls, and a developer would act on the wrong one.
|
|
148
|
+
*/
|
|
149
|
+
it('runForBlock scopes to the mode, so the two self-tests never report each other', () => {
|
|
150
|
+
store.upsert(run('r1', { blockId: 'blk_frame', mode: 'provision', status: 'succeeded' }))
|
|
151
|
+
store.upsert(
|
|
152
|
+
run('r2', {
|
|
153
|
+
blockId: 'blk_frame',
|
|
154
|
+
mode: 'agent-probe',
|
|
155
|
+
status: 'failed',
|
|
156
|
+
failedStage: 'probing',
|
|
157
|
+
updatedAt: 9,
|
|
158
|
+
}),
|
|
159
|
+
)
|
|
160
|
+
expect(store.runForBlock('blk_frame', 'provision')!.id).toBe('r1')
|
|
161
|
+
expect(store.runForBlock('blk_frame', 'agent-probe')!.id).toBe('r2')
|
|
135
162
|
})
|
|
136
163
|
})
|
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
import { defineStore } from 'pinia'
|
|
2
2
|
import { ref } from 'vue'
|
|
3
|
+
import type { EnvironmentTestMode } from '@cat-factory/contracts'
|
|
3
4
|
import type { EnvironmentTestRun } from '~/types/domain'
|
|
4
5
|
import { useWorkspaceStore } from '~/stores/workspace'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
|
-
* Ephemeral-environment self-test runs
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* Ephemeral-environment self-test runs, in both modes: the provisioning self-test and the AGENT
|
|
9
|
+
* DRY RUN, which adds a `probing` stage and comes back with a report. A developer starts one from
|
|
10
|
+
* a service frame's inspector (`POST …/blocks/:id/environment-test`); the backend drives the
|
|
11
|
+
* create-branch → provision → [probe] → tear-down → delete-branch cycle durably and pushes live
|
|
12
|
+
* `envTest` stage events, which
|
|
10
13
|
* `useWorkspaceStream` folds in via {@link upsert}. In-flight runs also arrive in the workspace
|
|
11
14
|
* snapshot ({@link hydrate}) so the inspector re-attaches to a running test after a reconnect.
|
|
12
15
|
*
|
|
13
16
|
* Runs are keyed by their FRAME block id for the inspector's per-service lookup ({@link runForBlock}
|
|
14
|
-
* returns the newest run for a block
|
|
15
|
-
* inspector can show the last outcome; the
|
|
17
|
+
* returns the newest run for a block IN ONE MODE, since the two self-tests render side by side).
|
|
18
|
+
* Terminal runs are kept in memory for the session so the inspector can show the last outcome; the
|
|
19
|
+
* snapshot only carries running ones.
|
|
16
20
|
*/
|
|
17
21
|
export const useEnvironmentTestStore = defineStore('environmentTest', () => {
|
|
18
22
|
const api = useApi()
|
|
@@ -113,15 +117,22 @@ export const useEnvironmentTestStore = defineStore('environmentTest', () => {
|
|
|
113
117
|
return runs.value.find((r) => r.id === id)
|
|
114
118
|
}
|
|
115
119
|
|
|
116
|
-
/**
|
|
117
|
-
|
|
118
|
-
|
|
120
|
+
/**
|
|
121
|
+
* The newest run for a service frame IN ONE MODE: the inspector's per-service attach point.
|
|
122
|
+
*
|
|
123
|
+
* Scoped by mode because the inspector shows the two self-tests side by side and each owns its
|
|
124
|
+
* own status line: an unscoped read would have a provisioning test's outcome appear under the
|
|
125
|
+
* agent dry run's button (and vice versa) whenever the other one ran more recently, which is
|
|
126
|
+
* the reading a developer would act on.
|
|
127
|
+
*/
|
|
128
|
+
function runForBlock(blockId: string, mode: EnvironmentTestMode): EnvironmentTestRun | undefined {
|
|
129
|
+
return runs.value.find((r) => r.blockId === blockId && r.mode === mode)
|
|
119
130
|
}
|
|
120
131
|
|
|
121
132
|
/** Start a self-test against a service frame; the returned run is tracked immediately. */
|
|
122
|
-
async function start(blockId: string): Promise<EnvironmentTestRun> {
|
|
133
|
+
async function start(blockId: string, mode: EnvironmentTestMode): Promise<EnvironmentTestRun> {
|
|
123
134
|
const ws = useWorkspaceStore()
|
|
124
|
-
const run = await api.startEnvironmentTest(ws.requireId(), blockId)
|
|
135
|
+
const run = await api.startEnvironmentTest(ws.requireId(), blockId, mode)
|
|
125
136
|
upsert(run)
|
|
126
137
|
return run
|
|
127
138
|
}
|
package/app/types/domain.ts
CHANGED
package/i18n/locales/de.json
CHANGED
|
@@ -1691,17 +1691,53 @@
|
|
|
1691
1691
|
"hint": "Führt den gesamten Lebenszyklus gegen einen Wegwerf-Branch aus: Branch erstellen, bereitstellen, abbauen, Branch löschen.",
|
|
1692
1692
|
"start": "Umgebungserstellung testen",
|
|
1693
1693
|
"stop": "Stopp",
|
|
1694
|
-
"infraless": "
|
|
1694
|
+
"infraless": "Konfigurieren Sie oben einen Bereitstellungstyp, um einen der beiden Selbsttests auszuführen.",
|
|
1695
1695
|
"running": "Wird getestet: {stage}",
|
|
1696
1696
|
"succeeded": "Test bestanden: Die Umgebung wurde erstellt und abgebaut, und der Branch wurde gelöscht.",
|
|
1697
1697
|
"failed": "Test fehlgeschlagen",
|
|
1698
1698
|
"stage": {
|
|
1699
1699
|
"creating_branch": "Branch wird erstellt",
|
|
1700
1700
|
"provisioning": "Umgebung wird bereitgestellt",
|
|
1701
|
+
"probing": "Prüfung durch einen Agenten",
|
|
1701
1702
|
"tearing_down": "Umgebung wird abgebaut",
|
|
1702
1703
|
"deleting_branch": "Branch wird gelöscht",
|
|
1703
1704
|
"done": "fertig"
|
|
1704
1705
|
}
|
|
1706
|
+
},
|
|
1707
|
+
"envProbe": {
|
|
1708
|
+
"title": "Agenten-Probelauf testen",
|
|
1709
|
+
"hint": "Stellt eine Wegwerf-Umgebung bereit, lässt einen Agenten darin einige echte Operationen versuchen und baut anschließend alles ab. Berichtet, was versucht wurde und was der Agent nicht herausfinden konnte.",
|
|
1710
|
+
"surfaceApi": "Dieser Dienst wird über HTTP angesprochen.",
|
|
1711
|
+
"surfaceUi": "Dieses Frontend wird im Browser bedient.",
|
|
1712
|
+
"start": "Agenten-Probelauf testen",
|
|
1713
|
+
"completed": "Probelauf abgeschlossen: Die Umgebung wurde abgebaut und der Branch gelöscht.",
|
|
1714
|
+
"failed": "Probelauf fehlgeschlagen",
|
|
1715
|
+
"verdict": {
|
|
1716
|
+
"operable": "Ein Agent kann diesen Dienst bedienen.",
|
|
1717
|
+
"partially_operable": "Ein Agent kann diesen Dienst nur teilweise bedienen.",
|
|
1718
|
+
"inoperable": "Ein Agent konnte diesen Dienst nicht bedienen."
|
|
1719
|
+
},
|
|
1720
|
+
"counts": "{succeeded} von {attempted} Operationen erfolgreich; {authenticated} davon mit Authentifizierung.",
|
|
1721
|
+
"missingContext": "Was dem Agenten nicht mitgeteilt wurde",
|
|
1722
|
+
"blockers": "Was den Probelauf gestoppt hat",
|
|
1723
|
+
"operations": "Versuchte Operationen",
|
|
1724
|
+
"operationsOmitted": "Gemeldete Operationen, die durch das Limit entfallen sind: {count}.",
|
|
1725
|
+
"operationsUnreadable": "Gemeldete Operationen, die die Plattform nicht lesen konnte: {count}.",
|
|
1726
|
+
"progress": "({completed} von {total} Schritten erledigt)",
|
|
1727
|
+
"authenticated": "(authentifiziert)",
|
|
1728
|
+
"model": "Modell: {model}",
|
|
1729
|
+
"failure": {
|
|
1730
|
+
"auth_missing": "Keine Zugangsdaten übergeben",
|
|
1731
|
+
"auth_rejected": "Zugangsdaten abgelehnt",
|
|
1732
|
+
"access_unclear": "Authentifizierungsweg unklar",
|
|
1733
|
+
"endpoint_unknown": "Aufrufziel nicht auffindbar",
|
|
1734
|
+
"unreachable": "Umgebung nicht erreichbar",
|
|
1735
|
+
"timeout": "Zeitüberschreitung",
|
|
1736
|
+
"server_error": "Fehler im Dienst",
|
|
1737
|
+
"bad_request": "Anfrage als fehlerhaft abgelehnt",
|
|
1738
|
+
"tooling_missing": "Kein Client oder Browser verfügbar",
|
|
1739
|
+
"other": "Sonstiges"
|
|
1740
|
+
}
|
|
1705
1741
|
}
|
|
1706
1742
|
},
|
|
1707
1743
|
"agentConfig": {
|
|
@@ -6163,6 +6199,9 @@
|
|
|
6163
6199
|
"env_test_not_provisionable": "Umgebungs-Handler nicht konfiguriert",
|
|
6164
6200
|
"env_test_no_vcs": "Git-Anbieter nicht verbunden",
|
|
6165
6201
|
"env_test_connection_failed": "Umgebungsverbindung fehlgeschlagen",
|
|
6202
|
+
"env_test_probe_unavailable": "Agenten-Probeläufe hier nicht verfügbar",
|
|
6203
|
+
"env_test_already_running": "Selbsttest läuft bereits",
|
|
6204
|
+
"env_test_over_budget": "Ausgabenbudget erreicht",
|
|
6166
6205
|
"prompt_revision_conflict": "Prompt von jemand anderem geändert",
|
|
6167
6206
|
"pipeline_schedule_attached": "Wiederkehrender Zeitplan nutzt diese Pipeline",
|
|
6168
6207
|
"pipeline_schedule_requires_recurring": "Wiederkehrender Zeitplan braucht diese Pipeline",
|
|
@@ -6212,6 +6251,9 @@
|
|
|
6212
6251
|
"env_test_no_vcs": "Der Selbsttest benötigt einen Git-Anbieter, um seinen Wegwerf-Branch zu erstellen und zu löschen, aber dieser Workspace ist mit keinem verbunden.",
|
|
6213
6252
|
"env_test_connection_failed": "Der Umgebungs-Handler dieses Dienstes hat seinen Verbindungstest nicht bestanden. Prüfen Sie Endpunkt, Anmeldedaten und Projekteinstellungen und testen Sie die Verbindung erneut.",
|
|
6214
6253
|
"env_test_connection_failed_detail": "Der Umgebungs-Handler dieses Dienstes hat seinen Verbindungstest nicht bestanden: {detail}. Prüfen Sie Endpunkt, Anmeldedaten und Projekteinstellungen und testen Sie die Verbindung erneut.",
|
|
6254
|
+
"env_test_probe_unavailable": "Ein Agenten-Probelauf benötigt einen Container-Runner, ein verbundenes Repository und ein Modell, das der LLM-Proxy dieses Deployments bereitstellen kann. Hier fehlt eines davon, daher lässt sich nur der Bereitstellungs-Selbsttest ausführen.",
|
|
6255
|
+
"env_test_already_running": "Für diesen Dienst läuft bereits ein Selbsttest. Jeder stellt seine eigene Wegwerf-Umgebung bereit, daher läuft immer nur einer. Warten Sie, bis er fertig ist, oder stoppen Sie ihn zuerst.",
|
|
6256
|
+
"env_test_over_budget": "Ein Agenten-Probelauf ist ein kostenpflichtiger Modellaufruf, und dieser Workspace hat sein Ausgabenbudget erreicht. Erhöhen Sie das Budget oder warten Sie auf den nächsten Abrechnungszeitraum. Der Bereitstellungs-Selbsttest kostet nichts und läuft weiterhin.",
|
|
6215
6257
|
"prompt_revision_conflict": "Eine andere Änderung an diesem Prompt war zuerst da. Laden Sie ihn neu und wenden Sie Ihre Änderung darauf erneut an.",
|
|
6216
6258
|
"pipeline_schedule_attached": "Ein wiederkehrender Zeitplan verweist noch auf diese Pipeline, und jeder von ihm gestartete Lauf löst die Pipeline über ihre ID auf. Lösen oder löschen Sie zuerst diesen Zeitplan und entfernen Sie danach die Pipeline.",
|
|
6217
6259
|
"pipeline_schedule_requires_recurring": "Ein wiederkehrender Zeitplan verweist noch auf diese Pipeline. Sie nur einmalig zu machen würde jeden künftigen Lauf unterbrechen. Lösen Sie zuerst diesen Zeitplan.",
|
package/i18n/locales/en.json
CHANGED
|
@@ -766,6 +766,9 @@
|
|
|
766
766
|
"env_test_not_provisionable": "Environment handler not configured",
|
|
767
767
|
"env_test_no_vcs": "Git provider not connected",
|
|
768
768
|
"env_test_connection_failed": "Environment connection failed",
|
|
769
|
+
"env_test_probe_unavailable": "Agent dry runs not available here",
|
|
770
|
+
"env_test_already_running": "Self-test already running",
|
|
771
|
+
"env_test_over_budget": "Spend budget reached",
|
|
769
772
|
"prompt_revision_conflict": "Prompt changed by someone else",
|
|
770
773
|
"pipeline_schedule_attached": "Recurring schedule uses this pipeline",
|
|
771
774
|
"pipeline_schedule_requires_recurring": "Recurring schedule needs this pipeline",
|
|
@@ -815,6 +818,9 @@
|
|
|
815
818
|
"env_test_no_vcs": "The self-test needs a git provider to create and delete its throwaway branch, but this workspace isn't connected to one.",
|
|
816
819
|
"env_test_connection_failed": "The environment handler for this service failed its connection test. Check its endpoint, credentials and project settings, then re-test the connection.",
|
|
817
820
|
"env_test_connection_failed_detail": "The environment handler for this service failed its connection test: {detail}. Check its endpoint, credentials and project settings, then re-test the connection.",
|
|
821
|
+
"env_test_probe_unavailable": "An agent dry run needs a container runner, a connected repository and a model this deployment's LLM proxy can serve. One of them is missing here, so only the provisioning self-test can run.",
|
|
822
|
+
"env_test_already_running": "A self-test is already running for this service. Each one provisions its own throwaway environment, so only one runs at a time. Wait for it to finish, or stop it first.",
|
|
823
|
+
"env_test_over_budget": "An agent dry run is a billable model call, and this workspace has reached a spend budget. Raise the budget or wait for the billing period to reset. The provisioning self-test costs nothing and still runs.",
|
|
818
824
|
"@env_test_connection_failed_detail": {
|
|
819
825
|
"description": "Keep the named placeholder for the failure detail intact (the environment provider's connection-test error message, injected at runtime)."
|
|
820
826
|
},
|
|
@@ -1231,17 +1237,68 @@
|
|
|
1231
1237
|
"hint": "Runs the whole lifecycle against a throwaway branch: create branch, provision, tear down, delete branch.",
|
|
1232
1238
|
"start": "Test environment creation",
|
|
1233
1239
|
"stop": "Stop",
|
|
1234
|
-
"infraless": "Configure a provision type above to
|
|
1240
|
+
"infraless": "Configure a provision type above to run either self-test.",
|
|
1235
1241
|
"running": "Testing: {stage}",
|
|
1236
1242
|
"succeeded": "Test passed: the environment was created and torn down, and the branch was deleted.",
|
|
1237
1243
|
"failed": "Test failed",
|
|
1238
1244
|
"stage": {
|
|
1239
1245
|
"creating_branch": "creating branch",
|
|
1240
1246
|
"provisioning": "provisioning environment",
|
|
1247
|
+
"probing": "probing with an agent",
|
|
1241
1248
|
"tearing_down": "tearing down environment",
|
|
1242
1249
|
"deleting_branch": "deleting branch",
|
|
1243
1250
|
"done": "done"
|
|
1244
1251
|
}
|
|
1252
|
+
},
|
|
1253
|
+
"envProbe": {
|
|
1254
|
+
"title": "Test agent dry run",
|
|
1255
|
+
"hint": "Provisions a throwaway environment, has an agent attempt a few real operations against it, then tears it all down. Reports what it tried and what it could not work out.",
|
|
1256
|
+
"surfaceApi": "This service is driven over HTTP.",
|
|
1257
|
+
"surfaceUi": "This frontend is driven in a browser.",
|
|
1258
|
+
"start": "Test agent dry run",
|
|
1259
|
+
"completed": "Dry run finished: the environment was torn down and the branch deleted.",
|
|
1260
|
+
"failed": "Dry run failed",
|
|
1261
|
+
"verdict": {
|
|
1262
|
+
"operable": "An agent can operate this service.",
|
|
1263
|
+
"partially_operable": "An agent can only partly operate this service.",
|
|
1264
|
+
"inoperable": "An agent could not operate this service."
|
|
1265
|
+
},
|
|
1266
|
+
"counts": "{succeeded} of {attempted} operations succeeded; {authenticated} of those went through authentication.",
|
|
1267
|
+
"@counts": {
|
|
1268
|
+
"description": "Keep all three named placeholders intact: how many operations succeeded, how many were attempted, and how many of the successes went through authentication. The platform computes these from the agent report; the phrasing may be reordered to suit the language."
|
|
1269
|
+
},
|
|
1270
|
+
"missingContext": "What the agent was not told",
|
|
1271
|
+
"blockers": "What stopped the dry run",
|
|
1272
|
+
"operations": "Operations attempted",
|
|
1273
|
+
"operationsOmitted": "Reported operations dropped at the cap: {count}.",
|
|
1274
|
+
"@operationsOmitted": {
|
|
1275
|
+
"description": "Keep the named placeholder for the number of reported operations the platform dropped at its cap. Deliberately not pluralized: the count follows a colon so no noun has to agree with it."
|
|
1276
|
+
},
|
|
1277
|
+
"operationsUnreadable": "Reported operations the platform could not read: {count}.",
|
|
1278
|
+
"@operationsUnreadable": {
|
|
1279
|
+
"description": "Keep the named placeholder for the number of reported operations the platform could not read (they carried no usable name). Deliberately not pluralized: the count follows a colon so no noun has to agree with it."
|
|
1280
|
+
},
|
|
1281
|
+
"progress": "({completed} of {total} steps done)",
|
|
1282
|
+
"@progress": {
|
|
1283
|
+
"description": "Keep both named placeholders: how many of the prober's steps are done, and how many there are in total. Rendered directly after the running stage label, in parentheses."
|
|
1284
|
+
},
|
|
1285
|
+
"authenticated": "(authenticated)",
|
|
1286
|
+
"model": "Model: {model}",
|
|
1287
|
+
"@model": {
|
|
1288
|
+
"description": "Keep the named placeholder for the model identifier (e.g. `workers-ai:qwen`), which is injected verbatim and never translated."
|
|
1289
|
+
},
|
|
1290
|
+
"failure": {
|
|
1291
|
+
"auth_missing": "No credential supplied",
|
|
1292
|
+
"auth_rejected": "Credential refused",
|
|
1293
|
+
"access_unclear": "Unclear how to authenticate",
|
|
1294
|
+
"endpoint_unknown": "Could not find what to call",
|
|
1295
|
+
"unreachable": "Environment unreachable",
|
|
1296
|
+
"timeout": "Timed out",
|
|
1297
|
+
"server_error": "Service error",
|
|
1298
|
+
"bad_request": "Request refused as malformed",
|
|
1299
|
+
"tooling_missing": "No client or browser available",
|
|
1300
|
+
"other": "Other"
|
|
1301
|
+
}
|
|
1245
1302
|
}
|
|
1246
1303
|
},
|
|
1247
1304
|
"agentConfig": {
|