376 lines
11 KiB
TypeScript
Vendored
376 lines
11 KiB
TypeScript
Vendored
import { strict as assert } from "node:assert";
|
|
import { test, vi } from "vitest";
|
|
|
|
const apiMock = vi.hoisted(() => ({
|
|
cancelDatabaseExport: vi.fn(),
|
|
cancelSqlFileExecution: vi.fn(),
|
|
cancelTableExport: vi.fn(),
|
|
cancelTransfer: vi.fn(),
|
|
startTransfer: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("@/lib/backend/api", () => apiMock);
|
|
|
|
const { useExportTracker } = await import("../../apps/desktop/src/composables/useExportTracker.ts");
|
|
|
|
test("tracks database export progress and cancels through database export API", async () => {
|
|
const tracker = useExportTracker();
|
|
const task = tracker.addDatabaseExportTask("db-export-1", "app", "/tmp/app.sql");
|
|
|
|
tracker.updateDatabaseExportTask("db-export-1", {
|
|
exportId: "db-export-1",
|
|
currentObject: "users",
|
|
objectIndex: 2,
|
|
totalObjects: 5,
|
|
rowsExported: 120,
|
|
totalRows: null,
|
|
status: "Running",
|
|
error: null,
|
|
});
|
|
|
|
assert.equal(task.kind, "database-export");
|
|
assert.equal(task.tableName, "users");
|
|
assert.equal(task.objectIndex, 2);
|
|
assert.equal(task.totalObjects, 5);
|
|
assert.equal(tracker.activeCount.value, 1);
|
|
|
|
await tracker.cancelTask("db-export-1");
|
|
assert.equal(apiMock.cancelDatabaseExport.mock.calls[0][0], "db-export-1");
|
|
|
|
tracker.clearFinished();
|
|
assert.equal(
|
|
tracker.tasks.value.some((item) => item.exportId === "db-export-1"),
|
|
true,
|
|
);
|
|
});
|
|
|
|
test("tracks SQL file progress and clears terminal tasks", () => {
|
|
const tracker = useExportTracker();
|
|
const task = tracker.addSqlFileTask("sql-1", "init.sql", "/tmp/init.sql");
|
|
|
|
tracker.updateSqlFileTask("sql-1", {
|
|
executionId: "sql-1",
|
|
status: "statementDone",
|
|
statementIndex: 3,
|
|
successCount: 2,
|
|
failureCount: 1,
|
|
affectedRows: 12,
|
|
elapsedMs: 1500,
|
|
statementSummary: "insert into users...",
|
|
error: null,
|
|
});
|
|
|
|
assert.equal(task.kind, "sql-file");
|
|
assert.equal(task.status, "Running");
|
|
assert.equal(task.rowsExported, 3);
|
|
assert.equal(task.totalRows, 3);
|
|
assert.equal(task.affectedRows, 12);
|
|
|
|
tracker.updateSqlFileTask("sql-1", {
|
|
executionId: "sql-1",
|
|
status: "done",
|
|
statementIndex: 3,
|
|
successCount: 3,
|
|
failureCount: 0,
|
|
affectedRows: 16,
|
|
elapsedMs: 2000,
|
|
statementSummary: "",
|
|
error: null,
|
|
});
|
|
|
|
assert.equal(task.status, "Done");
|
|
tracker.clearFinished();
|
|
assert.equal(
|
|
tracker.tasks.value.some((item) => item.exportId === "sql-1"),
|
|
false,
|
|
);
|
|
});
|
|
|
|
test("keeps the legacy table export task API and routes cancel to table export API", async () => {
|
|
const tracker = useExportTracker();
|
|
const task = tracker.addTask("orders", "csv", "/tmp/orders.csv");
|
|
|
|
tracker.updateTableExportTask(task.exportId, {
|
|
exportId: task.exportId,
|
|
tableName: "orders",
|
|
rowsExported: 10,
|
|
totalRows: 20,
|
|
status: "Writing",
|
|
errorMessage: null,
|
|
});
|
|
|
|
assert.equal(task.kind, "table-export");
|
|
assert.equal(task.status, "Writing");
|
|
assert.equal(task.rowsExported, 10);
|
|
|
|
await tracker.cancelTask(task.exportId);
|
|
assert.equal(apiMock.cancelTableExport.mock.calls[0][0], task.exportId);
|
|
});
|
|
|
|
test("tracks data transfer progress and routes cancel to transfer API", async () => {
|
|
const tracker = useExportTracker();
|
|
const task = tracker.addDataTransferTask("transfer-1", "source → target", 4);
|
|
|
|
tracker.updateDataTransferTask("transfer-1", {
|
|
transferId: "transfer-1",
|
|
table: "users",
|
|
tableIndex: 2,
|
|
totalTables: 4,
|
|
rowsTransferred: 250,
|
|
totalRows: 500,
|
|
status: "running",
|
|
error: null,
|
|
terminal: false,
|
|
});
|
|
|
|
assert.equal(task.kind, "data-transfer");
|
|
assert.equal(task.currentTable, "users");
|
|
assert.equal(task.tableIndex, 2);
|
|
assert.equal(task.totalTables, 4);
|
|
assert.equal(task.rowsExported, 250);
|
|
assert.equal(task.totalRows, 500);
|
|
assert.equal(task.status, "Running");
|
|
|
|
await tracker.cancelTask("transfer-1");
|
|
assert.equal(apiMock.cancelTransfer.mock.calls[0][0], "transfer-1");
|
|
});
|
|
|
|
test("keeps data transfer active after a non-terminal table error", () => {
|
|
const tracker = useExportTracker();
|
|
const task = tracker.addDataTransferTask("transfer-error-1", "source → target", 2);
|
|
|
|
tracker.updateDataTransferTask("transfer-error-1", {
|
|
transferId: "transfer-error-1",
|
|
table: "users",
|
|
tableIndex: 0,
|
|
totalTables: 2,
|
|
rowsTransferred: 0,
|
|
totalRows: null,
|
|
status: "error",
|
|
error: "permission denied",
|
|
terminal: false,
|
|
});
|
|
|
|
assert.equal(task.status, "Running");
|
|
assert.equal(task.errorMessage, "permission denied");
|
|
|
|
tracker.clearFinished();
|
|
assert.equal(
|
|
tracker.tasks.value.some((item) => item.exportId === "transfer-error-1"),
|
|
true,
|
|
);
|
|
|
|
tracker.updateDataTransferTask("transfer-error-1", {
|
|
transferId: "transfer-error-1",
|
|
table: "orders",
|
|
tableIndex: 1,
|
|
totalTables: 2,
|
|
rowsTransferred: 10,
|
|
totalRows: 50,
|
|
status: "running",
|
|
error: null,
|
|
terminal: false,
|
|
});
|
|
|
|
assert.equal(task.status, "Running");
|
|
assert.equal(task.errorMessage, "permission denied");
|
|
assert.equal(task.currentTable, "orders");
|
|
assert.equal(task.tableIndex, 1);
|
|
assert.equal(task.rowsExported, 10);
|
|
});
|
|
|
|
test("marks data transfer failed only on terminal error summary", () => {
|
|
const tracker = useExportTracker();
|
|
const task = tracker.addDataTransferTask("transfer-terminal-error-1", "source → target", 2);
|
|
|
|
tracker.updateDataTransferTask("transfer-terminal-error-1", {
|
|
transferId: "transfer-terminal-error-1",
|
|
table: "users",
|
|
tableIndex: 0,
|
|
totalTables: 2,
|
|
rowsTransferred: 0,
|
|
totalRows: null,
|
|
status: "error",
|
|
error: "permission denied",
|
|
terminal: false,
|
|
});
|
|
tracker.updateDataTransferTask("transfer-terminal-error-1", {
|
|
transferId: "transfer-terminal-error-1",
|
|
table: "",
|
|
tableIndex: 2,
|
|
totalTables: 2,
|
|
rowsTransferred: 0,
|
|
totalRows: null,
|
|
status: "error",
|
|
error: "1 table(s) failed: users",
|
|
terminal: true,
|
|
});
|
|
|
|
assert.equal(task.status, "Error");
|
|
assert.equal(task.errorMessage, "1 table(s) failed: users");
|
|
assert.equal(task.tableIndex, 2);
|
|
});
|
|
|
|
test("keeps data transfer row counts when terminal summary does not include rows", () => {
|
|
const tracker = useExportTracker();
|
|
const task = tracker.addDataTransferTask("transfer-rows-1", "source → target", 1);
|
|
|
|
tracker.updateDataTransferTask("transfer-rows-1", {
|
|
transferId: "transfer-rows-1",
|
|
table: "users",
|
|
tableIndex: 0,
|
|
totalTables: 1,
|
|
rowsTransferred: 200,
|
|
totalRows: 200,
|
|
status: "tableDone",
|
|
error: null,
|
|
terminal: false,
|
|
});
|
|
tracker.updateDataTransferTask("transfer-rows-1", {
|
|
transferId: "transfer-rows-1",
|
|
table: "",
|
|
tableIndex: 1,
|
|
totalTables: 1,
|
|
rowsTransferred: 0,
|
|
totalRows: null,
|
|
status: "done",
|
|
error: null,
|
|
terminal: true,
|
|
});
|
|
|
|
assert.equal(task.status, "Done");
|
|
assert.equal(task.rowsExported, 200);
|
|
assert.equal(task.totalRows, 200);
|
|
assert.equal(task.tableIndex, 1);
|
|
});
|
|
|
|
test("starts independent data transfer background tasks and routes progress by transfer id", async () => {
|
|
const tracker = useExportTracker();
|
|
const callbacks = new Map<string, (progress: any) => void>();
|
|
const resolvers = new Map<string, () => void>();
|
|
const onDone = vi.fn();
|
|
|
|
apiMock.startTransfer.mockImplementation((request: any, onProgress: (progress: any) => void) => {
|
|
callbacks.set(request.transferId, onProgress);
|
|
return new Promise<void>((resolve) => resolvers.set(request.transferId, resolve));
|
|
});
|
|
|
|
const firstRequest = {
|
|
transferId: "parallel-transfer-1",
|
|
sourceConnectionId: "source-a",
|
|
sourceDatabase: "app_a",
|
|
sourceSchema: "public",
|
|
targetConnectionId: "target-a",
|
|
targetDatabase: "warehouse_a",
|
|
targetSchema: "public",
|
|
tables: ["users"],
|
|
createTable: true,
|
|
mode: "append",
|
|
targetTableNameCase: "preserve",
|
|
batchSize: 1000,
|
|
};
|
|
const secondRequest = {
|
|
...firstRequest,
|
|
transferId: "parallel-transfer-2",
|
|
sourceDatabase: "app_b",
|
|
targetDatabase: "warehouse_b",
|
|
tables: ["orders"],
|
|
};
|
|
|
|
const firstTask = tracker.startDataTransferTask(firstRequest, "app_a → warehouse_a", { onDone });
|
|
const secondTask = tracker.startDataTransferTask(secondRequest, "app_b → warehouse_b");
|
|
|
|
assert.equal(apiMock.startTransfer.mock.calls.at(-2)?.[0].transferId, "parallel-transfer-1");
|
|
assert.equal(apiMock.startTransfer.mock.calls.at(-1)?.[0].transferId, "parallel-transfer-2");
|
|
|
|
callbacks.get("parallel-transfer-2")?.({
|
|
transferId: "parallel-transfer-2",
|
|
table: "orders",
|
|
tableIndex: 0,
|
|
totalTables: 1,
|
|
rowsTransferred: 40,
|
|
totalRows: 100,
|
|
status: "running",
|
|
error: null,
|
|
terminal: false,
|
|
});
|
|
callbacks.get("parallel-transfer-1")?.({
|
|
transferId: "parallel-transfer-1",
|
|
table: "users",
|
|
tableIndex: 0,
|
|
totalTables: 1,
|
|
rowsTransferred: 10,
|
|
totalRows: 50,
|
|
status: "running",
|
|
error: null,
|
|
terminal: false,
|
|
});
|
|
|
|
assert.equal(firstTask.currentTable, "users");
|
|
assert.equal(firstTask.rowsExported, 10);
|
|
assert.equal(secondTask.currentTable, "orders");
|
|
assert.equal(secondTask.rowsExported, 40);
|
|
|
|
callbacks.get("parallel-transfer-1")?.({
|
|
transferId: "parallel-transfer-1",
|
|
table: "",
|
|
tableIndex: 1,
|
|
totalTables: 1,
|
|
rowsTransferred: 50,
|
|
totalRows: 50,
|
|
status: "done",
|
|
error: null,
|
|
terminal: true,
|
|
});
|
|
resolvers.get("parallel-transfer-1")?.();
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
|
|
assert.equal(firstTask.status, "Done");
|
|
assert.equal(secondTask.status, "Running");
|
|
assert.equal(onDone.mock.calls.length, 1);
|
|
});
|
|
|
|
test("blocks concurrent data transfers that write the same target table", () => {
|
|
const tracker = useExportTracker();
|
|
apiMock.startTransfer.mockImplementation(() => new Promise<void>(() => {}));
|
|
|
|
const firstRequest = {
|
|
transferId: "same-target-transfer-1",
|
|
sourceConnectionId: "source-a",
|
|
sourceDatabase: "app_a",
|
|
sourceSchema: "public",
|
|
targetConnectionId: "target-a",
|
|
targetDatabase: "warehouse",
|
|
targetSchema: "public",
|
|
tables: ["users"],
|
|
createTable: true,
|
|
mode: "append",
|
|
targetTableNameCase: "preserve",
|
|
batchSize: 1000,
|
|
};
|
|
const secondRequest = {
|
|
...firstRequest,
|
|
transferId: "same-target-transfer-2",
|
|
sourceDatabase: "app_b",
|
|
};
|
|
|
|
const firstTask = tracker.startDataTransferTask(firstRequest, "app_a → warehouse");
|
|
const secondTask = tracker.startDataTransferTask(secondRequest, "app_b → warehouse");
|
|
|
|
assert.equal(firstTask.status, "Running");
|
|
assert.equal(secondTask.status, "Error");
|
|
assert.match(secondTask.errorMessage ?? "", /already running/);
|
|
|
|
tracker.updateDataTransferTask("same-target-transfer-1", {
|
|
transferId: "same-target-transfer-1",
|
|
table: "",
|
|
tableIndex: 1,
|
|
totalTables: 1,
|
|
rowsTransferred: 0,
|
|
totalRows: null,
|
|
status: "done",
|
|
error: null,
|
|
terminal: true,
|
|
});
|
|
});
|