diff --git a/README.md b/README.md index a774e0d..7e0c80c 100644 --- a/README.md +++ b/README.md @@ -103,13 +103,25 @@ v8-runner syntax designer-modules --server v8-runner test yaxunit all ``` +Для уже подготовленной файловой или серверной ИБ можно явно пропустить build: + +```bash +v8-runner test --no-build yaxunit all +``` + +Для файловой ИБ этот режим до запуска 1С проверяет наличие `1Cv8.1CD`. +Проверка конфигурации не требует наличия project source-set: нужны только настройки ИБ, +платформы и выбранного test engine. Для server connection отдельный portable preflight без +запуска платформы пока недоступен, поэтому соединение проверяет сам test engine. + ### Или тесты Vanessa Automation: ```bash v8-runner test va ``` -Команда сначала выполняет `build`, затем запускает настроенный профиль Vanessa Automation. +По умолчанию команда сначала выполняет `build`, затем запускает настроенный профиль Vanessa +Automation. Для подготовленной ИБ используйте `v8-runner test --no-build va`. Для отладки и написания тестов Vanessa Automation запустите ее в режиме MCP и, если агенту нужно сразу подключаться к endpoint, дождитесь готовности: diff --git a/SKILL/SKILL.md b/SKILL/SKILL.md index 2b0e4c5..394883c 100644 --- a/SKILL/SKILL.md +++ b/SKILL/SKILL.md @@ -73,7 +73,8 @@ v8-runner init - Only one source-set changed: use commands that accept `--source-set ` instead of rebuilding or materializing everything. - Branch switch, rebase, large object moves, stale source-backed tool extension state, or suspicious incremental state: run `v8-runner build --full-rebuild`. - Syntax check: inspect `format` and `builder`, then choose `syntax designer-modules`, `syntax designer-config`, or `syntax edt`. -- Behavior validation: run the relevant `v8-runner test ...` command; tests build first. +- Behavior validation: run the relevant `v8-runner test ...` command; tests build first unless the + caller explicitly requests `--no-build` for an already prepared infobase. - Missing local YAxUnit, Vanessa Automation, or onec-client-mcp-devkit setup: run `v8-runner tools download yaxunit --sources`, `v8-runner tools download vanessa`, and `v8-runner tools download client-mcp --sources` for source-backed setup. Omit diff --git a/SKILL/references/testing.md b/SKILL/references/testing.md index 535b91e..5bab3b7 100644 --- a/SKILL/references/testing.md +++ b/SKILL/references/testing.md @@ -1,6 +1,6 @@ # Testing -Use tests when behavior matters. Test commands build first, so do not run a separate `build` unless the user specifically asked for a build-only diagnosis. +Use tests when behavior matters. Test commands build first, so do not run a separate `build` unless the user specifically asked for a build-only diagnosis. For an immutable prepared infobase clone, use `test --no-build`; never infer this mode merely because a previous build appears successful. ## YaXUnit @@ -9,6 +9,7 @@ All tests: ```bash v8-runner test yaxunit all v8-runner test yaxunit --full all +v8-runner test --no-build yaxunit all ``` Target one module: @@ -26,12 +27,16 @@ Run the configured Vanessa Automation profile: ```bash v8-runner test va +v8-runner test --no-build va ``` If the user points to a specific feature or profile, inspect `tests.va` in `v8project.yaml` before changing the command. `test va` uses the configured `tests.va.profile`; do not invent ad hoc feature paths without updating config or using the repo's established wrapper. +`--no-build` requires an existing `1Cv8.1CD` for file infobases. Server infobases are validated by the test-engine connection because a local filesystem preflight is not possible. +This mode does not require project source-set directories or build tooling to be present; runner and platform inputs are still validated. + When driving tests through the MCP `run_all_tests` tool, pass `runner: "vanessa"` plus optional `profile`, `feature`, `filterTag`, `ignoreTag`, or `scenarioFilter`; do not use the default YaXUnit runner for functional `.feature` acceptance scenarios. `tests.va.fail_fast` defaults to `false`. diff --git a/docs/CAPABILITIES.md b/docs/CAPABILITIES.md index 6483c06..0ad1eeb 100644 --- a/docs/CAPABILITIES.md +++ b/docs/CAPABILITIES.md @@ -30,7 +30,8 @@ CLI help, доверяйте текущему коду и затем синхр | `extensions` | `format=DESIGNER` или `format=EDT` | Обновляет свойства extension `source-set` | | `build` | `format=DESIGNER` + `builder=DESIGNER|IBCMD` | Выполняет incremental/full загрузку в ИБ | | `build` | `format=EDT` + `builder=DESIGNER|IBCMD` | Экспортирует изменённые EDT `source-set`, затем грузит generated Designer output | -| `test` | Та же матрица, что и у `build` | Всегда сначала запускает `build` | +| `test` | Та же матрица, что и у `build` | По умолчанию запускает `build` | +| `test --no-build` | Подготовленная file/server ИБ; source-set и build tooling не требуются | Запускает выбранный test engine без build | | `dump` | `format=DESIGNER` + `builder=DESIGNER` | Полная, инкрементальная или object-scoped partial выгрузка | | `dump` | `format=DESIGNER` + `builder=IBCMD` | Полная и инкрементальная выгрузка; `partial` деградирует в incremental с warning; standalone-server state изолирован в `workPath/ibcmd-data` | | `dump` | `format=EDT` + `builder=DESIGNER|IBCMD` | Reverse sync из ИБ через internal Designer snapshot и EDT import | @@ -193,13 +194,18 @@ v8-runner build [--source-set ] [--full-rebuild] ### `test` ```bash -v8-runner test yaxunit [--full] all -v8-runner test yaxunit [--full] module -v8-runner test va -v8-runner test va --feature login --filter-tag @smoke +v8-runner test [--full] [--no-build] yaxunit all +v8-runner test [--full] [--no-build] yaxunit module +v8-runner test [--no-build] va +v8-runner test [--no-build] va --feature login --filter-tag @smoke ``` -- Всегда сначала запускает `build`. +- По умолчанию сначала запускает `build`. `--no-build` отмечает build-step как `skipped` и + запускает тесты на подготовленной ИБ; для file connection до запуска платформы требуется + `/1Cv8.1CD`, для server connection доступность подтверждается запуском test engine. +- В `--no-build` source-set и build tooling не проходят filesystem/layout validation: исходники + configuration могут отсутствовать. Валидация ИБ, платформы и настроек test engine сохраняется. +- `--no-build` является CLI-only контрактом; MCP `run_all_tests` сохраняет build-first поведение. - `test yaxunit module ` требует непустое имя модуля. - `test va` использует профиль из `tests.va.profile`; `--feature`, `--filter-tag`, `--ignore-tag` и `--scenario-filter` переопределяют соответствующие списки выбранного профиля diff --git a/docs/superpowers/plans/2026-07-22-test-no-build.md b/docs/superpowers/plans/2026-07-22-test-no-build.md new file mode 100644 index 0000000..4b48390 --- /dev/null +++ b/docs/superpowers/plans/2026-07-22-test-no-build.md @@ -0,0 +1,70 @@ +# Test Without Build Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add an opt-in CLI test mode that runs against an already prepared infobase without building it. + +**Architecture:** Map the CLI flag into a typed transport-neutral build policy. Branch in the test coordinator before artifact creation: validate a file infobase and emit a skipped build step, or execute the existing build-first path. + +**Tech Stack:** Rust, clap, serde, existing execution-step and test-result models. + +## Global Constraints + +- Default test behavior remains build-first. +- MCP remains build-first. +- Skip mode must not invoke build/load/update operations. +- File infobases require an existing `1Cv8.1CD` marker. +- Use Result-based errors and exhaustive enum matches. + +--- + +### Task 1: CLI and typed request policy + +**Files:** +- Modify: `src/cli/args.rs` +- Modify: `src/cli/execute.rs` +- Modify: `src/use_cases/request.rs` +- Test: `tests/cli_help.rs` + +**Interfaces:** +- Produces: `TestBuildPolicy::{BuildFirst, Skip}` and `TestRequest.build_policy`. + +- [ ] Add a failing help test for `test --no-build`. +- [ ] Run the focused help test and confirm it fails because the flag is absent. +- [ ] Add `--no-build`, the enum, and mapping with build-first defaults for non-CLI callers. +- [ ] Run the focused help test and request-mapping tests. + +### Task 2: Skip behavior and file-infobase preflight + +**Files:** +- Modify: `src/use_cases/run_tests/coordinator.rs` +- Modify: `src/use_cases/run_tests/helpers.rs` +- Modify: `src/domain/test.rs` +- Test: `tests/cli_test.rs` + +**Interfaces:** +- Consumes: `TestRequest.build_policy`. +- Produces: skipped `build` step or typed `infobase_unavailable` failure. + +- [ ] Add failing YaXUnit tests for skipped build and missing `1Cv8.1CD`. +- [ ] Run them and confirm the missing option/behavior failures. +- [ ] Implement preflight, skipped step, and exhaustive test error mapping. +- [ ] Run the focused YaXUnit tests. +- [ ] Add a failing Vanessa no-build test, then implement only any missing shared behavior. +- [ ] Run the focused Vanessa test. + +### Task 3: Documentation and verification + +**Files:** +- Modify: `README.md` +- Modify: `docs/CAPABILITIES.md` +- Modify: `SKILL/SKILL.md` +- Modify: `SKILL/references/testing.md` + +**Interfaces:** +- Documents the CLI-only prepared-infobase workflow and file/server distinction. + +- [ ] Update user and agent guidance. +- [ ] Run formatter, focused suites, check, and diff-check. +- [ ] Run independent Rust and contract reviews; fix or explicitly waive every finding. +- [ ] Commit, push, and create the upstream PR. diff --git a/docs/superpowers/specs/2026-07-22-test-no-build-design.md b/docs/superpowers/specs/2026-07-22-test-no-build-design.md new file mode 100644 index 0000000..6891cbc --- /dev/null +++ b/docs/superpowers/specs/2026-07-22-test-no-build-design.md @@ -0,0 +1,38 @@ +# Test Without Build Design + +## Goal + +Allow CLI users to run YaXUnit or Vanessa Automation against a prepared infobase without invoking the build pipeline. + +## Contract + +`v8-runner test --no-build yaxunit all` and `v8-runner test --no-build va` select a typed `TestBuildPolicy::Skip`. The default remains `TestBuildPolicy::BuildFirst`. MCP requests keep the default and do not expose the new CLI-only option. + +The result keeps a `build` execution step. In skip mode that step has status `skipped` and a stable message stating that the caller explicitly skipped the prerequisite. + +## Infobase preflight + +For file connections, skip mode requires the configured infobase directory to contain `1Cv8.1CD`. Failure is returned before test artifacts or a platform process are created, using typed test error code `infobase_unavailable`. + +Prepared-test config loading deliberately skips source-set and build-tool filesystem validation, while retaining base/work path, connection, platform, timeout, and test configuration validation. This lets immutable CI clones run after their configuration sources have been removed. + +Accepted portability waiver: the current public server connection contract does not carry cluster-administration credentials or a portable non-1C management adapter, so existence of a named server infobase cannot be proven before starting 1C without introducing a false-positive TCP check or a new external dependency. Server availability therefore remains established by the test-engine connection and its typed process errors; file infobases receive the strict preflight required by this change. + +Server connections cannot be proven available without contacting the server through a platform process. Skip mode therefore validates their configuration using the existing loader and lets the selected test engine establish connectivity; it does not introduce a hidden probe command. + +## Components + +- CLI maps `--no-build` to the typed build policy. +- The transport-neutral request owns the policy. +- The test coordinator performs file-infobase preflight or the existing build prerequisite. +- Existing step serialization reports the explicit skip. +- CLI integration tests cover YaXUnit, Vanessa, and a missing file infobase. +- README, capabilities, and repo-local skill guidance describe the workflow. + +## Error handling and compatibility + +Default behavior is unchanged. Skip mode never invokes build, load, or update-database operations. Missing file state returns the existing runtime CLI error class plus the new typed test error. Credentials and launch options continue through existing code paths. + +## Testing + +Tests first demonstrate that the option is absent. After implementation they assert no build script invocation, a skipped build step in JSON, successful YaXUnit and Vanessa execution, and failure before platform launch when `1Cv8.1CD` is missing. diff --git a/src/app.rs b/src/app.rs index 0dd5c68..211aec7 100644 --- a/src/app.rs +++ b/src/app.rs @@ -10,7 +10,8 @@ use crate::cli::execute; use crate::cli::output::{failure_envelope, print_command_error}; use crate::command_envelope::Envelope; use crate::config::loader::{ - load_config, load_config_for_tools_download, resolve_primary_config_path, + load_config, load_config_for_prepared_test, load_config_for_tools_download, + resolve_primary_config_path, }; use crate::output::presenter::Presenter; use crate::output::text::{TimelineItem, TimelineStatus}; @@ -148,6 +149,8 @@ fn load_cli_config( }) ) { load_config_for_tools_download(cli.config.as_deref(), cli.workdir.as_deref()) + } else if matches!(&cli.command, Command::Test(args) if args.no_build) { + load_config_for_prepared_test(cli.config.as_deref(), cli.workdir.as_deref()) } else { load_config(cli.config.as_deref(), cli.workdir.as_deref()) } diff --git a/src/cli/args.rs b/src/cli/args.rs index 666c767..1a6f82c 100644 --- a/src/cli/args.rs +++ b/src/cli/args.rs @@ -60,7 +60,7 @@ pub enum Command { Build(BuildArgs), /// Apply built release artifacts to the infobase Load(LoadArgs), - /// Build first, then run YaXUnit or Vanessa Automation tests + /// Run YaXUnit or Vanessa Automation tests, building first by default Test(TestArgs), /// Dump infobase state back to project files Dump(DumpArgs), @@ -246,6 +246,10 @@ pub struct TestArgs { #[arg(long, global = true)] pub full: bool, + /// Run tests against the configured prepared infobase without building sources first + #[arg(long, global = true)] + pub no_build: bool, + /// Client mode used for enterprise launch during test execution #[arg(long = "client-mode", value_parser = ["designer", "thin", "thick", "ordinary"])] pub client_mode: Option, diff --git a/src/cli/execute.rs b/src/cli/execute.rs index 927e7b2..79f9bfb 100644 --- a/src/cli/execute.rs +++ b/src/cli/execute.rs @@ -869,18 +869,25 @@ fn map_tools_download_force(args: &ToolsDownloadArgs) -> bool { fn map_test_request(config: &AppConfig, args: &TestArgs) -> Result { let client_mode = map_test_client_mode(args.client_mode.as_deref())?; + let build_policy = if args.no_build { + crate::use_cases::request::TestBuildPolicy::Skip + } else { + crate::use_cases::request::TestBuildPolicy::BuildFirst + }; match &args.runner { TestRunner::Yaxunit(TestYaxunitArgs { scope }) => { let scope = map_yaxunit_scope(scope)?; Ok(TestRequest { execution: build_yaxunit_execution(config, &args.launch, client_mode)?, full: args.full, + build_policy, scope, }) } TestRunner::Va(_) => Ok(TestRequest { execution: build_vanessa_execution(config, &args.launch, client_mode)?, full: args.full, + build_policy, scope: TestScopeRequest::All, }), } @@ -2484,7 +2491,7 @@ mod tests { use crate::use_cases::request::{ ArtifactsModeRequest, ClientMcpAddonRequest, ClientMcpMode, ClientMcpOptionsRequest, DesignerClientScope, DesignerConfigCheck, DumpModeRequest, LaunchRequest, - LaunchTargetRequest, SyntaxTargetRequest, TestScopeRequest, + LaunchTargetRequest, SyntaxTargetRequest, TestBuildPolicy, TestScopeRequest, }; use crate::use_cases::result::{UseCaseError, UseCaseErrorKind}; use crate::use_cases::workspace_lock::workspace_lock_path; @@ -2500,6 +2507,7 @@ mod tests { &config, &TestArgs { full: true, + no_build: false, client_mode: None, launch: TestLaunchOptionsArgs::default(), runner: TestRunner::Yaxunit(TestYaxunitArgs { @@ -2512,6 +2520,7 @@ mod tests { .expect("request"); assert!(request.full); + assert_eq!(request.build_policy, TestBuildPolicy::BuildFirst); assert_eq!( request.scope, TestScopeRequest::Module { @@ -2520,6 +2529,27 @@ mod tests { ); } + #[test] + fn maps_no_build_yaxunit_request() { + let work = tempdir().expect("tempdir"); + let config = sample_config(work.path()); + let request = map_test_request( + &config, + &TestArgs { + full: false, + no_build: true, + client_mode: None, + launch: TestLaunchOptionsArgs::default(), + runner: TestRunner::Yaxunit(TestYaxunitArgs { + scope: TestScope::All, + }), + }, + ) + .expect("request"); + + assert_eq!(request.build_policy, TestBuildPolicy::Skip); + } + #[test] fn rejects_blank_test_module_request() { let work = tempdir().expect("tempdir"); @@ -2528,6 +2558,7 @@ mod tests { &config, &TestArgs { full: false, + no_build: false, client_mode: None, launch: TestLaunchOptionsArgs::default(), runner: TestRunner::Yaxunit(TestYaxunitArgs { @@ -2575,6 +2606,7 @@ mod tests { &config, &TestArgs { full: false, + no_build: false, client_mode: None, launch: TestLaunchOptionsArgs::default(), runner: TestRunner::Va(TestVaArgs::default()), @@ -2584,8 +2616,23 @@ mod tests { assert_eq!(request.execution.profile.kind, RunnerKind::Vanessa); assert_eq!(request.execution.profile.id, "smoke"); + assert_eq!(request.build_policy, TestBuildPolicy::BuildFirst); assert_eq!(request.scope, TestScopeRequest::All); assert_eq!(request.execution.timeouts.total_ms, Some(300_000)); + + let no_build_request = map_test_request( + &config, + &TestArgs { + full: false, + no_build: true, + client_mode: None, + launch: TestLaunchOptionsArgs::default(), + runner: TestRunner::Va(TestVaArgs::default()), + }, + ) + .expect("no-build request"); + + assert_eq!(no_build_request.build_policy, TestBuildPolicy::Skip); } #[test] @@ -3011,6 +3058,7 @@ mod tests { &config, &Command::Test(TestArgs { full: false, + no_build: false, client_mode: None, launch: TestLaunchOptionsArgs::default(), runner: TestRunner::Yaxunit(TestYaxunitArgs { @@ -3075,6 +3123,7 @@ mod tests { &config, &Command::Test(TestArgs { full: false, + no_build: false, client_mode: None, launch: TestLaunchOptionsArgs::default(), runner: TestRunner::Yaxunit(TestYaxunitArgs { diff --git a/src/config/loader.rs b/src/config/loader.rs index 4252661..b91489c 100644 --- a/src/config/loader.rs +++ b/src/config/loader.rs @@ -5,7 +5,9 @@ use crate::config::model::AppConfig; use crate::config::schema::{ validate_local_overlay_schema_boundary, validate_main_config_schema_boundary, }; -use crate::config::validate::{validate, validate_tools_download_bootstrap, ConfigValidationError}; +use crate::config::validate::{ + validate, validate_prepared_test, validate_tools_download_bootstrap, ConfigValidationError, +}; use crate::support::path::normalize_windows_verbatim_path; pub const DEFAULT_CONFIG_FILE_NAME: &str = "v8project.yaml"; @@ -59,8 +61,20 @@ pub fn load_config_for_tools_download( ) } +pub fn load_config_for_prepared_test( + config_path: Option<&str>, + workdir_override: Option<&str>, +) -> Result { + load_config_with_mode( + config_path, + workdir_override, + ConfigValidationMode::PreparedTest, + ) +} + enum ConfigValidationMode { Full, + PreparedTest, ToolsDownload, } @@ -103,6 +117,7 @@ fn load_config_with_mode( match validation_mode { ConfigValidationMode::Full => validate(&config)?, + ConfigValidationMode::PreparedTest => validate_prepared_test(&config)?, ConfigValidationMode::ToolsDownload => validate_tools_download_bootstrap(&config)?, } Ok(config) diff --git a/src/config/validate.rs b/src/config/validate.rs index 85ebd29..65c5d22 100644 --- a/src/config/validate.rs +++ b/src/config/validate.rs @@ -194,7 +194,8 @@ pub fn validate(config: &AppConfig) -> Result<(), ConfigValidationError> { validate_work_path(&config.work_path)?; validate_matrix(config)?; validate_source_sets(config)?; - validate_connection(config)?; + validate_connection_contract(config)?; + validate_ibcmd_server_dbms(config)?; validate_platform_version(config)?; validate_build_config(config)?; validate_execution_timeout(config)?; @@ -213,7 +214,8 @@ pub fn validate_tools_download_bootstrap(config: &AppConfig) -> Result<(), Confi validate_base_path(&config.base_path)?; validate_work_path(&config.work_path)?; validate_matrix(config)?; - validate_connection(config)?; + validate_connection_contract(config)?; + validate_ibcmd_server_dbms(config)?; validate_platform_version(config)?; validate_build_config(config)?; validate_execution_timeout(config)?; @@ -222,6 +224,20 @@ pub fn validate_tools_download_bootstrap(config: &AppConfig) -> Result<(), Confi Ok(()) } +/// Validate only the configuration required to run tests against a prepared infobase. +/// +/// Source trees and build tooling are intentionally excluded because `test --no-build` +/// is a consumer of an immutable infobase and must remain independent of build inputs. +pub fn validate_prepared_test(config: &AppConfig) -> Result<(), ConfigValidationError> { + validate_base_path(&config.base_path)?; + validate_work_path(&config.work_path)?; + validate_connection_contract(config)?; + validate_platform_version(config)?; + validate_execution_timeout(config)?; + validate_test_config(config)?; + Ok(()) +} + fn validate_base_path(path: &Path) -> Result<(), ConfigValidationError> { if !path.exists() || !path.is_dir() { return Err(ConfigValidationError::BasePathInvalid( @@ -587,7 +603,7 @@ fn validate_source_set_name(name: &str) -> Result<(), ConfigValidationError> { Ok(()) } -fn validate_connection(config: &AppConfig) -> Result<(), ConfigValidationError> { +fn validate_connection_contract(config: &AppConfig) -> Result<(), ConfigValidationError> { if config.infobase.connection.trim().is_empty() { return Err(ConfigValidationError::EmptyConnection); } @@ -600,6 +616,14 @@ fn validate_connection(config: &AppConfig) -> Result<(), ConfigValidationError> return Ok(()); } + Ok(()) +} + +fn validate_ibcmd_server_dbms(config: &AppConfig) -> Result<(), ConfigValidationError> { + let is_file_connection = config.v8_connection().file_path().is_some(); + if is_file_connection { + return Ok(()); + } if config.builder != BuilderBackend::Ibcmd { return Ok(()); } diff --git a/src/domain/test.rs b/src/domain/test.rs index 5d450fa..f7829d8 100644 --- a/src/domain/test.rs +++ b/src/domain/test.rs @@ -10,6 +10,7 @@ use crate::domain::execution::{ }; pub const TEST_ERROR_CODE_BUILD_FAILED: &str = "build_failed"; +pub const TEST_ERROR_CODE_INFOBASE_UNAVAILABLE: &str = "infobase_unavailable"; pub const TEST_ERROR_CODE_TEST_SETUP_FAILED: &str = "test_setup_failed"; pub const TEST_ERROR_CODE_ENTERPRISE_SPAWN_FAILED: &str = "enterprise_spawn_failed"; pub const TEST_ERROR_CODE_ENTERPRISE_STARTUP_CHECK_FAILED: &str = "enterprise_startup_check_failed"; @@ -42,6 +43,7 @@ pub enum TestOutputMode { #[serde(rename_all = "snake_case")] pub enum TestErrorKind { BuildFailed, + InfobaseUnavailable, TestSetupFailed, EnterpriseSpawnFailed, EnterpriseStartupCheckFailed, @@ -60,6 +62,7 @@ impl TestErrorKind { pub const fn code(self) -> &'static str { match self { Self::BuildFailed => TEST_ERROR_CODE_BUILD_FAILED, + Self::InfobaseUnavailable => TEST_ERROR_CODE_INFOBASE_UNAVAILABLE, Self::TestSetupFailed => TEST_ERROR_CODE_TEST_SETUP_FAILED, Self::EnterpriseSpawnFailed => TEST_ERROR_CODE_ENTERPRISE_SPAWN_FAILED, Self::EnterpriseStartupCheckFailed => TEST_ERROR_CODE_ENTERPRISE_STARTUP_CHECK_FAILED, @@ -78,6 +81,7 @@ impl TestErrorKind { pub fn from_code(code: &str) -> Option { Some(match code { TEST_ERROR_CODE_BUILD_FAILED => Self::BuildFailed, + TEST_ERROR_CODE_INFOBASE_UNAVAILABLE => Self::InfobaseUnavailable, TEST_ERROR_CODE_TEST_SETUP_FAILED => Self::TestSetupFailed, TEST_ERROR_CODE_ENTERPRISE_SPAWN_FAILED => Self::EnterpriseSpawnFailed, TEST_ERROR_CODE_ENTERPRISE_STARTUP_CHECK_FAILED => Self::EnterpriseStartupCheckFailed, @@ -289,6 +293,7 @@ pub fn test_execution_status(kind: Option, ok: bool) -> Execution ) => ExecutionStatus::InvalidOutput, Some( TestErrorKind::BuildFailed + | TestErrorKind::InfobaseUnavailable | TestErrorKind::TestSetupFailed | TestErrorKind::EnterpriseSpawnFailed | TestErrorKind::EnterpriseStartupCheckFailed @@ -330,6 +335,7 @@ mod tests { #[test] fn test_error_kind_codes_roundtrip_for_setup_and_process_failures() { let kinds = [ + TestErrorKind::InfobaseUnavailable, TestErrorKind::TestSetupFailed, TestErrorKind::EnterpriseSpawnFailed, TestErrorKind::EnterpriseStartupCheckFailed, @@ -348,6 +354,7 @@ mod tests { #[test] fn test_error_status_maps_new_process_failures_explicitly() { for kind in [ + TestErrorKind::InfobaseUnavailable, TestErrorKind::TestSetupFailed, TestErrorKind::EnterpriseSpawnFailed, TestErrorKind::EnterpriseStartupCheckFailed, diff --git a/src/mcp/service.rs b/src/mcp/service.rs index 66cc806..7129e30 100644 --- a/src/mcp/service.rs +++ b/src/mcp/service.rs @@ -30,8 +30,8 @@ use crate::use_cases::request::{ effective_test_timeouts, BuildRequest, ClientMcpAddonRequest, ClientMcpMode, ClientMcpOptionsRequest, DesignerClientScope, DesignerClientScopes, DesignerConfigCheck, DesignerConfigChecks, DesignerConfigSyntaxRequest, DesignerModulesSyntaxRequest, - DumpModeRequest, DumpRequest, LaunchRequest, SyntaxRequest, SyntaxTargetRequest, TestRequest, - TestScopeRequest, + DumpModeRequest, DumpRequest, LaunchRequest, SyntaxRequest, SyntaxTargetRequest, + TestBuildPolicy, TestRequest, TestScopeRequest, }; use crate::use_cases::result::{UseCaseError, UseCaseErrorKind, UseCaseFailure, UseCaseResult}; @@ -131,6 +131,7 @@ where let use_case_request = TestRequest { execution: TestRequest::default_execution(), full: request.full.unwrap_or(false), + build_policy: TestBuildPolicy::BuildFirst, scope: TestScopeRequest::Module { name: module_name }, }; @@ -347,6 +348,7 @@ fn map_run_all_tests_request( TestRequest { execution: TestRequest::default_execution(), full: request.full.unwrap_or(false), + build_policy: TestBuildPolicy::BuildFirst, scope: TestScopeRequest::All, }, )) @@ -359,6 +361,7 @@ fn map_run_all_tests_request( TestRequest { execution, full: request.full.unwrap_or(false), + build_policy: TestBuildPolicy::BuildFirst, scope: TestScopeRequest::All, }, )) diff --git a/src/use_cases/request.rs b/src/use_cases/request.rs index bee6132..6abbf25 100644 --- a/src/use_cases/request.rs +++ b/src/use_cases/request.rs @@ -45,12 +45,20 @@ pub struct LoadRequest { } /// Transport-neutral request for the `test` use case. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TestBuildPolicy { + BuildFirst, + Skip, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct TestRequest { /// Shared runner execution block reused by future test/package scenarios. pub execution: ScenarioExecutionRequest, /// When `true`, the use case may request a full build before test execution. pub full: bool, + /// Whether the test coordinator prepares the infobase from configured sources. + pub build_policy: TestBuildPolicy, /// Selected test scope. Module targets require a non-empty module name. pub scope: TestScopeRequest, } diff --git a/src/use_cases/run_tests.rs b/src/use_cases/run_tests.rs index 08e123f..370c8cd 100644 --- a/src/use_cases/run_tests.rs +++ b/src/use_cases/run_tests.rs @@ -37,7 +37,8 @@ use self::helpers::{ build_enterprise_dsl, build_platform_launch, build_summary, capped_timeout_ms, collect_diagnostics, degraded_step, enterprise_error_kind, failed_step, interrupted_test_failure, make_test_result, prepare_runner_artifacts, prepared_run_summary, - succeeded_step, validate_runner_profile_id, validate_target, with_retained_artifacts, + skipped_step, succeeded_step, validate_runner_profile_id, validate_target, + with_retained_artifacts, }; pub fn execute( @@ -717,6 +718,7 @@ mod tests { let args = crate::use_cases::request::TestRequest { full: false, + build_policy: crate::use_cases::request::TestBuildPolicy::BuildFirst, scope: crate::use_cases::request::TestScopeRequest::All, execution: crate::domain::runner::ScenarioExecutionRequest { profile: RunnerProfile { @@ -748,6 +750,7 @@ mod tests { let context = ExecutionContext::cli(CommandName::Test).with_cancellation(cancellation); let args = TestRequest { full: false, + build_policy: crate::use_cases::request::TestBuildPolicy::BuildFirst, scope: TestScopeRequest::All, execution: ScenarioExecutionRequest { profile: RunnerProfile { diff --git a/src/use_cases/run_tests/coordinator.rs b/src/use_cases/run_tests/coordinator.rs index 2a7bf44..e1a5006 100644 --- a/src/use_cases/run_tests/coordinator.rs +++ b/src/use_cases/run_tests/coordinator.rs @@ -1,5 +1,6 @@ use super::*; use crate::use_cases::progress::log_live_stage; +use crate::use_cases::request::TestBuildPolicy; pub(super) fn run_tests( context: &ExecutionContext, @@ -64,62 +65,104 @@ pub(super) fn run_tests( } }; - debug!("running build prerequisite for tests"); - log_live_stage( - "test: build prerequisite", - "[Build] preparing test infobase", - ); let build_started = Instant::now(); - let build_result = match build_project::execute( - context, - config, - &BuildArgs { - full_rebuild: false, - source_set: None, - }, - ) { - Ok(result) => result, - Err(failure) => { - let summary = failure - .payload - .as_ref() - .map(build_summary) - .unwrap_or_else(|| failure.error.to_string()); - steps.push( - failed_step( - "build", - ExecutionStepKind::PlatformCommand, - build_started.elapsed().as_millis() as u64, - summary.clone(), - ) - .with_errors(vec![test_execution_error( - TestErrorKind::BuildFailed, - summary.clone(), - )]), - ); - let outcome = ExecutionOutcome::new(ExecutionStatus::Failed) - .with_diagnostics(vec![summary.clone()]) - .with_errors(vec![test_execution_error( - TestErrorKind::BuildFailed, - summary.clone(), - )]); - let result = make_test_result( - target, - mode, - outcome, - warnings, - steps, - started.elapsed().as_millis() as u64, + match args.build_policy { + TestBuildPolicy::BuildFirst => { + debug!("running build prerequisite for tests"); + log_live_stage( + "test: build prerequisite", + "[Build] preparing test infobase", ); - return Err(TestExecutionFailure::with_payload(failure.error, result)); + let build_result = match build_project::execute( + context, + config, + &BuildArgs { + full_rebuild: false, + source_set: None, + }, + ) { + Ok(result) => result, + Err(failure) => { + let summary = failure + .payload + .as_ref() + .map(build_summary) + .unwrap_or_else(|| failure.error.to_string()); + steps.push( + failed_step( + "build", + ExecutionStepKind::PlatformCommand, + build_started.elapsed().as_millis() as u64, + summary.clone(), + ) + .with_errors(vec![test_execution_error( + TestErrorKind::BuildFailed, + summary.clone(), + )]), + ); + let outcome = ExecutionOutcome::new(ExecutionStatus::Failed) + .with_diagnostics(vec![summary.clone()]) + .with_errors(vec![test_execution_error( + TestErrorKind::BuildFailed, + summary.clone(), + )]); + let result = make_test_result( + target, + mode, + outcome, + warnings, + steps, + started.elapsed().as_millis() as u64, + ); + return Err(TestExecutionFailure::with_payload(failure.error, result)); + } + }; + steps.push(succeeded_step( + "build", + ExecutionStepKind::PlatformCommand, + build_started.elapsed().as_millis() as u64, + build_summary(&build_result), + )); } - }; - steps.push(succeeded_step( - "build", - ExecutionStepKind::PlatformCommand, - build_started.elapsed().as_millis() as u64, - build_summary(&build_result), - )); + TestBuildPolicy::Skip => { + steps.push(skipped_step( + "build", + ExecutionStepKind::PlatformCommand, + build_started.elapsed().as_millis() as u64, + "build prerequisite explicitly skipped by --no-build", + )); + if let Err(error) = validate_prepared_infobase(config) { + let message = error.to_string(); + steps.push( + failed_step( + "preflight_infobase", + ExecutionStepKind::Validation, + 0, + message.clone(), + ) + .with_errors(vec![test_execution_error( + TestErrorKind::InfobaseUnavailable, + message.clone(), + )]), + ); + let outcome = ExecutionOutcome::new(ExecutionStatus::Failed) + .with_diagnostics(vec![message.clone()]) + .with_errors(vec![test_execution_error( + TestErrorKind::InfobaseUnavailable, + message, + )]); + let result = make_test_result( + target, + mode, + outcome, + warnings, + steps, + started.elapsed().as_millis() as u64, + ); + return Err(TestExecutionFailure::with_payload(error, result)); + } + } + } debug!("preparing test run artifacts"); let prepare_artifacts_started = Instant::now(); @@ -487,3 +530,19 @@ pub(super) fn run_tests( started.elapsed().as_millis() as u64, )) } + +fn validate_prepared_infobase(config: &AppConfig) -> Result<(), AppError> { + let connection = config.v8_connection(); + let Some(file_path) = connection.file_path() else { + return Ok(()); + }; + let marker = Path::new(file_path).join("1Cv8.1CD"); + if marker.is_file() { + Ok(()) + } else { + Err(AppError::Runtime(format!( + "prepared file infobase is unavailable: expected '{}'", + marker.display() + ))) + } +} diff --git a/src/use_cases/run_tests/helpers.rs b/src/use_cases/run_tests/helpers.rs index 6020f1e..c11ccde 100644 --- a/src/use_cases/run_tests/helpers.rs +++ b/src/use_cases/run_tests/helpers.rs @@ -3,7 +3,8 @@ use std::time::{Duration, Instant}; use crate::config::model::AppConfig; use crate::domain::artifact::ArtifactSet; use crate::domain::execution::{ - ExecutionInterruptionDetails, ExecutionOutcome, ExecutionStatus, ExecutionStepKind, StepResult, + ExecutionInterruptionDetails, ExecutionOutcome, ExecutionStatus, ExecutionStepKind, + ExecutionStepStatus, StepResult, }; use crate::domain::runner::{LaunchClientModeRequest, LaunchOptions, RunnerKind}; use crate::domain::test::{TestErrorKind, TestOutputMode, TestReport, TestRunResult, TestTarget}; @@ -152,6 +153,15 @@ pub(super) fn succeeded_step( StepResult::succeeded(name, kind, duration_ms).with_message(message) } +pub(super) fn skipped_step( + name: &str, + kind: ExecutionStepKind, + duration_ms: u64, + message: impl Into, +) -> StepResult { + StepResult::new(name, kind, ExecutionStepStatus::Skipped, duration_ms).with_message(message) +} + pub(super) fn failed_step( name: &str, kind: ExecutionStepKind, diff --git a/tests/cli_help.rs b/tests/cli_help.rs index 81c1bcc..7297775 100644 --- a/tests/cli_help.rs +++ b/tests/cli_help.rs @@ -66,6 +66,18 @@ fn build_help_exposes_source_set_selector() { assert!(stdout.contains("--json-message")); } +#[test] +fn test_help_exposes_no_build_option() { + let output = v8_runner_command() + .args(["test", "--help"]) + .output() + .expect("run command"); + + assert!(output.status.success()); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("--no-build")); +} + #[test] fn dump_help_clarifies_object_selector_compatibility() { let output = v8_runner_command() diff --git a/tests/cli_test.rs b/tests/cli_test.rs index afa0928..9e88e6c 100644 --- a/tests/cli_test.rs +++ b/tests/cli_test.rs @@ -189,6 +189,36 @@ fn setup_project( ) } +enum FileInfobaseState { + Prepared, + MissingMarker, +} + +fn configure_file_infobase(config_path: &Path, infobase_path: &Path, state: FileInfobaseState) { + fs::create_dir_all(infobase_path).expect("infobase directory"); + match state { + FileInfobaseState::Prepared => { + fs::write(infobase_path.join("1Cv8.1CD"), "prepared").expect("infobase marker"); + } + FileInfobaseState::MissingMarker => {} + } + let config = fs::read_to_string(config_path).expect("config"); + fs::write( + config_path, + config.replace("File=/tmp/ib", &format!("File={}", infobase_path.display())), + ) + .expect("updated config"); +} + +fn configure_server_infobase(config_path: &Path) { + let config = fs::read_to_string(config_path).expect("config"); + fs::write( + config_path, + config.replace("File=/tmp/ib", "Srvr=cluster:1541;Ref=prepared"), + ) + .expect("updated config"); +} + fn setup_project_with_additional_launch_keys( work_dir_name: &str, report_xml: &str, @@ -412,6 +442,162 @@ fn test_all_full_json_runs_build_first_and_returns_report() { assert_eq!(payload["data"]["retained_paths"], Value::Null); } +#[test] +fn test_yaxunit_no_build_skips_build_for_prepared_file_infobase() { + let (dir, config_path, build_calls, test_calls, _captured_config) = setup_project( + "work", + JUNIT_SMOKE_REPORT_FIXTURE, + "12:00:00.000 [INF] ok", + 0, + false, + 5, + None, + ); + let infobase_path = dir.path().join("prepared-ib"); + configure_file_infobase(&config_path, &infobase_path, FileInfobaseState::Prepared); + let database_path = infobase_path.join("1Cv8.1CD"); + let database_before = fs::read(&database_path).expect("database contents before test"); + let modified_before = fs::metadata(&database_path) + .and_then(|metadata| metadata.modified()) + .expect("database timestamp before test"); + + let output = v8_runner_command() + .args([ + "--config", + &config_path.display().to_string(), + "--json-message", + "test", + "--no-build", + "yaxunit", + "all", + ]) + .output() + .expect("run"); + + assert!(output.status.success()); + assert!( + !build_calls.exists(), + "no-build must not invoke Designer build" + ); + assert!(test_calls.exists(), "test engine must still run"); + assert_eq!( + fs::read(&database_path).expect("database contents after test"), + database_before + ); + assert_eq!( + fs::metadata(&database_path) + .and_then(|metadata| metadata.modified()) + .expect("database timestamp after test"), + modified_before + ); + let payload: Value = serde_json::from_slice(&output.stdout).expect("json"); + let build_step = payload["steps"] + .as_array() + .expect("steps") + .iter() + .find(|step| step["name"] == "build") + .expect("build step"); + assert_eq!(build_step["status"], "skipped"); +} + +#[test] +fn test_no_build_does_not_require_edt_source_tree() { + let (dir, config_path, build_calls, test_calls, _captured_config) = + setup_project("work", JUNIT_SMOKE_REPORT_FIXTURE, "", 0, false, 5, None); + configure_file_infobase( + &config_path, + &dir.path().join("prepared-edt-ib"), + FileInfobaseState::Prepared, + ); + let config = fs::read_to_string(&config_path).expect("config"); + fs::write( + &config_path, + config.replace("format: DESIGNER", "format: EDT"), + ) + .expect("EDT config"); + fs::remove_dir_all(dir.path().join("project").join("main")).expect("remove sources"); + + let output = v8_runner_command() + .args([ + "--config", + &config_path.display().to_string(), + "--json-message", + "test", + "--no-build", + "yaxunit", + "all", + ]) + .output() + .expect("run"); + + assert!( + output.status.success(), + "status={:?}\nstdout={}\nstderr={}", + output.status.code(), + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + assert!(!build_calls.exists()); + assert!(test_calls.exists()); +} + +#[test] +fn test_yaxunit_no_build_runs_for_server_infobase() { + let (_dir, config_path, build_calls, test_calls, _captured_config) = + setup_project("work", JUNIT_SMOKE_REPORT_FIXTURE, "", 0, false, 5, None); + configure_server_infobase(&config_path); + + let output = v8_runner_command() + .args([ + "--config", + &config_path.display().to_string(), + "--json-message", + "test", + "--no-build", + "yaxunit", + "all", + ]) + .output() + .expect("run"); + + assert!(output.status.success()); + assert!(!build_calls.exists()); + assert!(test_calls.exists()); +} + +#[test] +fn test_no_build_rejects_missing_file_infobase_before_platform_launch() { + let (dir, config_path, build_calls, test_calls, _captured_config) = + setup_project("work", JUNIT_SMOKE_REPORT_FIXTURE, "", 0, false, 5, None); + configure_file_infobase( + &config_path, + &dir.path().join("missing-marker-ib"), + FileInfobaseState::MissingMarker, + ); + + let output = v8_runner_command() + .args([ + "--config", + &config_path.display().to_string(), + "--json-message", + "test", + "--no-build", + "yaxunit", + "all", + ]) + .output() + .expect("run"); + + assert!(!output.status.success()); + assert!(!build_calls.exists()); + assert!(!test_calls.exists()); + let payload: Value = serde_json::from_slice(&output.stdout).expect("json"); + assert_eq!( + payload["data"]["execution"]["errors"][0]["code"], + "infobase_unavailable" + ); +} + #[test] fn test_run_appends_enterprise_additional_launch_keys() { let (_dir, config_path, _build_calls, test_calls, _captured_config) = @@ -808,6 +994,60 @@ fn test_va_builds_vanessa_command_and_overlay() { ); } +#[test] +fn test_va_no_build_skips_build_for_prepared_file_infobase() { + let (dir, config_path, build_calls, test_calls, _captured_params) = + setup_va_project(JUNIT_SMOKE_REPORT_FIXTURE, &[]); + let infobase_path = dir.path().join("prepared-va-ib"); + configure_file_infobase(&config_path, &infobase_path, FileInfobaseState::Prepared); + let database_path = infobase_path.join("1Cv8.1CD"); + let database_before = fs::read(&database_path).expect("database contents before test"); + let modified_before = fs::metadata(&database_path) + .and_then(|metadata| metadata.modified()) + .expect("database timestamp before test"); + + let output = v8_runner_command() + .args([ + "--config", + &config_path.display().to_string(), + "--json-message", + "test", + "--no-build", + "va", + ]) + .output() + .expect("run"); + + assert!( + output.status.success(), + "status={:?}\nstdout={}\nstderr={}", + output.status.code(), + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + assert!( + !build_calls.exists(), + "no-build must not invoke Designer build" + ); + assert!(test_calls.exists(), "Vanessa test engine must still run"); + assert_eq!( + fs::read(&database_path).expect("database contents after test"), + database_before + ); + assert_eq!( + fs::metadata(&database_path) + .and_then(|metadata| metadata.modified()) + .expect("database timestamp after test"), + modified_before + ); + let payload: Value = serde_json::from_slice(&output.stdout).expect("json"); + assert!(payload["steps"] + .as_array() + .expect("steps") + .iter() + .any(|step| step["name"] == "build" && step["status"] == "skipped")); +} + #[test] fn test_va_cli_filter_options_override_configured_profile_lists() { let (_dir, config_path, _build_calls, _test_calls, captured_params) =