From 3971a40654d25961edc6d55b5f61b68e32e50512 Mon Sep 17 00:00:00 2001 From: "release-feldera-feldera[bot]" Date: Sat, 4 Apr 2026 07:52:22 +0000 Subject: [PATCH 1/4] ci: Prepare for v0.282.0 --- Cargo.lock | 40 ++++++++++++++++++++-------------------- Cargo.toml | 24 ++++++++++++------------ openapi.json | 2 +- python/pyproject.toml | 2 +- python/uv.lock | 4 ++-- 5 files changed, 36 insertions(+), 36 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3cd9d96da5..179949b262 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3761,7 +3761,7 @@ dependencies = [ [[package]] name = "dbsp" -version = "0.281.0" +version = "0.282.0" dependencies = [ "anyhow", "arc-swap", @@ -3849,7 +3849,7 @@ dependencies = [ [[package]] name = "dbsp_adapters" -version = "0.281.0" +version = "0.282.0" dependencies = [ "actix", "actix-codec", @@ -3986,7 +3986,7 @@ dependencies = [ [[package]] name = "dbsp_nexmark" -version = "0.281.0" +version = "0.282.0" dependencies = [ "anyhow", "ascii_table", @@ -4861,7 +4861,7 @@ dependencies = [ [[package]] name = "fda" -version = "0.281.0" +version = "0.282.0" dependencies = [ "anyhow", "arrow", @@ -4913,7 +4913,7 @@ dependencies = [ [[package]] name = "feldera-adapterlib" -version = "0.281.0" +version = "0.282.0" dependencies = [ "actix-web", "anyhow", @@ -4944,7 +4944,7 @@ dependencies = [ [[package]] name = "feldera-buffer-cache" -version = "0.281.0" +version = "0.282.0" dependencies = [ "crossbeam-utils", "enum-map", @@ -4972,7 +4972,7 @@ dependencies = [ [[package]] name = "feldera-datagen" -version = "0.281.0" +version = "0.282.0" dependencies = [ "anyhow", "async-channel 2.5.0", @@ -4998,7 +4998,7 @@ dependencies = [ [[package]] name = "feldera-fxp" -version = "0.281.0" +version = "0.282.0" dependencies = [ "bytecheck", "dbsp", @@ -5018,7 +5018,7 @@ dependencies = [ [[package]] name = "feldera-iceberg" -version = "0.281.0" +version = "0.282.0" dependencies = [ "anyhow", "chrono", @@ -5038,7 +5038,7 @@ dependencies = [ [[package]] name = "feldera-ir" -version = "0.281.0" +version = "0.282.0" dependencies = [ "proptest", "proptest-derive", @@ -5050,7 +5050,7 @@ dependencies = [ [[package]] name = "feldera-macros" -version = "0.281.0" +version = "0.282.0" dependencies = [ "prettyplease", "proc-macro2", @@ -5060,7 +5060,7 @@ dependencies = [ [[package]] name = "feldera-observability" -version = "0.281.0" +version = "0.282.0" dependencies = [ "actix-http", "awc", @@ -5075,7 +5075,7 @@ dependencies = [ [[package]] name = "feldera-rest-api" -version = "0.281.0" +version = "0.282.0" dependencies = [ "chrono", "feldera-observability", @@ -5109,7 +5109,7 @@ dependencies = [ [[package]] name = "feldera-sqllib" -version = "0.281.0" +version = "0.282.0" dependencies = [ "arcstr", "base58", @@ -5150,7 +5150,7 @@ dependencies = [ [[package]] name = "feldera-storage" -version = "0.281.0" +version = "0.282.0" dependencies = [ "anyhow", "crossbeam", @@ -5173,7 +5173,7 @@ dependencies = [ [[package]] name = "feldera-types" -version = "0.281.0" +version = "0.282.0" dependencies = [ "actix-web", "anyhow", @@ -8094,7 +8094,7 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pipeline-manager" -version = "0.281.0" +version = "0.282.0" dependencies = [ "actix-cors", "actix-files", @@ -9188,7 +9188,7 @@ dependencies = [ [[package]] name = "readers" -version = "0.281.0" +version = "0.282.0" dependencies = [ "async-std", "csv", @@ -10764,7 +10764,7 @@ dependencies = [ [[package]] name = "sltsqlvalue" -version = "0.281.0" +version = "0.282.0" dependencies = [ "dbsp", "feldera-sqllib", @@ -11067,7 +11067,7 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "storage-test-compat" -version = "0.281.0" +version = "0.282.0" dependencies = [ "dbsp", "derive_more 1.0.0", diff --git a/Cargo.toml b/Cargo.toml index 848c71cb47..10d170e216 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace.package] authors = ["Feldera Team "] -version = "0.281.0" +version = "0.282.0" license = "MIT OR Apache-2.0" homepage = "https://github.com/feldera/feldera" repository = "https://github.com/feldera/feldera" @@ -101,7 +101,7 @@ csv = "1.2.2" csv-core = "0.1.10" dashmap = "6.1.0" datafusion = "51.0" -dbsp = { path = "crates/dbsp", version = "0.281.0" } +dbsp = { path = "crates/dbsp", version = "0.282.0" } dbsp_nexmark = { path = "crates/nexmark" } deadpool-postgres = "0.14.1" #deltalake = "0.30.2" @@ -121,19 +121,19 @@ erased-serde = "0.3.31" fake = "2.10" fastbloom = "0.14.0" fdlimit = "0.3.0" -feldera-buffer-cache = { version = "0.281.0", path = "crates/buffer-cache" } +feldera-buffer-cache = { version = "0.282.0", path = "crates/buffer-cache" } feldera-cloud1-client = "0.1.2" feldera-datagen = { path = "crates/datagen" } -feldera-fxp = { version = "0.281.0", path = "crates/fxp", features = ["dbsp"] } +feldera-fxp = { version = "0.282.0", path = "crates/fxp", features = ["dbsp"] } feldera-iceberg = { path = "crates/iceberg" } -feldera-observability = { version = "0.281.0", path = "crates/feldera-observability" } -feldera-macros = { version = "0.281.0", path = "crates/feldera-macros" } -feldera-sqllib = { version = "0.281.0", path = "crates/sqllib" } -feldera-storage = { version = "0.281.0", path = "crates/storage" } -feldera-types = { version = "0.281.0", path = "crates/feldera-types" } -feldera-rest-api = { version = "0.281.0", path = "crates/rest-api" } -feldera-ir = { version = "0.281.0", path = "crates/ir" } -feldera-adapterlib = { version = "0.281.0", path = "crates/adapterlib" } +feldera-observability = { version = "0.282.0", path = "crates/feldera-observability" } +feldera-macros = { version = "0.282.0", path = "crates/feldera-macros" } +feldera-sqllib = { version = "0.282.0", path = "crates/sqllib" } +feldera-storage = { version = "0.282.0", path = "crates/storage" } +feldera-types = { version = "0.282.0", path = "crates/feldera-types" } +feldera-rest-api = { version = "0.282.0", path = "crates/rest-api" } +feldera-ir = { version = "0.282.0", path = "crates/ir" } +feldera-adapterlib = { version = "0.282.0", path = "crates/adapterlib" } flate2 = "1.1.0" form_urlencoded = "1.2.0" futures = "0.3.30" diff --git a/openapi.json b/openapi.json index 20ede77252..3b951bec1a 100644 --- a/openapi.json +++ b/openapi.json @@ -10,7 +10,7 @@ "license": { "name": "MIT OR Apache-2.0" }, - "version": "0.281.0" + "version": "0.282.0" }, "paths": { "/config/authentication": { diff --git a/python/pyproject.toml b/python/pyproject.toml index 4de5966d8c..6d2d94693f 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" name = "feldera" readme = "README.md" description = "The feldera python client" -version = "0.281.0" +version = "0.282.0" license = "MIT" requires-python = ">=3.10" authors = [ diff --git a/python/uv.lock b/python/uv.lock index 7876f86939..036a757848 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -12,7 +12,7 @@ resolution-markers = [ ] [options] -exclude-newer = "2026-03-26T19:13:33.259555Z" +exclude-newer = "2026-03-28T07:51:53.482034304Z" exclude-newer-span = "P1W" [[package]] @@ -221,7 +221,7 @@ wheels = [ [[package]] name = "feldera" -version = "0.281.0" +version = "0.282.0" source = { editable = "." } dependencies = [ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, From 26c1ad8ad47f4c55524808d7f71fa6d7ebaf5509 Mon Sep 17 00:00:00 2001 From: Karakatiza666 Date: Wed, 25 Mar 2026 13:08:53 +0000 Subject: [PATCH 2/4] [web-console] Fix failing to display the connector errors if the table or connector name includes URL-unsafe characters (e.g. '.') Remove unnecessary conversion of URL-safe pipeline_name-s Signed-off-by: Karakatiza666 --- .../src/lib/services/pipelineManager.ts | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/js-packages/web-console/src/lib/services/pipelineManager.ts b/js-packages/web-console/src/lib/services/pipelineManager.ts index b34a6c378b..0180da6c55 100644 --- a/js-packages/web-console/src/lib/services/pipelineManager.ts +++ b/js-packages/web-console/src/lib/services/pipelineManager.ts @@ -364,7 +364,7 @@ export const getExtendedPipeline = async ( ) => { return mapResponse( _getPipeline({ - path: { pipeline_name: encodeURIComponent(pipeline_name) }, + path: { pipeline_name }, ...options }), toExtendedPipeline, @@ -398,7 +398,7 @@ export const putPipeline = async ( await mapResponse( _putPipeline({ body: newPipeline, - path: { pipeline_name: encodeURIComponent(pipeline_name) }, + path: { pipeline_name }, ...options }), (v) => v @@ -412,7 +412,7 @@ export const patchPipeline = async ( ) => { return mapResponse( _patchPipeline({ - path: { pipeline_name: encodeURIComponent(pipeline_name) }, + path: { pipeline_name }, body: fromPipeline(pipeline), ...options }), @@ -433,7 +433,7 @@ export const getPipelines = async (options?: FetchOptions): Promise { return mapResponse( _getPipeline({ - path: { pipeline_name: encodeURIComponent(pipeline_name) }, + path: { pipeline_name }, query: { selector: 'status' }, ...options }), @@ -450,7 +450,7 @@ export const getPipelineStatus = async (pipeline_name: string, options?: FetchOp export const getPipelineStats = async (pipeline_name: string, options?: FetchOptions) => { return mapResponse( _getPipelineStats({ - path: { pipeline_name: encodeURIComponent(pipeline_name) }, + path: { pipeline_name }, ...options }), (status) => ({ @@ -477,7 +477,7 @@ export const getInputConnectorStatus = ( ) => mapResponse( _getPipelineInputConnectorStatus({ - path: { pipeline_name, table_name, connector_name }, + path: { pipeline_name, table_name: encodeURIComponent(table_name), connector_name: encodeURIComponent(connector_name) }, ...options }), (v) => v @@ -491,7 +491,7 @@ export const getOutputConnectorStatus = ( ) => mapResponse( _getPipelineOutputConnectorStatus({ - path: { pipeline_name, view_name, connector_name }, + path: { pipeline_name, view_name: encodeURIComponent(view_name), connector_name: encodeURIComponent(connector_name) }, ...options }), (v) => v @@ -550,7 +550,7 @@ export const postApiKey = (name: string, options?: FetchOptions) => export const deleteApiKey = (name: string, options?: FetchOptions) => mapResponse( - _deleteApiKey({ path: { api_key_name: name }, ...options }), + _deleteApiKey({ path: { api_key_name: encodeURIComponent(name) }, ...options }), (v) => v, () => { throw new Error(`Failed to delete ${name} API key`) @@ -578,7 +578,7 @@ export const getClusterEvent = (eventId: string) => const getSamplyProfileStream = (pipelineName: string, latest: boolean) => { const result = streamingFetch( getAuthenticatedFetch(), - `${felderaEndpoint}/v0/pipelines/${encodeURIComponent(pipelineName)}/samply_profile${latest ? '?latest=true' : ''}`, + `${felderaEndpoint}/v0/pipelines/${pipelineName}/samply_profile${latest ? '?latest=true' : ''}`, { method: 'GET' } @@ -929,7 +929,7 @@ export const relationIngress = async ( options?: FetchOptions ) => { return httpInput({ - path: { pipeline_name: pipelineName, table_name: relationName }, + path: { pipeline_name: pipelineName, table_name: encodeURIComponent(relationName) }, parseAs: 'text', // Response is empty, so no need to parse it as JSON query: { format: 'json', array: true, update_format: 'insert_delete', force: !!force }, body: data as any, @@ -967,5 +967,5 @@ export const getPipelineSupportBundleUrl = ( for (const [key, value] of Object.entries(options)) { query.append(key, String(value)) } - return `${felderaEndpoint}/v0/pipelines/${encodeURIComponent(pipelineName)}/support_bundle?${query.toString()}` + return `${felderaEndpoint}/v0/pipelines/${pipelineName}/support_bundle?${query.toString()}` } From 84efa2655bec62e58ace8c27a3010d65f1cfd5ea Mon Sep 17 00:00:00 2001 From: Karakatiza666 Date: Wed, 25 Mar 2026 19:15:16 +0000 Subject: [PATCH 3/4] [web-console] Integration tests for retrieving connector errors Signed-off-by: Karakatiza666 --- .github/workflows/test-web-console-e2e.yml | 7 + js-packages/web-console/package.json | 3 +- .../editor/performance/ConnectorErrors.svelte | 10 +- .../src/lib/functions/felderaRelation.ts | 5 +- .../src/lib/services/pipelineManager.test.ts | 125 ++++++++++++++++++ .../src/lib/services/pipelineManager.ts | 8 +- js-packages/web-console/vite.config.ts | 68 ++++++---- 7 files changed, 190 insertions(+), 36 deletions(-) create mode 100644 js-packages/web-console/src/lib/services/pipelineManager.test.ts diff --git a/.github/workflows/test-web-console-e2e.yml b/.github/workflows/test-web-console-e2e.yml index bfa94dd84f..31f116d206 100644 --- a/.github/workflows/test-web-console-e2e.yml +++ b/.github/workflows/test-web-console-e2e.yml @@ -44,6 +44,13 @@ jobs: - name: Verify pipeline-manager is reachable run: curl -fsSL --retry 5 --retry-delay 2 --retry-connrefused http://localhost:8080/healthz + - name: Run vitest integration tests + if: ${{ vars.CI_DRY_RUN != 'true' }} + run: bun run test-integration + working-directory: js-packages/web-console + env: + FELDERA_API_URL: http://localhost:8080 + - name: Run Playwright e2e tests if: ${{ vars.CI_DRY_RUN != 'true' }} run: bun run test-e2e diff --git a/js-packages/web-console/package.json b/js-packages/web-console/package.json index 6e65110688..5108adf739 100644 --- a/js-packages/web-console/package.json +++ b/js-packages/web-console/package.json @@ -132,7 +132,8 @@ "test-prepare": "git clone --depth 1 https://github.com/feldera/playwright-snapshots.git || true && mkdir -p playwright-snapshots/e2e playwright-snapshots/component", "test-update-snapshots": "bun run test -- --update && bun playwright test --update-snapshots", "test-unit": "vitest", - "test": "bun run test-unit -- --run" + "test-integration": "vitest --run --project integration --project integration-client", + "test": "bun run test-unit -- --run --project client --project server" }, "trustedDependencies": ["@axa-fr/oidc-client", "sk-oidc-oauth", "svelte-preprocess"], "type": "module" diff --git a/js-packages/web-console/src/lib/components/pipelines/editor/performance/ConnectorErrors.svelte b/js-packages/web-console/src/lib/components/pipelines/editor/performance/ConnectorErrors.svelte index 3f765991d8..1b942d3ad0 100644 --- a/js-packages/web-console/src/lib/components/pipelines/editor/performance/ConnectorErrors.svelte +++ b/js-packages/web-console/src/lib/components/pipelines/editor/performance/ConnectorErrors.svelte @@ -12,6 +12,7 @@ type OutputEndpointStatus } from '$lib/services/manager' import { getInputConnectorStatus, getOutputConnectorStatus } from '$lib/services/pipelineManager' + import { getCaseDependentName } from '$lib/functions/felderaRelation' export type ConnectorErrorFilter = 'all' | 'parse' | 'transport' | 'encode' @@ -61,6 +62,8 @@ tagsFilter = filter }) + const strippedConnectorName = $derived(connectorName.slice(getCaseDependentName(relationName).name.length + 1)) + $effect(() => { pipelineName relationName @@ -69,11 +72,10 @@ loading = true status = null - const stripped = connectorName.slice(connectorName.indexOf('.') + 1) const request = direction === 'input' - ? getInputConnectorStatus(pipelineName, relationName, stripped) - : getOutputConnectorStatus(pipelineName, relationName, stripped) + ? getInputConnectorStatus(pipelineName, relationName, strippedConnectorName) + : getOutputConnectorStatus(pipelineName, relationName, strippedConnectorName) request.then((s) => { status = s @@ -137,7 +139,7 @@
-
{connectorName.replace('.', ' · ')}
+
{relationName} · {strippedConnectorName}
diff --git a/js-packages/web-console/src/lib/functions/felderaRelation.ts b/js-packages/web-console/src/lib/functions/felderaRelation.ts index 61ca010584..5b289aaf00 100644 --- a/js-packages/web-console/src/lib/functions/felderaRelation.ts +++ b/js-packages/web-console/src/lib/functions/felderaRelation.ts @@ -21,10 +21,7 @@ export const normalizeCaseIndependentName = ({ export const getCaseDependentName = (caseIndependentName: string) => { const case_sensitive = isCaseSensitive(caseIndependentName) return { - name: normalizeCaseIndependentName({ - name: caseIndependentName.replaceAll('"', ''), - case_sensitive - }), + name: caseIndependentName.replaceAll('"', ''), case_sensitive } } diff --git a/js-packages/web-console/src/lib/services/pipelineManager.test.ts b/js-packages/web-console/src/lib/services/pipelineManager.test.ts new file mode 100644 index 0000000000..8fc7391920 --- /dev/null +++ b/js-packages/web-console/src/lib/services/pipelineManager.test.ts @@ -0,0 +1,125 @@ +/** + * Integration tests for pipelineManager.ts that require a running Feldera instance. + * Run via the 'integration' vitest project: + * + * FELDERA_API_URL=http://localhost:8080 bun run test-integration + */ +import { afterAll, beforeAll, describe, expect, it } from 'vitest' + +const BASE_URL = process.env.FELDERA_API_URL ?? 'http://localhost:8080' + +// Pipeline name is URL-safe by convention, but table/view/connector names are not. +const PIPELINE_NAME = 'test-special-chars-connector-status' + +// Names with dots and URL-unsafe characters +const TABLE_NAME = 'my.input.table' +const VIEW_NAME = 'my.output.view' +const INPUT_CONNECTOR_NAME = 'http.input/special&name' +const OUTPUT_CONNECTOR_NAME = 'http.output/special&name' + +const api = (path: string, init?: RequestInit) => + fetch(`${BASE_URL}${path}`, { + ...init, + headers: { 'Content-Type': 'application/json', ...init?.headers } + }) + +describe('pipelineManager connector status with special characters', () => { + beforeAll(async () => { + // Clean up any leftover pipeline from a previous run + const del = await api(`/v0/pipelines/${PIPELINE_NAME}`, { method: 'DELETE' }) + if (del.ok || del.status === 404) { + // ok + } else { + throw new Error(`Cleanup failed: ${del.status} ${await del.text()}`) + } + + // SQL program with quoted identifiers containing dots + const programCode = ` + CREATE TABLE "${TABLE_NAME}" (id INT NOT NULL, val VARCHAR) + WITH ( + 'connectors' = '[{ + "name": "${INPUT_CONNECTOR_NAME}", + "transport": { "name": "http_input" }, + "format": { "name": "json", "config": { "update_format": "raw" } } + }]' + ); + CREATE VIEW "${VIEW_NAME}" AS SELECT * FROM "${TABLE_NAME}" + WITH ( + 'connectors' = '[{ + "name": "${OUTPUT_CONNECTOR_NAME}", + "transport": { "name": "http_output" }, + "format": { "name": "json", "config": { "update_format": "raw" } } + }]' + ); + ` + + // Create the pipeline + const put = await api(`/v0/pipelines/${PIPELINE_NAME}`, { + method: 'PUT', + body: JSON.stringify({ + name: PIPELINE_NAME, + description: 'Integration test for special character handling', + program_code: programCode, + runtime_config: {} + }) + }) + expect(put.ok, `PUT pipeline: ${put.status} ${await put.clone().text()}`).toBe(true) + + // Wait for compilation + for (let i = 0; i < 120; i++) { + const res = await api(`/v0/pipelines/${PIPELINE_NAME}`) + const info = await res.json() + if (info.program_status === 'Success') break + if (info.program_status?.SqlError || info.program_status?.RustError || info.program_status?.SystemError) { + throw new Error(`Compilation failed: ${JSON.stringify(info.program_status)}`) + } + await new Promise((r) => setTimeout(r, 1000)) + } + + // Start the pipeline + const start = await api(`/v0/pipelines/${PIPELINE_NAME}/start`, { method: 'POST' }) + expect(start.ok, `POST start: ${start.status} ${await start.clone().text()}`).toBe(true) + + // Wait for pipeline to be running + for (let i = 0; i < 60; i++) { + const res = await api(`/v0/pipelines/${PIPELINE_NAME}`) + const info = await res.json() + if (info.deployment_status === 'Running') break + if (info.deployment_error) { + throw new Error(`Deployment failed: ${JSON.stringify(info.deployment_error)}`) + } + await new Promise((r) => setTimeout(r, 1000)) + } + }, 180_000) + + afterAll(async () => { + // Shutdown and delete + await api(`/v0/pipelines/${PIPELINE_NAME}/shutdown`, { method: 'POST' }) + // Wait for shutdown + for (let i = 0; i < 30; i++) { + const res = await api(`/v0/pipelines/${PIPELINE_NAME}`) + const info = await res.json() + if (info.deployment_status === 'Shutdown') break + await new Promise((r) => setTimeout(r, 1000)) + } + await api(`/v0/pipelines/${PIPELINE_NAME}`, { method: 'DELETE' }) + }, 60_000) + + it('getInputConnectorStatus succeeds with dot and URL-unsafe characters in table and connector name', async () => { + const url = `/v0/pipelines/${PIPELINE_NAME}/tables/${encodeURIComponent(TABLE_NAME)}/connectors/${encodeURIComponent(INPUT_CONNECTOR_NAME)}/status` + const res = await api(url) + expect(res.ok, `GET input connector status: ${res.status} ${await res.clone().text()}`).toBe(true) + const body = await res.json() + expect(body).toHaveProperty('num_parse_errors') + expect(body).toHaveProperty('num_transport_errors') + }) + + it('getOutputConnectorStatus succeeds with dot and URL-unsafe characters in view and connector name', async () => { + const url = `/v0/pipelines/${PIPELINE_NAME}/views/${encodeURIComponent(VIEW_NAME)}/connectors/${encodeURIComponent(OUTPUT_CONNECTOR_NAME)}/status` + const res = await api(url) + expect(res.ok, `GET output connector status: ${res.status} ${await res.clone().text()}`).toBe(true) + const body = await res.json() + expect(body).toHaveProperty('num_encode_errors') + expect(body).toHaveProperty('num_transport_errors') + }) +}) diff --git a/js-packages/web-console/src/lib/services/pipelineManager.ts b/js-packages/web-console/src/lib/services/pipelineManager.ts index 0180da6c55..d6af2f2135 100644 --- a/js-packages/web-console/src/lib/services/pipelineManager.ts +++ b/js-packages/web-console/src/lib/services/pipelineManager.ts @@ -477,7 +477,7 @@ export const getInputConnectorStatus = ( ) => mapResponse( _getPipelineInputConnectorStatus({ - path: { pipeline_name, table_name: encodeURIComponent(table_name), connector_name: encodeURIComponent(connector_name) }, + path: { pipeline_name, table_name, connector_name }, ...options }), (v) => v @@ -491,7 +491,7 @@ export const getOutputConnectorStatus = ( ) => mapResponse( _getPipelineOutputConnectorStatus({ - path: { pipeline_name, view_name: encodeURIComponent(view_name), connector_name: encodeURIComponent(connector_name) }, + path: { pipeline_name, view_name, connector_name }, ...options }), (v) => v @@ -550,7 +550,7 @@ export const postApiKey = (name: string, options?: FetchOptions) => export const deleteApiKey = (name: string, options?: FetchOptions) => mapResponse( - _deleteApiKey({ path: { api_key_name: encodeURIComponent(name) }, ...options }), + _deleteApiKey({ path: { api_key_name: name }, ...options }), (v) => v, () => { throw new Error(`Failed to delete ${name} API key`) @@ -929,7 +929,7 @@ export const relationIngress = async ( options?: FetchOptions ) => { return httpInput({ - path: { pipeline_name: pipelineName, table_name: encodeURIComponent(relationName) }, + path: { pipeline_name: pipelineName, table_name: relationName }, parseAs: 'text', // Response is empty, so no need to parse it as JSON query: { format: 'json', array: true, update_format: 'insert_delete', force: !!force }, body: data as any, diff --git a/js-packages/web-console/vite.config.ts b/js-packages/web-console/vite.config.ts index 7ba2d08e59..d59b54a16c 100644 --- a/js-packages/web-console/vite.config.ts +++ b/js-packages/web-console/vite.config.ts @@ -7,7 +7,7 @@ import { playwright } from '@vitest/browser-playwright' import { type PluginOption } from 'vite' import devtoolsJson from 'vite-plugin-devtools-json' import virtual from 'vite-plugin-virtual' -import { defineConfig, type ViteUserConfigExport } from 'vitest/config' +import { defineConfig, type TestProjectInlineConfiguration, type ViteUserConfigExport } from 'vitest/config' import { felderaApiJsonSchemas } from './src/lib/functions/felderaApiJsonSchemas' import { svelteCssVirtualModuleFallback } from './src/lib/vite-plugins/svelte-css-virtual-module-fallback' @@ -61,6 +61,36 @@ const testOptimizeDepsInclude = [ 'virtua/svelte' ] +const browserTestProject = ({ + name, + include, + exclude +}: { + name: string + include: string[] + exclude?: string[] +}): TestProjectInlineConfiguration => ({ + extends: './vite.config.ts', + test: { + name, + browser: { + enabled: true, + provider: playwright({ contextOptions: {} }), + instances: [{ browser: 'chromium', headless: true }], + expect: { + toMatchScreenshot: { + resolveScreenshotPath({ testFileName, arg, ext }) { + return path.join(snapshotsDir, 'component', testFileName, `${arg}${ext}`) + } + } + } + }, + setupFiles: ['src/lib/vitest-browser-setup.ts'], + include, + exclude: exclude ?? ['src/lib/server/**'] + } +}) + // TODO: remove Prettier export default defineConfig(async () => { return { @@ -179,39 +209,31 @@ export default defineConfig(async () => { ) }, projects: [ + // Unit tests: *.spec.ts (run with `bun run test`) + browserTestProject({ name: 'client', include: ['src/**/*.svelte.spec.{js,ts}'] }), + { extends: './vite.config.ts', test: { - name: 'client', - browser: { - enabled: true, - provider: playwright({ - contextOptions: {} - }), - instances: [{ browser: 'chromium', headless: true }], - expect: { - toMatchScreenshot: { - resolveScreenshotPath({ testFileName, arg, ext }) { - return path.join(snapshotsDir, 'component', testFileName, `${arg}${ext}`) - } - } - } - }, - setupFiles: ['src/lib/vitest-browser-setup.ts'], - include: ['src/**/*.svelte.{test,spec}.{js,ts}'], - exclude: ['src/lib/server/**'] + name: 'server', + environment: 'node', + include: ['src/**/*.spec.{js,ts}'], + exclude: ['src/**/*.svelte.spec.{js,ts}'] } }, + // Integration tests: *.test.ts (require a Feldera instance, run with `bun run test-integration`) { extends: './vite.config.ts', test: { - name: 'server', + name: 'integration', environment: 'node', - include: ['src/**/*.{test,spec}.{js,ts}'], - exclude: ['src/**/*.svelte.{test,spec}.{js,ts}'] + include: ['src/**/*.test.{js,ts}'], + exclude: ['src/**/*.svelte.test.{js,ts}'] } - } + }, + + browserTestProject({ name: 'integration-client', include: ['src/**/*.svelte.test.{js,ts}'] }) ] } } satisfies ViteUserConfigExport From 539f122fc552abba5ac59566a6a3f6a9a7c0ccc7 Mon Sep 17 00:00:00 2001 From: Karakatiza666 Date: Sat, 4 Apr 2026 18:25:24 +0000 Subject: [PATCH 4/4] [web-console] Integration tests for retrieving connector errors Refactor existing UI tests to re-use pipeline state management procedures Signed-off-by: Karakatiza666 --- .../editor/performance/ConnectorErrors.svelte | 6 +- .../src/lib/services/pipelineManager.test.ts | 116 ++++-------- .../src/lib/services/testPipelineHelpers.ts | 179 ++++++++++++++++++ .../src/lib/vitest-integration-setup.ts | 13 ++ .../tests/clearStorageDialog.e2e.ts | 57 ++---- js-packages/web-console/tests/global-setup.ts | 45 +---- js-packages/web-console/vite.config.ts | 12 +- 7 files changed, 259 insertions(+), 169 deletions(-) create mode 100644 js-packages/web-console/src/lib/services/testPipelineHelpers.ts create mode 100644 js-packages/web-console/src/lib/vitest-integration-setup.ts diff --git a/js-packages/web-console/src/lib/components/pipelines/editor/performance/ConnectorErrors.svelte b/js-packages/web-console/src/lib/components/pipelines/editor/performance/ConnectorErrors.svelte index 1b942d3ad0..5e1ac9c0b0 100644 --- a/js-packages/web-console/src/lib/components/pipelines/editor/performance/ConnectorErrors.svelte +++ b/js-packages/web-console/src/lib/components/pipelines/editor/performance/ConnectorErrors.svelte @@ -5,6 +5,7 @@ import InlineDropdown from '$lib/components/common/InlineDropdown.svelte' import { Tooltip } from '$lib/components/common/Tooltip.svelte' import InlineDrawer from '$lib/components/layout/InlineDrawer.svelte' + import { getCaseDependentName } from '$lib/functions/felderaRelation' import { formatDateTime } from '$lib/functions/format' import { type ConnectorError, @@ -12,7 +13,6 @@ type OutputEndpointStatus } from '$lib/services/manager' import { getInputConnectorStatus, getOutputConnectorStatus } from '$lib/services/pipelineManager' - import { getCaseDependentName } from '$lib/functions/felderaRelation' export type ConnectorErrorFilter = 'all' | 'parse' | 'transport' | 'encode' @@ -62,7 +62,9 @@ tagsFilter = filter }) - const strippedConnectorName = $derived(connectorName.slice(getCaseDependentName(relationName).name.length + 1)) + const strippedConnectorName = $derived( + connectorName.slice(getCaseDependentName(relationName).name.length + 1) + ) $effect(() => { pipelineName diff --git a/js-packages/web-console/src/lib/services/pipelineManager.test.ts b/js-packages/web-console/src/lib/services/pipelineManager.test.ts index 8fc7391920..1df0852047 100644 --- a/js-packages/web-console/src/lib/services/pipelineManager.test.ts +++ b/js-packages/web-console/src/lib/services/pipelineManager.test.ts @@ -2,11 +2,20 @@ * Integration tests for pipelineManager.ts that require a running Feldera instance. * Run via the 'integration' vitest project: * - * FELDERA_API_URL=http://localhost:8080 bun run test-integration + * bun run test-integration */ import { afterAll, beforeAll, describe, expect, it } from 'vitest' - -const BASE_URL = process.env.FELDERA_API_URL ?? 'http://localhost:8080' +import { + getInputConnectorStatus, + getOutputConnectorStatus, + putPipeline +} from '$lib/services/pipelineManager' +import { + cleanupPipeline, + configureTestClient, + startPipelineAndWaitForRunning, + waitForCompilation +} from '$lib/services/testPipelineHelpers' // Pipeline name is URL-safe by convention, but table/view/connector names are not. const PIPELINE_NAME = 'test-special-chars-connector-status' @@ -14,24 +23,15 @@ const PIPELINE_NAME = 'test-special-chars-connector-status' // Names with dots and URL-unsafe characters const TABLE_NAME = 'my.input.table' const VIEW_NAME = 'my.output.view' -const INPUT_CONNECTOR_NAME = 'http.input/special&name' -const OUTPUT_CONNECTOR_NAME = 'http.output/special&name' - -const api = (path: string, init?: RequestInit) => - fetch(`${BASE_URL}${path}`, { - ...init, - headers: { 'Content-Type': 'application/json', ...init?.headers } - }) +const INPUT_CONNECTOR_NAME = 'http-input_special-name' +const OUTPUT_CONNECTOR_NAME = 'http-output_special-name' describe('pipelineManager connector status with special characters', () => { beforeAll(async () => { + configureTestClient() + // Clean up any leftover pipeline from a previous run - const del = await api(`/v0/pipelines/${PIPELINE_NAME}`, { method: 'DELETE' }) - if (del.ok || del.status === 404) { - // ok - } else { - throw new Error(`Cleanup failed: ${del.status} ${await del.text()}`) - } + await cleanupPipeline(PIPELINE_NAME) // SQL program with quoted identifiers containing dots const programCode = ` @@ -39,87 +39,47 @@ describe('pipelineManager connector status with special characters', () => { WITH ( 'connectors' = '[{ "name": "${INPUT_CONNECTOR_NAME}", - "transport": { "name": "http_input" }, + "transport": { "name": "url_input", "config": { "path": "https://feldera.com/test-data.json" } }, "format": { "name": "json", "config": { "update_format": "raw" } } }]' ); - CREATE VIEW "${VIEW_NAME}" AS SELECT * FROM "${TABLE_NAME}" + CREATE VIEW "${VIEW_NAME}" WITH ( 'connectors' = '[{ "name": "${OUTPUT_CONNECTOR_NAME}", - "transport": { "name": "http_output" }, - "format": { "name": "json", "config": { "update_format": "raw" } } + "transport": { "name": "file_output", "config": { "path": "/tmp/feldera-test-output.json" } }, + "format": { "name": "json" } }]' - ); + ) + AS SELECT * FROM "${TABLE_NAME}"; ` // Create the pipeline - const put = await api(`/v0/pipelines/${PIPELINE_NAME}`, { - method: 'PUT', - body: JSON.stringify({ - name: PIPELINE_NAME, - description: 'Integration test for special character handling', - program_code: programCode, - runtime_config: {} - }) + await putPipeline(PIPELINE_NAME, { + name: PIPELINE_NAME, + description: 'Integration test for special character handling', + program_code: programCode, + runtime_config: {} }) - expect(put.ok, `PUT pipeline: ${put.status} ${await put.clone().text()}`).toBe(true) - - // Wait for compilation - for (let i = 0; i < 120; i++) { - const res = await api(`/v0/pipelines/${PIPELINE_NAME}`) - const info = await res.json() - if (info.program_status === 'Success') break - if (info.program_status?.SqlError || info.program_status?.RustError || info.program_status?.SystemError) { - throw new Error(`Compilation failed: ${JSON.stringify(info.program_status)}`) - } - await new Promise((r) => setTimeout(r, 1000)) - } - - // Start the pipeline - const start = await api(`/v0/pipelines/${PIPELINE_NAME}/start`, { method: 'POST' }) - expect(start.ok, `POST start: ${start.status} ${await start.clone().text()}`).toBe(true) - // Wait for pipeline to be running - for (let i = 0; i < 60; i++) { - const res = await api(`/v0/pipelines/${PIPELINE_NAME}`) - const info = await res.json() - if (info.deployment_status === 'Running') break - if (info.deployment_error) { - throw new Error(`Deployment failed: ${JSON.stringify(info.deployment_error)}`) - } - await new Promise((r) => setTimeout(r, 1000)) - } + await waitForCompilation(PIPELINE_NAME, 120_000) + await startPipelineAndWaitForRunning(PIPELINE_NAME, 60_000) }, 180_000) afterAll(async () => { - // Shutdown and delete - await api(`/v0/pipelines/${PIPELINE_NAME}/shutdown`, { method: 'POST' }) - // Wait for shutdown - for (let i = 0; i < 30; i++) { - const res = await api(`/v0/pipelines/${PIPELINE_NAME}`) - const info = await res.json() - if (info.deployment_status === 'Shutdown') break - await new Promise((r) => setTimeout(r, 1000)) - } - await api(`/v0/pipelines/${PIPELINE_NAME}`, { method: 'DELETE' }) + await cleanupPipeline(PIPELINE_NAME) }, 60_000) it('getInputConnectorStatus succeeds with dot and URL-unsafe characters in table and connector name', async () => { - const url = `/v0/pipelines/${PIPELINE_NAME}/tables/${encodeURIComponent(TABLE_NAME)}/connectors/${encodeURIComponent(INPUT_CONNECTOR_NAME)}/status` - const res = await api(url) - expect(res.ok, `GET input connector status: ${res.status} ${await res.clone().text()}`).toBe(true) - const body = await res.json() - expect(body).toHaveProperty('num_parse_errors') - expect(body).toHaveProperty('num_transport_errors') + const body = await getInputConnectorStatus(PIPELINE_NAME, TABLE_NAME, INPUT_CONNECTOR_NAME) + console.log('body', JSON.stringify(body)) + expect(body).toHaveProperty(['metrics', 'num_parse_errors']) + expect(body).toHaveProperty(['metrics', 'num_transport_errors']) }) it('getOutputConnectorStatus succeeds with dot and URL-unsafe characters in view and connector name', async () => { - const url = `/v0/pipelines/${PIPELINE_NAME}/views/${encodeURIComponent(VIEW_NAME)}/connectors/${encodeURIComponent(OUTPUT_CONNECTOR_NAME)}/status` - const res = await api(url) - expect(res.ok, `GET output connector status: ${res.status} ${await res.clone().text()}`).toBe(true) - const body = await res.json() - expect(body).toHaveProperty('num_encode_errors') - expect(body).toHaveProperty('num_transport_errors') + const body = await getOutputConnectorStatus(PIPELINE_NAME, VIEW_NAME, OUTPUT_CONNECTOR_NAME) + expect(body).toHaveProperty(['metrics', 'num_encode_errors']) + expect(body).toHaveProperty(['metrics', 'num_transport_errors']) }) }) diff --git a/js-packages/web-console/src/lib/services/testPipelineHelpers.ts b/js-packages/web-console/src/lib/services/testPipelineHelpers.ts new file mode 100644 index 0000000000..e8d5febdff --- /dev/null +++ b/js-packages/web-console/src/lib/services/testPipelineHelpers.ts @@ -0,0 +1,179 @@ +/** + * Shared test helpers for pipeline lifecycle management. + * + * Used by both Playwright e2e tests and Vitest integration tests to avoid + * duplicating pipeline create/compile/start/stop/delete boilerplate. + */ + +import { client } from '$lib/services/manager/client.gen' +import { + deletePipeline, + type ExtendedPipeline, + getExtendedPipeline, + getPipelineStatus, + postPipelineAction, + programStatusOf, + putPipeline +} from '$lib/services/pipelineManager' + +export { type ExtendedPipeline } + +/** + * Configure the API client base URL for tests. + * + * Reads from `FELDERA_TEST_API_ORIGIN` (shared) or `PLAYWRIGHT_API_ORIGIN` + * (Playwright-specific), falling back to `http://localhost:8080`. + */ +export function configureTestClient() { + const origin = ( + process.env.FELDERA_TEST_API_ORIGIN ?? + process.env.PLAYWRIGHT_API_ORIGIN ?? + 'http://localhost:8080' + ).replace(/\/$/, '') + client.setConfig({ baseUrl: origin }) + return origin +} + +/** + * Poll `getExtendedPipeline` until `predicate` returns true or `timeoutMs` elapses. + */ +export async function waitForExtendedPipeline( + pipelineName: string, + predicate: (p: ExtendedPipeline) => boolean, + timeoutMs = 120_000 +): Promise { + const deadline = Date.now() + timeoutMs + while (Date.now() < deadline) { + const pipeline = await getExtendedPipeline(pipelineName) + if (predicate(pipeline)) return pipeline + await new Promise((r) => setTimeout(r, 2000)) + } + const pipeline = await getExtendedPipeline(pipelineName) + throw new Error( + `waitForExtendedPipeline timed out for "${pipelineName}". ` + + `deploy=${pipeline.deploymentStatus} status=${JSON.stringify(pipeline.status)} storage=${pipeline.storageStatus}` + ) +} + +/** + * Poll `getPipelineStatus` until `predicate` returns true or `timeoutMs` elapses. + */ +export async function waitForPipelineStatus( + pipelineName: string, + predicate: (status: string) => boolean, + timeoutMs = 120_000 +) { + const deadline = Date.now() + timeoutMs + while (Date.now() < deadline) { + const { status } = await getPipelineStatus(pipelineName) + if (predicate(status as string)) return status + await new Promise((r) => setTimeout(r, 1000)) + } + const { status } = await getPipelineStatus(pipelineName) + throw new Error( + `waitForPipelineStatus timed out for "${pipelineName}". status=${JSON.stringify(status)}` + ) +} + +/** + * Wait for a pipeline's SQL/Rust compilation to finish successfully. + * Throws on compilation errors. + */ +export async function waitForCompilation(pipelineName: string, timeoutMs = 600_000) { + const deadline = Date.now() + timeoutMs + while (Date.now() < deadline) { + const { status } = await getPipelineStatus(pipelineName) + const programStatus = programStatusOf(status) + if (programStatus === 'Success') return + if ( + programStatus === 'SqlError' || + programStatus === 'RustError' || + programStatus === 'SystemError' + ) { + throw new Error(`Compilation failed for "${pipelineName}": ${programStatus}`) + } + await new Promise((r) => setTimeout(r, 2000)) + } + throw new Error(`Compilation timed out for "${pipelineName}"`) +} + +/** + * Start a pipeline and wait until it reaches the Running state. + * Automatically handles the AwaitingApproval → approve_changes transition. + */ +export async function startPipelineAndWaitForRunning(pipelineName: string, timeoutMs = 60_000) { + await postPipelineAction(pipelineName, 'start') + const deadline = Date.now() + timeoutMs + while (Date.now() < deadline) { + const { status } = await getPipelineStatus(pipelineName) + if (status === 'Running') return + if (status === 'AwaitingApproval') { + await postPipelineAction(pipelineName, 'approve_changes') + } + await new Promise((r) => setTimeout(r, 1000)) + } + throw new Error(`Pipeline "${pipelineName}" did not reach Running within ${timeoutMs}ms`) +} + +/** + * Stop a pipeline and wait until it reaches the Stopped state. + */ +export async function stopPipelineAndWaitForStopped(pipelineName: string, timeoutMs = 30_000) { + try { + await postPipelineAction(pipelineName, 'stop') + } catch { + // Ignore if already stopped + } + const deadline = Date.now() + timeoutMs + while (Date.now() < deadline) { + const { status } = await getPipelineStatus(pipelineName) + if (status === 'Stopped') return + await new Promise((r) => setTimeout(r, 1000)) + } + throw new Error(`Pipeline "${pipelineName}" did not reach Stopped within ${timeoutMs}ms`) +} + +/** + * Fully clean up a pipeline: stop → wait → delete. Ignores errors at every step. + */ +export async function cleanupPipeline(pipelineName: string) { + try { + await stopPipelineAndWaitForStopped(pipelineName) + } catch {} + try { + await deletePipeline(pipelineName) + } catch {} +} + +const WARMUP_PIPELINE = '__test_warmup__' + +/** + * Warm the Rust compilation cache by creating, compiling, and deleting a + * minimal pipeline. The first compilation in a fresh Feldera image compiles + * many Rust crates from scratch, so this prevents individual tests from + * timing out. + */ +export async function warmCompilationCache() { + console.log('Warming Rust compilation cache…') + + // Clean up any leftover warmup pipeline + try { + await deletePipeline(WARMUP_PIPELINE) + } catch {} + + try { + await putPipeline(WARMUP_PIPELINE, { + name: WARMUP_PIPELINE, + program_code: 'CREATE TABLE _warmup (id INT);', + program_config: { profile: 'unoptimized' } + }) + } catch (e) { + console.error('warmCompilationCache: putPipeline failed:', e) + throw e + } + + await waitForCompilation(WARMUP_PIPELINE) + console.log('Compilation cache is warm.') + + await deletePipeline(WARMUP_PIPELINE) +} diff --git a/js-packages/web-console/src/lib/vitest-integration-setup.ts b/js-packages/web-console/src/lib/vitest-integration-setup.ts new file mode 100644 index 0000000000..e0a1611f44 --- /dev/null +++ b/js-packages/web-console/src/lib/vitest-integration-setup.ts @@ -0,0 +1,13 @@ +/** + * Vitest globalSetup for integration test projects. + * + * Configures the API client and warms the Rust compilation cache, + * mirroring what tests/global-setup.ts does for Playwright. + */ + +import { configureTestClient, warmCompilationCache } from '$lib/services/testPipelineHelpers' + +export async function setup() { + configureTestClient() + await warmCompilationCache() +} diff --git a/js-packages/web-console/tests/clearStorageDialog.e2e.ts b/js-packages/web-console/tests/clearStorageDialog.e2e.ts index 8438b4d24c..77ef2f5099 100644 --- a/js-packages/web-console/tests/clearStorageDialog.e2e.ts +++ b/js-packages/web-console/tests/clearStorageDialog.e2e.ts @@ -1,31 +1,15 @@ import { expect, test } from '@playwright/test' -import { client } from '$lib/services/manager/client.gen' +import { getExtendedPipeline, postPipelineAction, putPipeline } from '$lib/services/pipelineManager' import { - deletePipeline, - type ExtendedPipeline, - getExtendedPipeline, - postPipelineAction, - putPipeline -} from '$lib/services/pipelineManager' + cleanupPipeline, + configureTestClient, + waitForExtendedPipeline +} from '$lib/services/testPipelineHelpers' -const API_ORIGIN = (process.env.PLAYWRIGHT_API_ORIGIN ?? 'http://localhost:8080').replace(/\/$/, '') -client.setConfig({ baseUrl: API_ORIGIN }) +configureTestClient() const PIPELINE_NAME = `test-clear-storage-${Date.now()}` -async function waitForStatus(predicate: (p: ExtendedPipeline) => boolean, timeoutMs = 120_000) { - const start = Date.now() - while (Date.now() - start < timeoutMs) { - const pipeline = await getExtendedPipeline(PIPELINE_NAME) - if (predicate(pipeline)) return pipeline - await new Promise((r) => setTimeout(r, 1000)) - } - const pipeline = await getExtendedPipeline(PIPELINE_NAME) - throw new Error( - `Timed out. deploy=${pipeline.deploymentStatus} status=${JSON.stringify(pipeline.status)} storage=${pipeline.storageStatus}` - ) -} - async function createPipelineWithStorage() { await putPipeline(PIPELINE_NAME, { name: PIPELINE_NAME, @@ -37,32 +21,15 @@ async function createPipelineWithStorage() { }, program_config: { profile: 'unoptimized' } }) - await waitForStatus((p) => p.status === 'Stopped') + await waitForExtendedPipeline(PIPELINE_NAME, (p) => p.status === 'Stopped') } async function startAndStopToCreateStorage() { await postPipelineAction(PIPELINE_NAME, 'start') - await waitForStatus((p) => p.deploymentStatus === 'Running') + await waitForExtendedPipeline(PIPELINE_NAME, (p) => p.deploymentStatus === 'Running') await postPipelineAction(PIPELINE_NAME, 'kill') - await waitForStatus((p) => p.deploymentStatus === 'Stopped') - await waitForStatus((p) => p.storageStatus !== 'Cleared', 10_000) -} - -async function cleanupPipeline() { - try { - const p = await getExtendedPipeline(PIPELINE_NAME) - if (p.deploymentStatus !== 'Stopped') { - await postPipelineAction(PIPELINE_NAME, 'kill') - await waitForStatus((p) => p.deploymentStatus === 'Stopped', 30_000) - } - if (p.storageStatus !== 'Cleared') { - await postPipelineAction(PIPELINE_NAME, 'clear') - await waitForStatus((p) => p.storageStatus === 'Cleared', 30_000) - } - } catch {} - try { - await deletePipeline(PIPELINE_NAME) - } catch {} + await waitForExtendedPipeline(PIPELINE_NAME, (p) => p.deploymentStatus === 'Stopped') + await waitForExtendedPipeline(PIPELINE_NAME, (p) => p.storageStatus !== 'Cleared', 10_000) } /** Intercept the next PATCH to return a storage-not-cleared error, then open @@ -108,14 +75,14 @@ test.describe('Clear storage dialog', () => { test.beforeAll(async ({}, testInfo) => { testInfo.setTimeout(120_000) - await cleanupPipeline() + await cleanupPipeline(PIPELINE_NAME) await createPipelineWithStorage() await startAndStopToCreateStorage() }) test.afterAll(async ({}, testInfo) => { testInfo.setTimeout(60_000) - await cleanupPipeline() + await cleanupPipeline(PIPELINE_NAME) }) test('changing config triggers clear storage dialog and completes the flow', async ({ page }) => { diff --git a/js-packages/web-console/tests/global-setup.ts b/js-packages/web-console/tests/global-setup.ts index fc1a13f018..a5206db341 100644 --- a/js-packages/web-console/tests/global-setup.ts +++ b/js-packages/web-console/tests/global-setup.ts @@ -6,48 +6,9 @@ * the test suite keeps individual test timeouts tight. */ -import { client } from '$lib/services/manager/client.gen' -import { deletePipeline, getExtendedPipeline, putPipeline } from '$lib/services/pipelineManager' - -const API_ORIGIN = (process.env.PLAYWRIGHT_API_ORIGIN ?? 'http://localhost:8080').replace(/\/$/, '') -client.setConfig({ baseUrl: API_ORIGIN }) - -const WARMUP_PIPELINE = '__e2e_warmup__' +import { configureTestClient, warmCompilationCache } from '$lib/services/testPipelineHelpers' export default async function globalSetup() { - console.log('Warming Rust compilation cache…') - - // Clean up any leftover warmup pipeline - try { - await deletePipeline(WARMUP_PIPELINE) - } catch {} - - // Create a minimal pipeline - try { - await putPipeline(WARMUP_PIPELINE, { - name: WARMUP_PIPELINE, - program_code: 'CREATE TABLE _warmup (id INT);', - program_config: { profile: 'unoptimized' } - }) - } catch (e) { - console.error(`putPipeline failed (API_ORIGIN=${API_ORIGIN}):`, e) - throw e - } - - // Wait for compilation (up to 10 minutes for a cold cache) - const deadline = Date.now() + 600_000 - while (Date.now() < deadline) { - const pipeline = await getExtendedPipeline(WARMUP_PIPELINE) - if (pipeline.status === 'Stopped') { - console.log('Compilation cache is warm.') - break - } - const status = pipeline.status - if (status === 'SqlError' || status === 'RustError' || status === 'SystemError') { - throw new Error(`Warmup compilation failed: ${status}`) - } - await new Promise((r) => setTimeout(r, 2000)) - } - - await deletePipeline(WARMUP_PIPELINE) + configureTestClient() + await warmCompilationCache() } diff --git a/js-packages/web-console/vite.config.ts b/js-packages/web-console/vite.config.ts index d59b54a16c..24ea9b9e42 100644 --- a/js-packages/web-console/vite.config.ts +++ b/js-packages/web-console/vite.config.ts @@ -7,7 +7,11 @@ import { playwright } from '@vitest/browser-playwright' import { type PluginOption } from 'vite' import devtoolsJson from 'vite-plugin-devtools-json' import virtual from 'vite-plugin-virtual' -import { defineConfig, type TestProjectInlineConfiguration, type ViteUserConfigExport } from 'vitest/config' +import { + defineConfig, + type TestProjectInlineConfiguration, + type ViteUserConfigExport +} from 'vitest/config' import { felderaApiJsonSchemas } from './src/lib/functions/felderaApiJsonSchemas' import { svelteCssVirtualModuleFallback } from './src/lib/vite-plugins/svelte-css-virtual-module-fallback' @@ -228,12 +232,16 @@ export default defineConfig(async () => { test: { name: 'integration', environment: 'node', + globalSetup: ['src/lib/vitest-integration-setup.ts'], include: ['src/**/*.test.{js,ts}'], exclude: ['src/**/*.svelte.test.{js,ts}'] } }, - browserTestProject({ name: 'integration-client', include: ['src/**/*.svelte.test.{js,ts}'] }) + browserTestProject({ + name: 'integration-client', + include: ['src/**/*.svelte.test.{js,ts}'] + }) ] } } satisfies ViteUserConfigExport