fix(copilot): harden provider configuration flow

This commit is contained in:
2026-07-13 14:57:28 +08:00
parent 19cee9af11
commit a712b91982
8 changed files with 153 additions and 36 deletions
@@ -86,4 +86,28 @@ describe('ProviderConfiguration', () => {
expect(wrapper.emitted('save')).toBeUndefined();
confirm.mockRestore();
});
it('allows clearing a saved key without allowing a connection test', async () => {
const confirm = vi.spyOn(window, 'confirm').mockReturnValue(true);
const wrapper = mount(ProviderConfiguration, {
props: { config: configuredProvider },
});
await wrapper.get('[data-test-id="clear-chat-api-key"]').setValue(true);
expect(
wrapper.get('[data-test-id="save-provider"]').attributes('disabled')
).toBeUndefined();
expect(
wrapper.get('[data-test-id="test-provider"]').attributes('disabled')
).toBeDefined();
await wrapper.get('[data-test-id="save-provider"]').trigger('click');
expect(confirm).toHaveBeenCalledOnce();
expect(wrapper.emitted('save')?.[0]?.[0].chat).toMatchObject({
api_key: '',
clear_api_key: true,
});
confirm.mockRestore();
});
});
@@ -130,21 +130,36 @@ const hasEmbeddingCredential = computed(
(Boolean(props.config?.embedding?.api_key?.configured) &&
!form.embedding.clearApiKey)
);
const isFormComplete = computed(
const isConfigurationValid = computed(
() =>
Boolean(
form.chat.provider &&
form.chat.baseUrl &&
form.chat.model &&
form.embedding.model &&
form.embedding.dimensions >= 1 &&
hasChatCredential.value &&
hasEmbeddingCredential.value
form.embedding.dimensions >= 1
) &&
(!isSeparateEmbedding.value ||
Boolean(form.embedding.provider && form.embedding.baseUrl)) &&
!invalidAnthropicReuse.value
);
const canTest = computed(
() =>
isConfigurationValid.value &&
hasChatCredential.value &&
hasEmbeddingCredential.value
);
const canSave = computed(
() =>
isConfigurationValid.value &&
(hasChatCredential.value ||
(Boolean(props.config?.chat?.api_key?.configured) &&
form.chat.clearApiKey)) &&
(!isSeparateEmbedding.value ||
hasEmbeddingCredential.value ||
(Boolean(props.config?.embedding?.api_key?.configured) &&
form.embedding.clearApiKey))
);
const activeHealth = computed(() => props.testResult || props.config?.health);
const reindexProgress = computed(() => {
if (!props.reindexStatus?.total) return 0;
@@ -296,7 +311,7 @@ const resultClass = ok =>
'CAPTAIN_SETTINGS.PROVIDER.API_KEY_CONFIGURED',
{
masked:
config.chat.api_key.masked_value ||
config.chat.api_key.masked_value ||
t(
'CAPTAIN_SETTINGS.PROVIDER.API_KEY_MASKED'
),
@@ -312,7 +327,11 @@ const resultClass = ok =>
v-if="config?.chat?.api_key?.configured && !readOnly"
class="flex items-center gap-2 text-xs text-n-ruby-11"
>
<input v-model="form.chat.clearApiKey" type="checkbox" />
<input
v-model="form.chat.clearApiKey"
data-test-id="clear-chat-api-key"
type="checkbox"
/>
{{ t('CAPTAIN_SETTINGS.PROVIDER.CLEAR_CHAT_KEY') }}
</label>
</label>
@@ -393,7 +412,8 @@ const resultClass = ok =>
'CAPTAIN_SETTINGS.PROVIDER.API_KEY_CONFIGURED',
{
masked:
config.embedding.api_key.masked_value ||
config.embedding.api_key
.masked_value ||
t(
'CAPTAIN_SETTINGS.PROVIDER.API_KEY_MASKED'
),
@@ -408,6 +428,7 @@ const resultClass = ok =>
>
<input
v-model="form.embedding.clearApiKey"
data-test-id="clear-embedding-api-key"
type="checkbox"
/>
{{ t('CAPTAIN_SETTINGS.PROVIDER.CLEAR_EMBEDDING_KEY') }}
@@ -618,7 +639,7 @@ const resultClass = ok =>
type="button"
data-test-id="test-provider"
class="rounded-lg border border-n-weak bg-n-solid-2 px-4 py-2 text-sm font-medium text-n-slate-12 hover:bg-n-alpha-2 disabled:opacity-60"
:disabled="isTesting || isSaving || !isFormComplete"
:disabled="isTesting || isSaving || !canTest"
@click="test"
>
{{
@@ -631,7 +652,7 @@ const resultClass = ok =>
type="button"
data-test-id="save-provider"
class="rounded-lg bg-n-brand px-4 py-2 text-sm font-medium text-white hover:bg-n-brand/90 disabled:opacity-60"
:disabled="isSaving || isTesting || !isFormComplete"
:disabled="isSaving || isTesting || !canSave"
@click="save"
>
{{