@cat-factory/app 0.115.3 → 0.116.1

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.
@@ -3,11 +3,13 @@ import { computed, onMounted, ref, watch } from 'vue'
3
3
  import type {
4
4
  Block,
5
5
  CloudProvider,
6
+ EnvironmentTestStage,
6
7
  InstanceSize,
7
8
  ProvisionType,
8
9
  ServiceProvisioning,
9
10
  } from '~/types/domain'
10
11
  import type {
12
+ ConflictReason,
11
13
  KubernetesManifestSource,
12
14
  KubernetesRenderer,
13
15
  ProvisioningComposeServiceCandidate,
@@ -19,6 +21,7 @@ import type {
19
21
  import RepoTreeBrowser from '~/components/github/RepoTreeBrowser.vue'
20
22
  import InspectorSection from '~/components/panels/inspector/InspectorSection.vue'
21
23
  import { apiErrorEnvelope } from '~/composables/api/errors'
24
+ import { parseConflict } from '~/composables/usePipelineErrorToast'
22
25
 
23
26
  // Service-level (frame) configuration: the service-owned PROVISIONING — the provision
24
27
  // TYPE this service produces (`infraless` / `docker-compose` / `kubernetes` / `custom`)
@@ -44,7 +47,7 @@ const services = useServicesStore()
44
47
  const infra = useInfraConfigStore()
45
48
  const agentRuns = useAgentRunsStore()
46
49
  const ui = useUiStore()
47
- const { t } = useI18n()
50
+ const { t, te } = useI18n()
48
51
 
49
52
  // The custom-manifest-type catalog feeds the `custom` picker. Cheap + shared (coalesced).
50
53
  // The repo list backs the detect-from-repo affordance (owner/name lookup).
@@ -247,6 +250,80 @@ async function generateOrFixManifest() {
247
250
  }
248
251
  }
249
252
 
253
+ // Ephemeral-environment self-test: run the whole create-branch → provision → tear-down →
254
+ // delete-branch cycle against this service's provisioning config and report success / the stage
255
+ // it failed at. The returned run is tracked live (by frame id) via the workspace stream store.
256
+ const envTest = useEnvironmentTestStore()
257
+ const envTestStarting = ref(false)
258
+ const envTestError = ref<string | null>(null)
259
+ // The newest self-test run for this frame — re-attaches after a reconnect (the run is carried in
260
+ // the snapshot while running), so the live stage keeps showing without a locally-held id.
261
+ const envTestRun = computed(() => envTest.runForBlock(props.block.id))
262
+ const envTestRunning = computed(() => envTestRun.value?.status === 'running')
263
+ // Nothing to provision for an `infraless` service, so there is nothing to test.
264
+ const canTestEnv = computed(() => provisionType.value !== 'infraless')
265
+
266
+ // Per-stage label KEYS, exhaustive over the contracts `EnvironmentTestStage` union: a new
267
+ // backend stage fails THIS typecheck until mapped (the key is resolved at runtime, so the
268
+ // typed-message-keys check can't see the `t()` lookup — the map's exhaustiveness is the
269
+ // drift guard, same pattern as `CONFLICT_TITLE_KEYS`).
270
+ const ENV_TEST_STAGE_KEYS: Record<EnvironmentTestStage, string> = {
271
+ creating_branch: 'inspector.testConfig.envTest.stage.creating_branch',
272
+ provisioning: 'inspector.testConfig.envTest.stage.provisioning',
273
+ tearing_down: 'inspector.testConfig.envTest.stage.tearing_down',
274
+ deleting_branch: 'inspector.testConfig.envTest.stage.deleting_branch',
275
+ done: 'inspector.testConfig.envTest.stage.done',
276
+ }
277
+
278
+ function envTestStageLabel(stage: EnvironmentTestStage): string {
279
+ const key = ENV_TEST_STAGE_KEYS[stage]
280
+ // `te`-guarded so a locale missing the key shows the raw stage id, never a raw message key.
281
+ return te(key) ? t(key) : stage
282
+ }
283
+
284
+ // The start preflight's machine-readable 409 reasons, mapped to their localized titles —
285
+ // exhaustive over the contracts `env_test_*` conflict reasons (same drift guard as above).
286
+ // The raw backend `message` is only the last-resort fallback for unmapped/non-conflict errors.
287
+ const ENV_TEST_CONFLICT_KEYS: Record<Extract<ConflictReason, `env_test_${string}`>, string> = {
288
+ env_test_not_a_frame: 'errors.conflict.title.env_test_not_a_frame',
289
+ env_test_infraless: 'errors.conflict.title.env_test_infraless',
290
+ env_test_not_provisionable: 'errors.conflict.title.env_test_not_provisionable',
291
+ env_test_no_vcs: 'errors.conflict.title.env_test_no_vcs',
292
+ }
293
+
294
+ function envTestErrorText(e: unknown): string {
295
+ const reason = parseConflict(e)?.reason
296
+ const key =
297
+ reason && reason in ENV_TEST_CONFLICT_KEYS
298
+ ? ENV_TEST_CONFLICT_KEYS[reason as keyof typeof ENV_TEST_CONFLICT_KEYS]
299
+ : undefined
300
+ if (key && te(key)) return t(key)
301
+ return apiErrorEnvelope(e)?.message ?? (e instanceof Error ? e.message : String(e))
302
+ }
303
+
304
+ async function startEnvTest() {
305
+ if (!canTestEnv.value || envTestStarting.value || envTestRunning.value) return
306
+ envTestStarting.value = true
307
+ envTestError.value = null
308
+ try {
309
+ await envTest.start(props.block.id)
310
+ } catch (e) {
311
+ envTestError.value = envTestErrorText(e)
312
+ } finally {
313
+ envTestStarting.value = false
314
+ }
315
+ }
316
+
317
+ async function stopEnvTest() {
318
+ const run = envTestRun.value
319
+ if (!run || run.status !== 'running') return
320
+ try {
321
+ await envTest.stop(run.id)
322
+ } catch (e) {
323
+ envTestError.value = envTestErrorText(e)
324
+ }
325
+ }
326
+
250
327
  // The provisioning hints (cloud provider + instance size) are advisory inputs to the
251
328
  // ephemeral-environment provisioner, not commonly tuned — keep them collapsed by default.
252
329
  const showProvisioning = ref(false)
@@ -923,5 +1000,80 @@ function setSize(value: InstanceSize) {
923
1000
  </div>
924
1001
  </div>
925
1002
  </InspectorSection>
1003
+
1004
+ <!-- Ephemeral-environment self-test: exercise the whole provisioning lifecycle against a
1005
+ throwaway branch and report success / the failing stage. Disabled for `infraless`. -->
1006
+ <div class="mt-3 space-y-2 border-t border-white/5 pt-3" data-testid="env-test-section">
1007
+ <div class="flex items-center justify-between gap-2">
1008
+ <div class="min-w-0">
1009
+ <p class="text-[11px] font-medium text-slate-300">
1010
+ {{ t('inspector.testConfig.envTest.title') }}
1011
+ </p>
1012
+ <p class="text-[11px] text-slate-400">{{ t('inspector.testConfig.envTest.hint') }}</p>
1013
+ </div>
1014
+ <UButton
1015
+ v-if="!envTestRunning"
1016
+ icon="i-lucide-flask-conical"
1017
+ size="xs"
1018
+ color="primary"
1019
+ variant="soft"
1020
+ data-testid="env-test-start"
1021
+ :loading="envTestStarting"
1022
+ :disabled="!canTestEnv"
1023
+ @click="startEnvTest"
1024
+ >
1025
+ {{ t('inspector.testConfig.envTest.start') }}
1026
+ </UButton>
1027
+ <UButton
1028
+ v-else
1029
+ icon="i-lucide-square"
1030
+ size="xs"
1031
+ color="neutral"
1032
+ variant="ghost"
1033
+ data-testid="env-test-stop"
1034
+ @click="stopEnvTest"
1035
+ >
1036
+ {{ t('inspector.testConfig.envTest.stop') }}
1037
+ </UButton>
1038
+ </div>
1039
+
1040
+ <p v-if="!canTestEnv" class="text-[11px] text-slate-500">
1041
+ {{ t('inspector.testConfig.envTest.infraless') }}
1042
+ </p>
1043
+
1044
+ <!-- Live stage + terminal outcome of the tracked run (pushed via the workspace stream). -->
1045
+ <p
1046
+ v-if="envTestRun"
1047
+ class="text-[11px]"
1048
+ :class="{
1049
+ 'text-sky-300/80': envTestRun.status === 'running',
1050
+ 'text-emerald-300/80': envTestRun.status === 'succeeded',
1051
+ 'text-rose-300/80': envTestRun.status === 'failed',
1052
+ }"
1053
+ data-testid="env-test-status"
1054
+ >
1055
+ <template v-if="envTestRun.status === 'running'">
1056
+ {{
1057
+ t('inspector.testConfig.envTest.running', {
1058
+ stage: envTestStageLabel(envTestRun.stage),
1059
+ })
1060
+ }}
1061
+ </template>
1062
+ <template v-else-if="envTestRun.status === 'succeeded'">
1063
+ {{ t('inspector.testConfig.envTest.succeeded') }}
1064
+ </template>
1065
+ <template v-else>
1066
+ {{ t('inspector.testConfig.envTest.failed') }}
1067
+ <template v-if="envTestRun.failedStage">
1068
+ ({{ envTestStageLabel(envTestRun.failedStage) }})
1069
+ </template>
1070
+ <span v-if="envTestRun.error" class="block text-rose-300/70">{{ envTestRun.error }}</span>
1071
+ </template>
1072
+ </p>
1073
+
1074
+ <p v-if="envTestError" class="text-[11px] text-rose-400" data-testid="env-test-error">
1075
+ {{ envTestError }}
1076
+ </p>
1077
+ </div>
926
1078
  </InspectorSection>
927
1079
  </template>
@@ -92,6 +92,18 @@ onMounted(() => void personal.load())
92
92
  const selectedMeta = computed(() => vendorMeta(vendor.value) ?? PERSONAL_VENDORS.value[0]!)
93
93
  const existing = computed(() => personal.subscriptions.find((s) => s.vendor === vendor.value))
94
94
 
95
+ /**
96
+ * Why the Connect button is disabled, or null when it's actionable. This is the single source
97
+ * of truth: the button's `:disabled` is bound to `disabledReason !== null`, and the same value
98
+ * renders in red next to it, so the button state and the shown reason can never disagree.
99
+ */
100
+ const disabledReason = computed(() => {
101
+ if (needsSignIn.value) return t('personalSubscriptions.disabledReason.signIn')
102
+ if (!token.value.trim()) return t('personalSubscriptions.disabledReason.token')
103
+ if (password.value.length < 6) return t('personalSubscriptions.disabledReason.password')
104
+ return null
105
+ })
106
+
95
107
  /** Renewal nudges for any connected subscription that's near or past expiry. */
96
108
  const renewals = computed(() =>
97
109
  personal.subscriptions
@@ -253,10 +265,11 @@ async function disconnect(v: SubscriptionVendor) {
253
265
  <UInput v-model="expiresOn" type="date" :disabled="needsSignIn" />
254
266
  </UFormField>
255
267
  </div>
256
- <div class="flex justify-end">
268
+ <div class="flex items-center justify-end gap-3">
269
+ <p v-if="disabledReason" class="text-sm text-rose-400">{{ disabledReason }}</p>
257
270
  <UButton
258
271
  :loading="busy"
259
- :disabled="needsSignIn || !token.trim() || password.length < 6"
272
+ :disabled="disabledReason !== null"
260
273
  icon="i-lucide-shield-check"
261
274
  @click="connect()"
262
275
  >
@@ -1,4 +1,10 @@
1
- import { listEnvironmentsContract, provisionEnvironmentContract } from '@cat-factory/contracts'
1
+ import {
2
+ getEnvironmentTestContract,
3
+ listEnvironmentsContract,
4
+ provisionEnvironmentContract,
5
+ startEnvironmentTestContract,
6
+ stopEnvironmentTestContract,
7
+ } from '@cat-factory/contracts'
2
8
  import type { ProvisionEnvironmentInput } from '@cat-factory/contracts'
3
9
  import type { ApiContext } from './context'
4
10
 
@@ -12,5 +18,14 @@ export function environmentsApi({ send, ws }: ApiContext) {
12
18
  // wizard's "trial provision" against the just-saved config. Returns the resulting handle.
13
19
  provisionEnvironment: (workspaceId: string, body: ProvisionEnvironmentInput) =>
14
20
  send(provisionEnvironmentContract, { pathPrefix: ws(workspaceId), body }),
21
+
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
+ startEnvironmentTest: (workspaceId: string, blockId: string) =>
25
+ send(startEnvironmentTestContract, { pathPrefix: ws(workspaceId), pathParams: { blockId } }),
26
+ getEnvironmentTest: (workspaceId: string, id: string) =>
27
+ send(getEnvironmentTestContract, { pathPrefix: ws(workspaceId), pathParams: { id } }),
28
+ stopEnvironmentTest: (workspaceId: string, id: string) =>
29
+ send(stopEnvironmentTestContract, { pathPrefix: ws(workspaceId), pathParams: { id } }),
15
30
  }
16
31
  }
@@ -55,6 +55,10 @@ const CONFLICT_TITLE_KEYS: Record<Exclude<ConflictReason, BespokeConflictReason>
55
55
  model_policy_blocked: 'errors.conflict.title.model_policy_blocked',
56
56
  model_policy_unsupported: 'errors.conflict.title.model_policy_unsupported',
57
57
  deployer_required_before_tester: 'errors.conflict.title.deployer_required_before_tester',
58
+ env_test_not_a_frame: 'errors.conflict.title.env_test_not_a_frame',
59
+ env_test_infraless: 'errors.conflict.title.env_test_infraless',
60
+ env_test_not_provisionable: 'errors.conflict.title.env_test_not_provisionable',
61
+ env_test_no_vcs: 'errors.conflict.title.env_test_no_vcs',
58
62
  }
59
63
 
60
64
  /**
@@ -18,6 +18,7 @@ export function useWorkspaceStream() {
18
18
  const execution = useExecutionStore()
19
19
  const board = useBoardStore()
20
20
  const agentRuns = useAgentRunsStore()
21
+ const environmentTest = useEnvironmentTestStore()
21
22
  const notifications = useNotificationsStore()
22
23
  const observability = useObservabilityStore()
23
24
  const requirements = useRequirementsStore()
@@ -102,6 +103,11 @@ export function useWorkspaceStream() {
102
103
  // the infrastructure-providers window's "repairing…" indicator updates in place
103
104
  // (then flips to ok / residual issues / a failure) without a refetch. No board block.
104
105
  agentRuns.upsertEnvConfigRepair(event.job)
106
+ } else if (event.type === 'envTest') {
107
+ // An ephemeral-environment self-test advanced a stage — patch the run so the service
108
+ // inspector's "Test environment creation" control shows the live stage + final
109
+ // outcome in place without a refetch. No board block.
110
+ environmentTest.upsert(event.run)
105
111
  } else if (event.type === 'notification') {
106
112
  // A PR needs a merge decision, a pipeline finished, or CI gave up — patch the
107
113
  // inbox + per-block badge in place (resolved ones drop out of the inbox).
@@ -0,0 +1,103 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest'
2
+ import type { EnvironmentTestRun } from '~/types/domain'
3
+ import { useEnvironmentTestStore } from '~/stores/environmentTest'
4
+
5
+ // The store resolves `useApi()` at setup; override the inert global stub from
6
+ // `test/setup.ts` with a per-suite mock so the hydrate reconcile point-read is observable.
7
+ const apiMock = { getEnvironmentTest: vi.fn() }
8
+ vi.stubGlobal('useApi', () => apiMock)
9
+
10
+ /** Minimal EnvironmentTestRun factory — only the fields the store's reconcile logic touches. */
11
+ function run(id: string, over: Partial<EnvironmentTestRun> = {}): EnvironmentTestRun {
12
+ return {
13
+ id,
14
+ workspaceId: 'ws_test',
15
+ blockId: `blk_${id}`,
16
+ status: 'running',
17
+ stage: 'provisioning',
18
+ branch: null,
19
+ envUrl: null,
20
+ error: null,
21
+ failedStage: null,
22
+ createdAt: 1,
23
+ updatedAt: 1,
24
+ ...over,
25
+ }
26
+ }
27
+
28
+ describe('environmentTest store — monotonic run reconcile', () => {
29
+ let store: ReturnType<typeof useEnvironmentTestStore>
30
+ beforeEach(() => {
31
+ apiMock.getEnvironmentTest = vi.fn(async () => {
32
+ throw new Error('not stubbed')
33
+ })
34
+ store = useEnvironmentTestStore()
35
+ })
36
+
37
+ it('hydrate does NOT regress a run a newer live event already advanced', () => {
38
+ // A live `envTest: failed` event landed first (newer updatedAt).
39
+ store.upsert(run('r1', { status: 'failed', failedStage: 'provisioning', updatedAt: 5 }))
40
+ // A lagging `workspace.refresh()` then hydrates a STALE snapshot that still saw the run
41
+ // as `running` (older updatedAt) — it must NOT clobber the terminal state (terminal runs
42
+ // emit nothing further, so the inspector would be stuck on "testing" forever).
43
+ store.hydrate([run('r1', { status: 'running', updatedAt: 2 })], 'ws_test')
44
+ expect(store.runForBlock('blk_r1')!.status).toBe('failed')
45
+ })
46
+
47
+ it('hydrate does NOT drop a live-added run the stale snapshot never saw', () => {
48
+ // Terminal runs are omitted from the snapshot BY DESIGN, so a just-finished run the
49
+ // inspector still shows must survive a full refresh.
50
+ store.upsert(run('r1', { status: 'succeeded', stage: 'done', updatedAt: 5 }))
51
+ store.hydrate([], 'ws_test')
52
+ expect(store.runForBlock('blk_r1')!.status).toBe('succeeded')
53
+ })
54
+
55
+ it('hydrate point-reads a preserved RUNNING run the snapshot omitted (finished offline)', async () => {
56
+ // The run was still `running` when the socket dropped; it finished while disconnected, so
57
+ // the reconnect snapshot no longer carries it and no event replays — the hydrate must
58
+ // re-read it to pick up the outcome instead of stranding a stale "testing" state.
59
+ store.upsert(run('r1', { status: 'running', updatedAt: 5 }))
60
+ apiMock.getEnvironmentTest = vi.fn(async () =>
61
+ run('r1', { status: 'succeeded', stage: 'done', updatedAt: 9 }),
62
+ )
63
+ store.hydrate([], 'ws_test')
64
+ expect(apiMock.getEnvironmentTest).toHaveBeenCalledWith('ws_test', 'r1')
65
+ await vi.waitFor(() => expect(store.runForBlock('blk_r1')!.status).toBe('succeeded'))
66
+ })
67
+
68
+ it('a STALE point-read cannot regress a run a live event advanced meanwhile', async () => {
69
+ store.upsert(run('r1', { status: 'running', updatedAt: 5 }))
70
+ // The reconcile read resolves with an OLDER view of the run than the live event that
71
+ // lands while it is in flight — the monotonic upsert must keep the newer state.
72
+ apiMock.getEnvironmentTest = vi.fn(async () => run('r1', { status: 'running', updatedAt: 4 }))
73
+ store.hydrate([], 'ws_test')
74
+ store.upsert(run('r1', { status: 'failed', failedStage: 'tearing_down', updatedAt: 8 }))
75
+ await vi.waitFor(() => expect(apiMock.getEnvironmentTest).toHaveBeenCalled())
76
+ await Promise.resolve()
77
+ expect(store.runForBlock('blk_r1')!.status).toBe('failed')
78
+ })
79
+
80
+ it('hydrate DROPS a cached run from a different workspace (board switch starts clean)', () => {
81
+ store.upsert(run('r1', { status: 'failed', updatedAt: 5, workspaceId: 'ws_other' }))
82
+ store.hydrate([run('r2', { workspaceId: 'ws_test' })], 'ws_test')
83
+ expect(store.runs.map((r) => r.id)).toEqual(['r2'])
84
+ })
85
+
86
+ it('hydrate DOES apply a genuinely newer snapshot', () => {
87
+ store.upsert(run('r1', { status: 'running', updatedAt: 2 }))
88
+ store.hydrate(
89
+ [run('r1', { status: 'running', stage: 'tearing_down', updatedAt: 9 })],
90
+ 'ws_test',
91
+ )
92
+ expect(store.runForBlock('blk_r1')!.stage).toBe('tearing_down')
93
+ })
94
+
95
+ it('upsert ignores an older/out-of-order write but applies newer/equal', () => {
96
+ store.upsert(run('r1', { status: 'failed', updatedAt: 5 }))
97
+ // e.g. a `start()` response resolving AFTER the fast-failing run's terminal event landed.
98
+ store.upsert(run('r1', { status: 'running', stage: 'creating_branch', updatedAt: 3 }))
99
+ expect(store.runForBlock('blk_r1')!.status).toBe('failed')
100
+ store.upsert(run('r1', { status: 'succeeded', stage: 'done', updatedAt: 5 }))
101
+ expect(store.runForBlock('blk_r1')!.status).toBe('succeeded')
102
+ })
103
+ })
@@ -0,0 +1,114 @@
1
+ import { defineStore } from 'pinia'
2
+ import { ref } from 'vue'
3
+ import type { EnvironmentTestRun } from '~/types/domain'
4
+ import { useWorkspaceStore } from '~/stores/workspace'
5
+
6
+ /**
7
+ * Ephemeral-environment self-test runs. A developer starts one from a service frame's inspector
8
+ * (`POST …/blocks/:id/environment-test`); the backend drives the create-branch → provision →
9
+ * tear-down → delete-branch cycle durably and pushes live `envTest` stage events, which
10
+ * `useWorkspaceStream` folds in via {@link upsert}. In-flight runs also arrive in the workspace
11
+ * snapshot ({@link hydrate}) so the inspector re-attaches to a running test after a reconnect.
12
+ *
13
+ * 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). Terminal runs are kept in memory for the session so the
15
+ * inspector can show the last outcome; the snapshot only carries running ones.
16
+ */
17
+ export const useEnvironmentTestStore = defineStore('environmentTest', () => {
18
+ const api = useApi()
19
+
20
+ /** All known runs (running + this session's terminal ones), newest first. */
21
+ const runs = ref<EnvironmentTestRun[]>([])
22
+
23
+ function sortByCreated(list: EnvironmentTestRun[]): EnvironmentTestRun[] {
24
+ return [...list].sort((a, b) => b.createdAt - a.createdAt)
25
+ }
26
+
27
+ /**
28
+ * Reconcile the cached runs with a server snapshot for `workspaceId`. A snapshot is
29
+ * authoritative EXCEPT where a live `envTest` event has already advanced (or ADDED) a run
30
+ * past what this (possibly stale) read observed — a `board`-event refresh or the on-connect
31
+ * resync can resolve AFTER a newer event already landed. Same two clobber hazards as
32
+ * `agentRuns.hydrate`, both handled here:
33
+ * - REGRESS: a run present in BOTH the snapshot and the cache — keep the newer-by-`updatedAt`
34
+ * version, so a lagging refresh can't revert a `failed`/`succeeded` run to `running`
35
+ * (terminal runs emit nothing further, so the inspector would be stuck on "testing").
36
+ * - DROP: a run a live event just ADDED that the (older) snapshot never saw — replacing from
37
+ * the snapshot alone would silently drop it (and terminal runs are omitted from the
38
+ * snapshot by design, so a finished run the inspector still shows would vanish).
39
+ * Preserve such cached runs, scoped to `workspaceId` so a board SWITCH still starts clean.
40
+ *
41
+ * A preserved RUNNING run absent from the snapshot may also have reached terminal while the
42
+ * socket was down (no event replays, and the snapshot omits terminal runs) — point-read it
43
+ * best-effort to pick up the outcome; {@link upsert}'s monotonic guard makes the read safe
44
+ * against racing live events.
45
+ */
46
+ function hydrate(snapshotRuns: EnvironmentTestRun[], workspaceId: string) {
47
+ const incomingIds = new Set(snapshotRuns.map((r) => r.id))
48
+ const held = new Map(runs.value.map((r) => [r.id, r]))
49
+ const reconciled = snapshotRuns.map((incoming) => {
50
+ const current = held.get(incoming.id)
51
+ return current && current.updatedAt > incoming.updatedAt ? current : incoming
52
+ })
53
+ const preserved = [...held.values()].filter(
54
+ (r) => !incomingIds.has(r.id) && r.workspaceId === workspaceId,
55
+ )
56
+ runs.value = sortByCreated([...reconciled, ...preserved])
57
+ // A still-`running` preserved run wasn't in the snapshot, so either the snapshot is stale
58
+ // (the run is genuinely newer) or the run FINISHED while we were disconnected — resolve
59
+ // which by re-reading it (non-blocking; failures leave the cached state as-is).
60
+ for (const r of preserved) {
61
+ if (r.status === 'running') void reconcileRun(workspaceId, r.id)
62
+ }
63
+ }
64
+
65
+ /** Best-effort point-read of one run, folded in through the monotonic {@link upsert}. */
66
+ async function reconcileRun(workspaceId: string, id: string) {
67
+ try {
68
+ upsert(await api.getEnvironmentTest(workspaceId, id))
69
+ } catch {
70
+ // Best-effort: a transient fetch failure just leaves the cached state; the next
71
+ // snapshot/event reconciles it.
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Fold a live-pushed (or freshly-started/stopped) run into the cache. Monotonic by
77
+ * `updatedAt`: never let a stale/out-of-order write regress a run a newer one already
78
+ * advanced — e.g. a `start()` response resolving AFTER a fast-failing run's terminal
79
+ * event already landed (same guard as {@link hydrate}).
80
+ */
81
+ function upsert(run: EnvironmentTestRun) {
82
+ const i = runs.value.findIndex((r) => r.id === run.id)
83
+ if (i >= 0) {
84
+ if (run.updatedAt >= runs.value[i]!.updatedAt) runs.value[i] = run
85
+ } else runs.value.unshift(run)
86
+ }
87
+
88
+ function runById(id: string): EnvironmentTestRun | undefined {
89
+ return runs.value.find((r) => r.id === id)
90
+ }
91
+
92
+ /** The newest run for a service frame — the inspector's per-service attach point. */
93
+ function runForBlock(blockId: string): EnvironmentTestRun | undefined {
94
+ return runs.value.find((r) => r.blockId === blockId)
95
+ }
96
+
97
+ /** Start a self-test against a service frame; the returned run is tracked immediately. */
98
+ async function start(blockId: string): Promise<EnvironmentTestRun> {
99
+ const ws = useWorkspaceStore()
100
+ const run = await api.startEnvironmentTest(ws.requireId(), blockId)
101
+ upsert(run)
102
+ return run
103
+ }
104
+
105
+ /** Stop a running self-test (best-effort cleanup, then failed). */
106
+ async function stop(id: string): Promise<EnvironmentTestRun> {
107
+ const ws = useWorkspaceStore()
108
+ const run = await api.stopEnvironmentTest(ws.requireId(), id)
109
+ upsert(run)
110
+ return run
111
+ }
112
+
113
+ return { runs, hydrate, upsert, runById, runForBlock, start, stop }
114
+ })
@@ -15,6 +15,7 @@ const INERT_STORES = [
15
15
  'useClarityStore',
16
16
  'useConsensusStore',
17
17
  'useDocInterviewStore',
18
+ 'useEnvironmentTestStore',
18
19
  'useExecutionStore',
19
20
  'useFragmentsStore',
20
21
  'useGitHubStore',
@@ -12,6 +12,7 @@ import { useBoardStore } from '~/stores/board'
12
12
  import { usePipelinesStore } from '~/stores/pipelines'
13
13
  import { useExecutionStore } from '~/stores/execution'
14
14
  import { useAgentRunsStore } from '~/stores/agentRuns'
15
+ import { useEnvironmentTestStore } from '~/stores/environmentTest'
15
16
  import { useNotificationsStore } from '~/stores/notifications'
16
17
  import { useRiskPoliciesStore } from '~/stores/riskPolicies'
17
18
  import { useSharedStacksStore } from '~/stores/sharedStacks'
@@ -120,6 +121,7 @@ export const useWorkspaceStore = defineStore(
120
121
  useExecutionStore().hydrate(snapshot.executions, snapshot.workspace.id)
121
122
  useAgentRunsStore().hydrate(snapshot.bootstrapJobs ?? [], snapshot.workspace.id)
122
123
  useAgentRunsStore().hydrateEnvConfigRepair(snapshot.envConfigRepairJobs ?? [])
124
+ useEnvironmentTestStore().hydrate(snapshot.environmentTestRuns ?? [], snapshot.workspace.id)
123
125
  useNotificationsStore().hydrate(snapshot.notifications ?? [])
124
126
  useRiskPoliciesStore().hydrate(
125
127
  snapshot.riskPolicies ?? [],
@@ -39,6 +39,9 @@ export type {
39
39
  FrontendBackendSource,
40
40
  ResolvedFrontendBinding,
41
41
  EnvironmentHandle,
42
+ EnvironmentTestRun,
43
+ EnvironmentTestStage,
44
+ EnvironmentTestStatus,
42
45
  ServiceConnection,
43
46
  FrontendBranch,
44
47
  FrontendPackageManager,
@@ -1093,6 +1093,23 @@
1093
1093
  "title": "Einrichtung der Compose-Umgebung",
1094
1094
  "hint": "Konfiguriere das Rezept, die Preflights und den Handler, damit der Deployer diesen Stack bereitstellt.",
1095
1095
  "open": "Assistent öffnen"
1096
+ },
1097
+ "envTest": {
1098
+ "title": "Umgebungserstellung testen",
1099
+ "hint": "Führt den gesamten Lebenszyklus gegen einen Wegwerf-Branch aus: Branch erstellen, bereitstellen, abbauen, Branch löschen.",
1100
+ "start": "Umgebungserstellung testen",
1101
+ "stop": "Stopp",
1102
+ "infraless": "Konfiguriere oben einen Bereitstellungstyp, um die Umgebungserstellung zu testen.",
1103
+ "running": "Wird getestet: {stage}",
1104
+ "succeeded": "Test bestanden: Die Umgebung wurde erstellt und abgebaut, und der Branch wurde gelöscht.",
1105
+ "failed": "Test fehlgeschlagen",
1106
+ "stage": {
1107
+ "creating_branch": "Branch wird erstellt",
1108
+ "provisioning": "Umgebung wird bereitgestellt",
1109
+ "tearing_down": "Umgebung wird abgebaut",
1110
+ "deleting_branch": "Branch wird gelöscht",
1111
+ "done": "fertig"
1112
+ }
1096
1113
  }
1097
1114
  },
1098
1115
  "agentConfig": {
@@ -2547,6 +2564,11 @@
2547
2564
  "renewsField": "Abonnement verlängert sich am (optional)",
2548
2565
  "connect": "Verbinden",
2549
2566
  "replace": "Ersetzen",
2567
+ "disabledReason": {
2568
+ "signIn": "Melden Sie sich an, um ein persönliches Abonnement zu verbinden",
2569
+ "token": "Geben Sie Ihr Token ein, um fortzufahren",
2570
+ "password": "Geben Sie ein persönliches Passwort mit mindestens 6 Zeichen ein"
2571
+ },
2550
2572
  "expires": "Läuft ab am {date}",
2551
2573
  "noExpiry": "Kein Ablaufdatum gesetzt",
2552
2574
  "renewal": {
@@ -3786,7 +3808,11 @@
3786
3808
  "visual_pipeline_no_frontend": "Kein Frontend zum Testen",
3787
3809
  "model_policy_blocked": "Modell durch Kontorichtlinie blockiert",
3788
3810
  "model_policy_unsupported": "Modellrichtlinie hier nicht verfügbar",
3789
- "deployer_required_before_tester": "Füge einen Deployer vor dem Tester hinzu"
3811
+ "deployer_required_before_tester": "Füge einen Deployer vor dem Tester hinzu",
3812
+ "env_test_not_a_frame": "Kein Dienst",
3813
+ "env_test_infraless": "Nichts zu testen",
3814
+ "env_test_not_provisionable": "Umgebungs-Handler nicht konfiguriert",
3815
+ "env_test_no_vcs": "Git-Anbieter nicht verbunden"
3790
3816
  },
3791
3817
  "fallbackMessage": "Diese Aktion steht im Konflikt mit dem aktuellen Zustand.",
3792
3818
  "providersUnconfigured": {
@@ -475,7 +475,11 @@
475
475
  "visual_pipeline_no_frontend": "No frontend to test",
476
476
  "model_policy_blocked": "Model blocked by account policy",
477
477
  "model_policy_unsupported": "Model policy not available here",
478
- "deployer_required_before_tester": "Add a Deployer before the Tester"
478
+ "deployer_required_before_tester": "Add a Deployer before the Tester",
479
+ "env_test_not_a_frame": "Not a service",
480
+ "env_test_infraless": "Nothing to test",
481
+ "env_test_not_provisionable": "Environment handler not configured",
482
+ "env_test_no_vcs": "Git provider not connected"
479
483
  },
480
484
  "fallbackMessage": "This action conflicts with the current state.",
481
485
  "providersUnconfigured": {
@@ -529,6 +533,11 @@
529
533
  "renewsField": "Subscription renews on (optional)",
530
534
  "connect": "Connect",
531
535
  "replace": "Replace",
536
+ "disabledReason": {
537
+ "signIn": "Sign in to connect a personal subscription",
538
+ "token": "Enter your token to continue",
539
+ "password": "Enter a personal password of at least 6 characters"
540
+ },
532
541
  "expires": "Expires {date}",
533
542
  "noExpiry": "No expiry set",
534
543
  "renewal": {
@@ -837,6 +846,23 @@
837
846
  "title": "Compose environment setup",
838
847
  "hint": "Configure the recipe, preflights, and handler so the Deployer provisions this stack.",
839
848
  "open": "Open wizard"
849
+ },
850
+ "envTest": {
851
+ "title": "Test environment creation",
852
+ "hint": "Runs the whole lifecycle against a throwaway branch: create branch, provision, tear down, delete branch.",
853
+ "start": "Test environment creation",
854
+ "stop": "Stop",
855
+ "infraless": "Configure a provision type above to test environment creation.",
856
+ "running": "Testing: {stage}",
857
+ "succeeded": "Test passed: the environment was created and torn down, and the branch was deleted.",
858
+ "failed": "Test failed",
859
+ "stage": {
860
+ "creating_branch": "creating branch",
861
+ "provisioning": "provisioning environment",
862
+ "tearing_down": "tearing down environment",
863
+ "deleting_branch": "deleting branch",
864
+ "done": "done"
865
+ }
840
866
  }
841
867
  },
842
868
  "agentConfig": {
@@ -436,7 +436,11 @@
436
436
  "visual_pipeline_no_frontend": "No hay frontend que probar",
437
437
  "model_policy_blocked": "Modelo bloqueado por la política de la cuenta",
438
438
  "model_policy_unsupported": "La política de modelos no está disponible aquí",
439
- "deployer_required_before_tester": "Añade un Deployer antes del Tester"
439
+ "deployer_required_before_tester": "Añade un Deployer antes del Tester",
440
+ "env_test_not_a_frame": "No es un servicio",
441
+ "env_test_infraless": "Nada que probar",
442
+ "env_test_not_provisionable": "Gestor de entorno no configurado",
443
+ "env_test_no_vcs": "Proveedor de Git no conectado"
440
444
  },
441
445
  "fallbackMessage": "Esta acción entra en conflicto con el estado actual.",
442
446
  "providersUnconfigured": {
@@ -478,6 +482,11 @@
478
482
  "renewsField": "La suscripción se renueva el (opcional)",
479
483
  "connect": "Conectar",
480
484
  "replace": "Reemplazar",
485
+ "disabledReason": {
486
+ "signIn": "Inicia sesión para conectar una suscripción personal",
487
+ "token": "Introduce tu token para continuar",
488
+ "password": "Introduce una contraseña personal de al menos 6 caracteres"
489
+ },
481
490
  "expires": "Caduca el {date}",
482
491
  "noExpiry": "Sin fecha de caducidad",
483
492
  "renewal": {
@@ -783,6 +792,23 @@
783
792
  "title": "Configuración de entorno Compose",
784
793
  "hint": "Configura la receta, las comprobaciones previas y el gestor para que el Deployer aprovisione este stack.",
785
794
  "open": "Abrir asistente"
795
+ },
796
+ "envTest": {
797
+ "title": "Probar la creación del entorno",
798
+ "hint": "Ejecuta todo el ciclo de vida sobre una rama desechable: crear rama, aprovisionar, desmontar, eliminar rama.",
799
+ "start": "Probar la creación del entorno",
800
+ "stop": "Detener",
801
+ "infraless": "Configura arriba un tipo de aprovisionamiento para probar la creación del entorno.",
802
+ "running": "Probando: {stage}",
803
+ "succeeded": "Prueba superada: el entorno se creó y se desmontó, y la rama se eliminó.",
804
+ "failed": "La prueba falló",
805
+ "stage": {
806
+ "creating_branch": "creando rama",
807
+ "provisioning": "aprovisionando entorno",
808
+ "tearing_down": "desmontando entorno",
809
+ "deleting_branch": "eliminando rama",
810
+ "done": "listo"
811
+ }
786
812
  }
787
813
  },
788
814
  "agentConfig": {
@@ -436,7 +436,11 @@
436
436
  "visual_pipeline_no_frontend": "Aucun frontend à tester",
437
437
  "model_policy_blocked": "Modèle bloqué par la politique du compte",
438
438
  "model_policy_unsupported": "La politique de modèles n'est pas disponible ici",
439
- "deployer_required_before_tester": "Ajoutez un Deployer avant le Testeur"
439
+ "deployer_required_before_tester": "Ajoutez un Deployer avant le Testeur",
440
+ "env_test_not_a_frame": "Pas un service",
441
+ "env_test_infraless": "Rien à tester",
442
+ "env_test_not_provisionable": "Gestionnaire d'environnement non configuré",
443
+ "env_test_no_vcs": "Fournisseur Git non connecté"
440
444
  },
441
445
  "fallbackMessage": "Cette action est en conflit avec l’état actuel.",
442
446
  "providersUnconfigured": {
@@ -478,6 +482,11 @@
478
482
  "renewsField": "L'abonnement se renouvelle le (facultatif)",
479
483
  "connect": "Connecter",
480
484
  "replace": "Remplacer",
485
+ "disabledReason": {
486
+ "signIn": "Connectez-vous pour associer un abonnement personnel",
487
+ "token": "Saisissez votre jeton pour continuer",
488
+ "password": "Saisissez un mot de passe personnel d'au moins 6 caractères"
489
+ },
481
490
  "expires": "Expire le {date}",
482
491
  "noExpiry": "Aucune date d'expiration",
483
492
  "renewal": {
@@ -783,6 +792,23 @@
783
792
  "title": "Configuration d'environnement Compose",
784
793
  "hint": "Configurez la recette, les vérifications préalables et le gestionnaire pour que le Deployer provisionne cette stack.",
785
794
  "open": "Ouvrir l'assistant"
795
+ },
796
+ "envTest": {
797
+ "title": "Tester la création de l'environnement",
798
+ "hint": "Exécute tout le cycle de vie sur une branche jetable : créer la branche, provisionner, démonter, supprimer la branche.",
799
+ "start": "Tester la création de l'environnement",
800
+ "stop": "Arrêter",
801
+ "infraless": "Configurez un type de provisionnement ci-dessus pour tester la création de l'environnement.",
802
+ "running": "Test en cours : {stage}",
803
+ "succeeded": "Test réussi : l'environnement a été créé puis démonté, et la branche a été supprimée.",
804
+ "failed": "Échec du test",
805
+ "stage": {
806
+ "creating_branch": "création de la branche",
807
+ "provisioning": "provisionnement de l'environnement",
808
+ "tearing_down": "démontage de l'environnement",
809
+ "deleting_branch": "suppression de la branche",
810
+ "done": "terminé"
811
+ }
786
812
  }
787
813
  },
788
814
  "agentConfig": {
@@ -436,7 +436,11 @@
436
436
  "visual_pipeline_no_frontend": "אין frontend לבדיקה",
437
437
  "model_policy_blocked": "המודל נחסם על ידי מדיניות החשבון",
438
438
  "model_policy_unsupported": "מדיניות המודלים אינה זמינה כאן",
439
- "deployer_required_before_tester": "הוסף Deployer לפני ה-Tester"
439
+ "deployer_required_before_tester": "הוסף Deployer לפני ה-Tester",
440
+ "env_test_not_a_frame": "לא שירות",
441
+ "env_test_infraless": "אין מה לבדוק",
442
+ "env_test_not_provisionable": "מטפל הסביבה אינו מוגדר",
443
+ "env_test_no_vcs": "ספק Git אינו מחובר"
440
444
  },
441
445
  "fallbackMessage": "פעולה זו מתנגשת עם המצב הנוכחי.",
442
446
  "providersUnconfigured": {
@@ -478,6 +482,11 @@
478
482
  "renewsField": "המנוי מתחדש בתאריך (אופציונלי)",
479
483
  "connect": "חבר",
480
484
  "replace": "החלף",
485
+ "disabledReason": {
486
+ "signIn": "היכנס כדי לחבר מנוי אישי",
487
+ "token": "הזן את הטוקן שלך כדי להמשיך",
488
+ "password": "הזן סיסמה אישית באורך 6 תווים לפחות"
489
+ },
481
490
  "expires": "פג בתאריך {date}",
482
491
  "noExpiry": "לא הוגדרה תפוגה",
483
492
  "renewal": {
@@ -783,6 +792,23 @@
783
792
  "title": "הגדרת סביבת Compose",
784
793
  "hint": "הגדירו את המתכון, בדיקות המוכנות והמטפל כדי שה-Deployer יקצה את הסטאק הזה.",
785
794
  "open": "פתיחת אשף"
795
+ },
796
+ "envTest": {
797
+ "title": "בדיקת יצירת סביבה",
798
+ "hint": "מריץ את כל מחזור החיים על ענף חד-פעמי: יצירת ענף, הקצאה, פירוק ומחיקת הענף.",
799
+ "start": "בדיקת יצירת סביבה",
800
+ "stop": "עצירה",
801
+ "infraless": "הגדירו סוג הקצאה למעלה כדי לבדוק את יצירת הסביבה.",
802
+ "running": "בבדיקה: {stage}",
803
+ "succeeded": "הבדיקה עברה: הסביבה נוצרה ופורקה, והענף נמחק.",
804
+ "failed": "הבדיקה נכשלה",
805
+ "stage": {
806
+ "creating_branch": "יוצר ענף",
807
+ "provisioning": "מקצה סביבה",
808
+ "tearing_down": "מפרק סביבה",
809
+ "deleting_branch": "מוחק ענף",
810
+ "done": "הושלם"
811
+ }
786
812
  }
787
813
  },
788
814
  "agentConfig": {
@@ -1093,6 +1093,23 @@
1093
1093
  "title": "Configurazione ambiente Compose",
1094
1094
  "hint": "Configura la ricetta, i preflight e l'handler affinche' il Deployer provisioni questo stack.",
1095
1095
  "open": "Apri la procedura guidata"
1096
+ },
1097
+ "envTest": {
1098
+ "title": "Prova la creazione dell'ambiente",
1099
+ "hint": "Esegue l'intero ciclo di vita su un branch usa e getta: crea branch, provisiona, smonta, elimina branch.",
1100
+ "start": "Prova la creazione dell'ambiente",
1101
+ "stop": "Ferma",
1102
+ "infraless": "Configura sopra un tipo di provisioning per provare la creazione dell'ambiente.",
1103
+ "running": "Prova in corso: {stage}",
1104
+ "succeeded": "Prova superata: l'ambiente è stato creato e smontato, e il branch è stato eliminato.",
1105
+ "failed": "Prova fallita",
1106
+ "stage": {
1107
+ "creating_branch": "creazione del branch",
1108
+ "provisioning": "provisioning dell'ambiente",
1109
+ "tearing_down": "smontaggio dell'ambiente",
1110
+ "deleting_branch": "eliminazione del branch",
1111
+ "done": "completato"
1112
+ }
1096
1113
  }
1097
1114
  },
1098
1115
  "agentConfig": {
@@ -2547,6 +2564,11 @@
2547
2564
  "renewsField": "L'abbonamento si rinnova il (opzionale)",
2548
2565
  "connect": "Collega",
2549
2566
  "replace": "Sostituisci",
2567
+ "disabledReason": {
2568
+ "signIn": "Accedi per collegare un abbonamento personale",
2569
+ "token": "Inserisci il tuo token per continuare",
2570
+ "password": "Inserisci una password personale di almeno 6 caratteri"
2571
+ },
2550
2572
  "expires": "Scade il {date}",
2551
2573
  "noExpiry": "Nessuna scadenza impostata",
2552
2574
  "renewal": {
@@ -3786,7 +3808,11 @@
3786
3808
  "visual_pipeline_no_frontend": "Nessun frontend da testare",
3787
3809
  "model_policy_blocked": "Modello bloccato dalla policy dell'account",
3788
3810
  "model_policy_unsupported": "Policy dei modelli non disponibile qui",
3789
- "deployer_required_before_tester": "Aggiungi un Deployer prima del Tester"
3811
+ "deployer_required_before_tester": "Aggiungi un Deployer prima del Tester",
3812
+ "env_test_not_a_frame": "Non è un servizio",
3813
+ "env_test_infraless": "Niente da testare",
3814
+ "env_test_not_provisionable": "Handler dell'ambiente non configurato",
3815
+ "env_test_no_vcs": "Provider Git non connesso"
3790
3816
  },
3791
3817
  "fallbackMessage": "Questa azione è in conflitto con lo stato attuale.",
3792
3818
  "providersUnconfigured": {
@@ -436,7 +436,11 @@
436
436
  "visual_pipeline_no_frontend": "テスト対象のフロントエンドがありません",
437
437
  "model_policy_blocked": "アカウントのポリシーによりモデルがブロックされています",
438
438
  "model_policy_unsupported": "モデルポリシーはここでは利用できません",
439
- "deployer_required_before_tester": "テスターの前にDeployerを追加してください"
439
+ "deployer_required_before_tester": "テスターの前にDeployerを追加してください",
440
+ "env_test_not_a_frame": "サービスではありません",
441
+ "env_test_infraless": "テストする対象がありません",
442
+ "env_test_not_provisionable": "環境ハンドラーが設定されていません",
443
+ "env_test_no_vcs": "Git プロバイダーが未接続です"
440
444
  },
441
445
  "fallbackMessage": "この操作は現在の状態と競合します。",
442
446
  "providersUnconfigured": {
@@ -478,6 +482,11 @@
478
482
  "renewsField": "サブスクリプションの更新日 (任意)",
479
483
  "connect": "接続",
480
484
  "replace": "置き換え",
485
+ "disabledReason": {
486
+ "signIn": "個人サブスクリプションを接続するにはサインインしてください",
487
+ "token": "続行するにはトークンを入力してください",
488
+ "password": "6文字以上の個人パスワードを入力してください"
489
+ },
481
490
  "expires": "有効期限 {date}",
482
491
  "noExpiry": "有効期限は未設定",
483
492
  "renewal": {
@@ -783,6 +792,23 @@
783
792
  "title": "Compose 環境のセットアップ",
784
793
  "hint": "レシピ・プリフライト・ハンドラーを設定して、Deployer がこのスタックをプロビジョニングできるようにします。",
785
794
  "open": "ウィザードを開く"
795
+ },
796
+ "envTest": {
797
+ "title": "環境作成のテスト",
798
+ "hint": "使い捨てブランチに対してライフサイクル全体を実行します。ブランチ作成、プロビジョニング、破棄、ブランチ削除の順に行います。",
799
+ "start": "環境作成のテスト",
800
+ "stop": "停止",
801
+ "infraless": "環境作成をテストするには、上でプロビジョニングの種類を設定してください。",
802
+ "running": "テスト中: {stage}",
803
+ "succeeded": "テスト成功: 環境が作成されて破棄され、ブランチが削除されました。",
804
+ "failed": "テスト失敗",
805
+ "stage": {
806
+ "creating_branch": "ブランチを作成中",
807
+ "provisioning": "環境をプロビジョニング中",
808
+ "tearing_down": "環境を破棄中",
809
+ "deleting_branch": "ブランチを削除中",
810
+ "done": "完了"
811
+ }
786
812
  }
787
813
  },
788
814
  "agentConfig": {
@@ -436,7 +436,11 @@
436
436
  "visual_pipeline_no_frontend": "Brak frontendu do przetestowania",
437
437
  "model_policy_blocked": "Model zablokowany przez politykę konta",
438
438
  "model_policy_unsupported": "Polityka modeli jest tu niedostępna",
439
- "deployer_required_before_tester": "Dodaj Deployer przed Testerem"
439
+ "deployer_required_before_tester": "Dodaj Deployer przed Testerem",
440
+ "env_test_not_a_frame": "To nie usługa",
441
+ "env_test_infraless": "Nie ma czego testować",
442
+ "env_test_not_provisionable": "Handler środowiska nie jest skonfigurowany",
443
+ "env_test_no_vcs": "Dostawca Git nie jest połączony"
440
444
  },
441
445
  "fallbackMessage": "Ta akcja jest sprzeczna z bieżącym stanem.",
442
446
  "providersUnconfigured": {
@@ -478,6 +482,11 @@
478
482
  "renewsField": "Subskrypcja odnawia się (opcjonalnie)",
479
483
  "connect": "Połącz",
480
484
  "replace": "Zastąp",
485
+ "disabledReason": {
486
+ "signIn": "Zaloguj się, aby połączyć subskrypcję osobistą",
487
+ "token": "Wprowadź token, aby kontynuować",
488
+ "password": "Wprowadź hasło osobiste o długości co najmniej 6 znaków"
489
+ },
481
490
  "expires": "Wygasa {date}",
482
491
  "noExpiry": "Brak daty wygaśnięcia",
483
492
  "renewal": {
@@ -783,6 +792,23 @@
783
792
  "title": "Konfiguracja środowiska Compose",
784
793
  "hint": "Skonfiguruj przepis, kontrole wstępne i handler, aby Deployer udostępnił ten stos.",
785
794
  "open": "Otwórz kreatora"
795
+ },
796
+ "envTest": {
797
+ "title": "Testowanie tworzenia środowiska",
798
+ "hint": "Uruchamia cały cykl życia na jednorazowej gałęzi: utworzenie gałęzi, udostępnienie, usunięcie zasobów, usunięcie gałęzi.",
799
+ "start": "Testowanie tworzenia środowiska",
800
+ "stop": "Zatrzymaj",
801
+ "infraless": "Skonfiguruj powyżej typ udostępniania, aby przetestować tworzenie środowiska.",
802
+ "running": "Testowanie: {stage}",
803
+ "succeeded": "Test zaliczony: środowisko zostało utworzone i usunięte, a gałąź została skasowana.",
804
+ "failed": "Test nie powiódł się",
805
+ "stage": {
806
+ "creating_branch": "tworzenie gałęzi",
807
+ "provisioning": "udostępnianie środowiska",
808
+ "tearing_down": "usuwanie środowiska",
809
+ "deleting_branch": "usuwanie gałęzi",
810
+ "done": "gotowe"
811
+ }
786
812
  }
787
813
  },
788
814
  "agentConfig": {
@@ -436,7 +436,11 @@
436
436
  "visual_pipeline_no_frontend": "Test edilecek bir frontend yok",
437
437
  "model_policy_blocked": "Model, hesap politikası tarafından engellendi",
438
438
  "model_policy_unsupported": "Model politikası burada kullanılamıyor",
439
- "deployer_required_before_tester": "Tester’dan önce bir Deployer ekleyin"
439
+ "deployer_required_before_tester": "Tester’dan önce bir Deployer ekleyin",
440
+ "env_test_not_a_frame": "Bir hizmet değil",
441
+ "env_test_infraless": "Test edilecek bir şey yok",
442
+ "env_test_not_provisionable": "Ortam işleyicisi yapılandırılmamış",
443
+ "env_test_no_vcs": "Git sağlayıcısı bağlı değil"
440
444
  },
441
445
  "fallbackMessage": "Bu eylem mevcut durumla çelişiyor.",
442
446
  "providersUnconfigured": {
@@ -478,6 +482,11 @@
478
482
  "renewsField": "Abonelik yenilenme tarihi (isteğe bağlı)",
479
483
  "connect": "Bağlan",
480
484
  "replace": "Değiştir",
485
+ "disabledReason": {
486
+ "signIn": "Kişisel bir abonelik bağlamak için oturum açın",
487
+ "token": "Devam etmek için token'ınızı girin",
488
+ "password": "En az 6 karakterlik kişisel bir parola girin"
489
+ },
481
490
  "expires": "Son kullanma {date}",
482
491
  "noExpiry": "Son kullanma tarihi ayarlanmadı",
483
492
  "renewal": {
@@ -783,6 +792,23 @@
783
792
  "title": "Compose ortam kurulumu",
784
793
  "hint": "Tarifi, ön kontrolleri ve işleyiciyi yapılandırın; böylece Deployer bu yığını sağlar.",
785
794
  "open": "Sihirbazı aç"
795
+ },
796
+ "envTest": {
797
+ "title": "Ortam oluşturmayı test et",
798
+ "hint": "Tek kullanımlık bir dal üzerinde tüm yaşam döngüsünü çalıştırır: dal oluştur, sağla, kaldır, dalı sil.",
799
+ "start": "Ortam oluşturmayı test et",
800
+ "stop": "Durdur",
801
+ "infraless": "Ortam oluşturmayı test etmek için yukarıdan bir sağlama türü yapılandırın.",
802
+ "running": "Test ediliyor: {stage}",
803
+ "succeeded": "Test başarılı: ortam oluşturuldu ve kaldırıldı, dal silindi.",
804
+ "failed": "Test başarısız",
805
+ "stage": {
806
+ "creating_branch": "dal oluşturuluyor",
807
+ "provisioning": "ortam sağlanıyor",
808
+ "tearing_down": "ortam kaldırılıyor",
809
+ "deleting_branch": "dal siliniyor",
810
+ "done": "tamamlandı"
811
+ }
786
812
  }
787
813
  },
788
814
  "agentConfig": {
@@ -436,7 +436,11 @@
436
436
  "visual_pipeline_no_frontend": "Немає фронтенду для тестування",
437
437
  "model_policy_blocked": "Модель заблоковано політикою облікового запису",
438
438
  "model_policy_unsupported": "Політика моделей тут недоступна",
439
- "deployer_required_before_tester": "Додайте Deployer перед Tester"
439
+ "deployer_required_before_tester": "Додайте Deployer перед Tester",
440
+ "env_test_not_a_frame": "Не є сервісом",
441
+ "env_test_infraless": "Немає чого тестувати",
442
+ "env_test_not_provisionable": "Обробник середовища не налаштовано",
443
+ "env_test_no_vcs": "Провайдер Git не підключено"
440
444
  },
441
445
  "fallbackMessage": "Ця дія суперечить поточному стану.",
442
446
  "providersUnconfigured": {
@@ -478,6 +482,11 @@
478
482
  "renewsField": "Підписка поновлюється (необов'язково)",
479
483
  "connect": "Підключити",
480
484
  "replace": "Замінити",
485
+ "disabledReason": {
486
+ "signIn": "Увійдіть, щоб підключити особисту підписку",
487
+ "token": "Введіть свій токен, щоб продовжити",
488
+ "password": "Введіть особистий пароль щонайменше з 6 символів"
489
+ },
481
490
  "expires": "Завершується {date}",
482
491
  "noExpiry": "Дата завершення не вказана",
483
492
  "renewal": {
@@ -783,6 +792,23 @@
783
792
  "title": "Налаштування середовища Compose",
784
793
  "hint": "Налаштуйте рецепт, попередні перевірки та обробник, щоб Deployer забезпечив цей стек.",
785
794
  "open": "Відкрити майстер"
795
+ },
796
+ "envTest": {
797
+ "title": "Тестування створення середовища",
798
+ "hint": "Виконує весь життєвий цикл на одноразовій гілці: створити гілку, забезпечити, згорнути, видалити гілку.",
799
+ "start": "Тестування створення середовища",
800
+ "stop": "Зупинити",
801
+ "infraless": "Налаштуйте тип забезпечення вище, щоб протестувати створення середовища.",
802
+ "running": "Тестування: {stage}",
803
+ "succeeded": "Тест пройдено: середовище було створено та згорнуто, а гілку видалено.",
804
+ "failed": "Тест не пройдено",
805
+ "stage": {
806
+ "creating_branch": "створення гілки",
807
+ "provisioning": "забезпечення середовища",
808
+ "tearing_down": "згортання середовища",
809
+ "deleting_branch": "видалення гілки",
810
+ "done": "готово"
811
+ }
786
812
  }
787
813
  },
788
814
  "agentConfig": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cat-factory/app",
3
- "version": "0.115.3",
3
+ "version": "0.116.1",
4
4
  "description": "Reusable Nuxt layer for the Agent Architecture Board SPA (components, stores, composables, pages). Consume it from a thin deployment app via `extends: ['@cat-factory/app']` and point it at your backend with NUXT_PUBLIC_API_BASE. See deploy/frontend for an example.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -34,7 +34,7 @@
34
34
  "valibot": "^1.4.2",
35
35
  "vue": "3.5.39",
36
36
  "wretch": "^3.0.9",
37
- "@cat-factory/contracts": "0.127.1"
37
+ "@cat-factory/contracts": "0.128.0"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@toad-contracts/testing": "0.3.2",