@cat-factory/app 0.300.2 → 0.301.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.
- package/app/components/panels/inspector/ServiceTestingContext.logic.spec.ts +38 -0
- package/app/components/panels/inspector/ServiceTestingContext.logic.ts +37 -0
- package/app/components/panels/inspector/ServiceTestingContext.vue +141 -0
- package/app/components/tasks/BugHuntModal.vue +4 -2
- package/app/composables/api/assistant.ts +7 -3
- package/app/composables/api/bugHunt.ts +10 -2
- package/app/modular/panels/inspector.logic.spec.ts +3 -0
- package/app/modular/panels/inspector.logic.ts +5 -0
- package/app/modular/panels/inspector.ts +2 -0
- package/app/stores/assistant.spec.ts +51 -0
- package/app/stores/assistant.ts +22 -3
- package/app/stores/bugHunt.spec.ts +29 -3
- package/app/stores/bugHunt.ts +29 -6
- package/i18n/locales/de.json +10 -0
- package/i18n/locales/en.json +16 -0
- package/i18n/locales/es.json +10 -0
- package/i18n/locales/fr.json +10 -0
- package/i18n/locales/he.json +10 -0
- package/i18n/locales/it.json +10 -0
- package/i18n/locales/ja.json +10 -0
- package/i18n/locales/pl.json +10 -0
- package/i18n/locales/tr.json +10 -0
- package/i18n/locales/uk.json +10 -0
- package/package.json +2 -2
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { rehydratedDraft } from './ServiceTestingContext.logic'
|
|
3
|
+
|
|
4
|
+
const TYPED = 'Sign in as $DEMO_USER. The seeded tenant is Acme.'
|
|
5
|
+
|
|
6
|
+
describe('rehydratedDraft', () => {
|
|
7
|
+
it('follows the board while the operator has typed nothing of their own', () => {
|
|
8
|
+
// A teammate saves in another tab: the live board event is the only thing that will ever tell
|
|
9
|
+
// this textarea, so an untouched draft has to take it.
|
|
10
|
+
const arrives = { draft: '', previous: '', incoming: TYPED, saving: false }
|
|
11
|
+
expect(rehydratedDraft(arrives)).toBe(TYPED)
|
|
12
|
+
expect(rehydratedDraft({ ...arrives, draft: 'old prose', previous: 'old prose' })).toBe(TYPED)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('leaves an in-progress edit alone when someone else saves', () => {
|
|
16
|
+
expect(
|
|
17
|
+
rehydratedDraft({ draft: TYPED, previous: '', incoming: 'their prose', saving: false }),
|
|
18
|
+
).toBe(TYPED)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('keeps unsaved prose through the whole of a REJECTED save', () => {
|
|
22
|
+
// The sequence the board store produces for a first-time write that fails: the optimistic
|
|
23
|
+
// patch announces our own text, then the rollback announces the value we started from. The
|
|
24
|
+
// second one is the dangerous one, because by then the draft matches what the block last said
|
|
25
|
+
// and the change reads exactly like a teammate's edit. Both land while `saving`.
|
|
26
|
+
const optimistic = { draft: TYPED, previous: '', incoming: TYPED, saving: true }
|
|
27
|
+
expect(rehydratedDraft(optimistic)).toBe(TYPED)
|
|
28
|
+
const rolledBack = { draft: TYPED, previous: TYPED, incoming: '', saving: true }
|
|
29
|
+
expect(rehydratedDraft(rolledBack)).toBe(TYPED)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('keeps keystrokes typed while a save is in flight', () => {
|
|
33
|
+
const stillTyping = `${TYPED} Never run the billing flow.`
|
|
34
|
+
expect(
|
|
35
|
+
rehydratedDraft({ draft: stillTyping, previous: '', incoming: TYPED, saving: true }),
|
|
36
|
+
).toBe(stillTyping)
|
|
37
|
+
})
|
|
38
|
+
})
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// The pure half of ServiceTestingContext: when a testing context arriving from the board may
|
|
2
|
+
// replace what the operator has typed into the textarea. Extracted for the reason every
|
|
3
|
+
// `*.logic.ts` here is (a decision worth a test should not need a mounted component to reach),
|
|
4
|
+
// and this one is a rule whose failure mode is silent and expensive.
|
|
5
|
+
|
|
6
|
+
/** The block value moving under the textarea, plus what the textarea currently holds. */
|
|
7
|
+
export interface DraftRehydration {
|
|
8
|
+
/** What the operator has in the textarea right now. */
|
|
9
|
+
draft: string
|
|
10
|
+
/** The persisted value the draft was last in step with (the watcher's previous value). */
|
|
11
|
+
previous: string
|
|
12
|
+
/** The persisted value that just arrived. */
|
|
13
|
+
incoming: string
|
|
14
|
+
/** Whether THIS panel has a save in flight. */
|
|
15
|
+
saving: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The draft to hold after the block's persisted testing context moved.
|
|
20
|
+
*
|
|
21
|
+
* An UNTOUCHED draft follows the board, so a teammate's edit lands live; a touched one is left
|
|
22
|
+
* alone. "Untouched" is measured against the value the draft was last in step with rather than
|
|
23
|
+
* against a dirty flag, because during an edit both are equally "different from what is stored"
|
|
24
|
+
* and only one of them may be overwritten.
|
|
25
|
+
*
|
|
26
|
+
* WHILE THIS PANEL IS SAVING, nothing is taken at all, and that is the case worth the extra state:
|
|
27
|
+
* the board store patches the block OPTIMISTICALLY and restores the old value if the request
|
|
28
|
+
* fails, so a rejected save arrives as a value change whose `previous` is our own optimistic write,
|
|
29
|
+
* which is exactly what an untouched draft looks like. Following it would hand the operator an
|
|
30
|
+
* emptied textarea, an "update failed" toast, and no copy of what they wrote. Every value change
|
|
31
|
+
* inside that window is ours (the optimistic write, the server's echo, the rollback), so ignoring
|
|
32
|
+
* the lot also keeps the keystrokes typed while the request was in flight.
|
|
33
|
+
*/
|
|
34
|
+
export function rehydratedDraft(state: DraftRehydration): string {
|
|
35
|
+
if (state.saving) return state.draft
|
|
36
|
+
return state.draft === state.previous ? state.incoming : state.draft
|
|
37
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref, watch } from 'vue'
|
|
3
|
+
import { TESTING_CONTEXT_MAX_LENGTH } from '@cat-factory/contracts'
|
|
4
|
+
import type { Block } from '~/types/domain'
|
|
5
|
+
import InspectorSection from '~/components/panels/inspector/InspectorSection.vue'
|
|
6
|
+
import { rehydratedDraft } from '~/components/panels/inspector/ServiceTestingContext.logic'
|
|
7
|
+
import { showOverrideField } from '~/utils/uiMode'
|
|
8
|
+
|
|
9
|
+
// Per-service (frame) TESTING CONTEXT: freeform prose about how this service is tested, which
|
|
10
|
+
// the engine injects verbatim into every tester prompt for it: the pipeline testers and the
|
|
11
|
+
// environment dry run's prober alike. It sits beside the sealed test credentials because the
|
|
12
|
+
// two are halves of one answer: the credentials are the material, this is what to do with it.
|
|
13
|
+
//
|
|
14
|
+
// Non-sensitive by contract: it is rendered INTO the prompt, so a real secret belongs one panel
|
|
15
|
+
// up, in the sealed store, and this prose refers to it by variable name. The banner says so.
|
|
16
|
+
//
|
|
17
|
+
// It is a plain block field (like the provisioning config), so it saves through the board store's
|
|
18
|
+
// `updateBlock` rather than a store of its own, and the draft below is what makes it an explicit
|
|
19
|
+
// save instead of a keystroke-per-request.
|
|
20
|
+
const props = defineProps<{ block: Block }>()
|
|
21
|
+
|
|
22
|
+
const board = useBoardStore()
|
|
23
|
+
const uiMode = useUiModeStore()
|
|
24
|
+
const toast = useToast()
|
|
25
|
+
const { t } = useI18n()
|
|
26
|
+
|
|
27
|
+
const busy = ref(false)
|
|
28
|
+
const draft = ref(props.block.testingContext ?? '')
|
|
29
|
+
|
|
30
|
+
// Re-hydrate from the block when the persisted value moves, but never over what the operator has
|
|
31
|
+
// typed (`rehydratedDraft` owns the rule and its spec states each case). The board store patches
|
|
32
|
+
// optimistically and ROLLS BACK on a rejected write, so a failed save arrives here looking exactly
|
|
33
|
+
// like a teammate's edit; taking it would erase the prose the toast is telling the operator to try
|
|
34
|
+
// saving again.
|
|
35
|
+
watch(
|
|
36
|
+
() => props.block.testingContext ?? '',
|
|
37
|
+
(incoming, previous) => {
|
|
38
|
+
draft.value = rehydratedDraft({ draft: draft.value, previous, incoming, saving: busy.value })
|
|
39
|
+
},
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
const saved = computed(() => props.block.testingContext ?? '')
|
|
43
|
+
// What a save would SEND, which is what the server would store: the request trims before it caps,
|
|
44
|
+
// so trailing whitespace is neither length spent nor a change worth a request.
|
|
45
|
+
const outgoing = computed(() => draft.value.trim())
|
|
46
|
+
const tooLong = computed(() => outgoing.value.length > TESTING_CONTEXT_MAX_LENGTH)
|
|
47
|
+
const dirty = computed(() => outgoing.value !== saved.value)
|
|
48
|
+
const canSave = computed(() => !busy.value && dirty.value && !tooLong.value)
|
|
49
|
+
|
|
50
|
+
// Standing per-service configuration, not part of the everyday delivery loop: a service is briefed
|
|
51
|
+
// once and every task then ships without anyone opening this. Absent, every tester prompt is
|
|
52
|
+
// byte-identical to one written before the field existed, which is what makes hiding it honest at
|
|
53
|
+
// the basic tier. `showOverrideField` (not a bare `isAdvanced`) because a service that HAS been
|
|
54
|
+
// briefed must show its prose to whoever opens the inspector: nothing else in the SPA surfaces
|
|
55
|
+
// what the testers are being told, so hiding a filled box would leave a basic-tier user unable to
|
|
56
|
+
// read, correct or clear it. The ROLE axis needs no separate answer: `intake` is capped at basic
|
|
57
|
+
// and never configures the platform, so this reaches the same people the tier bar admits.
|
|
58
|
+
const show = computed(() => showOverrideField(uiMode.isAdvanced, saved.value))
|
|
59
|
+
|
|
60
|
+
async function save() {
|
|
61
|
+
busy.value = true
|
|
62
|
+
try {
|
|
63
|
+
// `updateBlock` reports its own failure (it rolls back and toasts), so only the success
|
|
64
|
+
// needs saying here; announcing it unconditionally would claim a save the rollback undid.
|
|
65
|
+
const persisted = await board.updateBlock(props.block.id, { testingContext: outgoing.value })
|
|
66
|
+
if (persisted) {
|
|
67
|
+
toast.add({
|
|
68
|
+
title: t('inspector.testingContext.savedToast'),
|
|
69
|
+
icon: 'i-lucide-check',
|
|
70
|
+
color: 'success',
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
} finally {
|
|
74
|
+
busy.value = false
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function revert() {
|
|
79
|
+
draft.value = saved.value
|
|
80
|
+
}
|
|
81
|
+
</script>
|
|
82
|
+
|
|
83
|
+
<template>
|
|
84
|
+
<InspectorSection
|
|
85
|
+
v-if="show"
|
|
86
|
+
:title="t('inspector.testingContext.title')"
|
|
87
|
+
:hint="t('inspector.testingContext.sectionHint')"
|
|
88
|
+
data-testid="service-testing-context"
|
|
89
|
+
>
|
|
90
|
+
<!-- This text reaches the model in the prompt, so it must never hold a real secret. -->
|
|
91
|
+
<div
|
|
92
|
+
class="flex items-start gap-2 rounded-md border border-slate-700 bg-slate-800/40 px-2.5 py-2 text-[11px] leading-snug text-slate-300"
|
|
93
|
+
>
|
|
94
|
+
<UIcon name="i-lucide-info" class="mt-0.5 h-4 w-4 shrink-0 text-slate-400" />
|
|
95
|
+
<span>{{ t('inspector.testingContext.notSecret') }}</span>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<UTextarea
|
|
99
|
+
v-model="draft"
|
|
100
|
+
:rows="8"
|
|
101
|
+
:placeholder="t('inspector.testingContext.placeholder')"
|
|
102
|
+
class="w-full"
|
|
103
|
+
data-testid="testing-context-input"
|
|
104
|
+
/>
|
|
105
|
+
|
|
106
|
+
<div class="flex items-center justify-between gap-2">
|
|
107
|
+
<p class="text-[11px] text-slate-500" :class="{ 'text-error-400': tooLong }">
|
|
108
|
+
{{
|
|
109
|
+
t('inspector.testingContext.length', {
|
|
110
|
+
count: outgoing.length,
|
|
111
|
+
max: TESTING_CONTEXT_MAX_LENGTH,
|
|
112
|
+
})
|
|
113
|
+
}}
|
|
114
|
+
</p>
|
|
115
|
+
<div class="flex items-center gap-2">
|
|
116
|
+
<UButton
|
|
117
|
+
v-if="dirty"
|
|
118
|
+
color="neutral"
|
|
119
|
+
variant="ghost"
|
|
120
|
+
size="xs"
|
|
121
|
+
data-testid="testing-context-revert"
|
|
122
|
+
@click="revert"
|
|
123
|
+
>
|
|
124
|
+
{{ t('inspector.testingContext.revert') }}
|
|
125
|
+
</UButton>
|
|
126
|
+
<UButton
|
|
127
|
+
color="primary"
|
|
128
|
+
variant="soft"
|
|
129
|
+
size="xs"
|
|
130
|
+
icon="i-lucide-save"
|
|
131
|
+
:loading="busy"
|
|
132
|
+
:disabled="!canSave"
|
|
133
|
+
data-testid="testing-context-save"
|
|
134
|
+
@click="save"
|
|
135
|
+
>
|
|
136
|
+
{{ t('inspector.testingContext.save') }}
|
|
137
|
+
</UButton>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
</InspectorSection>
|
|
141
|
+
</template>
|
|
@@ -274,8 +274,10 @@ function loadBoardsFor(next: TaskSourceKind | undefined) {
|
|
|
274
274
|
async function runHunt() {
|
|
275
275
|
const input = request.value
|
|
276
276
|
if (!source.value || !input) return
|
|
277
|
-
|
|
278
|
-
|
|
277
|
+
// Only a real failure is reported. A cancelled credential prompt leaves nothing to say, and
|
|
278
|
+
// `repo_not_linked` is worded by the panel itself beside the scope it invalidates.
|
|
279
|
+
const attempt = await hunt.hunt(source.value, input)
|
|
280
|
+
if (attempt === 'failed' && !huntNeedsRepo.value) {
|
|
279
281
|
toast.add({
|
|
280
282
|
title: t('bugHunt.huntFailed'),
|
|
281
283
|
description: refusalText(hunt.huntErrorReason, hunt.huntError) ?? undefined,
|
|
@@ -3,7 +3,7 @@ import type { AssistantAnswer } from '~/types/domain'
|
|
|
3
3
|
import type { ApiContext } from './context'
|
|
4
4
|
|
|
5
5
|
/** In-app assistant: what it can do here, and one prompt-to-action turn. */
|
|
6
|
-
export function assistantApi({ send, ws }: ApiContext) {
|
|
6
|
+
export function assistantApi({ send, sendWith, ws, pwHeaders }: ApiContext) {
|
|
7
7
|
return {
|
|
8
8
|
// Whether a model is wired and which actions this deployment offers. Read before the prompt
|
|
9
9
|
// box is shown, so an unconfigured deployment says so instead of failing on submit. `signal`
|
|
@@ -14,8 +14,12 @@ export function assistantApi({ send, ws }: ApiContext) {
|
|
|
14
14
|
|
|
15
15
|
// Run one turn. A live model call plus a board write, so it can take a couple of seconds:
|
|
16
16
|
// the modal shows progress and the outcome is rendered from the returned data.
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
//
|
|
18
|
+
// Carries the personal password header, like a run start does, because the turn resolves the
|
|
19
|
+
// workspace's own preset: a workspace pinned to an individual-usage subscription runs this
|
|
20
|
+
// surface on it, and without the header it could only ever fall back to another model.
|
|
21
|
+
runAssistantTurn: (workspaceId: string, prompt: string, password?: string) =>
|
|
22
|
+
sendWith(pwHeaders(password), runAssistantTurnContract, {
|
|
19
23
|
pathPrefix: ws(workspaceId),
|
|
20
24
|
body: { kind: 'prompt', prompt },
|
|
21
25
|
}),
|
|
@@ -16,8 +16,16 @@ export function bugHuntApi({ send, sendWith, ws, pwHeaders }: ApiContext) {
|
|
|
16
16
|
|
|
17
17
|
// Scan the board for open, unassigned bugs and rank them by impact vs complexity. A live
|
|
18
18
|
// external call plus a model call, so it can take a while — the modal shows progress.
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
// Carries the personal password for the same reason `adoptBugHuntCandidate` below does: the
|
|
20
|
+
// RANKING is a model call resolved under the workspace's preset, so a subscription-pinned
|
|
21
|
+
// workspace needs the credential here and not only at adoption.
|
|
22
|
+
runBugHunt: (
|
|
23
|
+
workspaceId: string,
|
|
24
|
+
source: TaskSourceKind,
|
|
25
|
+
body: RunBugHuntInput,
|
|
26
|
+
password?: string,
|
|
27
|
+
) =>
|
|
28
|
+
sendWith(pwHeaders(password), runBugHuntContract, {
|
|
21
29
|
pathPrefix: ws(workspaceId),
|
|
22
30
|
pathParams: { source },
|
|
23
31
|
body,
|
|
@@ -39,6 +39,7 @@ describe('inspector panel group', () => {
|
|
|
39
39
|
'service-connections',
|
|
40
40
|
'service-test-config',
|
|
41
41
|
'service-test-secrets',
|
|
42
|
+
'service-testing-context',
|
|
42
43
|
'service-fragments',
|
|
43
44
|
'service-release-health',
|
|
44
45
|
'service-validation-checks',
|
|
@@ -58,6 +59,7 @@ describe('inspector panel group', () => {
|
|
|
58
59
|
'container-summary',
|
|
59
60
|
'service-test-config',
|
|
60
61
|
'service-test-secrets',
|
|
62
|
+
'service-testing-context',
|
|
61
63
|
'service-fragments',
|
|
62
64
|
'service-release-health',
|
|
63
65
|
'service-validation-checks',
|
|
@@ -70,6 +72,7 @@ describe('inspector panel group', () => {
|
|
|
70
72
|
'frontend-config',
|
|
71
73
|
'service-test-config',
|
|
72
74
|
'service-test-secrets',
|
|
75
|
+
'service-testing-context',
|
|
73
76
|
'service-fragments',
|
|
74
77
|
'service-release-health',
|
|
75
78
|
'service-validation-checks',
|
|
@@ -53,6 +53,7 @@ export const INSPECTOR_PANEL_IDS = [
|
|
|
53
53
|
'service-connections',
|
|
54
54
|
'service-test-config',
|
|
55
55
|
'service-test-secrets',
|
|
56
|
+
'service-testing-context',
|
|
56
57
|
'service-fragments',
|
|
57
58
|
'service-release-health',
|
|
58
59
|
'service-validation-checks',
|
|
@@ -146,6 +147,10 @@ export const INSPECTOR_PANEL_SPECS: readonly InspectorPanelSpec[] = [
|
|
|
146
147
|
{ id: 'service-connections', order: 130, when: (b) => isFrame(b) && b.type === 'service' },
|
|
147
148
|
{ id: 'service-test-config', order: 140, when: isDeployableFrame },
|
|
148
149
|
{ id: 'service-test-secrets', order: 150, when: isDeployableFrame },
|
|
150
|
+
// Immediately after the credentials: the prose that says what to DO with them, and the other
|
|
151
|
+
// half of what a tester is handed about this service. Same gate for the same reason (a doc
|
|
152
|
+
// repo runs no tester at all).
|
|
153
|
+
{ id: 'service-testing-context', order: 155, when: isDeployableFrame },
|
|
149
154
|
{ id: 'service-fragments', order: 160, when: isFrame },
|
|
150
155
|
{ id: 'service-release-health', order: 170, when: isDeployableFrame },
|
|
151
156
|
// Pre-PR validation checks: the commands the harness runs before opening this service's PRs.
|
|
@@ -28,6 +28,7 @@ import FrontendConfig from '~/components/panels/inspector/FrontendConfig.vue'
|
|
|
28
28
|
import ServiceConnections from '~/components/panels/inspector/ServiceConnections.vue'
|
|
29
29
|
import ServiceTestConfig from '~/components/panels/inspector/ServiceTestConfig.vue'
|
|
30
30
|
import ServiceTestSecrets from '~/components/panels/inspector/ServiceTestSecrets.vue'
|
|
31
|
+
import ServiceTestingContext from '~/components/panels/inspector/ServiceTestingContext.vue'
|
|
31
32
|
import ServiceFragments from '~/components/panels/inspector/ServiceFragments.vue'
|
|
32
33
|
import ServiceReleaseHealthConfig from '~/components/panels/inspector/ServiceReleaseHealthConfig.vue'
|
|
33
34
|
import ServiceValidationConfig from '~/components/panels/inspector/ServiceValidationConfig.vue'
|
|
@@ -83,6 +84,7 @@ const COMPONENTS: Record<InspectorPanelId, Component> = {
|
|
|
83
84
|
'service-connections': ServiceConnections,
|
|
84
85
|
'service-test-config': ServiceTestConfig,
|
|
85
86
|
'service-test-secrets': ServiceTestSecrets,
|
|
87
|
+
'service-testing-context': ServiceTestingContext,
|
|
86
88
|
'service-fragments': ServiceFragments,
|
|
87
89
|
'service-release-health': ServiceReleaseHealthConfig,
|
|
88
90
|
'service-validation-checks': ServiceValidationConfig,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
|
2
2
|
import { useAssistantStore } from '~/stores/assistant'
|
|
3
3
|
import { useWorkspaceStore } from '~/stores/workspace'
|
|
4
|
+
import { usePersonalSubscriptionsStore } from '~/stores/personalSubscriptions'
|
|
4
5
|
import { ApiError } from '~/composables/api/errors'
|
|
5
6
|
import type { AssistantCapability } from '~/types/domain'
|
|
6
7
|
|
|
@@ -181,3 +182,53 @@ describe('assistant store: the capability read deadline', () => {
|
|
|
181
182
|
expect(store.capabilityRead).toBe('error')
|
|
182
183
|
})
|
|
183
184
|
})
|
|
185
|
+
|
|
186
|
+
// The credential half of a turn: a workspace whose preset pins an individual-usage subscription
|
|
187
|
+
// runs this surface on it, which means the turn has to be able to ASK for the password and to
|
|
188
|
+
// survive being refused one.
|
|
189
|
+
describe('assistant store: the personal-credential flow', () => {
|
|
190
|
+
beforeEach(() => {
|
|
191
|
+
useWorkspaceStore().workspaceId = 'ws1'
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
function stubTurn(
|
|
195
|
+
withCredential: (action: (password?: string) => Promise<void>) => Promise<boolean>,
|
|
196
|
+
) {
|
|
197
|
+
const calls: (string | undefined)[] = []
|
|
198
|
+
vi.stubGlobal('useApi', () => ({
|
|
199
|
+
getAssistantCapability: async () => ({ available: true, actions: ['add-service-from-repo'] }),
|
|
200
|
+
runAssistantTurn: async (_ws: string, _prompt: string, password?: string) => {
|
|
201
|
+
calls.push(password)
|
|
202
|
+
return { outcome: { status: 'declined', reason: 'no_matching_action' }, model: null }
|
|
203
|
+
},
|
|
204
|
+
}))
|
|
205
|
+
usePersonalSubscriptionsStore().withCredential = withCredential as unknown as ReturnType<
|
|
206
|
+
typeof usePersonalSubscriptionsStore
|
|
207
|
+
>['withCredential']
|
|
208
|
+
return { store: useAssistantStore(), calls }
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
it('carries the password the credential flow supplies into the turn', async () => {
|
|
212
|
+
// Without it a workspace pinned to a personal subscription could only ever be answered by
|
|
213
|
+
// some other model, which is the failure this whole path exists to close.
|
|
214
|
+
const { store, calls } = stubTurn(async (action) => {
|
|
215
|
+
await action('correct horse')
|
|
216
|
+
return true
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
const turn = await store.run('add the payments repo')
|
|
220
|
+
|
|
221
|
+
expect(calls).toEqual(['correct horse'])
|
|
222
|
+
expect(turn).not.toBeNull()
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('answers null when the person cancels the password prompt', async () => {
|
|
226
|
+
// A cancel is not a failed turn and not a declined one: nothing ran, so there is no outcome
|
|
227
|
+
// to render. Reporting it as either would put a refusal on screen that nobody caused.
|
|
228
|
+
const { store, calls } = stubTurn(async () => false)
|
|
229
|
+
|
|
230
|
+
expect(await store.run('add the payments repo')).toBeNull()
|
|
231
|
+
expect(calls).toEqual([])
|
|
232
|
+
expect(store.turn).toBeNull()
|
|
233
|
+
})
|
|
234
|
+
})
|
package/app/stores/assistant.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { computed, ref } from 'vue'
|
|
|
3
3
|
import type { AssistantAnswer, AssistantCapability, AssistantTurn } from '~/types/domain'
|
|
4
4
|
import type { LoadState } from '~/types/load-state'
|
|
5
5
|
import { useWorkspaceStore } from '~/stores/workspace'
|
|
6
|
+
import { usePersonalSubscriptionsStore } from '~/stores/personalSubscriptions'
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* How long the capability read waits before it counts as failed.
|
|
@@ -99,10 +100,28 @@ export const useAssistantStore = defineStore('assistant', () => {
|
|
|
99
100
|
|
|
100
101
|
/**
|
|
101
102
|
* Run one turn. Throws on a refusal so the caller can hand it to the error funnel; the three
|
|
102
|
-
* outcomes (performed / needs_input / declined) come back as the resolved value
|
|
103
|
+
* outcomes (performed / needs_input / declined) come back as the resolved value, and `null`
|
|
104
|
+
* when the person cancelled the credential prompt.
|
|
103
105
|
*/
|
|
104
|
-
async function run(prompt: string): Promise<AssistantTurn> {
|
|
105
|
-
|
|
106
|
+
async function run(prompt: string): Promise<AssistantTurn | null> {
|
|
107
|
+
// Through the credential flow, exactly as a run start goes: the turn resolves the workspace's
|
|
108
|
+
// OWN preset, so on a workspace pinned to an individual-usage subscription the first turn 428s,
|
|
109
|
+
// the modal collects the password, and it rides transparently from the cache after that.
|
|
110
|
+
//
|
|
111
|
+
// `null` for a CANCELLED prompt, the shape every gated surface here uses. A cancel is not a
|
|
112
|
+
// failed turn and not a declined one either: nothing ran, so there is no outcome to render and
|
|
113
|
+
// nothing to report. `record` never runs, so the kept `turn` is whatever it already was, which
|
|
114
|
+
// for the modal is nothing: editing the prompt cleared it before submit.
|
|
115
|
+
//
|
|
116
|
+
// The completed turn is captured in a local of its OWN name rather than read back off the
|
|
117
|
+
// store's `turn` ref, which this would otherwise shadow: two bindings spelled the same in one
|
|
118
|
+
// function is how a later edit writes to the wrong one with nothing failing.
|
|
119
|
+
const personal = usePersonalSubscriptionsStore()
|
|
120
|
+
let completed: AssistantTurn | null = null
|
|
121
|
+
const ran = await personal.withCredential(async (password) => {
|
|
122
|
+
completed = await record(() => api.runAssistantTurn(workspace.requireId(), prompt, password))
|
|
123
|
+
})
|
|
124
|
+
return ran ? completed : null
|
|
106
125
|
}
|
|
107
126
|
|
|
108
127
|
/**
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
2
2
|
import { useBugHuntStore } from '~/stores/bugHunt'
|
|
3
3
|
import { useWorkspaceStore } from '~/stores/workspace'
|
|
4
|
+
import { usePersonalSubscriptionsStore } from '~/stores/personalSubscriptions'
|
|
4
5
|
import { ApiError } from '~/composables/api/errors'
|
|
5
6
|
import type { BugHuntResult, TaskSourceKind, TrackerBoardsView } from '~/types/domain'
|
|
6
7
|
|
|
@@ -181,7 +182,7 @@ describe('bug hunt store — scan failures', () => {
|
|
|
181
182
|
const { store, serveHunt } = stubApi()
|
|
182
183
|
serveHunt(() => Promise.reject(apiError(422, 'validation', { reason: 'repo_not_linked' })))
|
|
183
184
|
|
|
184
|
-
expect(await store.hunt('github', SCAN)).toBe(
|
|
185
|
+
expect(await store.hunt('github', SCAN)).toBe('failed')
|
|
185
186
|
|
|
186
187
|
expect(store.huntErrorReason).toBe('repo_not_linked')
|
|
187
188
|
expect(store.huntError).toBeTruthy()
|
|
@@ -192,7 +193,7 @@ describe('bug hunt store — scan failures', () => {
|
|
|
192
193
|
const { store, serveHunt } = stubApi()
|
|
193
194
|
serveHunt(() => Promise.reject(apiError(502, 'upstream')))
|
|
194
195
|
|
|
195
|
-
expect(await store.hunt('jira', { containerId: 'blk_auth', board: 'PROJ' })).toBe(
|
|
196
|
+
expect(await store.hunt('jira', { containerId: 'blk_auth', board: 'PROJ' })).toBe('failed')
|
|
196
197
|
|
|
197
198
|
expect(store.huntErrorReason).toBeNull()
|
|
198
199
|
expect(store.huntError).toBeTruthy()
|
|
@@ -204,7 +205,7 @@ describe('bug hunt store — scan failures', () => {
|
|
|
204
205
|
await store.hunt('github', SCAN)
|
|
205
206
|
|
|
206
207
|
serveHunt(() => Promise.resolve(huntResult()))
|
|
207
|
-
expect(await store.hunt('github', SCAN)).toBe(
|
|
208
|
+
expect(await store.hunt('github', SCAN)).toBe('ran')
|
|
208
209
|
|
|
209
210
|
expect(store.huntError).toBeNull()
|
|
210
211
|
expect(store.huntErrorReason).toBeNull()
|
|
@@ -222,3 +223,28 @@ describe('bug hunt store — scan failures', () => {
|
|
|
222
223
|
expect(store.huntError).toBeNull()
|
|
223
224
|
})
|
|
224
225
|
})
|
|
226
|
+
|
|
227
|
+
describe('bug hunt store: a dismissed credential prompt', () => {
|
|
228
|
+
beforeEach(() => {
|
|
229
|
+
useWorkspaceStore().workspaceId = 'ws1'
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it('reports `cancelled` and drops the ranking the previous board left', async () => {
|
|
233
|
+
// The scan needs the personal password on a subscription-pinned workspace, so the prompt can
|
|
234
|
+
// be cancelled. Nothing ran: reporting that as a failure raises an error toast with no
|
|
235
|
+
// description for an action the person themselves called off, and leaving the earlier result
|
|
236
|
+
// mounted would show one board's candidates under another board's selection.
|
|
237
|
+
const { store } = stubApi()
|
|
238
|
+
expect(await store.hunt('github', SCAN)).toBe('ran')
|
|
239
|
+
expect(store.result).not.toBeNull()
|
|
240
|
+
|
|
241
|
+
usePersonalSubscriptionsStore().withCredential = (async () => false) as unknown as ReturnType<
|
|
242
|
+
typeof usePersonalSubscriptionsStore
|
|
243
|
+
>['withCredential']
|
|
244
|
+
|
|
245
|
+
expect(await store.hunt('github', SCAN)).toBe('cancelled')
|
|
246
|
+
expect(store.result).toBeNull()
|
|
247
|
+
expect(store.huntError).toBeNull()
|
|
248
|
+
expect(store.huntErrorReason).toBeNull()
|
|
249
|
+
})
|
|
250
|
+
})
|
package/app/stores/bugHunt.ts
CHANGED
|
@@ -5,6 +5,12 @@ import { apiErrorReason } from '~/composables/api/errors'
|
|
|
5
5
|
import { useWorkspaceStore } from '~/stores/workspace'
|
|
6
6
|
import { usePersonalSubscriptionsStore } from '~/stores/personalSubscriptions'
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* What one attempt did. `cancelled` is distinct from `failed` because the surface reports a
|
|
10
|
+
* failure: a dismissed credential prompt is the person's own decision, not an outcome to toast.
|
|
11
|
+
*/
|
|
12
|
+
export type BugHuntAttempt = 'ran' | 'cancelled' | 'failed'
|
|
13
|
+
|
|
8
14
|
/**
|
|
9
15
|
* Bug-hunt state: the boards of the tracker being browsed, the last hunt's ranked candidates,
|
|
10
16
|
* and the actions behind the three steps (list boards → run the hunt → adopt one candidate).
|
|
@@ -95,19 +101,36 @@ export const useBugHuntStore = defineStore('bugHunt', () => {
|
|
|
95
101
|
boardsLoading.value = false
|
|
96
102
|
}
|
|
97
103
|
|
|
98
|
-
/**
|
|
99
|
-
|
|
104
|
+
/**
|
|
105
|
+
* Run a hunt and keep its ranked result.
|
|
106
|
+
*
|
|
107
|
+
* Three outcomes, not two, because the caller reports a failure and only a failure. `cancelled`
|
|
108
|
+
* is a hunt that never ran: the person dismissed the credential prompt (or the prompt's own
|
|
109
|
+
* retry failed and the credential modal already said so), so there is nothing to tell them that
|
|
110
|
+
* they did not just do. Collapsing it into `failed` is an error toast for an action they
|
|
111
|
+
* cancelled, with no description to put in it.
|
|
112
|
+
*/
|
|
113
|
+
async function hunt(source: TaskSourceKind, input: RunBugHuntInput): Promise<BugHuntAttempt> {
|
|
100
114
|
hunting.value = true
|
|
101
115
|
huntError.value = null
|
|
102
116
|
huntErrorReason.value = null
|
|
117
|
+
// A new scan supersedes whatever is on screen the moment it starts. Clearing HERE rather than
|
|
118
|
+
// on each way out is what keeps a cancelled prompt from leaving the previous board's ranking
|
|
119
|
+
// under the current selection, where adopting one would file a task against the wrong board.
|
|
120
|
+
result.value = null
|
|
103
121
|
try {
|
|
104
|
-
|
|
105
|
-
|
|
122
|
+
// Gated like the adoption below, and for the reason the ranking is the model call: on a
|
|
123
|
+
// workspace pinned to an individual-usage subscription the scan needs the credential HERE,
|
|
124
|
+
// not only when a candidate is adopted.
|
|
125
|
+
const personal = usePersonalSubscriptionsStore()
|
|
126
|
+
const ran = await personal.withCredential(async (password) => {
|
|
127
|
+
result.value = await api.runBugHunt(workspace.requireId(), source, input, password)
|
|
128
|
+
})
|
|
129
|
+
return ran ? 'ran' : 'cancelled'
|
|
106
130
|
} catch (e) {
|
|
107
|
-
result.value = null
|
|
108
131
|
huntError.value = e instanceof Error ? e.message : String(e)
|
|
109
132
|
huntErrorReason.value = apiErrorReason(e)
|
|
110
|
-
return
|
|
133
|
+
return 'failed'
|
|
111
134
|
} finally {
|
|
112
135
|
hunting.value = false
|
|
113
136
|
}
|
package/i18n/locales/de.json
CHANGED
|
@@ -1596,6 +1596,16 @@
|
|
|
1596
1596
|
"configNoun": "die sensiblen Test-Anmeldedaten",
|
|
1597
1597
|
"duplicateKey": "Jeder Variablenname muss eindeutig sein."
|
|
1598
1598
|
},
|
|
1599
|
+
"testingContext": {
|
|
1600
|
+
"title": "Testkontext",
|
|
1601
|
+
"sectionHint": "Freitext dazu, wie dieser Service getestet wird: welche Abläufe wichtig sind, welche Testkonten es gibt und wie man sich damit anmeldet, was die eingespielten Daten bedeuten, was unangetastet bleiben soll. Jeder Tester-Lauf für diesen Service bekommt diesen Text wörtlich, der Probelauf der Umgebung ebenso.",
|
|
1602
|
+
"notSecret": "Dieser Text geht unverändert in den Prompt des Testers. Echte Geheimnisse gehören nicht hierher: Trage sie oben unter „Test-Anmeldedaten“ ein und verweise hier nur über den Variablennamen darauf.",
|
|
1603
|
+
"placeholder": "z. B. als $DEMO_USER anmelden; der eingespielte Mandant ist Acme mit drei Projekten; den Abrechnungsablauf nie ausführen, er belastet eine echte Karte.",
|
|
1604
|
+
"length": "{count} von {max} Zeichen",
|
|
1605
|
+
"save": "Testkontext speichern",
|
|
1606
|
+
"revert": "Änderungen verwerfen",
|
|
1607
|
+
"savedToast": "Testkontext gespeichert"
|
|
1608
|
+
},
|
|
1599
1609
|
"testConfig": {
|
|
1600
1610
|
"title": "Testinfrastruktur",
|
|
1601
1611
|
"hint": "Wie eine Testumgebung für diesen Service aufgesetzt wird, wenn eine Pipeline ihn ausführen muss: keine Infrastruktur, eine Docker-Compose-Datei, Kubernetes-Manifeste oder ein benutzerdefinierter Manifesttyp.",
|
package/i18n/locales/en.json
CHANGED
|
@@ -1147,6 +1147,22 @@
|
|
|
1147
1147
|
"configNoun": "the sensitive test credentials",
|
|
1148
1148
|
"duplicateKey": "Each variable name must be unique."
|
|
1149
1149
|
},
|
|
1150
|
+
"testingContext": {
|
|
1151
|
+
"title": "Testing context",
|
|
1152
|
+
"@title": {
|
|
1153
|
+
"description": "Section header for the freeform notes a team writes about how their service should be tested. 'Context' here means background information for whoever tests it, not a programming context object."
|
|
1154
|
+
},
|
|
1155
|
+
"sectionHint": "Freeform notes about how this service is tested: which flows matter, which test accounts exist and how to sign in as one, what the seeded data means, what to leave alone. Every tester run for this service is handed this text word for word, and so is the environment dry run.",
|
|
1156
|
+
"notSecret": "This text goes straight into the tester's prompt, so keep real secrets out of it. Put a secret in Test credentials above and refer to it here by its variable name.",
|
|
1157
|
+
"placeholder": "e.g. sign in as $DEMO_USER; the seeded tenant is Acme with three projects; never run the billing flow, it charges a real card.",
|
|
1158
|
+
"length": "{count} of {max} characters",
|
|
1159
|
+
"@length": {
|
|
1160
|
+
"description": "Character counter under a long text box. {count} is how many characters are typed so far, {max} the limit."
|
|
1161
|
+
},
|
|
1162
|
+
"save": "Save testing context",
|
|
1163
|
+
"revert": "Discard changes",
|
|
1164
|
+
"savedToast": "Testing context saved"
|
|
1165
|
+
},
|
|
1150
1166
|
"testConfig": {
|
|
1151
1167
|
"title": "Test infrastructure",
|
|
1152
1168
|
"hint": "How a test environment is stood up for this service when a pipeline needs to run it: no infrastructure, a Docker Compose file, Kubernetes manifests, or a custom manifest type.",
|
package/i18n/locales/es.json
CHANGED
|
@@ -1051,6 +1051,16 @@
|
|
|
1051
1051
|
"configNoun": "las credenciales de prueba sensibles",
|
|
1052
1052
|
"duplicateKey": "Cada nombre de variable debe ser único."
|
|
1053
1053
|
},
|
|
1054
|
+
"testingContext": {
|
|
1055
|
+
"title": "Contexto de pruebas",
|
|
1056
|
+
"sectionHint": "Notas libres sobre cómo se prueba este servicio: qué flujos importan, qué cuentas de prueba existen y cómo iniciar sesión con ellas, qué significan los datos precargados y qué no hay que tocar. Cada ejecución del Tester para este servicio recibe este texto tal cual, igual que la prueba en seco del entorno.",
|
|
1057
|
+
"notSecret": "Este texto se envía tal cual al prompt del Tester, así que no pongas secretos reales aquí. Guárdalos arriba, en «Credenciales de prueba», y menciónalos aquí solo por el nombre de la variable.",
|
|
1058
|
+
"placeholder": "p. ej. inicia sesión como $DEMO_USER; el inquilino precargado es Acme con tres proyectos; nunca ejecutes el flujo de facturación, cobra a una tarjeta real.",
|
|
1059
|
+
"length": "{count} de {max} caracteres",
|
|
1060
|
+
"save": "Guardar contexto de pruebas",
|
|
1061
|
+
"revert": "Descartar cambios",
|
|
1062
|
+
"savedToast": "Contexto de pruebas guardado"
|
|
1063
|
+
},
|
|
1054
1064
|
"testConfig": {
|
|
1055
1065
|
"title": "Infraestructura de pruebas",
|
|
1056
1066
|
"hint": "Cómo se levanta un entorno de prueba para este servicio cuando un pipeline necesita ejecutarlo: sin infraestructura, un archivo de Docker Compose, manifiestos de Kubernetes o un tipo de manifiesto personalizado.",
|
package/i18n/locales/fr.json
CHANGED
|
@@ -1051,6 +1051,16 @@
|
|
|
1051
1051
|
"configNoun": "les identifiants de test sensibles",
|
|
1052
1052
|
"duplicateKey": "Chaque nom de variable doit être unique."
|
|
1053
1053
|
},
|
|
1054
|
+
"testingContext": {
|
|
1055
|
+
"title": "Contexte de test",
|
|
1056
|
+
"sectionHint": "Notes libres sur la façon de tester ce service : quels parcours comptent, quels comptes de test existent et comment s'y connecter, ce que signifient les données préchargées, ce qu'il ne faut pas toucher. Chaque exécution du testeur pour ce service reçoit ce texte mot pour mot, tout comme l'essai à blanc de l'environnement.",
|
|
1057
|
+
"notSecret": "Ce texte part tel quel dans l'invite du testeur : n'y mettez pas de véritables secrets. Saisissez-les au-dessus, dans « Identifiants de test », et n'y faites référence ici que par le nom de la variable.",
|
|
1058
|
+
"placeholder": "ex. connectez-vous en tant que $DEMO_USER ; le locataire préchargé est Acme avec trois projets ; ne lancez jamais le parcours de facturation, il débite une vraie carte.",
|
|
1059
|
+
"length": "{count} sur {max} caractères",
|
|
1060
|
+
"save": "Enregistrer le contexte de test",
|
|
1061
|
+
"revert": "Annuler les modifications",
|
|
1062
|
+
"savedToast": "Contexte de test enregistré"
|
|
1063
|
+
},
|
|
1054
1064
|
"testConfig": {
|
|
1055
1065
|
"title": "Infrastructure de test",
|
|
1056
1066
|
"hint": "Comment un environnement de test est mis en place pour ce service quand un pipeline doit l'exécuter : sans infrastructure, un fichier Docker Compose, des manifestes Kubernetes ou un type de manifeste personnalisé.",
|
package/i18n/locales/he.json
CHANGED
|
@@ -1051,6 +1051,16 @@
|
|
|
1051
1051
|
"configNoun": "פרטי הגישה הרגישים לבדיקה",
|
|
1052
1052
|
"duplicateKey": "כל שם משתנה חייב להיות ייחודי."
|
|
1053
1053
|
},
|
|
1054
|
+
"testingContext": {
|
|
1055
|
+
"title": "הקשר לבדיקות",
|
|
1056
|
+
"sectionHint": "טקסט חופשי על אופן הבדיקה של השירות הזה: אילו תהליכים חשובים, אילו חשבונות בדיקה קיימים וכיצד מתחברים איתם, מה המשמעות של הנתונים שנטענו ובמה אסור לגעת. כל הרצת בודק עבור השירות הזה מקבלת את הטקסט הזה מילה במילה, וכך גם הרצת היבש של הסביבה.",
|
|
1057
|
+
"notSecret": "הטקסט הזה נכנס כמו שהוא לפרומפט של הבודק, ולכן אין לכתוב בו סודות אמיתיים. שמרו אותם למעלה, תחת «פרטי גישה לבדיקה», והזכירו אותם כאן רק בשם המשתנה.",
|
|
1058
|
+
"placeholder": "לדוגמה: התחברו כ-$DEMO_USER; הדייר שנטען הוא Acme עם שלושה פרויקטים; לעולם אל תריצו את תהליך החיוב, הוא מחייב כרטיס אמיתי.",
|
|
1059
|
+
"length": "{count} מתוך {max} תווים",
|
|
1060
|
+
"save": "שמירת ההקשר לבדיקות",
|
|
1061
|
+
"revert": "ביטול השינויים",
|
|
1062
|
+
"savedToast": "ההקשר לבדיקות נשמר"
|
|
1063
|
+
},
|
|
1054
1064
|
"testConfig": {
|
|
1055
1065
|
"title": "תשתית בדיקות",
|
|
1056
1066
|
"hint": "כיצד מוקמת סביבת בדיקה לשירות זה כאשר פייפליין צריך להריץ אותו: ללא תשתית, קובץ Docker Compose, מניפסטים של Kubernetes או סוג מניפסט מותאם אישית.",
|
package/i18n/locales/it.json
CHANGED
|
@@ -1596,6 +1596,16 @@
|
|
|
1596
1596
|
"configNoun": "le credenziali di test sensibili",
|
|
1597
1597
|
"duplicateKey": "Ogni nome di variabile deve essere univoco."
|
|
1598
1598
|
},
|
|
1599
|
+
"testingContext": {
|
|
1600
|
+
"title": "Contesto di test",
|
|
1601
|
+
"sectionHint": "Note libere su come si testa questo servizio: quali flussi contano, quali account di prova esistono e come accedervi, che cosa significano i dati precaricati, che cosa non va toccato. Ogni esecuzione del Tester per questo servizio riceve questo testo alla lettera, così come la prova a vuoto dell'ambiente.",
|
|
1602
|
+
"notSecret": "Questo testo finisce così com'è nel prompt del Tester, quindi non inserirci segreti veri. Mettili sopra, in «Credenziali di test», e qui richiamali solo con il nome della variabile.",
|
|
1603
|
+
"placeholder": "es. accedi come $DEMO_USER; il tenant precaricato è Acme con tre progetti; non eseguire mai il flusso di fatturazione, addebita una carta vera.",
|
|
1604
|
+
"length": "{count} di {max} caratteri",
|
|
1605
|
+
"save": "Salva il contesto di test",
|
|
1606
|
+
"revert": "Annulla le modifiche",
|
|
1607
|
+
"savedToast": "Contesto di test salvato"
|
|
1608
|
+
},
|
|
1599
1609
|
"testConfig": {
|
|
1600
1610
|
"title": "Infrastruttura di test",
|
|
1601
1611
|
"hint": "Come viene predisposto un ambiente di test per questo servizio quando una pipeline deve eseguirlo: nessuna infrastruttura, un file Docker Compose, manifest Kubernetes, o un tipo di manifest personalizzato.",
|
package/i18n/locales/ja.json
CHANGED
|
@@ -1051,6 +1051,16 @@
|
|
|
1051
1051
|
"configNoun": "機密のテスト用認証情報",
|
|
1052
1052
|
"duplicateKey": "変数名はそれぞれ一意である必要があります。"
|
|
1053
1053
|
},
|
|
1054
|
+
"testingContext": {
|
|
1055
|
+
"title": "テストの前提情報",
|
|
1056
|
+
"sectionHint": "このサービスをどうテストするかについての自由記述です。重要なフロー、用意されているテストアカウントとそのログイン方法、投入済みデータの意味、触れてはいけない箇所などを書きます。このサービスのテスターは毎回この文章をそのまま渡され、環境のドライランでも同じ文章が使われます。",
|
|
1057
|
+
"notSecret": "この文章はそのままテスターのプロンプトに入ります。本物の秘密情報は書かないでください。秘密情報は上の「テスト用認証情報」に登録し、ここでは変数名だけで参照してください。",
|
|
1058
|
+
"placeholder": "例: $DEMO_USER でログインする。投入済みのテナントは Acme で、プロジェクトが 3 件ある。課金フローは実際のカードに請求されるので絶対に実行しない。",
|
|
1059
|
+
"length": "{max} 文字中 {count} 文字",
|
|
1060
|
+
"save": "前提情報を保存",
|
|
1061
|
+
"revert": "変更を破棄",
|
|
1062
|
+
"savedToast": "テストの前提情報を保存しました"
|
|
1063
|
+
},
|
|
1054
1064
|
"testConfig": {
|
|
1055
1065
|
"title": "テストインフラ",
|
|
1056
1066
|
"hint": "パイプラインがこのサービスを実行する必要があるときに、テスト環境をどう立ち上げるか: インフラなし、Docker Compose ファイル、Kubernetes マニフェスト、またはカスタムマニフェストタイプ。",
|
package/i18n/locales/pl.json
CHANGED
|
@@ -1051,6 +1051,16 @@
|
|
|
1051
1051
|
"configNoun": "wrażliwe poświadczenia testowe",
|
|
1052
1052
|
"duplicateKey": "Każda nazwa zmiennej musi być unikalna."
|
|
1053
1053
|
},
|
|
1054
|
+
"testingContext": {
|
|
1055
|
+
"title": "Kontekst testowania",
|
|
1056
|
+
"sectionHint": "Dowolne notatki o tym, jak testuje się tę usługę: które przepływy są ważne, jakie konta testowe istnieją i jak się na nie zalogować, co oznaczają wgrane dane i czego nie ruszać. Każde uruchomienie Testera dla tej usługi dostaje ten tekst dosłownie, tak samo jak próbne uruchomienie środowiska.",
|
|
1057
|
+
"notSecret": "Ten tekst trafia wprost do promptu Testera, więc nie wpisuj tu prawdziwych sekretów. Zapisz je wyżej, w „Poświadczeniach testowych”, i odwołuj się do nich tutaj tylko przez nazwę zmiennej.",
|
|
1058
|
+
"placeholder": "np. zaloguj się jako $DEMO_USER; wgrany najemca to Acme z trzema projektami; nigdy nie uruchamiaj przepływu płatności, obciąża prawdziwą kartę.",
|
|
1059
|
+
"length": "{count} z {max} znaków",
|
|
1060
|
+
"save": "Zapisz kontekst testowania",
|
|
1061
|
+
"revert": "Odrzuć zmiany",
|
|
1062
|
+
"savedToast": "Zapisano kontekst testowania"
|
|
1063
|
+
},
|
|
1054
1064
|
"testConfig": {
|
|
1055
1065
|
"title": "Infrastruktura testowa",
|
|
1056
1066
|
"hint": "Jak stawiane jest środowisko testowe dla tej usługi, gdy potok musi ją uruchomić: bez infrastruktury, plik Docker Compose, manifesty Kubernetes lub niestandardowy typ manifestu.",
|
package/i18n/locales/tr.json
CHANGED
|
@@ -1051,6 +1051,16 @@
|
|
|
1051
1051
|
"configNoun": "hassas test kimlik bilgileri",
|
|
1052
1052
|
"duplicateKey": "Her değişken adı benzersiz olmalıdır."
|
|
1053
1053
|
},
|
|
1054
|
+
"testingContext": {
|
|
1055
|
+
"title": "Test bağlamı",
|
|
1056
|
+
"sectionHint": "Bu servisin nasıl test edildiğine dair serbest notlar: hangi akışlar önemli, hangi test hesapları var ve bunlarla nasıl oturum açılır, yüklü veriler ne anlama geliyor, neye dokunulmamalı. Bu servis için her Test Edici çalışması bu metni harfi harfine alır; ortamın deneme çalışması da öyle.",
|
|
1057
|
+
"notSecret": "Bu metin doğrudan Test Edici'nin istemine girer, bu yüzden gerçek sırları buraya yazmayın. Onları yukarıdaki «Test kimlik bilgileri» bölümüne kaydedin ve burada yalnızca değişken adıyla anın.",
|
|
1058
|
+
"placeholder": "ör. $DEMO_USER olarak oturum açın; yüklü kiracı üç projeli Acme'dir; faturalama akışını asla çalıştırmayın, gerçek bir kartı borçlandırır.",
|
|
1059
|
+
"length": "{count} / {max} karakter",
|
|
1060
|
+
"save": "Test bağlamını kaydet",
|
|
1061
|
+
"revert": "Değişiklikleri geri al",
|
|
1062
|
+
"savedToast": "Test bağlamı kaydedildi"
|
|
1063
|
+
},
|
|
1054
1064
|
"testConfig": {
|
|
1055
1065
|
"title": "Test altyapısı",
|
|
1056
1066
|
"hint": "Bir pipeline bu servisi çalıştırmak istediğinde test ortamının nasıl kurulacağı: altyapısız, bir Docker Compose dosyası, Kubernetes manifestoları veya özel bir manifest türü.",
|
package/i18n/locales/uk.json
CHANGED
|
@@ -1051,6 +1051,16 @@
|
|
|
1051
1051
|
"configNoun": "конфіденційні тестові облікові дані",
|
|
1052
1052
|
"duplicateKey": "Кожна назва змінної має бути унікальною."
|
|
1053
1053
|
},
|
|
1054
|
+
"testingContext": {
|
|
1055
|
+
"title": "Контекст тестування",
|
|
1056
|
+
"sectionHint": "Довільні нотатки про те, як тестують цю службу: які сценарії важливі, які тестові облікові записи існують і як під ними увійти, що означають наповнені дані та чого не чіпати. Кожен запуск Тестувальника для цієї служби отримує цей текст дослівно, так само як і пробний запуск середовища.",
|
|
1057
|
+
"notSecret": "Цей текст потрапляє просто в підказку Тестувальника, тож не пишіть тут справжніх секретів. Зберігайте їх вище, у «Тестових облікових даних», а тут посилайтеся лише на назву змінної.",
|
|
1058
|
+
"placeholder": "напр. увійдіть як $DEMO_USER; наповнений орендар: Acme з трьома проєктами; ніколи не запускайте сценарій оплати, він списує кошти зі справжньої картки.",
|
|
1059
|
+
"length": "{count} з {max} символів",
|
|
1060
|
+
"save": "Зберегти контекст тестування",
|
|
1061
|
+
"revert": "Скасувати зміни",
|
|
1062
|
+
"savedToast": "Контекст тестування збережено"
|
|
1063
|
+
},
|
|
1054
1064
|
"testConfig": {
|
|
1055
1065
|
"title": "Тестова інфраструктура",
|
|
1056
1066
|
"hint": "Як розгортається тестове середовище для цього сервісу, коли конвеєру потрібно його запустити: без інфраструктури, файл Docker Compose, маніфести Kubernetes або власний тип маніфесту.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.301.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",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"access": "public"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@cat-factory/contracts": "0.
|
|
21
|
+
"@cat-factory/contracts": "0.353.0",
|
|
22
22
|
"@modular-frontend/core": "0.6.0",
|
|
23
23
|
"@modular-vue/core": "^1.5.0",
|
|
24
24
|
"@modular-vue/journeys": "^1.4.0",
|