diff --git a/.claude/skills/cfe-patch-method/SKILL.md b/.claude/skills/cfe-patch-method/SKILL.md index 3cc4915cb..94e43461f 100644 --- a/.claude/skills/cfe-patch-method/SKILL.md +++ b/.claude/skills/cfe-patch-method/SKILL.md @@ -107,6 +107,8 @@ allowed-tools: Повторный вызов `Before`/`After`/`Instead` для уже перехваченного метода дубль не создаёт (`[ПРОПУЩЕН]`). +`-Check` смотрит исходники. Вердикт платформы — уже после загрузки в базу: `/db-cfe-admin check`. + ## Команда ```powershell diff --git a/.claude/skills/cfe-validate/SKILL.md b/.claude/skills/cfe-validate/SKILL.md index 7b14c124a..7440680bf 100644 --- a/.claude/skills/cfe-validate/SKILL.md +++ b/.claude/skills/cfe-validate/SKILL.md @@ -12,6 +12,8 @@ allowed-tools: Проверяет структурную корректность расширения: XML-формат, свойства, состав, заимствованные объекты, права ролей. Аналог `/cf-validate`, но для расширений. +Проверяются исходники. Применимость — уже после загрузки в базу: `/db-cfe-admin check`. + ## Параметры | Параметр | Обяз. | Умолч. | Описание | diff --git a/.claude/skills/db-cfe-admin/scripts/db-cfe-admin.ps1 b/.claude/skills/db-cfe-admin/scripts/db-cfe-admin.ps1 index afe49dfea..63996d563 100644 --- a/.claude/skills/db-cfe-admin/scripts/db-cfe-admin.ps1 +++ b/.claude/skills/db-cfe-admin/scripts/db-cfe-admin.ps1 @@ -858,6 +858,9 @@ if ($cmd -eq 'check') { $done = 0 $rows = @() + # Тот же вопрос платформе задаёт постусловие загрузчиков (семья Invoke-ApplyCheck в + # db-load-xml): там ответ нужен одной строкой предупреждения, здесь — строкой отчёта + # рядом с остальными проверками, поэтому запуск идёт своим Invoke-Designer. if ($checkList -contains 'apply') { $opArgs = @('/CheckCanApplyConfigurationExtensions') if ($target) { $opArgs += '-Extension', "`"$target`"" } diff --git a/.claude/skills/db-cfe-admin/scripts/db-cfe-admin.py b/.claude/skills/db-cfe-admin/scripts/db-cfe-admin.py index 151f43f75..c62d459a6 100644 --- a/.claude/skills/db-cfe-admin/scripts/db-cfe-admin.py +++ b/.claude/skills/db-cfe-admin/scripts/db-cfe-admin.py @@ -880,6 +880,9 @@ def main(): done = 0 rows = [] + # Тот же вопрос платформе задаёт постусловие загрузчиков (семья run_apply_check в + # db-load-xml): там ответ нужен одной строкой предупреждения, здесь — строкой отчёта + # рядом с остальными проверками, поэтому запуск идёт своим invoke_designer. if "apply" in check_list: op_args = ["/CheckCanApplyConfigurationExtensions"] if target: diff --git a/.claude/skills/db-list/SKILL.md b/.claude/skills/db-list/SKILL.md index 4c6741739..896109874 100644 --- a/.claude/skills/db-list/SKILL.md +++ b/.claude/skills/db-list/SKILL.md @@ -77,6 +77,7 @@ allowed-tools: | `v8args` | array | Доп. аргументы запуска `1cv8.exe` для всех навыков, напр. `["/UseHwLicenses+"]` | | `ibcmdargs` | array | Доп. аргументы `ibcmd` (форма `--ключ=значение`) | | `newObjectPosition` | `"end"` / `"byName"` | Куда попадает новый объект в составе конфигурации: в конец своего вида (по умолчанию) или на место по имени. Переопределяется в `databases[]` | +| `extensionApplyCheck` | bool | Проверять ли применимость расширения после загрузки в базу (по умолчанию `true`). Разово отключается ключом `-NoApplyCheck` | | `databases` | array | Массив баз данных | | `default` | string | id базы по умолчанию | diff --git a/.claude/skills/db-load-cf/SKILL.md b/.claude/skills/db-load-cf/SKILL.md index 6c25533b7..d9ba9766e 100644 --- a/.claude/skills/db-load-cf/SKILL.md +++ b/.claude/skills/db-load-cf/SKILL.md @@ -51,6 +51,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" <п | `-Password <пароль>` | нет | Пароль | | `-InputFile <путь>` | да | Путь к CF-файлу | | `-Extension <имя>` | нет | Загрузить как расширение | +| `-NoApplyCheck` | нет | Не проверять применимость расширения после загрузки | | `-AllExtensions` | нет | Загрузить все расширения из архива | | `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` | | `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` | diff --git a/.claude/skills/db-load-cf/scripts/db-load-cf.ps1 b/.claude/skills/db-load-cf/scripts/db-load-cf.ps1 index a61e56347..ce6b98e4a 100644 --- a/.claude/skills/db-load-cf/scripts/db-load-cf.ps1 +++ b/.claude/skills/db-load-cf/scripts/db-load-cf.ps1 @@ -78,6 +78,16 @@ param( [Parameter(Mandatory=$false)] [switch]$AllExtensions, + [Parameter(Mandatory=$false)] + # Ключ для регрессов и верификации снапшотов, не для повседневного вызова: в SKILL.md + # намеренно не выносится. Поднимает код возврата, если платформа отчиталась об успехе, + # но расширение при этом неприменимо. + [switch]$StrictLog, + + # Пропустить проверку применимости расширения после загрузки. + [Parameter(Mandatory=$false)] + [switch]$NoApplyCheck, + [Parameter(Mandatory=$false)] [string[]]$AdditionalV8Arguments = @(), @@ -403,6 +413,85 @@ function Write-PlatformOutput { } +function Find-V8Project([string]$startDir) { + $d = $startDir + for ($i = 0; $i -lt 20 -and $d; $i++) { + $pj = Join-Path $d ".v8-project.json" + if (Test-Path $pj) { return $pj } + $parent = [System.IO.Path]::GetDirectoryName($d) + if ($parent -eq $d) { break } + $d = $parent + } + return $null +} + +# Постусловие применимости расширения: платформа отчитывается успехом и о расширении, которое +# не применит — отказ всплывает лениво, при первом вызове метода, записью в журнал регистрации. +# +# Запуск ОБЯЗАТЕЛЬНО отдельный. Дописать эту команду в строку операции нельзя: в одной командной +# строке DESIGNER выполняет только ПОСЛЕДНЮЮ пакетную команду, остальные молча отбрасывает — +# проверено на 8.3.24, /LoadConfigFromFiles вместе с /CheckCanApplyConfigurationExtensions +# завершились кодом 0 с пустым логом, и загрузка не состоялась. +# +# Проверку умеет только 1cv8; если навык работал через ibcmd, берём соседний исполняемый файл. +function Invoke-ApplyCheck { + param([string]$Exe, [string[]]$ConnArgs, [string]$Extension, [string[]]$ExtraArgs) + $exeDir = Split-Path $Exe -Parent + $exeLeaf = Split-Path $Exe -Leaf + $v8 = if ($exeLeaf -match '^ibcmd') { Join-Path $exeDir ("1cv8" + [System.IO.Path]::GetExtension($Exe)) } else { $Exe } + if (-not (Test-Path $v8)) { return @{ Skipped = $true; Reason = "1cv8 not found at $v8"; ExitCode = 0; Lines = @() } } + $dir = Join-Path $env:TEMP "apply_check_$(Get-Random)" + New-Item -ItemType Directory -Path $dir -Force | Out-Null + try { + $a = @("DESIGNER") + $ConnArgs + @("/CheckCanApplyConfigurationExtensions") + if ($Extension) { $a += "-Extension", "`"$Extension`"" } + $outFile = Join-Path $dir "check_log.txt" + $a += "/Out", "`"$outFile`"", "/DisableStartupDialogs" + $a += $ExtraArgs + $res = Invoke-PlatformProcess $v8 $a -PreQuoted + $lines = @() + if (Test-Path $outFile) { + $raw = Get-Content $outFile -Raw -ErrorAction SilentlyContinue + if ($raw) { $lines = @($raw -split "`r?`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' }) } + } + return @{ Skipped = $false; Reason = ''; ExitCode = $res.ExitCode; Lines = $lines } + } finally { + if (Test-Path $dir) { Remove-Item -Path $dir -Recurse -Force -ErrorAction SilentlyContinue } + } +} + +# Проверить и напечатать. $true, если платформа расширение не применит — вызывающий решает, +# поднимать ли код возврата (строгий режим). +function Invoke-ApplyCheckReport { + param([string]$Exe, [string[]]$ConnArgs, [string]$Extension, [string[]]$ExtraArgs) + $ac = Invoke-ApplyCheck $Exe $ConnArgs $Extension $ExtraArgs + if ($ac.Skipped) { + Write-Host "[note] applicability check skipped: $($ac.Reason)" -ForegroundColor Yellow + return $false + } + if ($ac.ExitCode -ne 0 -or $ac.Lines.Count -gt 0) { + Write-Host "[warning] the extension is loaded, but the platform will not apply it:" -ForegroundColor Yellow + foreach ($l in $ac.Lines) { Write-Host " $l" -ForegroundColor Yellow } + return $true + } + return $false +} + +# Проверять ли применимость: -NoApplyCheck сильнее настройки проекта. +function Get-ApplyCheckEnabled { + param([switch]$Disabled) + if ($Disabled) { return $false } + $pf = Find-V8Project (Get-Location).Path + if ($pf) { + try { + $proj = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json + if ($null -ne $proj.extensionApplyCheck) { return [bool]$proj.extensionApplyCheck } + } catch {} + } + return $true +} + + $engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" } # --- Resolve additional arguments for the selected engine --- @@ -453,22 +542,32 @@ try { } else { Write-Host "Error loading configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red } + # Проверку применимости умеет только 1cv8 — соединение для неё собираем в его форме. + if ($exitCode -eq 0 -and ($Extension -or $AllExtensions) -and (Get-ApplyCheckEnabled -Disabled:$NoApplyCheck)) { + $acConn = @("/F", "`"$InfoBasePath`"") + if ($UserName) { $acConn += "/N`"$UserName`"" } + if ($Password) { $acConn += "/P`"$Password`"" } + if ((Invoke-ApplyCheckReport $V8Path $acConn $Extension @()) -and $StrictLog) { $exitCode = 1 } + } Write-PlatformOutput $output exit $exitCode } # --- 1cv8 branch --- # --- Build arguments --- - $arguments = @("DESIGNER") + # Аргументы соединения собираем отдельно: тем же набором пойдёт запуск проверки применимости. + $connArgs = @() if ($InfoBaseServer -and $InfoBaseRef) { - $arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`"" + $connArgs += "/S", "`"$InfoBaseServer/$InfoBaseRef`"" } else { - $arguments += "/F", "`"$InfoBasePath`"" + $connArgs += "/F", "`"$InfoBasePath`"" } - if ($UserName) { $arguments += "/N`"$UserName`"" } - if ($Password) { $arguments += "/P`"$Password`"" } + if ($UserName) { $connArgs += "/N`"$UserName`"" } + if ($Password) { $connArgs += "/P`"$Password`"" } + + $arguments = @("DESIGNER") + $connArgs $arguments += "/LoadCfg", "`"$InputFile`"" @@ -507,6 +606,11 @@ try { } Write-PlatformOutput $__v8.Output + # Расширение могло загрузиться «успешно» и остаться неприменимым — спрашиваем платформу. + if ($exitCode -eq 0 -and ($Extension -or $AllExtensions) -and (Get-ApplyCheckEnabled -Disabled:$NoApplyCheck)) { + if ((Invoke-ApplyCheckReport $V8Path $connArgs $Extension $extraArgs) -and $StrictLog) { $exitCode = 1 } + } + exit $exitCode } finally { diff --git a/.claude/skills/db-load-cf/scripts/db-load-cf.py b/.claude/skills/db-load-cf/scripts/db-load-cf.py index a299026a5..888e72911 100644 --- a/.claude/skills/db-load-cf/scripts/db-load-cf.py +++ b/.claude/skills/db-load-cf/scripts/db-load-cf.py @@ -379,6 +379,91 @@ def print_platform_output(result): print("--- End ---") +def _sg_find_v8project(start_dir): + d = start_dir + for _ in range(20): + if not d: + break + pj = os.path.join(d, ".v8-project.json") + if os.path.isfile(pj): + return pj + parent = os.path.dirname(d) + if parent == d: + break + d = parent + return None + + +def run_apply_check(exe, conn_args, extension, extra_args): + """Постусловие применимости расширения: платформа отчитывается успехом и о расширении, + которое не применит — отказ всплывает лениво, при первом вызове метода, записью в журнал + регистрации. + + Запуск ОБЯЗАТЕЛЬНО отдельный. Дописать эту команду в строку операции нельзя: в одной + командной строке DESIGNER выполняет только ПОСЛЕДНЮЮ пакетную команду, остальные молча + отбрасывает — проверено на 8.3.24, /LoadConfigFromFiles вместе с + /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка не + состоялась. + + Проверку умеет только 1cv8; если навык работал через ibcmd, берём соседний файл. + """ + exe_dir = os.path.dirname(exe) + leaf = os.path.basename(exe) + if leaf.lower().startswith("ibcmd"): + v8 = os.path.join(exe_dir, "1cv8" + os.path.splitext(leaf)[1]) + else: + v8 = exe + if not os.path.isfile(v8): + return {"skipped": True, "reason": f"1cv8 not found at {v8}", "exit": 0, "lines": []} + temp_dir = tempfile.mkdtemp(prefix="apply_check_") + try: + a = ["DESIGNER"] + list(conn_args) + ["/CheckCanApplyConfigurationExtensions"] + if extension: + a += ["-Extension", f'"{extension}"'] + out_file = os.path.join(temp_dir, "check_log.txt") + a += ["/Out", f'"{out_file}"', "/DisableStartupDialogs"] + a += list(extra_args) + r = run_v8(v8, a) + lines = [] + if os.path.isfile(out_file): + with open(out_file, encoding="utf-8-sig", errors="replace") as f: + lines = [x.strip() for x in f.read().splitlines() if x.strip()] + return {"skipped": False, "reason": "", "exit": r.returncode, "lines": lines} + finally: + shutil.rmtree(temp_dir, ignore_errors=True) + + +def apply_check_report(exe, conn_args, extension, extra_args): + """Проверить и напечатать. True, если платформа расширение не применит — вызывающий решает, + поднимать ли код возврата (строгий режим).""" + ac = run_apply_check(exe, conn_args, extension, extra_args) + if ac["skipped"]: + print(f"[note] applicability check skipped: {ac['reason']}") + return False + if ac["exit"] != 0 or ac["lines"]: + print("[warning] the extension is loaded, but the platform will not apply it:") + for line in ac["lines"]: + print(f" {line}") + return True + return False + + +def apply_check_enabled(disabled): + """Проверять ли применимость: -NoApplyCheck сильнее настройки проекта.""" + if disabled: + return False + pf = _sg_find_v8project(os.getcwd()) + if pf: + try: + with open(pf, encoding="utf-8-sig") as f: + proj = json.load(f) + if proj.get("extensionApplyCheck") is not None: + return bool(proj.get("extensionApplyCheck")) + except Exception: + pass + return True + + def run_ibcmd(cmd, has_username=False, warn_no_user=True): """Run an ibcmd command non-interactively. @@ -444,6 +529,12 @@ def main(): parser.add_argument("-InputFile", required=True) parser.add_argument("-Extension", default="") parser.add_argument("-AllExtensions", action="store_true") + # Ключ для регрессов и верификации снапшотов, не для повседневного вызова: в SKILL.md + # намеренно не выносится. Поднимает код возврата, если платформа отчиталась об успехе, + # но расширение при этом неприменимо. + parser.add_argument("-StrictLog", action="store_true") + # Пропустить проверку применимости расширения после загрузки. + parser.add_argument("-NoApplyCheck", action="store_true") parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[], help="Extra 1cv8 arguments, e.g. /UseHwLicenses+") parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[], @@ -509,6 +600,18 @@ def main(): print(f"Configuration loaded successfully from: {args.InputFile}") else: print(f"Error loading configuration (code: {result.returncode}){describe_exit(result.returncode)}") + # Проверку применимости умеет только 1cv8 — соединение для неё собираем в его форме. + exit_code = result.returncode + if (exit_code == 0 and (args.Extension or args.AllExtensions) + and apply_check_enabled(args.NoApplyCheck)): + ac_conn = ["/F", f'"{args.InfoBasePath}"'] + if args.UserName: + ac_conn.append(f'/N"{args.UserName}"') + if args.Password: + ac_conn.append(f'/P"{args.Password}"') + if apply_check_report(v8path, ac_conn, args.Extension, []) and args.StrictLog: + exit_code = 1 + sys.exit(result.returncode) # --- Temp dir --- @@ -517,17 +620,20 @@ def main(): try: # --- Build arguments --- - arguments = ["DESIGNER"] + # Аргументы соединения собираем отдельно: тем же набором пойдёт проверка применимости. + conn_args = [] if args.InfoBaseServer and args.InfoBaseRef: - arguments.extend(["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']) + conn_args.extend(["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']) else: - arguments.extend(["/F", f'"{args.InfoBasePath}"']) + conn_args.extend(["/F", f'"{args.InfoBasePath}"']) if args.UserName: - arguments.append(f'/N"{args.UserName}"') + conn_args.append(f'/N"{args.UserName}"') if args.Password: - arguments.append(f'/P"{args.Password}"') + conn_args.append(f'/P"{args.Password}"') + + arguments = ["DESIGNER"] + conn_args arguments.extend(["/LoadCfg", f'"{args.InputFile}"']) @@ -565,6 +671,12 @@ def main(): except Exception: pass + # Расширение могло загрузиться «успешно» и остаться неприменимым — спрашиваем платформу. + if (exit_code == 0 and (args.Extension or args.AllExtensions) + and apply_check_enabled(args.NoApplyCheck)): + if apply_check_report(v8path, conn_args, args.Extension, extra_args) and args.StrictLog: + exit_code = 1 + print_platform_output(result) sys.exit(exit_code) diff --git a/.claude/skills/db-load-git/SKILL.md b/.claude/skills/db-load-git/SKILL.md index 5e96dd75e..20c2e678e 100644 --- a/.claude/skills/db-load-git/SKILL.md +++ b/.claude/skills/db-load-git/SKILL.md @@ -55,6 +55,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" < | `-Source <источник>` | нет | `All` (по умолч.) / `Staged` / `Unstaged` / `Commit` | | `-CommitRange ` | для Commit | Диапазон коммитов (напр. `HEAD~3..HEAD`) | | `-Extension <имя>` | нет | Загрузить в расширение | +| `-NoApplyCheck` | нет | Не проверять применимость расширения после загрузки | | `-AllExtensions` | нет | Загрузить все расширения | | `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` | | `-DryRun` | нет | Только показать что будет загружено (без загрузки) | diff --git a/.claude/skills/db-load-git/scripts/db-load-git.ps1 b/.claude/skills/db-load-git/scripts/db-load-git.ps1 index fdddbf35a..49c64ad36 100644 --- a/.claude/skills/db-load-git/scripts/db-load-git.ps1 +++ b/.claude/skills/db-load-git/scripts/db-load-git.ps1 @@ -116,6 +116,10 @@ param( # но в логе есть отбраковка. [switch]$StrictLog, + # Пропустить проверку применимости расширения после загрузки. + [Parameter(Mandatory=$false)] + [switch]$NoApplyCheck, + [Parameter(Mandatory=$false)] [string]$RepositoryPath, @@ -573,7 +577,74 @@ function Find-SilentRejections { } - $engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" } + # Постусловие применимости расширения: платформа отчитывается успехом и о расширении, которое +# не применит — отказ всплывает лениво, при первом вызове метода, записью в журнал регистрации. +# +# Запуск ОБЯЗАТЕЛЬНО отдельный. Дописать эту команду в строку операции нельзя: в одной командной +# строке DESIGNER выполняет только ПОСЛЕДНЮЮ пакетную команду, остальные молча отбрасывает — +# проверено на 8.3.24, /LoadConfigFromFiles вместе с /CheckCanApplyConfigurationExtensions +# завершились кодом 0 с пустым логом, и загрузка не состоялась. +# +# Проверку умеет только 1cv8; если навык работал через ibcmd, берём соседний исполняемый файл. +function Invoke-ApplyCheck { + param([string]$Exe, [string[]]$ConnArgs, [string]$Extension, [string[]]$ExtraArgs) + $exeDir = Split-Path $Exe -Parent + $exeLeaf = Split-Path $Exe -Leaf + $v8 = if ($exeLeaf -match '^ibcmd') { Join-Path $exeDir ("1cv8" + [System.IO.Path]::GetExtension($Exe)) } else { $Exe } + if (-not (Test-Path $v8)) { return @{ Skipped = $true; Reason = "1cv8 not found at $v8"; ExitCode = 0; Lines = @() } } + $dir = Join-Path $env:TEMP "apply_check_$(Get-Random)" + New-Item -ItemType Directory -Path $dir -Force | Out-Null + try { + $a = @("DESIGNER") + $ConnArgs + @("/CheckCanApplyConfigurationExtensions") + if ($Extension) { $a += "-Extension", "`"$Extension`"" } + $outFile = Join-Path $dir "check_log.txt" + $a += "/Out", "`"$outFile`"", "/DisableStartupDialogs" + $a += $ExtraArgs + $res = Invoke-PlatformProcess $v8 $a -PreQuoted + $lines = @() + if (Test-Path $outFile) { + $raw = Get-Content $outFile -Raw -ErrorAction SilentlyContinue + if ($raw) { $lines = @($raw -split "`r?`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' }) } + } + return @{ Skipped = $false; Reason = ''; ExitCode = $res.ExitCode; Lines = $lines } + } finally { + if (Test-Path $dir) { Remove-Item -Path $dir -Recurse -Force -ErrorAction SilentlyContinue } + } +} + +# Проверить и напечатать. $true, если платформа расширение не применит — вызывающий решает, +# поднимать ли код возврата (строгий режим). +function Invoke-ApplyCheckReport { + param([string]$Exe, [string[]]$ConnArgs, [string]$Extension, [string[]]$ExtraArgs) + $ac = Invoke-ApplyCheck $Exe $ConnArgs $Extension $ExtraArgs + if ($ac.Skipped) { + Write-Host "[note] applicability check skipped: $($ac.Reason)" -ForegroundColor Yellow + return $false + } + if ($ac.ExitCode -ne 0 -or $ac.Lines.Count -gt 0) { + Write-Host "[warning] the extension is loaded, but the platform will not apply it:" -ForegroundColor Yellow + foreach ($l in $ac.Lines) { Write-Host " $l" -ForegroundColor Yellow } + return $true + } + return $false +} + +# Проверять ли применимость: -NoApplyCheck сильнее настройки проекта. +function Get-ApplyCheckEnabled { + param([switch]$Disabled) + if ($Disabled) { return $false } + $pf = Find-V8Project (Get-Location).Path + if ($pf) { + try { + $proj = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json + if ($null -ne $proj.extensionApplyCheck) { return [bool]$proj.extensionApplyCheck } + } catch {} + } + return $true +} + + +$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" } if ($engine -eq "ibcmd") { if (-not $InfoBasePath) { Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red @@ -782,6 +853,13 @@ try { } else { Write-Host "Error updating database configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red } + # Проверку применимости умеет только 1cv8 — соединение для неё собираем в его форме. + if ($exitCode -eq 0 -and ($Extension -or $AllExtensions) -and (Get-ApplyCheckEnabled -Disabled:$NoApplyCheck)) { + $acConn = @("/F", "`"$InfoBasePath`"") + if ($UserName) { $acConn += "/N`"$UserName`"" } + if ($Password) { $acConn += "/P`"$Password`"" } + if ((Invoke-ApplyCheckReport $V8Path $acConn $Extension @()) -and $StrictLog) { $exitCode = 1 } + } Write-PlatformOutput $applyOut } exit $exitCode @@ -794,21 +872,24 @@ try { [System.IO.File]::WriteAllLines($listFile, $configFiles, $utf8Bom) # --- Build arguments --- - $arguments = @("DESIGNER") + # Аргументы соединения собираем отдельно: тем же набором пойдёт запуск проверки применимости. + $connArgs = @() if ($InfoBaseServer -and $InfoBaseRef) { - $arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`"" + $connArgs += "/S", "`"$InfoBaseServer/$InfoBaseRef`"" } else { - $arguments += "/F", "`"$InfoBasePath`"" + $connArgs += "/F", "`"$InfoBasePath`"" } - if ($UserName) { $arguments += "/N`"$UserName`"" } - if ($Password) { $arguments += "/P`"$Password`"" } + if ($UserName) { $connArgs += "/N`"$UserName`"" } + if ($Password) { $connArgs += "/P`"$Password`"" } # База под хранилищем не примет НИ ОДНОЙ операции конфигуратора без этих реквизитов, а для # базы вне хранилища они безвредны — поэтому подставляем всегда, когда они известны. $__repo = Resolve-RepositorySettings - $arguments += Get-RepositoryArgs $__repo + $connArgs += Get-RepositoryArgs $__repo + + $arguments = @("DESIGNER") + $connArgs $arguments += "/LoadConfigFromFiles", "`"$ConfigDir`"" $arguments += "-listFile", "`"$listFile`"" @@ -872,6 +953,11 @@ try { if ($StrictLog -and $exitCode -eq 0) { $exitCode = 1 } } + # Расширение могло загрузиться «успешно» и остаться неприменимым — спрашиваем платформу. + if ($exitCode -eq 0 -and ($Extension -or $AllExtensions) -and (Get-ApplyCheckEnabled -Disabled:$NoApplyCheck)) { + if ((Invoke-ApplyCheckReport $V8Path $connArgs $Extension $extraArgs) -and $StrictLog) { $exitCode = 1 } + } + exit $exitCode } finally { diff --git a/.claude/skills/db-load-git/scripts/db-load-git.py b/.claude/skills/db-load-git/scripts/db-load-git.py index 64e2bc4fb..9a835b482 100644 --- a/.claude/skills/db-load-git/scripts/db-load-git.py +++ b/.claude/skills/db-load-git/scripts/db-load-git.py @@ -517,6 +517,76 @@ def find_silent_rejections(log_text): return found +def run_apply_check(exe, conn_args, extension, extra_args): + """Постусловие применимости расширения: платформа отчитывается успехом и о расширении, + которое не применит — отказ всплывает лениво, при первом вызове метода, записью в журнал + регистрации. + + Запуск ОБЯЗАТЕЛЬНО отдельный. Дописать эту команду в строку операции нельзя: в одной + командной строке DESIGNER выполняет только ПОСЛЕДНЮЮ пакетную команду, остальные молча + отбрасывает — проверено на 8.3.24, /LoadConfigFromFiles вместе с + /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка не + состоялась. + + Проверку умеет только 1cv8; если навык работал через ibcmd, берём соседний файл. + """ + exe_dir = os.path.dirname(exe) + leaf = os.path.basename(exe) + if leaf.lower().startswith("ibcmd"): + v8 = os.path.join(exe_dir, "1cv8" + os.path.splitext(leaf)[1]) + else: + v8 = exe + if not os.path.isfile(v8): + return {"skipped": True, "reason": f"1cv8 not found at {v8}", "exit": 0, "lines": []} + temp_dir = tempfile.mkdtemp(prefix="apply_check_") + try: + a = ["DESIGNER"] + list(conn_args) + ["/CheckCanApplyConfigurationExtensions"] + if extension: + a += ["-Extension", f'"{extension}"'] + out_file = os.path.join(temp_dir, "check_log.txt") + a += ["/Out", f'"{out_file}"', "/DisableStartupDialogs"] + a += list(extra_args) + r = run_v8(v8, a) + lines = [] + if os.path.isfile(out_file): + with open(out_file, encoding="utf-8-sig", errors="replace") as f: + lines = [x.strip() for x in f.read().splitlines() if x.strip()] + return {"skipped": False, "reason": "", "exit": r.returncode, "lines": lines} + finally: + shutil.rmtree(temp_dir, ignore_errors=True) + + +def apply_check_report(exe, conn_args, extension, extra_args): + """Проверить и напечатать. True, если платформа расширение не применит — вызывающий решает, + поднимать ли код возврата (строгий режим).""" + ac = run_apply_check(exe, conn_args, extension, extra_args) + if ac["skipped"]: + print(f"[note] applicability check skipped: {ac['reason']}") + return False + if ac["exit"] != 0 or ac["lines"]: + print("[warning] the extension is loaded, but the platform will not apply it:") + for line in ac["lines"]: + print(f" {line}") + return True + return False + + +def apply_check_enabled(disabled): + """Проверять ли применимость: -NoApplyCheck сильнее настройки проекта.""" + if disabled: + return False + pf = _sg_find_v8project(os.getcwd()) + if pf: + try: + with open(pf, encoding="utf-8-sig") as f: + proj = json.load(f) + if proj.get("extensionApplyCheck") is not None: + return bool(proj.get("extensionApplyCheck")) + except Exception: + pass + return True + + def run_ibcmd(cmd, has_username=False, warn_no_user=True): """Run an ibcmd command non-interactively. @@ -626,6 +696,8 @@ def main(): # намеренно не выносится. Поднимает код возврата, если платформа отчиталась об успехе, # но в логе есть отбраковка. parser.add_argument("-StrictLog", action="store_true") + # Пропустить проверку применимости расширения после загрузки. + parser.add_argument("-NoApplyCheck", action="store_true") parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[], help="Extra 1cv8 arguments, e.g. /UseHwLicenses+") parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[], @@ -828,6 +900,17 @@ def main(): else: print(f"Error updating database configuration (code: {exit_code}){describe_exit(exit_code)}") print_platform_output(ar) + # Проверку применимости умеет только 1cv8 — соединение для неё собираем в его форме. + if (exit_code == 0 and (args.Extension or args.AllExtensions) + and apply_check_enabled(args.NoApplyCheck)): + ac_conn = ["/F", f'"{args.InfoBasePath}"'] + if args.UserName: + ac_conn.append(f'/N"{args.UserName}"') + if args.Password: + ac_conn.append(f'/P"{args.Password}"') + if apply_check_report(v8path, ac_conn, args.Extension, []) and args.StrictLog: + exit_code = 1 + sys.exit(exit_code) # --- Write list file (UTF-8 with BOM) --- @@ -836,22 +919,25 @@ def main(): f.write("\n".join(config_files)) # --- Build arguments --- - arguments = ["DESIGNER"] + # Аргументы соединения собираем отдельно: тем же набором пойдёт проверка применимости. + conn_args = [] if args.InfoBaseServer and args.InfoBaseRef: - arguments += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"'] + conn_args += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"'] else: - arguments += ["/F", f'"{args.InfoBasePath}"'] + conn_args += ["/F", f'"{args.InfoBasePath}"'] if args.UserName: - arguments.append(f'/N"{args.UserName}"') + conn_args.append(f'/N"{args.UserName}"') if args.Password: - arguments.append(f'/P"{args.Password}"') + conn_args.append(f'/P"{args.Password}"') # База под хранилищем не примет НИ ОДНОЙ операции конфигуратора без этих реквизитов, а для # базы вне хранилища они безвредны — поэтому подставляем всегда, когда они известны. repo = resolve_repository_settings(args) - arguments.extend(repository_args(repo)) + conn_args.extend(repository_args(repo)) + + arguments = ["DESIGNER"] + conn_args arguments += ["/LoadConfigFromFiles", f'"{args.ConfigDir}"'] arguments += ["-listFile", f'"{list_file}"'] @@ -919,6 +1005,12 @@ def main(): if args.StrictLog and exit_code == 0: exit_code = 1 + # Расширение могло загрузиться «успешно» и остаться неприменимым — спрашиваем платформу. + if (exit_code == 0 and (args.Extension or args.AllExtensions) + and apply_check_enabled(args.NoApplyCheck)): + if apply_check_report(v8path, conn_args, args.Extension, extra_args) and args.StrictLog: + exit_code = 1 + sys.exit(exit_code) finally: diff --git a/.claude/skills/db-load-xml/SKILL.md b/.claude/skills/db-load-xml/SKILL.md index a0a94d08d..b7c13335f 100644 --- a/.claude/skills/db-load-xml/SKILL.md +++ b/.claude/skills/db-load-xml/SKILL.md @@ -58,6 +58,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" < | `-ListFile <путь>` | для Partial | Путь к файлу со списком (альтернатива `-Files`) | | `-Extension <имя>` | нет | Загрузить в расширение | | `-AllExtensions` | нет | Загрузить все расширения | +| `-NoApplyCheck` | нет | Не проверять применимость расширения после загрузки | | `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` | | `-UpdateDB` | нет | После загрузки сразу обновить конфигурацию БД (`/UpdateDBCfg`) | | `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` | diff --git a/.claude/skills/db-load-xml/scripts/db-load-xml.ps1 b/.claude/skills/db-load-xml/scripts/db-load-xml.ps1 index da0c35283..daaa9eee6 100644 --- a/.claude/skills/db-load-xml/scripts/db-load-xml.ps1 +++ b/.claude/skills/db-load-xml/scripts/db-load-xml.ps1 @@ -112,6 +112,10 @@ param( [Parameter(Mandatory=$false)] [switch]$StrictLog, + # Пропустить проверку применимости расширения после загрузки. + [Parameter(Mandatory=$false)] + [switch]$NoApplyCheck, + [Parameter(Mandatory=$false)] [string]$RepositoryPath, @@ -591,6 +595,73 @@ function Find-SilentRejections { } +# Постусловие применимости расширения: платформа отчитывается успехом и о расширении, которое +# не применит — отказ всплывает лениво, при первом вызове метода, записью в журнал регистрации. +# +# Запуск ОБЯЗАТЕЛЬНО отдельный. Дописать эту команду в строку операции нельзя: в одной командной +# строке DESIGNER выполняет только ПОСЛЕДНЮЮ пакетную команду, остальные молча отбрасывает — +# проверено на 8.3.24, /LoadConfigFromFiles вместе с /CheckCanApplyConfigurationExtensions +# завершились кодом 0 с пустым логом, и загрузка не состоялась. +# +# Проверку умеет только 1cv8; если навык работал через ibcmd, берём соседний исполняемый файл. +function Invoke-ApplyCheck { + param([string]$Exe, [string[]]$ConnArgs, [string]$Extension, [string[]]$ExtraArgs) + $exeDir = Split-Path $Exe -Parent + $exeLeaf = Split-Path $Exe -Leaf + $v8 = if ($exeLeaf -match '^ibcmd') { Join-Path $exeDir ("1cv8" + [System.IO.Path]::GetExtension($Exe)) } else { $Exe } + if (-not (Test-Path $v8)) { return @{ Skipped = $true; Reason = "1cv8 not found at $v8"; ExitCode = 0; Lines = @() } } + $dir = Join-Path $env:TEMP "apply_check_$(Get-Random)" + New-Item -ItemType Directory -Path $dir -Force | Out-Null + try { + $a = @("DESIGNER") + $ConnArgs + @("/CheckCanApplyConfigurationExtensions") + if ($Extension) { $a += "-Extension", "`"$Extension`"" } + $outFile = Join-Path $dir "check_log.txt" + $a += "/Out", "`"$outFile`"", "/DisableStartupDialogs" + $a += $ExtraArgs + $res = Invoke-PlatformProcess $v8 $a -PreQuoted + $lines = @() + if (Test-Path $outFile) { + $raw = Get-Content $outFile -Raw -ErrorAction SilentlyContinue + if ($raw) { $lines = @($raw -split "`r?`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' }) } + } + return @{ Skipped = $false; Reason = ''; ExitCode = $res.ExitCode; Lines = $lines } + } finally { + if (Test-Path $dir) { Remove-Item -Path $dir -Recurse -Force -ErrorAction SilentlyContinue } + } +} + +# Проверить и напечатать. $true, если платформа расширение не применит — вызывающий решает, +# поднимать ли код возврата (строгий режим). +function Invoke-ApplyCheckReport { + param([string]$Exe, [string[]]$ConnArgs, [string]$Extension, [string[]]$ExtraArgs) + $ac = Invoke-ApplyCheck $Exe $ConnArgs $Extension $ExtraArgs + if ($ac.Skipped) { + Write-Host "[note] applicability check skipped: $($ac.Reason)" -ForegroundColor Yellow + return $false + } + if ($ac.ExitCode -ne 0 -or $ac.Lines.Count -gt 0) { + Write-Host "[warning] the extension is loaded, but the platform will not apply it:" -ForegroundColor Yellow + foreach ($l in $ac.Lines) { Write-Host " $l" -ForegroundColor Yellow } + return $true + } + return $false +} + +# Проверять ли применимость: -NoApplyCheck сильнее настройки проекта. +function Get-ApplyCheckEnabled { + param([switch]$Disabled) + if ($Disabled) { return $false } + $pf = Find-V8Project (Get-Location).Path + if ($pf) { + try { + $proj = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json + if ($null -ne $proj.extensionApplyCheck) { return [bool]$proj.extensionApplyCheck } + } catch {} + } + return $true +} + + $engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" } # --- Resolve additional arguments for the selected engine --- @@ -700,26 +771,36 @@ try { } Write-PlatformOutput $applyOut } + # Проверку применимости умеет только 1cv8 — соединение для неё собираем в его форме. + if ($exitCode -eq 0 -and ($Extension -or $AllExtensions) -and (Get-ApplyCheckEnabled -Disabled:$NoApplyCheck)) { + $acConn = @("/F", "`"$InfoBasePath`"") + if ($UserName) { $acConn += "/N`"$UserName`"" } + if ($Password) { $acConn += "/P`"$Password`"" } + if ((Invoke-ApplyCheckReport $V8Path $acConn $Extension @()) -and $StrictLog) { $exitCode = 1 } + } exit $exitCode } # --- 1cv8 branch --- # --- Build arguments --- - $arguments = @("DESIGNER") + # Аргументы соединения собираем отдельно: тем же набором пойдёт запуск проверки применимости. + $connArgs = @() if ($InfoBaseServer -and $InfoBaseRef) { - $arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`"" + $connArgs += "/S", "`"$InfoBaseServer/$InfoBaseRef`"" } else { - $arguments += "/F", "`"$InfoBasePath`"" + $connArgs += "/F", "`"$InfoBasePath`"" } - if ($UserName) { $arguments += "/N`"$UserName`"" } - if ($Password) { $arguments += "/P`"$Password`"" } + if ($UserName) { $connArgs += "/N`"$UserName`"" } + if ($Password) { $connArgs += "/P`"$Password`"" } # База под хранилищем не примет НИ ОДНОЙ операции конфигуратора без этих реквизитов, а для # базы вне хранилища они безвредны — поэтому подставляем всегда, когда они известны. $__repo = Resolve-RepositorySettings - $arguments += Get-RepositoryArgs $__repo + $connArgs += Get-RepositoryArgs $__repo + + $arguments = @("DESIGNER") + $connArgs $arguments += "/LoadConfigFromFiles", "`"$ConfigDir`"" @@ -825,6 +906,11 @@ try { if ($StrictLog -and $exitCode -eq 0) { $exitCode = 1 } } + # Расширение могло загрузиться «успешно» и при этом остаться неприменимым — спрашиваем платформу. + if ($exitCode -eq 0 -and ($Extension -or $AllExtensions) -and (Get-ApplyCheckEnabled -Disabled:$NoApplyCheck)) { + if ((Invoke-ApplyCheckReport $V8Path $connArgs $Extension $extraArgs) -and $StrictLog) { $exitCode = 1 } + } + exit $exitCode } finally { diff --git a/.claude/skills/db-load-xml/scripts/db-load-xml.py b/.claude/skills/db-load-xml/scripts/db-load-xml.py index 69801cd6b..23c81b9bd 100644 --- a/.claude/skills/db-load-xml/scripts/db-load-xml.py +++ b/.claude/skills/db-load-xml/scripts/db-load-xml.py @@ -517,6 +517,76 @@ def find_silent_rejections(log_text): return found +def run_apply_check(exe, conn_args, extension, extra_args): + """Постусловие применимости расширения: платформа отчитывается успехом и о расширении, + которое не применит — отказ всплывает лениво, при первом вызове метода, записью в журнал + регистрации. + + Запуск ОБЯЗАТЕЛЬНО отдельный. Дописать эту команду в строку операции нельзя: в одной + командной строке DESIGNER выполняет только ПОСЛЕДНЮЮ пакетную команду, остальные молча + отбрасывает — проверено на 8.3.24, /LoadConfigFromFiles вместе с + /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка не + состоялась. + + Проверку умеет только 1cv8; если навык работал через ibcmd, берём соседний файл. + """ + exe_dir = os.path.dirname(exe) + leaf = os.path.basename(exe) + if leaf.lower().startswith("ibcmd"): + v8 = os.path.join(exe_dir, "1cv8" + os.path.splitext(leaf)[1]) + else: + v8 = exe + if not os.path.isfile(v8): + return {"skipped": True, "reason": f"1cv8 not found at {v8}", "exit": 0, "lines": []} + temp_dir = tempfile.mkdtemp(prefix="apply_check_") + try: + a = ["DESIGNER"] + list(conn_args) + ["/CheckCanApplyConfigurationExtensions"] + if extension: + a += ["-Extension", f'"{extension}"'] + out_file = os.path.join(temp_dir, "check_log.txt") + a += ["/Out", f'"{out_file}"', "/DisableStartupDialogs"] + a += list(extra_args) + r = run_v8(v8, a) + lines = [] + if os.path.isfile(out_file): + with open(out_file, encoding="utf-8-sig", errors="replace") as f: + lines = [x.strip() for x in f.read().splitlines() if x.strip()] + return {"skipped": False, "reason": "", "exit": r.returncode, "lines": lines} + finally: + shutil.rmtree(temp_dir, ignore_errors=True) + + +def apply_check_report(exe, conn_args, extension, extra_args): + """Проверить и напечатать. True, если платформа расширение не применит — вызывающий решает, + поднимать ли код возврата (строгий режим).""" + ac = run_apply_check(exe, conn_args, extension, extra_args) + if ac["skipped"]: + print(f"[note] applicability check skipped: {ac['reason']}") + return False + if ac["exit"] != 0 or ac["lines"]: + print("[warning] the extension is loaded, but the platform will not apply it:") + for line in ac["lines"]: + print(f" {line}") + return True + return False + + +def apply_check_enabled(disabled): + """Проверять ли применимость: -NoApplyCheck сильнее настройки проекта.""" + if disabled: + return False + pf = _sg_find_v8project(os.getcwd()) + if pf: + try: + with open(pf, encoding="utf-8-sig") as f: + proj = json.load(f) + if proj.get("extensionApplyCheck") is not None: + return bool(proj.get("extensionApplyCheck")) + except Exception: + pass + return True + + def run_ibcmd(cmd, has_username=False, warn_no_user=True): """Run an ibcmd command non-interactively. @@ -605,6 +675,8 @@ def main(): action="store_true", help="Treat silent rejection warnings in the log as errors (elevate exit code to 1)", ) + # Пропустить проверку применимости расширения после загрузки. + parser.add_argument("-NoApplyCheck", action="store_true") parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[], help="Extra 1cv8 arguments, e.g. /UseHwLicenses+") parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[], @@ -725,6 +797,17 @@ def main(): else: print(f"Error updating database configuration (code: {exit_code}){describe_exit(exit_code)}") print_platform_output(ar) + # Проверку применимости умеет только 1cv8 — соединение для неё собираем в его форме. + if (exit_code == 0 and (args.Extension or args.AllExtensions) + and apply_check_enabled(args.NoApplyCheck)): + ac_conn = ["/F", f'"{args.InfoBasePath}"'] + if args.UserName: + ac_conn.append(f'/N"{args.UserName}"') + if args.Password: + ac_conn.append(f'/P"{args.Password}"') + if apply_check_report(v8path, ac_conn, args.Extension, []) and args.StrictLog: + exit_code = 1 + sys.exit(exit_code) # --- Temp dir --- @@ -733,22 +816,25 @@ def main(): try: # --- Build arguments --- - arguments = ["DESIGNER"] + # Аргументы соединения собираем отдельно: тем же набором пойдёт проверка применимости. + conn_args = [] if args.InfoBaseServer and args.InfoBaseRef: - arguments += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"'] + conn_args += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"'] else: - arguments += ["/F", f'"{args.InfoBasePath}"'] + conn_args += ["/F", f'"{args.InfoBasePath}"'] if args.UserName: - arguments.append(f'/N"{args.UserName}"') + conn_args.append(f'/N"{args.UserName}"') if args.Password: - arguments.append(f'/P"{args.Password}"') + conn_args.append(f'/P"{args.Password}"') # База под хранилищем не примет НИ ОДНОЙ операции конфигуратора без этих реквизитов, а для # базы вне хранилища они безвредны — поэтому подставляем всегда, когда они известны. repo = resolve_repository_settings(args) - arguments.extend(repository_args(repo)) + conn_args.extend(repository_args(repo)) + + arguments = ["DESIGNER"] + conn_args arguments += ["/LoadConfigFromFiles", f'"{args.ConfigDir}"'] @@ -859,6 +945,12 @@ def main(): if args.StrictLog and exit_code == 0: exit_code = 1 + # Расширение могло загрузиться «успешно» и остаться неприменимым — спрашиваем платформу. + if (exit_code == 0 and (args.Extension or args.AllExtensions) + and apply_check_enabled(args.NoApplyCheck)): + if apply_check_report(v8path, conn_args, args.Extension, extra_args) and args.StrictLog: + exit_code = 1 + sys.exit(exit_code) finally: diff --git a/.claude/skills/db-update/SKILL.md b/.claude/skills/db-update/SKILL.md index 9f9fdc55c..3f7536c56 100644 --- a/.claude/skills/db-update/SKILL.md +++ b/.claude/skills/db-update/SKILL.md @@ -52,6 +52,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" <п | `-Password <пароль>` | нет | Пароль | | `-Extension <имя>` | нет | Обновить расширение | | `-AllExtensions` | нет | Обновить все расширения | +| `-NoApplyCheck` | нет | Не проверять применимость расширения после обновления | | `-Dynamic ` | нет | `on` — динамическое обновление, без монопольного доступа к базе; `off` — отключить | | `-Server` | нет | Обновление на стороне сервера | | `-WarningsAsErrors` | нет | Предупреждения считать ошибками | diff --git a/.claude/skills/db-update/scripts/db-update.ps1 b/.claude/skills/db-update/scripts/db-update.ps1 index 2c92bf42c..1321bcc74 100644 --- a/.claude/skills/db-update/scripts/db-update.ps1 +++ b/.claude/skills/db-update/scripts/db-update.ps1 @@ -99,6 +99,10 @@ param( # но в логе есть отбраковка. [switch]$StrictLog, + # Пропустить проверку применимости расширения после загрузки. + [Parameter(Mandatory=$false)] + [switch]$NoApplyCheck, + [Parameter(Mandatory=$false)] [string]$RepositoryPath, @@ -553,6 +557,73 @@ function Find-SilentRejections { } +# Постусловие применимости расширения: платформа отчитывается успехом и о расширении, которое +# не применит — отказ всплывает лениво, при первом вызове метода, записью в журнал регистрации. +# +# Запуск ОБЯЗАТЕЛЬНО отдельный. Дописать эту команду в строку операции нельзя: в одной командной +# строке DESIGNER выполняет только ПОСЛЕДНЮЮ пакетную команду, остальные молча отбрасывает — +# проверено на 8.3.24, /LoadConfigFromFiles вместе с /CheckCanApplyConfigurationExtensions +# завершились кодом 0 с пустым логом, и загрузка не состоялась. +# +# Проверку умеет только 1cv8; если навык работал через ibcmd, берём соседний исполняемый файл. +function Invoke-ApplyCheck { + param([string]$Exe, [string[]]$ConnArgs, [string]$Extension, [string[]]$ExtraArgs) + $exeDir = Split-Path $Exe -Parent + $exeLeaf = Split-Path $Exe -Leaf + $v8 = if ($exeLeaf -match '^ibcmd') { Join-Path $exeDir ("1cv8" + [System.IO.Path]::GetExtension($Exe)) } else { $Exe } + if (-not (Test-Path $v8)) { return @{ Skipped = $true; Reason = "1cv8 not found at $v8"; ExitCode = 0; Lines = @() } } + $dir = Join-Path $env:TEMP "apply_check_$(Get-Random)" + New-Item -ItemType Directory -Path $dir -Force | Out-Null + try { + $a = @("DESIGNER") + $ConnArgs + @("/CheckCanApplyConfigurationExtensions") + if ($Extension) { $a += "-Extension", "`"$Extension`"" } + $outFile = Join-Path $dir "check_log.txt" + $a += "/Out", "`"$outFile`"", "/DisableStartupDialogs" + $a += $ExtraArgs + $res = Invoke-PlatformProcess $v8 $a -PreQuoted + $lines = @() + if (Test-Path $outFile) { + $raw = Get-Content $outFile -Raw -ErrorAction SilentlyContinue + if ($raw) { $lines = @($raw -split "`r?`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' }) } + } + return @{ Skipped = $false; Reason = ''; ExitCode = $res.ExitCode; Lines = $lines } + } finally { + if (Test-Path $dir) { Remove-Item -Path $dir -Recurse -Force -ErrorAction SilentlyContinue } + } +} + +# Проверить и напечатать. $true, если платформа расширение не применит — вызывающий решает, +# поднимать ли код возврата (строгий режим). +function Invoke-ApplyCheckReport { + param([string]$Exe, [string[]]$ConnArgs, [string]$Extension, [string[]]$ExtraArgs) + $ac = Invoke-ApplyCheck $Exe $ConnArgs $Extension $ExtraArgs + if ($ac.Skipped) { + Write-Host "[note] applicability check skipped: $($ac.Reason)" -ForegroundColor Yellow + return $false + } + if ($ac.ExitCode -ne 0 -or $ac.Lines.Count -gt 0) { + Write-Host "[warning] the extension is loaded, but the platform will not apply it:" -ForegroundColor Yellow + foreach ($l in $ac.Lines) { Write-Host " $l" -ForegroundColor Yellow } + return $true + } + return $false +} + +# Проверять ли применимость: -NoApplyCheck сильнее настройки проекта. +function Get-ApplyCheckEnabled { + param([switch]$Disabled) + if ($Disabled) { return $false } + $pf = Find-V8Project (Get-Location).Path + if ($pf) { + try { + $proj = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json + if ($null -ne $proj.extensionApplyCheck) { return [bool]$proj.extensionApplyCheck } + } catch {} + } + return $true +} + + $engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" } # --- Resolve additional arguments for the selected engine --- @@ -598,27 +669,37 @@ try { } else { Write-Host "Error updating database configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red } + # Проверку применимости умеет только 1cv8 — соединение для неё собираем в его форме. + if ($exitCode -eq 0 -and ($Extension -or $AllExtensions) -and (Get-ApplyCheckEnabled -Disabled:$NoApplyCheck)) { + $acConn = @("/F", "`"$InfoBasePath`"") + if ($UserName) { $acConn += "/N`"$UserName`"" } + if ($Password) { $acConn += "/P`"$Password`"" } + if ((Invoke-ApplyCheckReport $V8Path $acConn $Extension @()) -and $StrictLog) { $exitCode = 1 } + } Write-PlatformOutput $output exit $exitCode } # --- 1cv8 branch --- # --- Build arguments --- - $arguments = @("DESIGNER") + # Аргументы соединения собираем отдельно: тем же набором пойдёт запуск проверки применимости. + $connArgs = @() if ($InfoBaseServer -and $InfoBaseRef) { - $arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`"" + $connArgs += "/S", "`"$InfoBaseServer/$InfoBaseRef`"" } else { - $arguments += "/F", "`"$InfoBasePath`"" + $connArgs += "/F", "`"$InfoBasePath`"" } - if ($UserName) { $arguments += "/N`"$UserName`"" } - if ($Password) { $arguments += "/P`"$Password`"" } + if ($UserName) { $connArgs += "/N`"$UserName`"" } + if ($Password) { $connArgs += "/P`"$Password`"" } # База под хранилищем не примет НИ ОДНОЙ операции конфигуратора без этих реквизитов, а для # базы вне хранилища они безвредны — поэтому подставляем всегда, когда они известны. $__repo = Resolve-RepositorySettings - $arguments += Get-RepositoryArgs $__repo + $connArgs += Get-RepositoryArgs $__repo + + $arguments = @("DESIGNER") + $connArgs $arguments += "/UpdateDBCfg" @@ -679,6 +760,11 @@ try { if ($StrictLog -and $exitCode -eq 0) { $exitCode = 1 } } + # Расширение могло загрузиться «успешно» и остаться неприменимым — спрашиваем платформу. + if ($exitCode -eq 0 -and ($Extension -or $AllExtensions) -and (Get-ApplyCheckEnabled -Disabled:$NoApplyCheck)) { + if ((Invoke-ApplyCheckReport $V8Path $connArgs $Extension $extraArgs) -and $StrictLog) { $exitCode = 1 } + } + exit $exitCode } finally { diff --git a/.claude/skills/db-update/scripts/db-update.py b/.claude/skills/db-update/scripts/db-update.py index a49196b3c..ec3d0f5f1 100644 --- a/.claude/skills/db-update/scripts/db-update.py +++ b/.claude/skills/db-update/scripts/db-update.py @@ -496,6 +496,76 @@ def find_silent_rejections(log_text): return found +def run_apply_check(exe, conn_args, extension, extra_args): + """Постусловие применимости расширения: платформа отчитывается успехом и о расширении, + которое не применит — отказ всплывает лениво, при первом вызове метода, записью в журнал + регистрации. + + Запуск ОБЯЗАТЕЛЬНО отдельный. Дописать эту команду в строку операции нельзя: в одной + командной строке DESIGNER выполняет только ПОСЛЕДНЮЮ пакетную команду, остальные молча + отбрасывает — проверено на 8.3.24, /LoadConfigFromFiles вместе с + /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка не + состоялась. + + Проверку умеет только 1cv8; если навык работал через ibcmd, берём соседний файл. + """ + exe_dir = os.path.dirname(exe) + leaf = os.path.basename(exe) + if leaf.lower().startswith("ibcmd"): + v8 = os.path.join(exe_dir, "1cv8" + os.path.splitext(leaf)[1]) + else: + v8 = exe + if not os.path.isfile(v8): + return {"skipped": True, "reason": f"1cv8 not found at {v8}", "exit": 0, "lines": []} + temp_dir = tempfile.mkdtemp(prefix="apply_check_") + try: + a = ["DESIGNER"] + list(conn_args) + ["/CheckCanApplyConfigurationExtensions"] + if extension: + a += ["-Extension", f'"{extension}"'] + out_file = os.path.join(temp_dir, "check_log.txt") + a += ["/Out", f'"{out_file}"', "/DisableStartupDialogs"] + a += list(extra_args) + r = run_v8(v8, a) + lines = [] + if os.path.isfile(out_file): + with open(out_file, encoding="utf-8-sig", errors="replace") as f: + lines = [x.strip() for x in f.read().splitlines() if x.strip()] + return {"skipped": False, "reason": "", "exit": r.returncode, "lines": lines} + finally: + shutil.rmtree(temp_dir, ignore_errors=True) + + +def apply_check_report(exe, conn_args, extension, extra_args): + """Проверить и напечатать. True, если платформа расширение не применит — вызывающий решает, + поднимать ли код возврата (строгий режим).""" + ac = run_apply_check(exe, conn_args, extension, extra_args) + if ac["skipped"]: + print(f"[note] applicability check skipped: {ac['reason']}") + return False + if ac["exit"] != 0 or ac["lines"]: + print("[warning] the extension is loaded, but the platform will not apply it:") + for line in ac["lines"]: + print(f" {line}") + return True + return False + + +def apply_check_enabled(disabled): + """Проверять ли применимость: -NoApplyCheck сильнее настройки проекта.""" + if disabled: + return False + pf = _sg_find_v8project(os.getcwd()) + if pf: + try: + with open(pf, encoding="utf-8-sig") as f: + proj = json.load(f) + if proj.get("extensionApplyCheck") is not None: + return bool(proj.get("extensionApplyCheck")) + except Exception: + pass + return True + + def run_ibcmd(cmd, has_username=False, warn_no_user=True): """Run an ibcmd command non-interactively. @@ -572,6 +642,8 @@ def main(): # намеренно не выносится. Поднимает код возврата, если платформа отчиталась об успехе, # но в логе есть отбраковка. parser.add_argument("-StrictLog", action="store_true") + # Пропустить проверку применимости расширения после загрузки. + parser.add_argument("-NoApplyCheck", action="store_true") parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[], help="Extra 1cv8 arguments, e.g. /UseHwLicenses+") parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[], @@ -638,6 +710,18 @@ def main(): print("Database configuration updated successfully") else: print(f"Error updating database configuration (code: {result.returncode}){describe_exit(result.returncode)}") + # Проверку применимости умеет только 1cv8 — соединение для неё собираем в его форме. + exit_code = result.returncode + if (exit_code == 0 and (args.Extension or args.AllExtensions) + and apply_check_enabled(args.NoApplyCheck)): + ac_conn = ["/F", f'"{args.InfoBasePath}"'] + if args.UserName: + ac_conn.append(f'/N"{args.UserName}"') + if args.Password: + ac_conn.append(f'/P"{args.Password}"') + if apply_check_report(v8path, ac_conn, args.Extension, []) and args.StrictLog: + exit_code = 1 + sys.exit(result.returncode) # --- Temp dir --- @@ -646,22 +730,25 @@ def main(): try: # --- Build arguments --- - arguments = ["DESIGNER"] + # Аргументы соединения собираем отдельно: тем же набором пойдёт проверка применимости. + conn_args = [] if args.InfoBaseServer and args.InfoBaseRef: - arguments.extend(["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']) + conn_args.extend(["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']) else: - arguments.extend(["/F", f'"{args.InfoBasePath}"']) + conn_args.extend(["/F", f'"{args.InfoBasePath}"']) if args.UserName: - arguments.append(f'/N"{args.UserName}"') + conn_args.append(f'/N"{args.UserName}"') if args.Password: - arguments.append(f'/P"{args.Password}"') + conn_args.append(f'/P"{args.Password}"') # База под хранилищем не примет НИ ОДНОЙ операции конфигуратора без этих реквизитов, а для # базы вне хранилища они безвредны — поэтому подставляем всегда, когда они известны. repo = resolve_repository_settings(args) - arguments.extend(repository_args(repo)) + conn_args.extend(repository_args(repo)) + + arguments = ["DESIGNER"] + conn_args arguments.append("/UpdateDBCfg") @@ -724,6 +811,12 @@ def main(): if args.StrictLog and exit_code == 0: exit_code = 1 + # Расширение могло загрузиться «успешно» и остаться неприменимым — спрашиваем платформу. + if (exit_code == 0 and (args.Extension or args.AllExtensions) + and apply_check_enabled(args.NoApplyCheck)): + if apply_check_report(v8path, conn_args, args.Extension, extra_args) and args.StrictLog: + exit_code = 1 + sys.exit(exit_code) finally: diff --git a/docs/v8-project-guide.md b/docs/v8-project-guide.md index d54eecd9f..34cf52b40 100644 --- a/docs/v8-project-guide.md +++ b/docs/v8-project-guide.md @@ -81,6 +81,7 @@ | `default` | string | нет | — | `id` базы по умолчанию | `/db-list` | | `editingAllowedCheck` | `"deny"`/`"warn"`/`"off"` | нет | `deny` | Глобальная реакция support-guard на правку объектов на замке (см. ниже) | Руками | | `newObjectPosition` | `"end"`/`"byName"` | нет | `end` | Куда навыки ставят новый объект в `` (см. ниже) | Руками | +| `extensionApplyCheck` | bool | нет | `true` | Проверять ли применимость расширения после загрузки в базу (навыки `db-load-*`, `db-update`); разово отключается ключом `-NoApplyCheck` | Руками | | `skillSuggester` | `"on"`/`"off"` | нет | `on` | Подсказки навыков от хука skill-suggester (только если хук включён, см. ниже) | Руками | | `webPath` | string | нет | `tools/apache24` | Каталог Apache HTTP Server | Руками | | `ffmpegPath` | string | нет | `tools/ffmpeg/bin/ffmpeg.exe` | Путь к ffmpeg | Руками | diff --git a/tests/skills/cases/db-load-xml/apply-check-clean.json b/tests/skills/cases/db-load-xml/apply-check-clean.json new file mode 100644 index 000000000..a8186c441 --- /dev/null +++ b/tests/skills/cases/db-load-xml/apply-check-clean.json @@ -0,0 +1,33 @@ +{ + "name": "Проверка прошла — предупреждения нет", + "fakePlatform": { + "log": "Загрузка конфигурации из файлов.\r\n", + "check": { + "log": "", + "exit": 0 + } + }, + "args_extra": [ + "-V8Path", + "{fakePlatform}", + "-InfoBasePath", + "{workDir}/ib", + "-ConfigDir", + "{workDir}/src", + "-Extension", + "Проба" + ], + "expect": { + "stdoutContains": "Load completed successfully", + "stdoutNotContains": "will not apply" + }, + "noSnapshot": "навык ничего не пишет в рабочий каталог — проверяется постусловие", + "preRun": [ + { + "writeFile": { + "path": "src/Configuration.xml", + "content": "\n" + } + } + ] +} diff --git a/tests/skills/cases/db-load-xml/apply-check-not-after-failed-load.json b/tests/skills/cases/db-load-xml/apply-check-not-after-failed-load.json new file mode 100644 index 000000000..4b20a95ab --- /dev/null +++ b/tests/skills/cases/db-load-xml/apply-check-not-after-failed-load.json @@ -0,0 +1,34 @@ +{ + "name": "Загрузка провалилась — применимость не проверяем (проверять нечего)", + "fakePlatform": { + "log": "Ошибка загрузки конфигурации\r\n", + "exit": 1, + "check": { + "log": "Проба: Список параметров метода \"Проба_Расчёт\" не соответствует методу \"Расчёт\".\r\n", + "exit": 1 + } + }, + "preRun": [ + { + "writeFile": { + "path": "src/Configuration.xml", + "content": "\n" + } + } + ], + "args_extra": [ + "-V8Path", + "{fakePlatform}", + "-InfoBasePath", + "{workDir}/ib", + "-ConfigDir", + "{workDir}/src", + "-Extension", + "Проба" + ], + "expectError": true, + "expect": { + "stdoutNotContains": "will not apply" + }, + "noSnapshot": "навык ничего не пишет в рабочий каталог — проверяется условие запуска" +} diff --git a/tests/skills/cases/db-load-xml/apply-check-not-for-main-config.json b/tests/skills/cases/db-load-xml/apply-check-not-for-main-config.json new file mode 100644 index 000000000..b6f744c49 --- /dev/null +++ b/tests/skills/cases/db-load-xml/apply-check-not-for-main-config.json @@ -0,0 +1,31 @@ +{ + "name": "Загрузка основной конфигурации: применимость не проверяется", + "fakePlatform": { + "log": "Загрузка конфигурации из файлов.\r\n", + "check": { + "log": "Проба: Список параметров метода \"Проба_Расчёт\" не соответствует методу \"Расчёт\".\r\n", + "exit": 1 + } + }, + "args_extra": [ + "-V8Path", + "{fakePlatform}", + "-InfoBasePath", + "{workDir}/ib", + "-ConfigDir", + "{workDir}/src" + ], + "expect": { + "stdoutContains": "Load completed successfully", + "stdoutNotContains": "will not apply" + }, + "noSnapshot": "навык ничего не пишет в рабочий каталог — проверяется условие запуска", + "preRun": [ + { + "writeFile": { + "path": "src/Configuration.xml", + "content": "\n" + } + } + ] +} diff --git a/tests/skills/cases/db-load-xml/apply-check-off-by-project.json b/tests/skills/cases/db-load-xml/apply-check-off-by-project.json new file mode 100644 index 000000000..11392cc31 --- /dev/null +++ b/tests/skills/cases/db-load-xml/apply-check-off-by-project.json @@ -0,0 +1,40 @@ +{ + "name": "extensionApplyCheck: false в .v8-project.json выключает проверку", + "fakePlatform": { + "log": "Загрузка конфигурации из файлов.\r\n", + "check": { + "log": "Проба: Список параметров метода \"Проба_Расчёт\" не соответствует методу \"Расчёт\".\r\n", + "exit": 1 + } + }, + "cwd": "workDir", + "preRun": [ + { + "writeFile": { + "path": "src/Configuration.xml", + "content": "\n" + } + }, + { + "writeFile": { + "path": ".v8-project.json", + "content": "{\n \"extensionApplyCheck\": false\n}" + } + } + ], + "args_extra": [ + "-V8Path", + "{fakePlatform}", + "-InfoBasePath", + "{workDir}/ib", + "-ConfigDir", + "{workDir}/src", + "-Extension", + "Проба" + ], + "expect": { + "stdoutContains": "Load completed successfully", + "stdoutNotContains": "will not apply" + }, + "noSnapshot": "навык ничего не пишет в рабочий каталог — проверяется настройка проекта" +} diff --git a/tests/skills/cases/db-load-xml/apply-check-skipped-by-switch.json b/tests/skills/cases/db-load-xml/apply-check-skipped-by-switch.json new file mode 100644 index 000000000..28c54e099 --- /dev/null +++ b/tests/skills/cases/db-load-xml/apply-check-skipped-by-switch.json @@ -0,0 +1,34 @@ +{ + "name": "-NoApplyCheck: проверка не запускается вовсе", + "fakePlatform": { + "log": "Загрузка конфигурации из файлов.\r\n", + "check": { + "log": "Проба: Список параметров метода \"Проба_Расчёт\" не соответствует методу \"Расчёт\".\r\n", + "exit": 1 + } + }, + "args_extra": [ + "-V8Path", + "{fakePlatform}", + "-InfoBasePath", + "{workDir}/ib", + "-ConfigDir", + "{workDir}/src", + "-Extension", + "Проба", + "-NoApplyCheck" + ], + "expect": { + "stdoutContains": "Load completed successfully", + "stdoutNotContains": "will not apply" + }, + "noSnapshot": "навык ничего не пишет в рабочий каталог — проверяется постусловие", + "preRun": [ + { + "writeFile": { + "path": "src/Configuration.xml", + "content": "\n" + } + } + ] +} diff --git a/tests/skills/cases/db-load-xml/apply-check-strict.json b/tests/skills/cases/db-load-xml/apply-check-strict.json new file mode 100644 index 000000000..6218eaf1e --- /dev/null +++ b/tests/skills/cases/db-load-xml/apply-check-strict.json @@ -0,0 +1,34 @@ +{ + "name": "То же с -StrictLog: предупреждение поднимает код возврата", + "fakePlatform": { + "log": "Загрузка конфигурации из файлов.\r\n", + "check": { + "log": "Проба: Список параметров метода \"Проба_Расчёт\" не соответствует методу \"Расчёт\".\r\n", + "exit": 1 + } + }, + "args_extra": [ + "-V8Path", + "{fakePlatform}", + "-InfoBasePath", + "{workDir}/ib", + "-ConfigDir", + "{workDir}/src", + "-Extension", + "Проба", + "-StrictLog" + ], + "expectError": true, + "expect": { + "stdoutContains": "will not apply it" + }, + "noSnapshot": "навык ничего не пишет в рабочий каталог — проверяется постусловие", + "preRun": [ + { + "writeFile": { + "path": "src/Configuration.xml", + "content": "\n" + } + } + ] +} diff --git a/tests/skills/cases/db-load-xml/apply-check-warns.json b/tests/skills/cases/db-load-xml/apply-check-warns.json new file mode 100644 index 000000000..18a19b4e8 --- /dev/null +++ b/tests/skills/cases/db-load-xml/apply-check-warns.json @@ -0,0 +1,36 @@ +{ + "name": "Расширение загрузилось, но платформа его не применит — предупреждение, код возврата не меняется", + "fakePlatform": { + "log": "Загрузка конфигурации из файлов.\r\n", + "check": { + "log": "Проба: Список параметров метода \"Проба_Расчёт\" не соответствует методу \"Расчёт\".\r\n", + "exit": 1 + } + }, + "args_extra": [ + "-V8Path", + "{fakePlatform}", + "-InfoBasePath", + "{workDir}/ib", + "-ConfigDir", + "{workDir}/src", + "-Extension", + "Проба" + ], + "expect": { + "stdoutContains": [ + "Load completed successfully", + "[warning] the extension is loaded, but the platform will not apply it:", + "не соответствует методу" + ] + }, + "noSnapshot": "навык ничего не пишет в рабочий каталог — проверяется постусловие", + "preRun": [ + { + "writeFile": { + "path": "src/Configuration.xml", + "content": "\n" + } + } + ] +} diff --git a/tests/skills/cases/db-update/apply-check-warns.json b/tests/skills/cases/db-update/apply-check-warns.json new file mode 100644 index 000000000..e7993c027 --- /dev/null +++ b/tests/skills/cases/db-update/apply-check-warns.json @@ -0,0 +1,25 @@ +{ + "name": "Обновление расширения прошло, но платформа его не применит — предупреждение", + "fakePlatform": { + "log": "Обновление конфигурации успешно завершено\r\n", + "check": { + "log": "Проба: Список параметров метода \"Проба_Расчёт\" не соответствует методу \"Расчёт\".\r\n", + "exit": 1 + } + }, + "args_extra": [ + "-V8Path", + "{fakePlatform}", + "-InfoBasePath", + "{workDir}/ib", + "-Extension", + "Проба" + ], + "expect": { + "stdoutContains": [ + "Database configuration updated successfully", + "will not apply it" + ] + }, + "noSnapshot": "навык ничего не пишет в рабочий каталог — проверяется постусловие" +} diff --git a/tests/skills/check-inline-drift.mjs b/tests/skills/check-inline-drift.mjs index 57fc17f39..23ae7e9e6 100644 --- a/tests/skills/check-inline-drift.mjs +++ b/tests/skills/check-inline-drift.mjs @@ -71,7 +71,7 @@ const FAMILIES = [ // вверх. Группа db-* использует её же, чтобы найти запись базы и взять реквизиты // хранилища — задача одна, поэтому семья общая, а не вторая с тем же телом. { id: 'full', authority: 'cf-edit', - consumers: ['cfe-borrow', 'db-cfe-admin', 'db-dump-xml', 'db-load-git', 'db-load-xml', 'db-repo', 'db-update', + consumers: ['cfe-borrow', 'db-cfe-admin', 'db-dump-xml', 'db-load-cf', 'db-load-git', 'db-load-xml', 'db-repo', 'db-update', 'form-add', 'form-compile', 'form-edit', 'help-add', 'interface-edit', 'meta-compile', 'meta-edit', 'meta-remove', 'mxl-compile', 'role-compile', 'skd-compile', 'skd-edit', 'subsystem-compile', 'subsystem-edit', 'template-add', 'xdto-compile', 'xdto-edit'] }, @@ -250,6 +250,50 @@ const FAMILIES = [ { id: 'base', authority: 'db-load-xml', consumers: ['db-load-git', 'db-update'] }, ], }, + // ─── Постусловие применимости расширения ──────────────────────────────── + // Проверку ОБЯЗАТЕЛЬНО запускать отдельным процессом: в одной командной строке DESIGNER + // выполняет только последнюю пакетную команду, и дописанная проверка отменяет саму загрузку. + // Разъехавшиеся копии означали бы, что один навык предупреждает о неприменимом расширении, + // а соседний по той же операции — молчит. + { + name: 'apply check: run', py: 'run_apply_check', ps1: 'Invoke-ApplyCheck', + variants: [ + { id: 'base', authority: 'db-load-xml', consumers: ['db-load-cf', 'db-load-git', 'db-update'] }, + ], + }, + { + name: 'apply check: report', py: 'apply_check_report', ps1: 'Invoke-ApplyCheckReport', + variants: [ + { id: 'base', authority: 'db-load-xml', consumers: ['db-load-cf', 'db-load-git', 'db-update'] }, + ], + }, + { + name: 'apply check: enabled', py: 'apply_check_enabled', ps1: 'Get-ApplyCheckEnabled', + variants: [ + { id: 'base', authority: 'db-load-xml', consumers: ['db-load-cf', 'db-load-git', 'db-update'] }, + ], + }, + // Дополнительные аргументы: и владение ключами, и запрет пакетных команд. Разъехавшиеся копии + // означали бы, что один навык отбивает команду, отменяющую его же операцию, а соседний — нет. + { + name: 'platform: assert_extra_args', py: 'assert_extra_args', ps1: 'Assert-ExtraArgs', + variants: [ + { id: 'base', authority: 'db-create', + consumers: ['db-cfe-admin', 'db-dump-cf', 'db-dump-dt', 'db-dump-xml', 'db-load-cf', 'db-load-dt', + 'db-load-git', 'db-load-xml', 'db-repo', 'db-run', 'db-update', 'epf-build', 'epf-dump'] }, + // NB: stub-db-create.py внутри epf-build держит свою копию с выводом в stderr, но гарду она + // не видна: индекс берёт первый файл навыка по алфавиту (epf-build.py). Правя эту семью, + // не забывать про стаб — автоматически он не проверяется. + ], + }, + { + name: 'platform: arg_key_match', py: 'arg_key_match', ps1: 'Test-ArgKeyMatch', + variants: [ + { id: 'base', authority: 'db-create', + consumers: ['db-cfe-admin', 'db-dump-cf', 'db-dump-dt', 'db-dump-xml', 'db-load-cf', 'db-load-dt', + 'db-load-git', 'db-load-xml', 'db-repo', 'db-run', 'db-update', 'epf-build', 'epf-dump'] }, + ], + }, { name: 'platform: resolve_extra_args', py: 'resolve_extra_args', ps1: 'Resolve-ExtraArgs', variants: [ diff --git a/tests/skills/runner.mjs b/tests/skills/runner.mjs index 07d466e8c..f2de641d6 100644 --- a/tests/skills/runner.mjs +++ b/tests/skills/runner.mjs @@ -373,15 +373,20 @@ function buildArgs(skillConfig, caseData, workDir, inputFilePath, runtime) { // под текущую ОС, поэтому один кейс проверяется на обеих. Раньше каждый такой сценарий // приходилось дублировать -posix двойником, и забытый двойник означал дыру: у db-repo на // маке выполнялся 1 кейс из 12, и заметили это случайно. -const FAKE_PLATFORM_CMD = "@echo off\r\nrem SELF запоминаем ДО цикла: shift сдвигает и %0, после него %~dp0 указывает не на скрипт\r\nset SELF=%~dp0\r\n:loop\r\nif \"%~1\"==\"\" goto done\r\nif /i \"%~1\"==\"/Out\" set OUT=%~2\r\nshift\r\ngoto loop\r\n:done\r\ncopy /y \"%SELF%log.txt\" \"%OUT%\" >nul\r\nexit /b 0\r\n"; -const FAKE_PLATFORM_SH = "#!/bin/sh\n# Фейк платформы для *nix: вычитывает путь из /Out и кладёт туда готовый лог.\n# Значение /Out несёт кавычки ВНУТРИ токена (соглашение 1С, см. run_v8) — в batch их\n# снимает %~2, в sh их надо снять руками, иначе cp целится в имя с кавычками.\nSELF=$(dirname \"$0\")\nOUT=\"\"\nwhile [ $# -gt 0 ]; do\n if [ \"$1\" = \"/Out\" ]; then\n OUT=\"$2\"\n OUT=\"${OUT#\\\"}\"\n OUT=\"${OUT%\\\"}\"\n fi\n shift\ndone\ncp \"$SELF/log.txt\" \"$OUT\"\nexit 0\n"; +// Второй ответ — на проверку применимости расширения: навык запускает её ОТДЕЛЬНЫМ процессом +// (в одной командной строке платформа выполнила бы только последнюю команду), поэтому фейк +// отличает проверку по составу аргументов, а не по номеру вызова. +const FAKE_PLATFORM_CMD = "@echo off\r\nrem SELF запоминаем ДО цикла: shift сдвигает и %0, после него %~dp0 указывает не на скрипт\r\nset SELF=%~dp0\r\nset KIND=main\r\n:loop\r\nif \"%~1\"==\"\" goto done\r\nif /i \"%~1\"==\"/Out\" set OUT=%~2\r\nif /i \"%~1\"==\"/CheckCanApplyConfigurationExtensions\" set KIND=check\r\nshift\r\ngoto loop\r\n:done\r\nif \"%KIND%\"==\"check\" if exist \"%SELF%log_check.txt\" (copy /y \"%SELF%log_check.txt\" \"%OUT%\" >nul & exit /b CHECKCODE)\r\ncopy /y \"%SELF%log.txt\" \"%OUT%\" >nul\r\nexit /b EXITCODE\r\n"; +const FAKE_PLATFORM_SH = "#!/bin/sh\n# Фейк платформы для *nix: вычитывает путь из /Out и кладёт туда готовый лог.\n# Значение /Out несёт кавычки ВНУТРИ токена (соглашение 1С, см. run_v8) — в batch их\n# снимает %~2, в sh их надо снять руками, иначе cp целится в имя с кавычками.\nSELF=$(dirname \"$0\")\nOUT=\"\"\nKIND=main\nwhile [ $# -gt 0 ]; do\n if [ \"$1\" = \"/CheckCanApplyConfigurationExtensions\" ]; then\n KIND=check\n fi\n if [ \"$1\" = \"/Out\" ]; then\n OUT=\"$2\"\n OUT=\"${OUT#\\\"}\"\n OUT=\"${OUT%\\\"}\"\n fi\n shift\ndone\nif [ \"$KIND\" = check ] && [ -f \"$SELF/log_check.txt\" ]; then\n cp \"$SELF/log_check.txt\" \"$OUT\"\n exit CHECKCODE\nfi\ncp \"$SELF/log.txt\" \"$OUT\"\nexit EXITCODE\n"; function writeFakePlatform(workDir, spec) { const isWin = process.platform === 'win32'; const code = Number.isInteger(spec.exit) ? spec.exit : 0; - const body = isWin - ? FAKE_PLATFORM_CMD.replace('exit /b 0', `exit /b ${code}`) - : FAKE_PLATFORM_SH.replace('exit 0', `exit ${code}`); + // spec.check — ответ на проверку применимости расширения (отдельный запуск платформы) + const checkCode = spec.check && Number.isInteger(spec.check.exit) ? spec.check.exit : 0; + const body = (isWin ? FAKE_PLATFORM_CMD : FAKE_PLATFORM_SH) + .replaceAll('EXITCODE', String(code)) + .replaceAll('CHECKCODE', String(checkCode)); const exe = join(workDir, isWin ? 'fake.cmd' : 'fake.sh'); writeFileSync(exe, body, 'utf8'); // Бит исполнения: на *nix навык запускает платформу через exec, без +x фейк не стартует. @@ -389,6 +394,7 @@ function writeFakePlatform(workDir, spec) { // Лог пишется как есть — вместе с BOM и CRLF, если кейс их объявил: /Out платформы // выглядит именно так, и разбор должен проверяться на настоящей форме. writeFileSync(join(workDir, 'log.txt'), spec.log ?? '', 'utf8'); + if (spec.check) writeFileSync(join(workDir, 'log_check.txt'), spec.check.log ?? '', 'utf8'); // Заглушка базы: навыки отказываются работать, не найдя 1Cv8.1CD, и это правильно. if (spec.baseStub !== false) { const ib = join(workDir, 'ib');