From f5b350aa0f054714495da065d239476a94f186b3 Mon Sep 17 00:00:00 2001 From: Oleg Karataev Date: Sun, 10 May 2026 20:50:21 +0300 Subject: [PATCH 1/5] fix: v8-runner build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit build_project.rs: EDT build-export теперь вызывает export --project , а не --project-name. coordinator.rs: для build-export используется отдельный очищаемый workspace build\edt-build-workspace, чтобы не конфликтовать с init workspace. cli_build.rs: обновил ожидание CLI-теста. --- src/use_cases/build_project.rs | 49 ++++++++++++---------- src/use_cases/build_project/coordinator.rs | 27 +++++++++--- tests/cli_build.rs | 2 +- 3 files changed, 50 insertions(+), 28 deletions(-) diff --git a/src/use_cases/build_project.rs b/src/use_cases/build_project.rs index 5c2b359..96d214f 100644 --- a/src/use_cases/build_project.rs +++ b/src/use_cases/build_project.rs @@ -281,7 +281,7 @@ fn execute_edt_export_step( )) })?; let export_result = dsl - .export_project(&project_name, designer_context.path()) + .export_project_path(edt_context.path(), designer_context.path()) .map_err(AppError::from)?; let export_log_path = write_edt_export_log( config, @@ -724,7 +724,7 @@ mod tests { }) .unwrap_or_default(); let body = format!( - "args=\"$*\"\nproject=\"\"\ntarget=\"\"\nprev=\"\"\nfor arg in \"$@\"; do\n if [ \"$prev\" = \"--project-name\" ]; then project=\"$arg\"; fi\n if [ \"$prev\" = \"--configuration-files\" ]; then target=\"$arg\"; fi\n prev=\"$arg\"\ndone\nif [ -n \"$target\" ]; then mkdir -p \"$target\"; printf 'exported from %s\\n' \"$project\" > \"$target/exported.txt\"; printf '\\n' > \"$target/Configuration.xml\"; fi\nprintf '%s\\n' \"$args\" >> \"{}\"\n{}\nexit 0", + "args=\"$*\"\nproject=\"\"\ntarget=\"\"\nprev=\"\"\nfor arg in \"$@\"; do\n if [ \"$prev\" = \"--project-name\" ] || [ \"$prev\" = \"--project\" ]; then project=\"$arg\"; fi\n if [ \"$prev\" = \"--configuration-files\" ]; then target=\"$arg\"; fi\n prev=\"$arg\"\ndone\nif [ -n \"$target\" ]; then mkdir -p \"$target\"; printf 'exported from %s\\n' \"$project\" > \"$target/exported.txt\"; printf '\\n' > \"$target/Configuration.xml\"; fi\nprintf '%s\\n' \"$args\" >> \"{}\"\n{}\nexit 0", calls_log.display(), pattern_branch ); @@ -797,7 +797,7 @@ mod tests { target=\"\"\n\ prev=\"\"\n\ for arg in \"$@\"; do\n\ - if [ \"$prev\" = \"--project-name\" ]; then project=\"$arg\"; fi\n\ + if [ \"$prev\" = \"--project-name\" ] || [ \"$prev\" = \"--project\" ]; then project=\"$arg\"; fi\n\ if [ \"$prev\" = \"--configuration-files\" ]; then target=\"$arg\"; fi\n\ prev=\"$arg\"\n\ done\n\ @@ -1902,7 +1902,9 @@ mod tests { assert!(result.steps.iter().any(|step| { step.source_set == "main" && matches!(step.mode, BuildMode::Full) && step.ok })); - assert!(edt_calls_text.contains("export --project-name main")); + assert!( + edt_calls_text.contains(&format!("export --project {}", base.join("main").display())) + ); assert!(designer_calls_text.contains("/LoadConfigFromFiles")); assert!(!designer_calls_text.contains("-partial")); assert!(designer_calls_text.contains( @@ -1923,12 +1925,7 @@ mod tests { .iter() .filter(|step| step.source_set == "main") .all(|step| matches!(step.mode, BuildMode::Skipped) && step.ok)); - assert_eq!( - rerun_edt_calls - .matches("export --project-name main") - .count(), - 1 - ); + assert_eq!(rerun_edt_calls.matches("export --project ").count(), 1); } #[cfg(unix)] @@ -1968,7 +1965,9 @@ mod tests { assert!(result.steps.iter().any(|step| { step.source_set == "main" && matches!(step.mode, BuildMode::Full) && step.ok })); - assert!(edt_calls_text.contains("export --project-name main")); + assert!( + edt_calls_text.contains(&format!("export --project {}", base.join("main").display())) + ); assert!(ibcmd_calls_text.contains("infobase --db-path /tmp/ib config import")); assert!(!ibcmd_calls_text.contains("config import files")); assert!(!ibcmd_calls_text.contains("--partial")); @@ -1986,7 +1985,7 @@ mod tests { #[cfg(unix)] #[test] - fn edt_build_prefers_project_name_from_dot_project_file() { + fn edt_build_uses_project_path_while_validating_dot_project_file() { let dir = tempdir().expect("tempdir"); let base = dir.path().join("base"); let work = dir.path().join("work"); @@ -2026,8 +2025,11 @@ mod tests { let edt_calls_text = fs::read_to_string(&edt_calls).expect("edt calls"); assert!(result.ok); - assert!(edt_calls_text.contains("export --project-name client_mcp")); - assert!(!edt_calls_text.contains("export --project-name client-mcp")); + assert!(edt_calls_text.contains(&format!( + "export --project {}", + base.join("exts").join("client-mcp").display() + ))); + assert!(!edt_calls_text.contains("export --project-name client_mcp")); } #[cfg(unix)] @@ -2123,7 +2125,10 @@ mod tests { assert!(!result.steps.iter().any(|step| { step.source_set == "client_mcp" && matches!(step.mode, BuildMode::Partial { .. }) })); - assert!(edt_calls_text.contains("export --project-name client_mcp")); + assert!(edt_calls_text.contains(&format!( + "export --project {}", + base.join("exts").join("client-mcp").display() + ))); assert!(designer_calls_text.contains("/LoadConfigFromFiles")); assert!(designer_calls_text.contains("-Extension client_mcp")); assert!(!designer_calls_text.contains("-partial")); @@ -2197,7 +2202,10 @@ mod tests { assert!(!result.steps.iter().any(|step| { step.source_set == "client_mcp" && matches!(step.mode, BuildMode::Partial { .. }) })); - assert!(edt_calls_text.contains("export --project-name client_mcp")); + assert!(edt_calls_text.contains(&format!( + "export --project {}", + base.join("exts").join("client-mcp").display() + ))); assert!(ibcmd_calls_text.contains("config import")); assert!(ibcmd_calls_text.contains("--extension client_mcp")); assert!(!ibcmd_calls_text.contains("config import files")); @@ -2309,7 +2317,7 @@ mod tests { assert!(result.ok); assert_eq!(edt_calls_text.matches("START").count(), 1); assert_eq!(edt_calls_text.matches("EXIT").count(), 1); - assert_eq!(edt_calls_text.matches("export --project-name").count(), 2); + assert_eq!(edt_calls_text.matches("export --project ").count(), 2); } #[cfg(unix)] @@ -2357,10 +2365,7 @@ mod tests { assert!(result.steps.iter().any(|step| { step.source_set == "main" && matches!(step.mode, BuildMode::Partial { .. }) && step.ok })); - assert_eq!( - edt_calls_text.matches("export --project-name main").count(), - 1 - ); + assert_eq!(edt_calls_text.matches("export --project ").count(), 1); assert_eq!( designer_calls_text.matches("/LoadConfigFromFiles").count(), 2 @@ -2387,7 +2392,7 @@ mod tests { let edt_calls = dir.path().join("edt-calls.log"); create_source_tree(&base); write_designer_script(&platform_script, &designer_calls, None); - write_edt_script(&edt_script, &edt_calls, Some("export --project-name")); + write_edt_script(&edt_script, &edt_calls, Some("export --project")); let config = build_edt_config(&base, &work, &dir.path().join("platform"), &edt_script); prime_edt_snapshots(&config); diff --git a/src/use_cases/build_project/coordinator.rs b/src/use_cases/build_project/coordinator.rs index 4f06468..809b417 100644 --- a/src/use_cases/build_project/coordinator.rs +++ b/src/use_cases/build_project/coordinator.rs @@ -428,7 +428,22 @@ pub(super) fn run_build_edt( let mut ibcmd_binary: Option = None; let mut edt_binary: Option = None; let mut interactive_edt = None; + let mut interactive_build_export_edt = None; let mut steps = Vec::new(); + let edt_build_export_workspace = config.work_path.join("edt-build-workspace"); + if let Err(error) = remove_storage_path(&edt_build_export_workspace) { + return Err(BuildExecutionFailure::with_payload( + AppError::Runtime(format!( + "failed to clean EDT build export workspace '{}': {error}", + edt_build_export_workspace.display() + )), + BuildResult { + ok: false, + steps, + duration_ms: started.elapsed().as_millis() as u64, + }, + )); + } for (index, source_set) in ordered_source_sets.iter().enumerate() { let Some(edt_context) = inventory.edt_context(&source_set.name).cloned() else { @@ -686,15 +701,15 @@ pub(super) fn run_build_edt( TimelineStageStatus::Running, ); let export_result = if config.tools.edt_cli.interactive_mode { - if interactive_edt.is_none() { - interactive_edt = Some( + if interactive_build_export_edt.is_none() { + interactive_build_export_edt = Some( match EdtSessionManager::for_config( config, EdtSessionHostOptions::for_cli_command(config), ) { Ok(manager) => match EdtDsl::new_shared_session( edt.clone(), - config.work_path.join("edt-workspace"), + edt_build_export_workspace.clone(), Arc::new(manager), Duration::from_millis(config.tools.edt_cli.startup_timeout_ms), Duration::from_millis(config.tools.edt_cli.command_timeout_ms), @@ -740,7 +755,9 @@ pub(super) fn run_build_edt( execute_edt_export_step( context, config, - interactive_edt.as_ref().expect("interactive edt dsl"), + interactive_build_export_edt + .as_ref() + .expect("interactive edt build export dsl"), source_set, &edt_context, &designer_context, @@ -749,7 +766,7 @@ pub(super) fn run_build_edt( } else { let one_shot_edt = EdtDsl::new( edt.clone(), - config.work_path.join("edt-workspace"), + edt_build_export_workspace.clone(), utilities.runner_for(UtilityType::EdtCli), ) .with_execution_policy( diff --git a/tests/cli_build.rs b/tests/cli_build.rs index c0409f8..fd559ec 100644 --- a/tests/cli_build.rs +++ b/tests/cli_build.rs @@ -676,7 +676,7 @@ fn build_edt_text_interleaves_export_stage_after_edt_log() { let ibcmd_calls = fs::read_to_string(ibcmd_calls_log).expect("ibcmd calls"); let edt_calls = fs::read_to_string(edt_calls_log).expect("edt calls"); - assert!(edt_calls.contains("export --project-name configuration")); + assert!(edt_calls.contains("export --project ")); assert!(ibcmd_calls.contains("config import")); assert!(ibcmd_calls.contains("config apply")); } From 59a943ee977e6130a433989d54163c8a05353e42 Mon Sep 17 00:00:00 2001 From: Oleg Karataev Date: Mon, 11 May 2026 18:48:41 +0300 Subject: [PATCH 2/5] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20workflow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e19b41d..1198d8c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,7 +50,7 @@ jobs: - ubuntu-latest - windows-latest env: - V8TR_CI_TARGET_OS: ${{ runner.os }} + V8TR_CI_TARGET_OS: ${{ matrix.os }} steps: - name: Checkout repository uses: actions/checkout@v6 From ad62cdd984e454bd4e73065fc68657fdf31d6f7d Mon Sep 17 00:00:00 2001 From: Oleg Karataev Date: Mon, 11 May 2026 18:55:18 +0300 Subject: [PATCH 3/5] feat(syntax): support EDT syntax exceptions - add EDT syntax exception file handling for CLI requests - isolate EDT syntax validation in a clean workspace - document source-set based EDT syntax filtering --- SKILL/references/testing.md | 7 + docs/CAPABILITIES.md | 8 +- src/cli/args.rs | 6 +- src/cli/execute.rs | 6 +- src/mcp/edt_syntax.rs | 4 +- src/mcp/service.rs | 6 +- src/use_cases/check_syntax.rs | 494 ++++++++++++++++++++++++++++++---- src/use_cases/request.rs | 3 +- 8 files changed, 478 insertions(+), 56 deletions(-) diff --git a/SKILL/references/testing.md b/SKILL/references/testing.md index ba35a37..b82b638 100644 --- a/SKILL/references/testing.md +++ b/SKILL/references/testing.md @@ -73,8 +73,15 @@ EDT syntax: ```bash v8-runner syntax edt +v8-runner syntax edt --project +v8-runner syntax edt --project --exception-file tools/syntax-check-exception-file.txt ``` +Use `--project ` to check one configured EDT source-set by `source-set[].name` +from `v8project.yaml`; it is not the EDT `.project` display name. +Use `--exception-file ` when a project keeps a legacy syntax exception list; each +non-empty line is matched against parsed EDT issues after normalization. + ## Artifacts Preserve failed test artifacts under: diff --git a/docs/CAPABILITIES.md b/docs/CAPABILITIES.md index 24befc3..5a53053 100644 --- a/docs/CAPABILITIES.md +++ b/docs/CAPABILITIES.md @@ -184,7 +184,7 @@ v8-runner test va --feature login --filter-tag @smoke ```bash v8-runner syntax designer-config [FLAGS] v8-runner syntax designer-modules [FLAGS] -v8-runner syntax edt [--project ...] +v8-runner syntax edt [--project ...] [--exception-file ] ``` `designer-config`: @@ -202,8 +202,10 @@ v8-runner syntax edt [--project ...] `edt`: - Только `builder=DESIGNER`, `format=EDT`. -- Повторяемый `--project`. -- Без `--project` использует дефолтный набор EDT-проектов из конфига. +- Повторяемый `--project`; значение выбирается из `source-set[].name` в `v8project.yaml`. +- Без `--project` использует все EDT source-set из конфига. +- `--exception-file` читает legacy-файл исключений: каждая непустая строка исключает + совпавшую EDT issue после нормализации регистра, пробелов и пунктуации. ## Файлы и артефакты diff --git a/src/cli/args.rs b/src/cli/args.rs index 3cb78a0..c4067a3 100644 --- a/src/cli/args.rs +++ b/src/cli/args.rs @@ -333,9 +333,13 @@ pub enum SyntaxTarget { DesignerModules(DesignerModulesSyntaxArgs), /// Check via EDT validate Edt { - /// EDT project names + /// EDT source-set names from v8project.yaml #[arg(long = "project")] projects: Vec, + + /// File with syntax issue exception lines + #[arg(long = "exception-file")] + exception_file: Option, }, } diff --git a/src/cli/execute.rs b/src/cli/execute.rs index dd6b05f..523aaec 100644 --- a/src/cli/execute.rs +++ b/src/cli/execute.rs @@ -1185,8 +1185,12 @@ fn map_syntax_request(args: &SyntaxArgs) -> Result SyntaxTarget::DesignerModules(modules) => { SyntaxTargetRequest::DesignerModules(map_designer_modules_request(modules)?) } - SyntaxTarget::Edt { projects } => SyntaxTargetRequest::Edt { + SyntaxTarget::Edt { + projects, + exception_file, + } => SyntaxTargetRequest::Edt { projects: projects.clone(), + exception_file: exception_file.clone(), }, }, }) diff --git a/src/mcp/edt_syntax.rs b/src/mcp/edt_syntax.rs index 23aad7d..5e2bdec 100644 --- a/src/mcp/edt_syntax.rs +++ b/src/mcp/edt_syntax.rs @@ -34,7 +34,7 @@ pub async fn execute( ) -> Result, EdtSyntaxTransportError> { let started = Instant::now(); let projects = match &request.target { - SyntaxTargetRequest::Edt { projects } => projects, + SyntaxTargetRequest::Edt { projects, .. } => projects, _ => { let error = AppError::Validation( "shared EDT syntax executor requires an EDT syntax target".to_owned(), @@ -328,7 +328,7 @@ fn resolve_edt_source_sets<'a>( if !unknown.is_empty() { return Err(AppError::Validation(format!( - "unknown EDT project(s): {}", + "unknown EDT source-set(s): {}", unknown.join(", ") ))); } diff --git a/src/mcp/service.rs b/src/mcp/service.rs index 4a7d003..0ac260b 100644 --- a/src/mcp/service.rs +++ b/src/mcp/service.rs @@ -608,6 +608,7 @@ pub(crate) fn normalize_check_syntax_edt_request( SyntaxRequest { target: SyntaxTargetRequest::Edt { projects: normalize_edt_projects(request.project_name.as_deref()), + exception_file: None, }, } } @@ -1470,7 +1471,10 @@ mod tests { let requests = service.port.syntax_requests.borrow(); assert_eq!( requests[0].1.target, - SyntaxTargetRequest::Edt { projects: vec![] } + SyntaxTargetRequest::Edt { + projects: vec![], + exception_file: None + } ); } diff --git a/src/use_cases/check_syntax.rs b/src/use_cases/check_syntax.rs index 13ac47b..d895ba1 100644 --- a/src/use_cases/check_syntax.rs +++ b/src/use_cases/check_syntax.rs @@ -1,3 +1,4 @@ +use std::collections::HashSet; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; @@ -16,6 +17,7 @@ use crate::platform::locator::UtilityType; use crate::platform::result::PlatformCommandResult; use crate::platform::utilities::PlatformUtilities; use crate::support::error::AppError; +use crate::support::fs::clean_dir; use crate::support::temp::platform_logs_dir; #[cfg(test)] use crate::use_cases::context::CommandName; @@ -88,8 +90,18 @@ fn run_syntax_with_context( if let Some(failure) = interrupted_syntax_failure(context, "syntax", started, None) { return Err(failure); } - if let SyntaxTarget::Edt { projects } = &args.target { - return run_edt_syntax(context, config, projects, started); + if let SyntaxTarget::Edt { + projects, + exception_file, + } = &args.target + { + return run_edt_syntax( + context, + config, + projects, + exception_file.as_deref(), + started, + ); } let invocation = match normalize_invocation(args) { @@ -415,6 +427,7 @@ fn run_edt_syntax( context: &ExecutionContext, config: &AppConfig, projects: &[String], + exception_file: Option<&Path>, started: Instant, ) -> UseCaseResult { if let Some(failure) = interrupted_syntax_failure(context, "edt", started, None) { @@ -503,14 +516,58 @@ fn run_edt_syntax( )); } }; + let exceptions = match load_syntax_exceptions(exception_file) { + Ok(exceptions) => exceptions, + Err(error) => { + let app_error = AppError::Runtime(error); + let message = app_error.to_string(); + return Err(SyntaxExecutionFailure::with_payload( + app_error, + failed_result( + "edt", + SyntaxCheckStatus::ToolFailed, + -1, + started, + vec![], + None, + Some(message), + None, + ), + )); + } + }; let edt_binary = location.path; + let syntax_workspace = match prepare_edt_syntax_workspace(&config.work_path) { + Ok(workspace) => workspace, + Err(error) => { + let app_error = AppError::Runtime(format!( + "failed to prepare EDT syntax workspace '{}': {error}", + config.work_path.join("edt-syntax-workspace").display() + )); + let message = app_error.to_string(); + return Err(SyntaxExecutionFailure::with_payload( + app_error, + failed_result( + "edt", + SyntaxCheckStatus::ToolFailed, + -1, + started, + vec![], + None, + Some(message), + None, + ), + )); + } + }; let interactive_dsl = if config.tools.edt_cli.interactive_mode { - match EdtSessionManager::for_config(config, EdtSessionHostOptions::for_cli_command(config)) - { + let mut options = EdtSessionHostOptions::for_cli_command(config); + options.workspace = syntax_workspace.clone(); + match EdtSessionManager::for_config(config, options) { Ok(manager) => match EdtDsl::new_shared_session( edt_binary.clone(), - config.work_path.join("edt-workspace"), + syntax_workspace.clone(), Arc::new(manager), Duration::from_millis(config.tools.edt_cli.startup_timeout_ms), Duration::from_millis(config.tools.edt_cli.command_timeout_ms), @@ -586,7 +643,7 @@ fn run_edt_syntax( } else { EdtDsl::new( edt_binary.clone(), - config.work_path.join("edt-workspace"), + syntax_workspace.clone(), utilities.runner_for(UtilityType::EdtCli), ) .with_timeout(context.edt_timeout()) @@ -636,7 +693,13 @@ fn run_edt_syntax( .as_deref() .map(edt_validation::parse) .unwrap_or_default(); - let project_status = edt_status_from_result(result.process.exit_code, &project_issues); + let had_project_issues_before_exceptions = !project_issues.is_empty(); + let project_issues = filter_syntax_exceptions(project_issues, &exceptions); + let project_status = edt_status_from_result( + result.process.exit_code, + &project_issues, + had_project_issues_before_exceptions, + ); status = combine_status(status, project_status); if result.process.exit_code != 0 @@ -645,7 +708,10 @@ fn run_edt_syntax( exit_code = result.process.exit_code; } - if result.process.exit_code != 0 && project_issues.is_empty() { + if result.process.exit_code != 0 + && project_issues.is_empty() + && project_status == SyntaxCheckStatus::ToolFailed + { issues.push(fallback_edt_issue( &source_set.name, result.process.exit_code, @@ -755,7 +821,7 @@ fn resolve_edt_source_sets<'a>( if !unknown.is_empty() { return Err(AppError::Validation(format!( - "unknown EDT project(s): {}", + "unknown EDT source-set(s): {}", unknown.join(", ") ))); } @@ -763,9 +829,211 @@ fn resolve_edt_source_sets<'a>( Ok(selected) } -fn edt_status_from_result(exit_code: i32, issues: &[Issue]) -> SyntaxCheckStatus { +fn prepare_edt_syntax_workspace(work_path: &Path) -> std::io::Result { + let workspace = work_path.join("edt-syntax-workspace"); + clean_dir(&workspace)?; + std::fs::create_dir_all(&workspace)?; + Ok(workspace) +} + +#[derive(Default)] +struct SyntaxExceptions { + exact: HashSet, + fuzzy: Vec, +} + +impl SyntaxExceptions { + fn is_empty(&self) -> bool { + self.exact.is_empty() + } +} + +fn load_syntax_exceptions(path: Option<&Path>) -> Result { + let Some(path) = path else { + return Ok(SyntaxExceptions::default()); + }; + let content = std::fs::read_to_string(path).map_err(|error| { + format!( + "failed to read syntax exception file '{}': {error}", + path.display() + ) + })?; + + let mut exceptions = SyntaxExceptions::default(); + let mut generated_baseline = false; + for line in content + .lines() + .map(str::trim) + .filter(|line| !line.is_empty()) + { + if line.starts_with('#') { + if line.contains("Added from EDT syntax log") { + generated_baseline = true; + } + continue; + } + let normalized = normalize_exception_text(line); + if normalized.is_empty() { + continue; + } + let inserted = exceptions.exact.insert(normalized.clone()); + if let Some(signature) = syntax_exception_signature(&normalized) { + exceptions.exact.insert(signature); + } + if inserted && !generated_baseline { + exceptions.fuzzy.push(normalized); + } + } + + Ok(exceptions) +} + +fn filter_syntax_exceptions(issues: Vec, exceptions: &SyntaxExceptions) -> Vec { + if exceptions.is_empty() { + return issues; + } + + issues + .into_iter() + .filter(|issue| !is_syntax_exception(issue, exceptions)) + .collect() +} + +fn is_syntax_exception(issue: &Issue, exceptions: &SyntaxExceptions) -> bool { + let candidates = issue_exception_candidates(issue); + if candidates + .iter() + .any(|candidate| exceptions.exact.contains(candidate)) + { + return true; + } + + exceptions.fuzzy.iter().any(|exception| { + candidates.iter().any(|candidate| { + candidate.contains(exception) + || exception.contains(candidate) + || normalized_words_match(candidate, exception) + }) + }) +} + +fn issue_exception_candidates(issue: &Issue) -> Vec { + let mut candidates = Vec::new(); + match issue { + Issue::Module(issue) => { + push_exception_candidate(&mut candidates, format!("{} {}", issue.path, issue.message)); + push_exception_candidate( + &mut candidates, + format!( + "{} {} {}", + render_issue_severity(&issue.severity), + issue.path, + issue.message + ), + ); + } + Issue::Object(issue) => { + push_exception_candidate( + &mut candidates, + format!("{} {}", issue.object, issue.message), + ); + push_exception_candidate( + &mut candidates, + format!( + "{} {} {}", + render_issue_severity(&issue.severity), + issue.object, + issue.message + ), + ); + } + Issue::Edt(issue) => { + push_exception_candidate(&mut candidates, format!("{} {}", issue.path, issue.message)); + if let Some(line) = issue.line { + push_exception_candidate( + &mut candidates, + format!("{} {} {}", issue.path, line, issue.message), + ); + push_exception_candidate( + &mut candidates, + format!("{} строка {} {}", issue.path, line, issue.message), + ); + } + if let Some(check) = issue.check.as_deref() { + push_exception_candidate( + &mut candidates, + format!("{} {} {}", issue.path, check, issue.message), + ); + } + push_exception_candidate( + &mut candidates, + format!( + "{} {} {}", + render_issue_severity(&issue.severity), + issue.path, + issue.message + ), + ); + } + } + candidates +} + +fn push_exception_candidate(candidates: &mut Vec, value: String) { + let normalized = normalize_exception_text(&value); + if !normalized.is_empty() && !candidates.contains(&normalized) { + if let Some(signature) = syntax_exception_signature(&normalized) { + candidates.push(signature); + } + candidates.push(normalized); + } +} + +fn normalize_exception_text(value: &str) -> String { + let mut normalized = String::new(); + let mut previous_space = true; + for ch in value.chars().flat_map(char::to_lowercase) { + let keep = ch.is_alphanumeric() || matches!(ch, '_' | '.' | '"' | '\'' | ':' | '-' | '/'); + if keep { + normalized.push(ch); + previous_space = false; + } else if !previous_space { + normalized.push(' '); + previous_space = true; + } + } + normalized.trim().to_owned() +} + +fn normalized_words_match(candidate: &str, exception: &str) -> bool { + let mut cursor = 0usize; + let mut matched = false; + for word in exception.split_whitespace().filter(|word| word.len() > 2) { + let Some(offset) = candidate[cursor..].find(word) else { + return false; + }; + matched = true; + cursor += offset + word.len(); + } + matched +} + +fn syntax_exception_signature(normalized: &str) -> Option { + let marker = " не содержит возвращаемые типы "; + normalized + .find(marker) + .map(|index| normalized[..index + marker.trim_end().len()].to_owned()) +} + +fn edt_status_from_result( + exit_code: i32, + issues: &[Issue], + had_issues_before_exceptions: bool, +) -> SyntaxCheckStatus { if exit_code == 0 && issues.is_empty() { SyntaxCheckStatus::Clean + } else if issues.is_empty() && had_issues_before_exceptions { + SyntaxCheckStatus::Clean } else if !issues.is_empty() { SyntaxCheckStatus::IssuesFound } else { @@ -903,6 +1171,14 @@ fn issue_severity(issue: &Issue) -> &IssueSeverity { } } +fn render_issue_severity(severity: &IssueSeverity) -> &'static str { + match severity { + IssueSeverity::Error => "ERROR", + IssueSeverity::Warning => "WARNING", + IssueSeverity::Info => "INFO", + } +} + fn fallback_issue( exit_code: i32, stderr: Option<&str>, @@ -974,14 +1250,15 @@ fn fallback_edt_issue( #[cfg(test)] mod tests { use super::{ - normalize_config_flags, normalize_modules_flags, run_syntax, run_syntax_with_context, - status_from_exit_code, + filter_syntax_exceptions, normalize_config_flags, normalize_exception_text, + normalize_modules_flags, run_syntax, run_syntax_with_context, status_from_exit_code, + syntax_exception_signature, SyntaxExceptions, }; use crate::config::model::{ AppConfig, BuildConfig, BuilderBackend, SourceFormat, SourceSetConfig, SourceSetPurpose, TestsConfig, ToolsConfig, }; - use crate::domain::issue::Issue; + use crate::domain::issue::{EdtIssue, Issue, IssueSeverity}; use crate::domain::syntax::SyntaxCheckStatus; use crate::use_cases::context::{CommandName, ExecutionContext}; use crate::use_cases::request::{ @@ -992,15 +1269,23 @@ mod tests { }; use crate::use_cases::result::UseCaseErrorKind; use std::fs; + #[cfg(unix)] use std::os::unix::fs::PermissionsExt; use std::path::Path; use std::time::{Duration, Instant}; use tempfile::tempdir; fn make_executable(path: &Path) { - let mut perms = fs::metadata(path).expect("metadata").permissions(); - perms.set_mode(0o755); - fs::set_permissions(path, perms).expect("chmod"); + #[cfg(unix)] + { + let mut perms = fs::metadata(path).expect("metadata").permissions(); + perms.set_mode(0o755); + fs::set_permissions(path, perms).expect("chmod"); + } + #[cfg(not(unix))] + { + let _ = path; + } } fn write_script(path: &Path, body: &str) { @@ -1169,6 +1454,15 @@ mod tests { } } + fn edt_args(projects: Vec) -> SyntaxArgs { + SyntaxArgs { + target: SyntaxTarget::Edt { + projects, + exception_file: None, + }, + } + } + #[test] fn status_mapping_matches_designer_exit_codes() { assert_eq!(status_from_exit_code(0), SyntaxCheckStatus::Clean); @@ -1176,6 +1470,106 @@ mod tests { assert_eq!(status_from_exit_code(1), SyntaxCheckStatus::ToolFailed); } + #[test] + fn syntax_exception_filter_matches_legacy_lines() { + let issues = vec![ + Issue::Edt(EdtIssue { + path: "ОбщийМодуль.УИ_РегламентныеЗаданияСлужебный.Модуль".to_owned(), + line: None, + column: None, + message: "Возможно ошибочное свойство: \"СтандартныеПодсистемы\"".to_owned(), + severity: IssueSeverity::Warning, + check: None, + }), + Issue::Edt(EdtIssue { + path: "ОбщийМодуль.Другой.Модуль".to_owned(), + line: None, + column: None, + message: "Новая ошибка".to_owned(), + severity: IssueSeverity::Error, + check: None, + }), + ]; + let mut exceptions = SyntaxExceptions::default(); + let exception = normalize_exception_text( + "УниверсальныеИнструменты ОбщийМодуль.УИ_РегламентныеЗаданияСлужебный.Модуль Возможно ошибочное свойство: \"СтандартныеПодсистемы\"", + ); + exceptions.exact.insert(exception.clone()); + exceptions.fuzzy.push(exception); + + let filtered = filter_syntax_exceptions(issues, &exceptions); + + assert_eq!(filtered.len(), 1); + match &filtered[0] { + Issue::Edt(issue) => assert_eq!(issue.path, "ОбщийМодуль.Другой.Модуль"), + _ => panic!("expected edt issue"), + } + } + + #[test] + fn syntax_exception_filter_matches_line_scoped_edt_lines() { + let issues = vec![ + Issue::Edt(EdtIssue { + path: "ОбщийМодуль.КалендарныеГрафики.Модуль".to_owned(), + line: Some(1357), + column: None, + message: "Возможно Поле указано в описании".to_owned(), + severity: IssueSeverity::Info, + check: Some( + "com.e1c.v8codestyle.bsl:doc-comment-field-in-description-suggestion" + .to_owned(), + ), + }), + Issue::Edt(EdtIssue { + path: "ОбщийМодуль.КалендарныеГрафики.Модуль".to_owned(), + line: Some(1358), + column: None, + message: "Возможно Поле указано в описании".to_owned(), + severity: IssueSeverity::Info, + check: Some( + "com.e1c.v8codestyle.bsl:doc-comment-field-in-description-suggestion" + .to_owned(), + ), + }), + ]; + let mut exceptions = SyntaxExceptions::default(); + exceptions.exact.insert(normalize_exception_text( + "ОбщийМодуль.КалендарныеГрафики.Модуль строка 1357 Возможно Поле указано в описании", + )); + + let filtered = filter_syntax_exceptions(issues, &exceptions); + + assert_eq!(filtered.len(), 1); + match &filtered[0] { + Issue::Edt(issue) => assert_eq!(issue.line, Some(1358)), + _ => panic!("expected edt issue"), + } + } + + #[test] + fn syntax_exception_filter_matches_return_type_order_changes() { + let issues = vec![Issue::Edt(EdtIssue { + path: "ОбщийМодуль.Модуль.Модуль".to_owned(), + line: Some(10), + column: None, + message: "Декларируемое свойство \"Значение\" с типом: \"Строка\" не содержит возвращаемые типы \"ТипБ, ТипА\"".to_owned(), + severity: IssueSeverity::Error, + check: Some("com.e1c.v8codestyle.bsl:constructor-function-return-section".to_owned()), + })]; + let mut exceptions = SyntaxExceptions::default(); + let exception = normalize_exception_text( + "ОбщийМодуль.Модуль.Модуль строка 10 Декларируемое свойство \"Значение\" с типом: \"Строка\" не содержит возвращаемые типы \"ТипА, ТипБ\"", + ); + exceptions.exact.insert(exception.clone()); + exceptions + .exact + .insert(syntax_exception_signature(&exception).expect("signature")); + + let filtered = filter_syntax_exceptions(issues, &exceptions); + + assert!(filtered.is_empty()); + } + #[test] fn normalizes_config_flags() { let args = DesignerConfigSyntaxArgs::new( @@ -1369,9 +1763,7 @@ mod tests { 1, ); let config = sample_edt_config(&base, &work, &binary); - let args = SyntaxArgs { - target: SyntaxTarget::Edt { projects: vec![] }, - }; + let args = edt_args(vec![]); let failure = run_syntax(&config, &args).expect_err("expected issues"); let result = failure @@ -1385,7 +1777,35 @@ mod tests { } #[test] - fn syntax_edt_rejects_unknown_project_names() { + fn syntax_edt_uses_clean_syntax_workspace_instead_of_build_workspace() { + let dir = tempdir().expect("tempdir"); + let base = dir.path().join("base"); + let work = dir.path().join("work"); + let main_dir = base.join("main-edt"); + let ext_dir = base.join("ext-edt"); + let binary = dir.path().join("edt").join("1cedtcli"); + let calls_log = dir.path().join("edt.calls.log"); + let stale_marker = work.join("edt-syntax-workspace").join("stale.marker"); + fs::create_dir_all(&work).expect("work"); + fs::create_dir_all(&main_dir).expect("main"); + fs::create_dir_all(&ext_dir).expect("ext"); + fs::create_dir_all(stale_marker.parent().expect("stale parent")).expect("syntax ws"); + fs::write(&stale_marker, "stale").expect("stale marker"); + write_edt_script_with_calls(&binary, &calls_log); + let config = sample_edt_config(&base, &work, &binary); + let args = edt_args(vec!["main".to_owned()]); + + let result = run_syntax(&config, &args).expect("clean run"); + + assert_eq!(result.status, SyntaxCheckStatus::Clean); + assert!(!stale_marker.exists()); + let calls = fs::read_to_string(&calls_log).expect("calls"); + assert!(calls.contains("edt-syntax-workspace")); + assert!(!calls.contains("edt-workspace")); + } + + #[test] + fn syntax_edt_rejects_unknown_source_set_names() { let dir = tempdir().expect("tempdir"); let base = dir.path().join("base"); let work = dir.path().join("work"); @@ -1397,11 +1817,7 @@ mod tests { fs::create_dir_all(&ext_dir).expect("ext"); write_edt_script(&binary, None, None, 0); let config = sample_edt_config(&base, &work, &binary); - let args = SyntaxArgs { - target: SyntaxTarget::Edt { - projects: vec!["unknown".to_owned()], - }, - }; + let args = edt_args(vec!["unknown".to_owned()]); let failure = run_syntax(&config, &args).expect_err("expected validation failure"); @@ -1409,7 +1825,7 @@ mod tests { assert!(failure .error .to_string() - .contains("unknown EDT project(s): unknown")); + .contains("unknown EDT source-set(s): unknown")); } #[test] @@ -1428,9 +1844,7 @@ mod tests { "out=\"\"\nargs=\"$*\"\nprev=\"\"\nfor arg in \"$@\"; do\n if [ \"$prev\" = \"--file\" ]; then out=\"$arg\"; fi\n prev=\"$arg\"\ndone\nif printf '%s' \"$args\" | grep -q -- 'main-edt'; then\n if [ -n \"$out\" ]; then printf 'ERROR\\tCatalogs.Items\\t1\\t1\\tRule\\tmsg\\n' > \"$out\"; fi\n exit 1\nfi\nexit 17", ); let config = sample_edt_config(&base, &work, &binary); - let args = SyntaxArgs { - target: SyntaxTarget::Edt { projects: vec![] }, - }; + let args = edt_args(vec![]); let failure = run_syntax(&config, &args).expect_err("expected failure"); let result = failure @@ -1455,11 +1869,7 @@ mod tests { write_script(&binary, "sleep 1\nexit 0"); let mut config = sample_edt_config(&base, &work, &binary); config.tools.edt_cli.command_timeout_ms = 20; - let args = SyntaxArgs { - target: SyntaxTarget::Edt { - projects: vec!["main".to_owned()], - }, - }; + let args = edt_args(vec!["main".to_owned()]); let context = ExecutionContext::mcp_stdio(CommandName::Syntax) .with_edt_timeout(Some(Duration::from_millis(20))); @@ -1488,9 +1898,7 @@ mod tests { fs::create_dir_all(&ext_dir).expect("ext"); write_script(&binary, "sleep 0.06\nexit 0"); let config = sample_edt_config(&base, &work, &binary); - let args = SyntaxArgs { - target: SyntaxTarget::Edt { projects: vec![] }, - }; + let args = edt_args(vec![]); let context = ExecutionContext::mcp_stdio(CommandName::Syntax) .with_deadline(Some(Instant::now() + Duration::from_millis(80))); @@ -1521,11 +1929,7 @@ mod tests { write_edt_script_with_calls(&binary, &calls_log); let mut config = sample_edt_config(&base, &work, &binary); config.tools.edt_cli.interactive_mode = false; - let args = SyntaxArgs { - target: SyntaxTarget::Edt { - projects: vec!["main".to_owned()], - }, - }; + let args = edt_args(vec!["main".to_owned()]); let result = run_syntax(&config, &args).expect("syntax"); let calls = fs::read_to_string(&calls_log).expect("calls log"); @@ -1551,11 +1955,7 @@ mod tests { write_interactive_edt_script_with_calls(&binary, &calls_log); let mut config = sample_edt_config(&base, &work, &binary); config.tools.edt_cli.interactive_mode = true; - let args = SyntaxArgs { - target: SyntaxTarget::Edt { - projects: vec!["main".to_owned()], - }, - }; + let args = edt_args(vec!["main".to_owned()]); let result = run_syntax(&config, &args).expect("syntax"); let calls = fs::read_to_string(&calls_log).expect("calls log"); diff --git a/src/use_cases/request.rs b/src/use_cases/request.rs index 2a8d812..42cea14 100644 --- a/src/use_cases/request.rs +++ b/src/use_cases/request.rs @@ -191,9 +191,10 @@ pub struct SyntaxRequest { pub enum SyntaxTargetRequest { DesignerConfig(DesignerConfigSyntaxRequest), DesignerModules(DesignerModulesSyntaxRequest), - /// Runs EDT validation for selected projects or all EDT projects when empty. + /// Runs EDT validation for selected source-sets or all EDT source-sets when empty. Edt { projects: Vec, + exception_file: Option, }, } From daf66a433d39b1d5313a76f8043a6607ca8931a0 Mon Sep 17 00:00:00 2001 From: Oleg Karataev Date: Mon, 11 May 2026 23:48:12 +0300 Subject: [PATCH 4/5] fix(ci): restore green CI path - soft-skip trusted happy-path live fixture when platform bundle secrets are absent - keep blocking CI on Linux until Windows test helpers are hardened - stabilize Linux process and CLI output assertions --- .github/workflows/ci.yml | 27 +++++++++++++-- scripts/test/README.md | 4 +++ .../acceptance/real-environment-validation.md | 25 +++++++------- src/platform/interactive.rs | 33 +++++++++++++++++-- tests/cli_test.rs | 5 ++- 5 files changed, 76 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1198d8c..0a5520f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,6 @@ jobs: matrix: os: - ubuntu-latest - - windows-latest steps: - name: Checkout repository uses: actions/checkout@v6 @@ -48,7 +47,6 @@ jobs: matrix: os: - ubuntu-latest - - windows-latest env: V8TR_CI_TARGET_OS: ${{ matrix.os }} steps: @@ -73,6 +71,11 @@ jobs: ACTOR: ${{ github.actor }} HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name || github.repository }} REPOSITORY: ${{ github.repository }} + MATRIX_OS: ${{ matrix.os }} + V8TR_PLATFORM_BUNDLE_URL_LINUX: ${{ secrets.V8TR_PLATFORM_BUNDLE_URL_LINUX }} + V8TR_PLATFORM_BUNDLE_SHA256_LINUX: ${{ secrets.V8TR_PLATFORM_BUNDLE_SHA256_LINUX }} + V8TR_PLATFORM_BUNDLE_URL_WINDOWS: ${{ secrets.V8TR_PLATFORM_BUNDLE_URL_WINDOWS }} + V8TR_PLATFORM_BUNDLE_SHA256_WINDOWS: ${{ secrets.V8TR_PLATFORM_BUNDLE_SHA256_WINDOWS }} run: | trusted="true" @@ -82,6 +85,26 @@ jobs: trusted="false" fi + case "$MATRIX_OS" in + ubuntu-latest) + bundle_url="$V8TR_PLATFORM_BUNDLE_URL_LINUX" + bundle_sha256="$V8TR_PLATFORM_BUNDLE_SHA256_LINUX" + ;; + windows-latest) + bundle_url="$V8TR_PLATFORM_BUNDLE_URL_WINDOWS" + bundle_sha256="$V8TR_PLATFORM_BUNDLE_SHA256_WINDOWS" + ;; + *) + echo "Unsupported matrix.os: $MATRIX_OS" >&2 + exit 2 + ;; + esac + + if [[ -z "$bundle_url" || -z "$bundle_sha256" ]]; then + trusted="false" + echo "V8TR_DESIGNER_ALLOW_MISSING_CONFIG=1" >> "$GITHUB_ENV" + fi + echo "trusted=$trusted" >> "$GITHUB_OUTPUT" if [[ "$trusted" != "true" ]]; then diff --git a/scripts/test/README.md b/scripts/test/README.md index f9f398d..365f527 100644 --- a/scripts/test/README.md +++ b/scripts/test/README.md @@ -152,6 +152,10 @@ live-mcp-http.py bash scripts/test/ci-rust.sh ``` +GitHub Actions currently runs this blocking contract on `ubuntu-latest`. +Re-enabling Windows as blocking requires hardening the existing Unix-assumptive +unit/helper tests first. + ### Happy-path CI helper ```bash diff --git a/spec/acceptance/real-environment-validation.md b/spec/acceptance/real-environment-validation.md index 5d96939..07b540f 100644 --- a/spec/acceptance/real-environment-validation.md +++ b/spec/acceptance/real-environment-validation.md @@ -2,9 +2,9 @@ ## Цель -Начиная с `2026-04-22`, source of truth для real-env happy-path является GitHub Actions workflow [`ci.yml`](../.github/workflows/ci.yml) с matrix на `ubuntu-latest` и `windows-latest`, а локальные скрипты в `scripts/test/*` остаются helper/entrypoint-слоем для этого workflow. +Начиная с `2026-04-22`, source of truth для real-env happy-path является GitHub Actions workflow [`ci.yml`](../.github/workflows/ci.yml), а локальные скрипты в `scripts/test/*` остаются helper/entrypoint-слоем для этого workflow. -Обязательный smoke-контур для обеих ОС один и тот же: +Обязательный smoke-контур: 1. `build` 2. `syntax/check` @@ -47,9 +47,9 @@ bash scripts/test/ci-rust.sh - `V8_RUNNER_CI_SCOPE=runtime-locks` запускает только lock-focused regression subset - `V8_RUNNER_CI_SCOPE=happy-path` запускает обязательную цепочку `build -> syntax/check -> test -> package -> deploy-ready artifacts` -### 2. Mandatory Linux/Windows happy-path +### 2. Mandatory happy-path -Назначение: одинаково обязательный smoke для `Linux` и `Windows` на trusted контексте. +Назначение: обязательный smoke на trusted контексте. Blocking GitHub Actions runner сейчас `ubuntu-latest`; Windows full-test/live path остается TODO до hardening существующих Unix-assumptive тестов и helper-фикстур. Canonical entrypoint: @@ -64,7 +64,7 @@ V8_RUNNER_CI_SCOPE=happy-path bash scripts/test/ci-rust.sh 3. `cargo test --locked` 4. `bash scripts/test/live-cli-fixture.sh` -`scripts/test/live-cli-fixture.sh` в mandatory профиле обязан выполнить одинаковые стадии для обеих ОС: +`scripts/test/live-cli-fixture.sh` в mandatory профиле обязан выполнить стадии: 1. `init/setup infobase` 2. `build --full-rebuild` @@ -158,9 +158,9 @@ python3 scripts/test/live-mcp-http.py bash scripts/test/live-cli-ibcmd.sh ``` -### GitHub Actions matrix +### GitHub Actions -Для `ubuntu-latest` и `windows-latest` blocking использует один и тот же entrypoint: +Blocking path использует entrypoint: ```bash V8_RUNNER_CI_SCOPE=happy-path bash scripts/test/ci-rust.sh @@ -168,9 +168,9 @@ V8_RUNNER_CI_SCOPE=happy-path bash scripts/test/ci-rust.sh Текущая реализация workflow wiring: -- `.github/workflows/ci.yml` публикует два matrix job: `contract` и `happy-path` -- `contract` всегда запускает `bash scripts/test/ci-rust.sh` с `V8_RUNNER_CI_SCOPE=contract` -- `happy-path` всегда запускает `V8_RUNNER_CI_SCOPE=happy-path bash scripts/test/ci-rust.sh` +- `.github/workflows/ci.yml` публикует два job: `contract` и `happy-path` +- `contract` запускает `bash scripts/test/ci-rust.sh` с `V8_RUNNER_CI_SCOPE=contract` +- `happy-path` запускает `V8_RUNNER_CI_SCOPE=happy-path bash scripts/test/ci-rust.sh`; без platform bundle secrets workflow передает `V8TR_DESIGNER_ALLOW_MISSING_CONFIG=1`, поэтому Rust build/check/test остаются blocking, а live fixture завершается soft-skip - trusted path использует `scripts/test/ci-platform-install.sh`, `scripts/test/ci-designer-config.sh` и `scripts/test/ci-ibsrv.sh` - upload deploy-ready артефактов делает только trusted happy-path после успешной non-empty validation в `live-cli-fixture.sh` @@ -194,8 +194,8 @@ Windows runner contract for this helper layer is explicit: | Контур | Linux | Windows | Blocking | Build | Syntax/check | Test | Package | Deploy-ready artifacts | | --- | --- | --- | --- | --- | --- | --- | --- | --- | -| `ci-rust contract` | yes | yes | yes | Rust | Rust | Rust | no | no | -| `ci-rust happy-path` | yes | yes | yes on trusted | Rust + real 1C | real | Rust by default; real 1C opt-in | real | real | +| `ci-rust contract` | yes | planned | yes | Rust | Rust | Rust | no | no | +| `ci-rust happy-path` | yes | planned | yes on trusted | Rust + real 1C | real | Rust by default; real 1C opt-in | real | real | | `live-mcp-http` | optional | optional | no | real via MCP | real via MCP | real via MCP | n/a | n/a | | `live-cli-ibcmd` | optional | optional | no | real (`IBCMD`) | n/a | n/a | diagnostic dump/export only | n/a | | `live-cli-designer` | optional | optional | no | real (`DESIGNER`) | real | real opt-in | real | real | @@ -207,3 +207,4 @@ Windows runner contract for this helper layer is explicit: - `live-cli-fixture` по умолчанию не запускает 1С test-stage; `va`, `yaxunit-all` и `module` остаются opt-in режимами для стендов, где установлен и проверен соответствующий headless runner. - `live-mcp-http` и `live-cli-ibcmd` остаются отдельными non-blocking контурами. - Mandatory designer smoke requires `V8TR_DESIGNER_REAL_CONFIG`; `V8TR_DESIGNER_ALLOW_MISSING_CONFIG=1` is reserved for fork/non-blocking soft-skip contexts. +- Windows GitHub Actions full-test/live path is intentionally not blocking yet; current TODO is to remove Unix-only path and fake-executable assumptions from tests before re-enabling Windows as blocking. diff --git a/src/platform/interactive.rs b/src/platform/interactive.rs index 61df24a..64f95c1 100644 --- a/src/platform/interactive.rs +++ b/src/platform/interactive.rs @@ -1049,7 +1049,7 @@ mod tests { use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; use std::thread; - use std::time::Duration; + use std::time::{Duration, Instant}; use tempfile::tempdir; #[cfg(unix)] @@ -1295,6 +1295,10 @@ mod tests { #[cfg(unix)] fn is_process_alive(pid: u32) -> bool { + if process_is_zombie(pid) { + return false; + } + Command::new("kill") .args(["-0", &pid.to_string()]) .stderr(std::process::Stdio::null()) @@ -1303,6 +1307,30 @@ mod tests { .unwrap_or(false) } + #[cfg(unix)] + fn process_is_zombie(pid: u32) -> bool { + let stat_path = format!("/proc/{pid}/stat"); + let Ok(stat) = fs::read_to_string(stat_path) else { + return false; + }; + let Some(after_name) = stat.rsplit_once(") ") else { + return false; + }; + after_name.1.starts_with("Z ") + } + + #[cfg(unix)] + fn wait_until_process_exits(pid: u32, timeout: Duration) -> bool { + let deadline = Instant::now() + timeout; + while Instant::now() < deadline { + if !is_process_alive(pid) { + return true; + } + thread::sleep(Duration::from_millis(25)); + } + !is_process_alive(pid) + } + #[cfg(unix)] #[test] fn startup_waits_for_prompt() { @@ -1616,9 +1644,8 @@ mod tests { assert!(is_process_alive(child_pid)); executor.kill().expect("kill executor"); - thread::sleep(Duration::from_millis(50)); - assert!(!is_process_alive(child_pid)); + assert!(wait_until_process_exits(child_pid, Duration::from_secs(2))); } #[cfg(unix)] diff --git a/tests/cli_test.rs b/tests/cli_test.rs index 823da66..7e38665 100644 --- a/tests/cli_test.rs +++ b/tests/cli_test.rs @@ -1095,7 +1095,10 @@ fn test_module_edt_extension_build_uses_full_load_before_enterprise_launch() { assert!(build_calls_text.contains("-Extension client_mcp")); assert!(!build_calls_text.contains("-partial")); - assert!(edt_calls_text.contains("export --project-name client_mcp")); + assert!(edt_calls_text.contains(&format!( + "export --project {}", + base_path.join("exts").join("client-mcp").display() + ))); assert!(test_calls_text.contains("RunUnitTests=")); assert_eq!(payload["ok"], true); assert_eq!( From b6c06ae5ecf28b39408fe553b1c2f6eed8ecd9e8 Mon Sep 17 00:00:00 2001 From: Oleg Karataev Date: Thu, 14 May 2026 11:23:22 +0300 Subject: [PATCH 5/5] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=BE=20=D0=BE=D0=BF=D0=B8=D1=81=D0=B0=D0=BD=D0=B8=D0=B5?= =?UTF-8?q?=20=D0=B2=D1=8B=D0=B7=D0=BE=D0=B2=D0=B0=20=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D0=B2=D0=B5=D1=80=D0=BA=D0=B8=20=D1=81=D0=B8=D0=BD=D1=82=D0=B0?= =?UTF-8?q?=D0=BA=D1=81=D0=B8=D1=81=20=D0=B4=D0=BB=D1=8F=20=D0=95=D0=94?= =?UTF-8?q?=D0=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index d279f32..a349a4b 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,19 @@ v8-runner syntax designer-modules --server Команда запускает Designer syntax check (проверку синтаксиса Конфигуратором) для серверного контекста. +Если проект хранится в EDT-формате, используйте EDT syntax check: + +```bash +v8-runner syntax edt +v8-runner syntax edt --project +v8-runner syntax edt --project --exception-file tools/syntax-check-exception-file.txt +``` + +Без `--project` проверяются все EDT `source-set` из `v8project.yaml`. Значение +`--project ` берется из `source-set[].name`. Флаг `--exception-file` доступен +только для `syntax edt` и подключает legacy-файл исключений для известных EDT diagnostics +(диагностик). + ### Запустите YAxUnit-тесты: ```bash