fix(connection): preserve edited name after tests
This commit is contained in:
parent
91abfb747b
commit
0fd7b0d41b
|
|
@ -21,7 +21,7 @@ import { CONNECTION_ATTEMPT_CANCELLED_MESSAGE, useConnectionStore } from "@/stor
|
|||
import { useTunnelProfileStore } from "@/stores/tunnelProfileStore";
|
||||
import { detachTunnelProfileLayer, tunnelProfileReferenceLayer, tunnelProfileSummary } from "@/lib/connection/tunnelProfiles";
|
||||
import { applySshConfigHostAliasPrefill as prefillSshConfigHostAlias } from "@/lib/connection/sshConfigHosts";
|
||||
import { connectionEditDraftSyncAction } from "./connectionEditDraftSync";
|
||||
import { canPersistConnectionTestResult, connectionEditDraftSyncAction } from "./connectionEditDraftSync";
|
||||
import { REDIS_SCAN_PAGE_SIZE_DEFAULT, REDIS_SCAN_PAGE_SIZE_MIN, REDIS_SCAN_PAGE_SIZE_MAX, REDIS_SCAN_PAGE_SIZE_OPTIONS } from "@/lib/redis/redisKeyPattern";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
|
|
@ -1299,12 +1299,35 @@ function applySuccessfulConnectionTest(result: ConnectionTestResult, config: Con
|
|||
testedGeneratedName.value = config.name;
|
||||
}
|
||||
|
||||
async function persistSuccessfulConnectionTest(result: ConnectionTestResult, config: ConnectionConfig, sourceName: string) {
|
||||
async function persistSuccessfulConnectionTest(result: ConnectionTestResult, config: ConnectionConfig, sourceName: string, runId: number) {
|
||||
if (!editingId.value || !result.databaseInfo || !savedConnectionConfigFingerprint.value) return;
|
||||
const fingerprint = connectionConfigFingerprint(config, sourceName);
|
||||
if (fingerprint !== savedConnectionConfigFingerprint.value) return;
|
||||
let currentDraftFingerprint: string;
|
||||
try {
|
||||
await store.updateConnectionDatabaseInfo(editingId.value, result.databaseInfo);
|
||||
const currentDraft = connectionConfigForSubmit(editingId.value, form.value.name);
|
||||
currentDraftFingerprint = connectionConfigFingerprint(currentDraft, form.value.name);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
// An in-flight test must not publish its saved snapshot after the user edits,
|
||||
// switches, or closes the draft that initiated it.
|
||||
if (
|
||||
!canPersistConnectionTestResult({
|
||||
testConfigId: config.id,
|
||||
activeDraftId: editingId.value,
|
||||
testRunId: runId,
|
||||
activeTestRunId: testRunId,
|
||||
submittedFingerprint: fingerprint,
|
||||
savedFingerprint: savedConnectionConfigFingerprint.value,
|
||||
currentDraftFingerprint,
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const persistedDraftId = editingId.value;
|
||||
try {
|
||||
await store.updateConnectionDatabaseInfo(persistedDraftId, result.databaseInfo);
|
||||
if (runId !== testRunId || editingId.value !== persistedDraftId) return;
|
||||
savedDatabaseInfo.value = { ...result.databaseInfo };
|
||||
savedDatabaseInfoFingerprint.value = fingerprint;
|
||||
} catch {
|
||||
|
|
@ -2358,7 +2381,7 @@ async function testConnection() {
|
|||
successfulConfig = connectionConfigForSubmit(config.id, config.name);
|
||||
}
|
||||
applySuccessfulConnectionTest(result, successfulConfig, submittedSourceName);
|
||||
void persistSuccessfulConnectionTest(result, successfulConfig, submittedSourceName);
|
||||
void persistSuccessfulConnectionTest(result, successfulConfig, submittedSourceName, runId);
|
||||
clearEditedConnectionErrorAfterSuccessfulTest();
|
||||
} catch (e: any) {
|
||||
if (runId !== testRunId) return;
|
||||
|
|
@ -2372,7 +2395,7 @@ async function testConnection() {
|
|||
}
|
||||
if (fallback) {
|
||||
applySuccessfulConnectionTest(fallback.result, fallback.config, submittedSourceName);
|
||||
void persistSuccessfulConnectionTest(fallback.result, fallback.config, submittedSourceName);
|
||||
void persistSuccessfulConnectionTest(fallback.result, fallback.config, submittedSourceName, runId);
|
||||
clearEditedConnectionErrorAfterSuccessfulTest();
|
||||
} else {
|
||||
clearTestedConnectionInfo();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { connectionEditDraftSyncAction } from "../connectionEditDraftSync";
|
||||
import { canPersistConnectionTestResult, connectionEditDraftSyncAction } from "../connectionEditDraftSync";
|
||||
|
||||
function deferred<T>() {
|
||||
let resolve!: (value: T) => void;
|
||||
const promise = new Promise<T>((resolvePromise) => {
|
||||
resolve = resolvePromise;
|
||||
});
|
||||
return { promise, resolve };
|
||||
}
|
||||
|
||||
describe("connectionEditDraftSyncAction", () => {
|
||||
it("hydrates normal connection edits without reloading an active draft", () => {
|
||||
|
|
@ -11,12 +19,64 @@ describe("connectionEditDraftSyncAction", () => {
|
|||
});
|
||||
|
||||
it("keeps a copied connection draft when its saved snapshot is refreshed", () => {
|
||||
const draft = { id: "connection-b", host: "edited.example.test", password: "edited-secret" };
|
||||
const savedCopySnapshot = { id: "connection-b", host: "original.example.test", password: "original-secret" };
|
||||
const draft = { id: "connection-b", name: "edited copy", host: "edited.example.test", password: "edited-secret" };
|
||||
const savedCopySnapshot = { id: "connection-b", name: "original copy", host: "original.example.test", password: "original-secret" };
|
||||
|
||||
const action = connectionEditDraftSyncAction(savedCopySnapshot.id, true, draft.id);
|
||||
|
||||
expect(action).toBe("preserve");
|
||||
expect(draft).toEqual({ id: "connection-b", host: "edited.example.test", password: "edited-secret" });
|
||||
expect(draft).toEqual({ id: "connection-b", name: "edited copy", host: "edited.example.test", password: "edited-secret" });
|
||||
});
|
||||
|
||||
it("does not persist an asynchronous test after the user edits the copied name", async () => {
|
||||
const pendingTest = deferred<string>();
|
||||
const draft = { id: "connection-b", name: "original copy", fingerprint: "saved-copy" };
|
||||
const completion = pendingTest.promise.then((submittedFingerprint) =>
|
||||
canPersistConnectionTestResult({
|
||||
testConfigId: draft.id,
|
||||
activeDraftId: draft.id,
|
||||
testRunId: 7,
|
||||
activeTestRunId: 7,
|
||||
submittedFingerprint,
|
||||
savedFingerprint: "saved-copy",
|
||||
currentDraftFingerprint: draft.fingerprint,
|
||||
}),
|
||||
);
|
||||
|
||||
draft.name = "edited copy";
|
||||
draft.fingerprint = "edited-copy";
|
||||
pendingTest.resolve("saved-copy");
|
||||
|
||||
expect(await completion).toBe(false);
|
||||
expect(draft.name).toBe("edited copy");
|
||||
});
|
||||
|
||||
it("rejects test side effects after switching targets or closing the dialog", () => {
|
||||
const baseState = {
|
||||
testConfigId: "connection-a",
|
||||
testRunId: 4,
|
||||
submittedFingerprint: "saved-a",
|
||||
savedFingerprint: "saved-a",
|
||||
currentDraftFingerprint: "saved-a",
|
||||
};
|
||||
|
||||
expect(canPersistConnectionTestResult({ ...baseState, activeDraftId: "connection-b", activeTestRunId: 5 })).toBe(false);
|
||||
expect(canPersistConnectionTestResult({ ...baseState, activeDraftId: null, activeTestRunId: 5 })).toBe(false);
|
||||
expect(connectionEditDraftSyncAction("connection-b", true, "connection-a")).toBe("hydrate");
|
||||
expect(connectionEditDraftSyncAction("connection-a", false, "connection-a")).toBe("preserve");
|
||||
});
|
||||
|
||||
it("preserves the normal unchanged test persistence path", () => {
|
||||
expect(
|
||||
canPersistConnectionTestResult({
|
||||
testConfigId: "connection-a",
|
||||
activeDraftId: "connection-a",
|
||||
testRunId: 3,
|
||||
activeTestRunId: 3,
|
||||
submittedFingerprint: "saved-a",
|
||||
savedFingerprint: "saved-a",
|
||||
currentDraftFingerprint: "saved-a",
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,15 @@
|
|||
export type ConnectionEditDraftSyncAction = "hydrate" | "reset" | "preserve";
|
||||
|
||||
export interface ConnectionTestPersistenceState {
|
||||
testConfigId: string;
|
||||
activeDraftId: string | null;
|
||||
testRunId: number;
|
||||
activeTestRunId: number;
|
||||
submittedFingerprint: string;
|
||||
savedFingerprint: string;
|
||||
currentDraftFingerprint: string;
|
||||
}
|
||||
|
||||
export function connectionEditDraftSyncAction(configId: string | null, isOpen: boolean, activeDraftId: string | null): ConnectionEditDraftSyncAction {
|
||||
if (!isOpen) return "preserve";
|
||||
// Saved snapshots for the active connection must not overwrite its draft,
|
||||
|
|
@ -7,3 +17,7 @@ export function connectionEditDraftSyncAction(configId: string | null, isOpen: b
|
|||
if (configId && configId === activeDraftId) return "preserve";
|
||||
return configId ? "hydrate" : "reset";
|
||||
}
|
||||
|
||||
export function canPersistConnectionTestResult(state: ConnectionTestPersistenceState): boolean {
|
||||
return state.testConfigId === state.activeDraftId && state.testRunId === state.activeTestRunId && state.submittedFingerprint === state.savedFingerprint && state.currentDraftFingerprint === state.submittedFingerprint;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue