feat(api): standalone xlvask-automation-migrate script + runbook section (#357)

Adds `scripts/xlvask-automation-migrate.php` mirroring the existing schema-script pattern (`check` / `apply --yes`), a new AUTOMATION_RUNBOOK §2a documenting both operator entry points, and `XLVaskAutomationMigrateScriptTest` pinning the gate, the WD check, and the bootstrap references.

Production autopilot-runs (POST /modules/xlvask/services/usage/autopilot-runs) was returning 500 with:
```
XL Vask automation schema is not ready. Apply migration 20260804_xlvask_ai_auto_policy_v2 explicitly.
```
because no operator had invoked the gated `applyExplicitMigration()` since PR #348 shipped the migration class. The frontend half of the fix is the companion change in pleno-vue#280 (missing `minimax_integration_enabled` key).

Operator action required once merged:
```
php index.php run xlvask-automation-migrate
# or
php scripts/xlvask-automation-migrate.php apply --yes
```
Both produce identical JSON status; retain the artifact and rerun `check` to confirm postflight is green.

Diff: +140/-0 (3 files). Tests: 87/87 XL Vask unit + 5/5 new migration script test pass.
This commit is contained in:
Jeppe B
2026-08-10 08:18:28 +02:00
committed by GitHub
parent 3e89085296
commit 801c8e1f6d
4 changed files with 140 additions and 0 deletions
@@ -0,0 +1,65 @@
<?php
/**
* Guard the operator entry point contract for the XL Vask automation
* schema migration. The migration itself is intentionally operator-only
* (see services/nginx/app/modules/xlvask/AUTOMATION_RUNBOOK.md §2), but
* the CLI surface that wraps it must remain gated, idempotent, and
* discoverable.
*
* When PLENO_REPO_ROOT_FOR_TESTS is set (the CI layout, where the repo
* root is bind-mounted alongside services/nginx/app), the test also
* inspects the standalone scripts/xlvask-automation-migrate.php wrapper
* to keep it in lockstep with the cron entry point.
*/
it('routes the xlvask automation migrate CLI command through the gated migration entry point', function (): void {
$cli = file_get_contents(WD . '/cli.php');
expect($cli)
->toContain("case 'xlvask-automation-migrate':")
->toContain("require_once 'cron/EnsureXLVaskAutomationSchema.php'");
});
it('keeps the cron entry point gated by the WD constant and the migration class', function (): void {
$cron = file_get_contents(WD . '/cron/EnsureXLVaskAutomationSchema.php');
expect($cron)
->toContain("if (!defined('WD'))")
->toContain('migration_20260804_xlvask_ai_auto_policy_v2::preflight')
->toContain('migration_20260804_xlvask_ai_auto_policy_v2::apply')
->toContain('xlvask_usage_logs_schema_bootstrap::applyWashIdUniquenessMigration');
});
it('keeps the migration class operator-only and references the bootstrap entry point', function (): void {
$migration = file_get_contents(WD . '/modules/xlvask/migrations/20260804_xlvask_ai_auto_policy_v2.php');
expect($migration)
->toContain('operator-invoked')
->toContain('xlvask_usage_logs_schema_bootstrap::applyExplicitMigration')
->toContain('xlvask_usage_logs_schema_bootstrap::migrationStatus');
});
it('documents both operator entry points in the XL Vask automation runbook', function (): void {
$runbook = file_get_contents(WD . '/modules/xlvask/AUTOMATION_RUNBOOK.md');
expect($runbook)
->toContain('## 2. Explicit schema migration')
->toContain('## 2a. Operator entry points')
->toContain('scripts/xlvask-automation-migrate.php')
->toContain("php index.php run xlvask-automation-migrate");
});
it('keeps the standalone xlvask automation migration script gated and idempotent when the repo root is mounted', function (): void {
$repoRoot = getenv('PLENO_REPO_ROOT_FOR_TESTS');
if ($repoRoot === false || $repoRoot === '') {
expect(true)->toBeTrue(); // covered by CI; local docker lacks the repo-root bind mount
return;
}
$scriptPath = realpath($repoRoot . '/scripts/xlvask-automation-migrate.php');
expect($scriptPath)->not->toBeFalse();
$source = file_get_contents($scriptPath);
expect($source)
->toContain("if (PHP_SAPI !== 'cli')")
->toContain("Refusing schema mutation without: apply --yes")
->toContain('xlvask_usage_logs_schema_bootstrap::applyExplicitMigration')
->toContain('xlvask_usage_logs_schema_bootstrap::migrationStatus');
});