diff --git a/.gitignore b/.gitignore index 15f06684a..35c352250 100644 --- a/.gitignore +++ b/.gitignore @@ -202,3 +202,6 @@ local-run.yaml .sisyphus/ # Per-developer feature design overrides (see docs/contributing/feature-design.config) .feature-design.config.local + +# Generated at e2e test time by the unified-mode --migrate-config step +tests/e2e/configuration/unified-mode/**/lightspeed-stack-unified-migrated.yaml diff --git a/tests/e2e/configuration/library-mode/lightspeed-stack-legacy.yaml b/tests/e2e/configuration/library-mode/lightspeed-stack-legacy.yaml new file mode 100644 index 000000000..993f0d812 --- /dev/null +++ b/tests/e2e/configuration/library-mode/lightspeed-stack-legacy.yaml @@ -0,0 +1,46 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Legacy two-file shape (R2 deprecation window): external run.yaml consumed + # via library_client_config_path; no unified synthesis input. Kept as a + # dedicated fixture because the standard library-mode baseline migrated to + # unified mode in LCORE-2342, which silently removed legacy boot coverage. + use_as_library_client: true + library_client_config_path: run.yaml +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini +byok_rag: + - rag_id: e2e-test-docs + rag_type: inline::faiss + embedding_model: sentence-transformers/all-mpnet-base-v2 + embedding_dimension: 768 + vector_db_id: ${env.FAISS_VECTOR_STORE_ID} + db_path: ${env.KV_RAG_PATH:=~/.llama/storage/rag/kv_store.db} + score_multiplier: 1.0 + +rag: + tool: + - e2e-test-docs + +shields: + - name: pii-redaction + provider_id: redaction + config: + rules: + - pattern: '\d+' + replacement: '[NUM]' + diff --git a/tests/e2e/configuration/unified-mode/README.md b/tests/e2e/configuration/unified-mode/README.md new file mode 100644 index 000000000..79eaaabe0 --- /dev/null +++ b/tests/e2e/configuration/unified-mode/README.md @@ -0,0 +1,24 @@ +# Unified-mode e2e configuration fixtures + +Fixtures for the five `unified-mode-*.feature` files (LCORE-2341/LCORE-2343). +Same layout as the parent directory: `library-mode/` and `server-mode/` +variants differing only in the `llama_stack` block; the harness resolves +`//` via the standard `configure_service` logic. + +All profile-based fixtures reference `run.yaml` — the repo-root copy the CI +harness materializes from `tests/e2e/configs/run-.yaml` — so they stay +provider-agnostic across the providers matrix. + +| Fixture | Purpose | +|---|---| +| `lightspeed-stack-unified-providers.yaml` | Minimal unified config driven only by top-level `inference.providers` (default baseline, R1/S5). openai-specific — used by `@openai-only` scenarios. | +| `lightspeed-stack-unified-config-only.yaml` | Unified config driven only by `llama_stack.config` (`profile: run.yaml`, R1). | +| `lightspeed-stack-unified-relative-profile.yaml` | Same shape as config-only; exists to pin R8 (relative `profile:` resolves against the config file's directory) as a distinct intent. | +| `lightspeed-stack-unified-absolute-profile.yaml` | `profile:` as a container-absolute path (differs per mode subdir). | +| `lightspeed-stack-unified-native-override-scalar.yaml` | `native_override` replaces an overlapping scalar key (R5). Synthesis-only; never booted. | +| `lightspeed-stack-unified-native-override-list.yaml` | `native_override` replaces an overlapping list wholesale (R5). Synthesis-only; never booted. | +| `lightspeed-stack-invalid-providers-and-legacy.yaml` | INVALID: `inference.providers` + `library_client_config_path` (mutual exclusion, R3). Validation-only. | +| `lightspeed-stack-invalid-config-and-legacy.yaml` | INVALID: `llama_stack.config` + `library_client_config_path` (R3). Validation-only. | +| `lightspeed-stack-invalid-version-legacy-unified-body.yaml` | INVALID: `config_format_version: legacy` on a unified-shaped body (R11, LCORE-2872). Validation-only. | +| `lightspeed-stack-legacy-for-migration.yaml` | Legacy half of "the legacy migration fixture pair"; paired with the repo-root `run.yaml`. Deliberately free of enrichment sections so migrate→synthesize round-trips losslessly (see LCORE-3370). | +| `lightspeed-stack-unified-migrated.yaml` | Generated at test time by the `--migrate-config` step; gitignored and cleaned up after each scenario. | diff --git a/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-invalid-config-and-legacy.yaml b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-invalid-config-and-legacy.yaml new file mode 100644 index 000000000..bdb8af911 --- /dev/null +++ b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-invalid-config-and-legacy.yaml @@ -0,0 +1,25 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Library mode - embeds the stack in-process + use_as_library_client: true + config: + profile: run.yaml + # INVALID: config block plus the legacy path (mutual exclusion, R3) + library_client_config_path: run.yaml +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini diff --git a/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-invalid-providers-and-legacy.yaml b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-invalid-providers-and-legacy.yaml new file mode 100644 index 000000000..124fd453c --- /dev/null +++ b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-invalid-providers-and-legacy.yaml @@ -0,0 +1,31 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Library mode - embeds the stack in-process + use_as_library_client: true + # INVALID: synthesis input plus the legacy path (mutual exclusion, R3) + library_client_config_path: run.yaml +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini + # Unified synthesis input (Decision S5): the high-level provider entry + # replaces the default baseline's openai provider by id at synthesis time. + providers: + - type: openai + id: openai + api_key_env: OPENAI_API_KEY + allowed_models: + - ${env.E2E_OPENAI_MODEL:=gpt-4o-mini} diff --git a/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-invalid-version-legacy-unified-body.yaml b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-invalid-version-legacy-unified-body.yaml new file mode 100644 index 000000000..9ae7b389d --- /dev/null +++ b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-invalid-version-legacy-unified-body.yaml @@ -0,0 +1,31 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Library mode - embeds the stack in-process + use_as_library_client: true +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini + # Unified synthesis input (Decision S5): the high-level provider entry + # replaces the default baseline's openai provider by id at synthesis time. + providers: + - type: openai + id: openai + api_key_env: OPENAI_API_KEY + allowed_models: + - ${env.E2E_OPENAI_MODEL:=gpt-4o-mini} +# INVALID: explicit legacy marker on a unified-shaped body (R11, LCORE-2872) +config_format_version: legacy diff --git a/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-legacy-for-migration.yaml b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-legacy-for-migration.yaml new file mode 100644 index 000000000..6393142b5 --- /dev/null +++ b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-legacy-for-migration.yaml @@ -0,0 +1,23 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Library mode - embeds the stack in-process + use_as_library_client: true + # Legacy two-file shape: external run.yaml, no synthesis input + library_client_config_path: run.yaml +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini diff --git a/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-absolute-profile.yaml b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-absolute-profile.yaml new file mode 100644 index 000000000..089fb0afb --- /dev/null +++ b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-absolute-profile.yaml @@ -0,0 +1,24 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Library mode - embeds the stack in-process + use_as_library_client: true + config: + # Absolute path as mounted in the library-mode container + profile: /app-root/run.yaml +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini diff --git a/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-config-only.yaml b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-config-only.yaml new file mode 100644 index 000000000..b3df26828 --- /dev/null +++ b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-config-only.yaml @@ -0,0 +1,24 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Library mode - embeds the stack in-process + use_as_library_client: true + config: + # Synthesis baseline: the CI-materialized run.yaml (provider-agnostic) + profile: run.yaml +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini diff --git a/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-native-override-list.yaml b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-native-override-list.yaml new file mode 100644 index 000000000..403cc7a00 --- /dev/null +++ b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-native-override-list.yaml @@ -0,0 +1,29 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Library mode - embeds the stack in-process + use_as_library_client: true + config: + profile: run.yaml + # R5: lists replace wholesale - the synthesized apis must equal exactly + # this list, not a merge with the baseline's (never booted - synthesis only) + native_override: + apis: + - inference + - tool_runtime +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini diff --git a/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-native-override-scalar.yaml b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-native-override-scalar.yaml new file mode 100644 index 000000000..02f67f028 --- /dev/null +++ b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-native-override-scalar.yaml @@ -0,0 +1,28 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Library mode - embeds the stack in-process + use_as_library_client: true + config: + profile: run.yaml + # R5: the raw escape hatch wins; this scalar replaces the baseline's + # safety.excluded_categories value wholesale (never booted - synthesis only) + native_override: + safety: + excluded_categories: unified-override-marker +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini diff --git a/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-providers.yaml b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-providers.yaml new file mode 100644 index 000000000..731c39b5d --- /dev/null +++ b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-providers.yaml @@ -0,0 +1,29 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Library mode - embeds the stack in-process + use_as_library_client: true +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini + # Unified synthesis input (Decision S5): the high-level provider entry + # replaces the default baseline's openai provider by id at synthesis time. + providers: + - type: openai + id: openai + api_key_env: OPENAI_API_KEY + allowed_models: + - ${env.E2E_OPENAI_MODEL:=gpt-4o-mini} diff --git a/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-relative-profile.yaml b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-relative-profile.yaml new file mode 100644 index 000000000..228b2d40b --- /dev/null +++ b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-relative-profile.yaml @@ -0,0 +1,24 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Library mode - embeds the stack in-process + use_as_library_client: true + config: + # R8: relative profile resolves against this file's loaded location + profile: run.yaml +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini diff --git a/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-invalid-config-and-legacy.yaml b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-invalid-config-and-legacy.yaml new file mode 100644 index 000000000..34dbfb06f --- /dev/null +++ b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-invalid-config-and-legacy.yaml @@ -0,0 +1,27 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Server mode - connects to the separate llama-stack service + use_as_library_client: false + url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + api_key: xyzzy + config: + profile: run.yaml + # INVALID: config block plus the legacy path (mutual exclusion, R3) + library_client_config_path: run.yaml +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini diff --git a/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-invalid-providers-and-legacy.yaml b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-invalid-providers-and-legacy.yaml new file mode 100644 index 000000000..6cbd5f50a --- /dev/null +++ b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-invalid-providers-and-legacy.yaml @@ -0,0 +1,33 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Server mode - connects to the separate llama-stack service + use_as_library_client: false + url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + api_key: xyzzy + # INVALID: synthesis input plus the legacy path (mutual exclusion, R3) + library_client_config_path: run.yaml +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini + # Unified synthesis input (Decision S5): the high-level provider entry + # replaces the default baseline's openai provider by id at synthesis time. + providers: + - type: openai + id: openai + api_key_env: OPENAI_API_KEY + allowed_models: + - ${env.E2E_OPENAI_MODEL:=gpt-4o-mini} diff --git a/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-invalid-version-legacy-unified-body.yaml b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-invalid-version-legacy-unified-body.yaml new file mode 100644 index 000000000..f3f9b9cdc --- /dev/null +++ b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-invalid-version-legacy-unified-body.yaml @@ -0,0 +1,33 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Server mode - connects to the separate llama-stack service + use_as_library_client: false + url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + api_key: xyzzy +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini + # Unified synthesis input (Decision S5): the high-level provider entry + # replaces the default baseline's openai provider by id at synthesis time. + providers: + - type: openai + id: openai + api_key_env: OPENAI_API_KEY + allowed_models: + - ${env.E2E_OPENAI_MODEL:=gpt-4o-mini} +# INVALID: explicit legacy marker on a unified-shaped body (R11, LCORE-2872) +config_format_version: legacy diff --git a/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-legacy-for-migration.yaml b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-legacy-for-migration.yaml new file mode 100644 index 000000000..76b2ac36d --- /dev/null +++ b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-legacy-for-migration.yaml @@ -0,0 +1,23 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Server mode - connects to the separate llama-stack service + use_as_library_client: false + url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + api_key: xyzzy +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini diff --git a/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-absolute-profile.yaml b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-absolute-profile.yaml new file mode 100644 index 000000000..1a1c4d8a1 --- /dev/null +++ b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-absolute-profile.yaml @@ -0,0 +1,26 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Server mode - connects to the separate llama-stack service + use_as_library_client: false + url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + api_key: xyzzy + config: + # Absolute path as mounted in the llama-stack container + profile: /opt/app-root/run.yaml +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini diff --git a/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-config-only.yaml b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-config-only.yaml new file mode 100644 index 000000000..3881fdf4e --- /dev/null +++ b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-config-only.yaml @@ -0,0 +1,26 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Server mode - connects to the separate llama-stack service + use_as_library_client: false + url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + api_key: xyzzy + config: + # Synthesis baseline: the CI-materialized run.yaml (provider-agnostic) + profile: run.yaml +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini diff --git a/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-native-override-list.yaml b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-native-override-list.yaml new file mode 100644 index 000000000..7454bab43 --- /dev/null +++ b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-native-override-list.yaml @@ -0,0 +1,31 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Server mode - connects to the separate llama-stack service + use_as_library_client: false + url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + api_key: xyzzy + config: + profile: run.yaml + # R5: lists replace wholesale - the synthesized apis must equal exactly + # this list, not a merge with the baseline's (never booted - synthesis only) + native_override: + apis: + - inference + - tool_runtime +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini diff --git a/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-native-override-scalar.yaml b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-native-override-scalar.yaml new file mode 100644 index 000000000..3451fb526 --- /dev/null +++ b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-native-override-scalar.yaml @@ -0,0 +1,30 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Server mode - connects to the separate llama-stack service + use_as_library_client: false + url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + api_key: xyzzy + config: + profile: run.yaml + # R5: the raw escape hatch wins; this scalar replaces the baseline's + # safety.excluded_categories value wholesale (never booted - synthesis only) + native_override: + safety: + excluded_categories: unified-override-marker +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini diff --git a/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-providers.yaml b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-providers.yaml new file mode 100644 index 000000000..4ca585947 --- /dev/null +++ b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-providers.yaml @@ -0,0 +1,31 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Server mode - connects to the separate llama-stack service + use_as_library_client: false + url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + api_key: xyzzy +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini + # Unified synthesis input (Decision S5): the high-level provider entry + # replaces the default baseline's openai provider by id at synthesis time. + providers: + - type: openai + id: openai + api_key_env: OPENAI_API_KEY + allowed_models: + - ${env.E2E_OPENAI_MODEL:=gpt-4o-mini} diff --git a/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-relative-profile.yaml b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-relative-profile.yaml new file mode 100644 index 000000000..3c6c8512a --- /dev/null +++ b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-relative-profile.yaml @@ -0,0 +1,26 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Server mode - connects to the separate llama-stack service + use_as_library_client: false + url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + api_key: xyzzy + config: + # R8: relative profile resolves against this file's loaded location + profile: run.yaml +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini diff --git a/tests/e2e/features/environment.py b/tests/e2e/features/environment.py index cd05d3bdc..78314f830 100644 --- a/tests/e2e/features/environment.py +++ b/tests/e2e/features/environment.py @@ -242,6 +242,21 @@ def before_scenario(context: Context, scenario: Scenario) -> None: scenario.skip("Skipped in Prow (requires Docker Compose services)") return + # Skip openai-specific scenarios on non-openai provider matrices: the + # providers workflow runs the full test list with E2E_DEFAULT_PROVIDER_OVERRIDE + # set (azure/watsonx/...), and fixtures that hardcode an openai provider + # (e.g. the unified-mode inference.providers fixture) cannot serve queries + # for those models. + provider_override = os.getenv("E2E_DEFAULT_PROVIDER_OVERRIDE", "") + if "openai-only" in scenario.effective_tags and provider_override not in ( + "", + "openai", + ): + scenario.skip( + f"Skipped on provider matrix '{provider_override}' (openai-only fixture)" + ) + return + # In Prow, verify the lightspeed port-forward is alive before each scenario. # Port-forwards can silently die between scenarios (e.g. pod restart, TCP reset). if is_prow_environment(): diff --git a/tests/e2e/features/steps/unified_mode.py b/tests/e2e/features/steps/unified_mode.py new file mode 100644 index 000000000..f865f913a --- /dev/null +++ b/tests/e2e/features/steps/unified_mode.py @@ -0,0 +1,482 @@ +"""Step definitions for the unified-mode e2e features (LCORE-2343). + +Covers configuration validation, legacy-to-unified migration, and run.yaml +synthesis for the five ``unified-mode-*.feature`` files. + +Design rules (from the LCORE-2343 planning notes): + +- Validation, migration, and synthesis steps operate on the **on-disk** + configuration artifacts — never the live service. "The active + configuration" is the repo-root ``lightspeed-stack.yaml`` copy that + ``configure_service`` applied. +- Migration and synthesis run the real CLIs as subprocesses — exactly the + surface the server entrypoint and operators use — and assertions parse + the produced YAML (data equality, never byte comparison). +- The synthesis-log step is mode-aware: in server mode the synthesis + evidence is emitted by the llama-stack container (entrypoint + CLI), not + the lightspeed-stack container the Gherkin names; the scenario's intent + (R10: the synthesized path is logged at startup) is asserted against the + container that actually synthesizes. +""" + +import difflib +import os +import re +import shutil +import stat +import subprocess +import sys +import tempfile +import time +from pathlib import Path +from typing import Any, Optional + +import yaml +from behave import given, step, then, when # pyright: ignore +from behave.runner import Context + +# Generated by the --migrate-config step; matches the .gitignore entry. +MIGRATED_CONFIG_BASENAME = "lightspeed-stack-unified-migrated.yaml" +# Legacy half of "the legacy migration fixture pair"; its run.yaml half is +# the repo-root run.yaml the CI harness materializes. +MIGRATION_PAIR_LCS_BASENAME = "lightspeed-stack-legacy-for-migration.yaml" + +CLI_TIMEOUT_SECONDS = 120 +CUSTOM_OUTPUT_POLL_SECONDS = 60 + + +def _mode_subdir(context: Context) -> str: + """Return the mode fixture subdirectory name for the current harness mode.""" + return "library-mode" if context.is_library_mode else "server-mode" + + +def _config_dir(context: Context) -> Path: + """Resolve the active fixture directory, mode subdir included when present. + + Mirrors ``configure_service``'s resolution so files referenced by name in + Gherkin (fixture pairs, migrated output) land where that step finds them. + """ + base = Path( + getattr(context, "lightspeed_stack_config_directory", "") + or "tests/e2e/configuration" + ) + mode_base = base / _mode_subdir(context) + return mode_base if mode_base.is_dir() else base + + +def _active_config_path() -> Path: + """Return the on-disk active configuration (the applied repo-root copy).""" + return Path("lightspeed-stack.yaml") + + +def _run_cli( + args: list[str], cwd: Optional[Path] = None +) -> subprocess.CompletedProcess: + """Run a repo CLI as a subprocess, capturing output, never raising.""" + return subprocess.run( + [sys.executable, *args], + cwd=str(cwd) if cwd else None, + capture_output=True, + text=True, + timeout=CLI_TIMEOUT_SECONDS, + check=False, + ) + + +def _load_yaml(path: Path) -> Any: + """Parse a YAML file.""" + with open(path, "r", encoding="utf-8") as f: + return yaml.safe_load(f) + + +def _native_override(config_path: Path) -> dict[str, Any]: + """Extract llama_stack.config.native_override from a config file.""" + config = _load_yaml(config_path) + override = ((config.get("llama_stack") or {}).get("config") or {}).get( + "native_override" + ) + assert override, f"{config_path} carries no llama_stack.config.native_override" + return override + + +def _synthesized(context: Context) -> Path: + """Return the synthesized run.yaml path recorded by an earlier step.""" + path = getattr(context, "synthesized_run_yaml_path", None) + assert path, "no synthesis step ran before this assertion" + return Path(path) + + +# --------------------------------------------------------------------------- +# Validation (unified-mode-validation.feature) +# --------------------------------------------------------------------------- + + +@when("configuration validation is attempted for the active configuration") +def attempt_configuration_validation(context: Context) -> None: + """Validate the on-disk active configuration via the service CLI. + + Runs ``lightspeed_stack.py --dump-configuration -c lightspeed-stack.yaml`` + as a black-box subprocess: ``main()`` loads (and thereby validates) the + configuration before any dump handling, so a Pydantic validation failure + surfaces on stderr with a non-zero exit code. cwd is the repo root so the + invalid fixtures' ``library_client_config_path: run.yaml`` resolves to the + harness-materialized run.yaml and the captured failure is the intended + cross-field error, not a file-not-found. + """ + result = _run_cli( + [ + "src/lightspeed_stack.py", + "--dump-configuration", + "-c", + str(_active_config_path()), + ] + ) + context.validation_returncode = result.returncode + context.validation_output = result.stdout + result.stderr + assert result.returncode != 0, ( + "expected the active configuration to fail validation, but the load " + f"succeeded (rc=0). Output:\n{context.validation_output}" + ) + + +@then("the validation error contains {text}") +def validation_error_contains(context: Context, text: str) -> None: + """Assert the captured validation failure mentions the given text.""" + output = getattr(context, "validation_output", None) + assert output is not None, "no validation attempt ran before this assertion" + assert ( + text.strip() in output + ), f"validation error does not contain {text!r}. Full output:\n{output}" + + +# --------------------------------------------------------------------------- +# Migration (unified-mode-migration.feature) +# --------------------------------------------------------------------------- + + +@step("lightspeed-stack --migrate-config is run for the legacy migration fixture pair") +def run_migrate_config(context: Context) -> None: + """Migrate the legacy fixture pair into the active fixture directory. + + The pair is ``lightspeed-stack-legacy-for-migration.yaml`` (mode subdir) + plus the repo-root ``run.yaml`` the harness materializes. The output lands + in the same mode subdir under the name later Gherkin steps reference, so + ``configure_service`` can boot it; it is gitignored and cleaned up after + the scenario. + """ + pair_lcs = _config_dir(context) / MIGRATION_PAIR_LCS_BASENAME + pair_run = Path("run.yaml") + output = _config_dir(context) / MIGRATED_CONFIG_BASENAME + assert pair_lcs.is_file(), f"missing migration fixture {pair_lcs}" + assert pair_run.is_file(), "repo-root run.yaml (harness-materialized) missing" + + result = _run_cli( + [ + "src/lightspeed_stack.py", + "--migrate-config", + "--run-yaml", + str(pair_run), + "-c", + str(pair_lcs), + "--migrate-output", + str(output), + ] + ) + assert result.returncode == 0 and output.is_file(), ( + f"--migrate-config failed (rc={result.returncode}).\n" + f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}" + ) + # The CLI writes 0600 (R10: migrated files may carry lifted secrets), but + # the container user must be able to read the copy configure_service puts + # at the repo root to boot it. The fixture pair is env-reference-only by + # design, so relaxing the harness copy is safe. + os.chmod(output, 0o644) + context.migrated_config_path = output + context.migration_pair_run_yaml = pair_run + context.add_cleanup(lambda: output.unlink(missing_ok=True)) + + +@then("the file {filename} contains {text}") +def file_contains(context: Context, filename: str, text: str) -> None: + """Assert a file in the active fixture directory contains a substring.""" + path = _config_dir(context) / filename.strip() + content = path.read_text(encoding="utf-8") + assert text.strip() in content, f"{path} does not contain {text!r}" + + +@then("the file {filename} does not contain {text}") +def file_does_not_contain(context: Context, filename: str, text: str) -> None: + """Assert a file in the active fixture directory lacks a substring.""" + path = _config_dir(context) / filename.strip() + content = path.read_text(encoding="utf-8") + assert text.strip() not in content, f"{path} unexpectedly contains {text!r}" + + +# --------------------------------------------------------------------------- +# Synthesis (unified-mode-synthesis.feature + migration round-trip) +# --------------------------------------------------------------------------- + + +@step("the active unified configuration is synthesized to run.yaml") +def synthesize_active_configuration(context: Context) -> None: + """Synthesize a run.yaml from the on-disk unified configuration. + + Runs the config CLI exactly as the server entrypoint does (unified + auto-detection dispatches to ``synthesize_to_file``, which also gives the + 0600 output mode). Source precedence: the migrated config when the + migration step ran in this scenario, else the active on-disk config. + When the custom-output service step ran instead, this step is a + pass-through — the service subprocess performs the synthesis. + """ + if getattr(context, "custom_output_path", None): + return + + source = getattr(context, "migrated_config_path", None) or _active_config_path() + scratch = Path(tempfile.mkdtemp(prefix="lcore-e2e-synthesis-")) + context.add_cleanup(lambda: shutil.rmtree(scratch, ignore_errors=True)) + output = scratch / "run.yaml" + + result = _run_cli( + ["src/llama_stack_configuration.py", "-c", str(source), "-o", str(output)] + ) + assert result.returncode == 0 and output.is_file(), ( + f"synthesis CLI failed (rc={result.returncode}) for {source}.\n" + f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}" + ) + context.synthesized_run_yaml_path = output + + +@then( + "the synthesized run.yaml parses to the same data as the legacy migration fixture run.yaml" +) +def synthesized_round_trips(context: Context) -> None: + """Assert migrate-then-synthesize reproduces the pair's run.yaml (data equality).""" + synthesized = _load_yaml(_synthesized(context)) + original = _load_yaml( + getattr(context, "migration_pair_run_yaml", None) or Path("run.yaml") + ) + if synthesized != original: + diff = "\n".join( + difflib.unified_diff( + yaml.dump(original, sort_keys=True).splitlines(), + yaml.dump(synthesized, sort_keys=True).splitlines(), + fromfile="pair run.yaml", + tofile="synthesized", + lineterm="", + ) + ) + raise AssertionError(f"round-trip data mismatch:\n{diff}") + + +@then( + "the synthesized run.yaml contains the native_override scalar value for safety.excluded_categories" +) +def synthesized_scalar_override(context: Context) -> None: + """Assert the override's scalar replaced the baseline value at that key (R5).""" + override_value = _native_override(_active_config_path())["safety"][ + "excluded_categories" + ] + synthesized = _load_yaml(_synthesized(context)) + actual = (synthesized.get("safety") or {}).get("excluded_categories") + assert actual == override_value, ( + f"safety.excluded_categories is {actual!r}, expected the " + f"native_override value {override_value!r}" + ) + baseline = _load_yaml(Path("run.yaml")) + baseline_value = (baseline.get("safety") or {}).get("excluded_categories") + assert baseline_value != override_value, ( + "fixture and baseline agree on safety.excluded_categories — the " + "replacement assertion would be vacuous" + ) + + +@then("the synthesized run.yaml contains exactly the native_override list for apis") +def synthesized_list_override(context: Context) -> None: + """Assert the override list replaced the baseline's apis wholesale (R5).""" + override_list = _native_override(_active_config_path())["apis"] + synthesized = _load_yaml(_synthesized(context)) + assert ( + synthesized.get("apis") == override_list + ), f"apis is {synthesized.get('apis')!r}, expected exactly {override_list!r}" + baseline = _load_yaml(Path("run.yaml")) + assert ( + baseline.get("apis") != override_list + ), "fixture and baseline agree on apis — wholesale replacement would be vacuous" + + +@then("the synthesized run.yaml contains ${{env.OPENAI_API_KEY}}") +def synthesized_keeps_env_reference(context: Context) -> None: + """Assert the emitted secret stays an environment reference on disk (R6).""" + content = _synthesized(context).read_text(encoding="utf-8") + assert ( + "${env.OPENAI_API_KEY}" in content + ), "synthesized run.yaml does not carry the ${env.OPENAI_API_KEY} reference" + + +@then("the synthesized run.yaml does not contain the resolved OPENAI_API_KEY value") +def synthesized_no_literal_secret(context: Context) -> None: + """Assert the literal secret value never lands on disk (R6).""" + secret = os.environ.get("OPENAI_API_KEY", "") + assert secret, ( + "OPENAI_API_KEY is not set in the harness environment — the " + "no-literal-secret assertion would be vacuous" + ) + content = _synthesized(context).read_text(encoding="utf-8") + assert ( + secret not in content + ), "synthesized run.yaml contains the resolved OPENAI_API_KEY value" + + +@then("the synthesized run.yaml file permissions are 0600") +def synthesized_permissions(context: Context) -> None: + """Assert the synthesized file is owner-read/write only (R10).""" + mode = stat.S_IMODE(os.stat(_synthesized(context)).st_mode) + assert mode == 0o600, f"synthesized run.yaml mode is {oct(mode)}, expected 0o600" + + +# --------------------------------------------------------------------------- +# --synthesized-config-output (unified-mode-synthesis.feature) +# --------------------------------------------------------------------------- + + +@given( + "lightspeed-stack is started with --synthesized-config-output set to a custom path" +) +def start_with_custom_synthesis_output(context: Context) -> None: + """Launch a short-lived local service with a custom synthesis output path. + + The flag only affects library-mode in-process synthesis, and the running + containers cannot be restarted with different CLI args — so this step + always uses the library-mode variant of the active fixture, copied to a + scratch directory with ``service.port`` rewritten to avoid clashing with + the running stack. The subprocess synthesizes during app startup; the + following Then steps poll for the file, and the process is killed on + scenario cleanup. + """ + active_basename = _active_config_path().name + fixture_basename = Path( + getattr(context, "feature_config", "") + or "lightspeed-stack-unified-providers.yaml" + ).name + base = Path( + getattr(context, "lightspeed_stack_config_directory", "") + or "tests/e2e/configuration" + ) + library_fixture = base / "library-mode" / fixture_basename + if not library_fixture.is_file(): + library_fixture = _config_dir(context) / fixture_basename + assert library_fixture.is_file(), f"no library-mode fixture for {active_basename}" + + scratch = Path(tempfile.mkdtemp(prefix="lcore-e2e-synthout-")) + context.add_cleanup(lambda: shutil.rmtree(scratch, ignore_errors=True)) + + config = _load_yaml(library_fixture) + config.setdefault("service", {})["port"] = 8099 + scratch_config = scratch / "lightspeed-stack.yaml" + with open(scratch_config, "w", encoding="utf-8") as f: + yaml.safe_dump(config, f) + + custom_output = scratch / "custom-run.yaml" + process = subprocess.Popen( # pylint: disable=consider-using-with + [ + sys.executable, + str(Path("src/lightspeed_stack.py").resolve()), + "-c", + str(scratch_config), + "--synthesized-config-output", + str(custom_output), + ], + cwd=str(scratch), + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + ) + context.custom_output_path = custom_output + context.custom_output_scratch = scratch + context.custom_output_process = process + + def _kill() -> None: + if process.poll() is None: + process.kill() + process.wait(timeout=10) + + context.add_cleanup(_kill) + + +@then("the synthesized run.yaml is written to the custom output path") +def custom_output_written(context: Context) -> None: + """Poll for the custom-path synthesis output and validate it parses.""" + custom_output = Path(context.custom_output_path) + process = context.custom_output_process + deadline = time.monotonic() + CUSTOM_OUTPUT_POLL_SECONDS + while time.monotonic() < deadline: + if custom_output.is_file() and custom_output.stat().st_size > 0: + break + if process.poll() is not None and not custom_output.is_file(): + out = process.stdout.read() if process.stdout else "" + raise AssertionError( + f"service exited (rc={process.returncode}) before writing the " + f"custom synthesis output.\n{out[-2000:]}" + ) + time.sleep(0.5) + assert custom_output.is_file(), ( + f"custom synthesis output {custom_output} did not appear within " + f"{CUSTOM_OUTPUT_POLL_SECONDS}s" + ) + assert isinstance(_load_yaml(custom_output), dict) + context.synthesized_run_yaml_path = custom_output + + +@then("the default synthesized run.yaml path does not exist") +def default_output_absent(context: Context) -> None: + """Assert the default synthesis location was not used (override took effect).""" + scratch = Path(context.custom_output_scratch) + default_path = scratch / ".generated" / "run.yaml" + assert ( + not default_path.exists() + ), f"default synthesis path {default_path} exists despite the override" + + +# --------------------------------------------------------------------------- +# Startup logging (unified-mode-synthesis.feature) — mode-aware, see module +# docstring and LCORE-2343 planning decision Q2. +# --------------------------------------------------------------------------- + + +@then("the lightspeed-stack container logs contain synthesized run.yaml") +def container_logs_show_synthesis(context: Context) -> None: + """Assert the container that synthesizes logged the synthesized-config path. + + Library mode: the lightspeed-stack container itself synthesizes in-process + and logs "Using synthesized Llama Stack config at ". Server mode: + synthesis happens in the llama-stack container (entrypoint + config CLI), + which logs the generated-config path — the Gherkin names lightspeed-stack, + but the scenario's intent (R10: the path is logged at startup) can only be + observed on the synthesizing container. Deviation agreed in planning (Q2). + """ + if context.is_library_mode: + container = "lightspeed-stack" + pattern = r"synthesized.*run\.yaml|Using synthesized Llama Stack config" + else: + container = "llama-stack" + pattern = ( + r"Wrote synthesized Llama Stack configuration" + r"|Using generated config:.*run\.yaml" + r"|mode auto-detected" + ) + + result = subprocess.run( + ["docker", "logs", container], + capture_output=True, + text=True, + timeout=60, + check=False, + ) + assert ( + result.returncode == 0 + ), f"docker logs {container} failed: {result.stderr[-500:]}" + logs = result.stdout + result.stderr + assert re.search(pattern, logs), ( + f"{container} logs carry no synthesis-path evidence " + f"(pattern {pattern!r} not found)" + ) diff --git a/tests/e2e/features/unified-mode-boot.feature b/tests/e2e/features/unified-mode-boot.feature index 7c1413818..a78928200 100644 --- a/tests/e2e/features/unified-mode-boot.feature +++ b/tests/e2e/features/unified-mode-boot.feature @@ -1,4 +1,4 @@ -@cfg_unified @skip +@cfg_unified @skip-in-prow Feature: Unified mode configuration boot Background: @@ -10,7 +10,7 @@ Feature: Unified mode configuration boot # --- library mode (@skip-in-server-mode) --- - @skip-in-server-mode + @skip-in-server-mode @openai-only Scenario: Unified config with inference.providers boots and serves requests in library mode Given The service uses the lightspeed-stack-unified-providers.yaml configuration And The service is restarted @@ -62,7 +62,7 @@ Feature: Unified mode configuration boot # --- server mode (@skip-in-library-mode) --- - @skip-in-library-mode + @skip-in-library-mode @openai-only Scenario: Unified config with inference.providers boots and serves requests in server mode Given The service uses the lightspeed-stack-unified-providers.yaml configuration And Llama Stack is restarted diff --git a/tests/e2e/features/unified-mode-legacy.feature b/tests/e2e/features/unified-mode-legacy.feature index 407c43e6a..e54a64950 100644 --- a/tests/e2e/features/unified-mode-legacy.feature +++ b/tests/e2e/features/unified-mode-legacy.feature @@ -1,4 +1,4 @@ -@cfg_unified @skip +@cfg_unified @skip-in-prow Feature: Legacy two-file configuration during deprecation window Background: @@ -12,7 +12,10 @@ Feature: Legacy two-file configuration during deprecation window @skip-in-server-mode Scenario: Legacy two-file configuration still boots and serves requests in library mode - Given The service uses the lightspeed-stack.yaml configuration + # lightspeed-stack-legacy.yaml (not the standard baseline): LCORE-2342 + # migrated the library-mode baseline to unified mode, so only a dedicated + # legacy-shaped fixture still exercises the deprecated two-file path (R2). + Given The service uses the lightspeed-stack-legacy.yaml configuration And The service is restarted When I access endpoint "readiness" using HTTP GET method Then The status code of the response is 200 diff --git a/tests/e2e/features/unified-mode-migration.feature b/tests/e2e/features/unified-mode-migration.feature index b5a2d06eb..68cd2aaeb 100644 --- a/tests/e2e/features/unified-mode-migration.feature +++ b/tests/e2e/features/unified-mode-migration.feature @@ -1,4 +1,4 @@ -@cfg_unified @skip +@cfg_unified @skip-in-prow Feature: Legacy to unified configuration migration Background: diff --git a/tests/e2e/features/unified-mode-synthesis.feature b/tests/e2e/features/unified-mode-synthesis.feature index 52b4254fa..3bfba3a65 100644 --- a/tests/e2e/features/unified-mode-synthesis.feature +++ b/tests/e2e/features/unified-mode-synthesis.feature @@ -1,4 +1,4 @@ -@cfg_unified @skip +@cfg_unified @skip-in-prow Feature: Unified mode configuration synthesis Background: diff --git a/tests/e2e/features/unified-mode-validation.feature b/tests/e2e/features/unified-mode-validation.feature index 5a1688335..0a03bb4f3 100644 --- a/tests/e2e/features/unified-mode-validation.feature +++ b/tests/e2e/features/unified-mode-validation.feature @@ -1,4 +1,4 @@ -@cfg_unified @skip +@cfg_unified @skip-in-prow Feature: Unified mode configuration validation Background: diff --git a/tests/e2e/utils/utils.py b/tests/e2e/utils/utils.py index 548d3e6a4..3365b0d6b 100644 --- a/tests/e2e/utils/utils.py +++ b/tests/e2e/utils/utils.py @@ -486,6 +486,14 @@ def restart_container(container_name: str) -> None: # that restart the container don't time out. wait_for_container_health(container_name) + # Docker health can report healthy before uvicorn binds the published + # port (the documented race wait_for_lightspeed_stack_http_ready exists + # for). Unified-mode first boots are the slowest restarts in the suite + # and hit that window reliably, so close it here for every restart + # rather than only in the proxy steps. + if container_name == "lightspeed-stack": + wait_for_lightspeed_stack_http_ready() + if container_name == "llama-stack": from tests.e2e.features.steps.health import ( reset_llama_stack_disrupt_once_tracking,