You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
pixelfed/tests/Feature/Config/ConfigCacheSyncTest.php

613 lines
24 KiB
PHP

<?php
use App\Models\ConfigCache as ConfigCacheModel;
use App\Services\ConfigCacheService;
use Illuminate\Console\Events\CommandFinished;
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
use Illuminate\Support\Env;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Config;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\NullOutput;
uses(LazilyRefreshDatabase::class);
/*
|--------------------------------------------------------------------------
| admin:pixelfed-config-cache-sync — sync command (Task 11)
|--------------------------------------------------------------------------
|
| Reconciles .env into config_cache for env-bound (ENVCONFIG) keys, hash-gated
| and single-flight. Cases:
| - empty table → seeds missing rows
| - env overwrite for env-present stale rows
| - env-absent / ADMINONLY existing rows untouched
| - idempotent second run
| - change-hash unchanged → no-op (stale row NOT fixed, proving the gate)
| - hash written only on success; marker-absent → sync re-runs
| - PIXELFED_CONFIG_CACHE_SYNC default true; false skips; --no-sync skips
| - lock prevents concurrent run
| - two containers converge (second no-ops on matching hash)
| - config:cache CommandFinished listener fires sync, and only for that command
| - pre-migration safety (structural note)
|
| Env presence uses the suite's putenv + $_ENV/$_SERVER + Env-repo-reset
| reflection helper pattern. The marker/lock cache keys are cleared in
| beforeEach to isolate cases.
|
| Validates: Requirements 3.*, 8.*, 9.*
|
*/
const SYNC_MARKER_KEY = 'config-cache:sync-hash';
const SYNC_LOCK_KEY = 'config-cache:sync';
// Env-bound string key. Env-authoritative (overwritten on drift) when its env
// var is present; DB-authoritative (preserved) when absent.
const SYNC_ENVBOUND_KEY = 'filesystems.disks.s3.region';
const SYNC_ENVBOUND_VAR = 'AWS_DEFAULT_REGION';
// Env-bound key with an in: rule (env authoritative when present + valid).
const SYNC_ENVCONFIG_KEY = 'captcha.driver';
const SYNC_ENVCONFIG_VAR = 'CAPTCHA_DRIVER';
// ADMINONLY key (never overwritten by sync once a row exists).
const SYNC_ADMINONLY_KEY = 'uikit.custom.css';
function syncResetEnvRepository(): void
{
$ref = new ReflectionClass(Env::class);
$prop = $ref->getProperty('repository');
$prop->setAccessible(true);
$prop->setValue(null, null);
}
function syncSetProcessEnv(string $name, ?string $value): void
{
if ($value === null) {
putenv($name);
unset($_ENV[$name], $_SERVER[$name]);
} else {
putenv("{$name}={$value}");
$_ENV[$name] = $value;
$_SERVER[$name] = $value;
}
syncResetEnvRepository();
}
/**
* Clear the sync marker + lock and forget the memoized read cache for the keys
* this suite touches, so each case starts from a clean slate.
*/
function syncReset(): void
{
Cache::forget(SYNC_MARKER_KEY);
Cache::forget(SYNC_LOCK_KEY);
foreach ([SYNC_ENVBOUND_KEY, SYNC_ENVCONFIG_KEY, SYNC_ADMINONLY_KEY] as $key) {
Cache::forget(ConfigCacheService::CACHE_KEY.$key);
}
}
/**
* Run the sync command directly and return its exit code.
*/
function runSync(array $params = []): int
{
return Artisan::call('admin:pixelfed-config-cache-sync', $params);
}
beforeEach(function () {
$this->originalEnv = [
SYNC_ENVBOUND_VAR => getenv(SYNC_ENVBOUND_VAR),
SYNC_ENVCONFIG_VAR => getenv(SYNC_ENVCONFIG_VAR),
'PIXELFED_CONFIG_CACHE_SYNC' => getenv('PIXELFED_CONFIG_CACHE_SYNC'),
];
$this->originalConfig = [
SYNC_ENVBOUND_KEY => Config::get(SYNC_ENVBOUND_KEY),
SYNC_ENVCONFIG_KEY => Config::get(SYNC_ENVCONFIG_KEY),
SYNC_ADMINONLY_KEY => Config::get(SYNC_ADMINONLY_KEY),
];
// Baseline: SYNC_ENVBOUND_* is the "env-authoritative" key (env present +
// valid), SYNC_ENVCONFIG_* is the "env-absent editable" key. Individual
// tests override as needed. Sync opt-in unset (default true).
syncSetProcessEnv(SYNC_ENVBOUND_VAR, 'us-east-1');
syncSetProcessEnv(SYNC_ENVCONFIG_VAR, null);
syncSetProcessEnv('PIXELFED_CONFIG_CACHE_SYNC', null);
syncReset();
});
afterEach(function () {
foreach ($this->originalEnv as $var => $value) {
syncSetProcessEnv($var, $value === false ? null : $value);
}
foreach ($this->originalConfig as $key => $value) {
Config::set($key, $value);
}
syncResetEnvRepository();
syncReset();
});
/*
|--------------------------------------------------------------------------
| empty table → sync seeds missing rows
|--------------------------------------------------------------------------
*/
test('empty table: sync seeds missing rows for keys whose config value is non-null (8.1, 8.2)', function () {
// The keys under test have no row yet (a stray boot-time seed of unrelated
// keys may leave a couple of rows, so we assert per-key absence rather than
// a globally empty table).
foreach ([SYNC_ENVBOUND_KEY, SYNC_ENVCONFIG_KEY, SYNC_ADMINONLY_KEY] as $key) {
ConfigCacheModel::where('k', $key)->delete();
expect(ConfigCacheModel::where('k', $key)->exists())->toBeFalse();
}
// Give the env-bound keys concrete non-null config values so they seed.
// s3.region is env-bound: present env → env-authoritative, seeds from config.
syncSetProcessEnv(SYNC_ENVBOUND_VAR, 'us-east-1');
Config::set(SYNC_ENVBOUND_KEY, 'us-east-1'); // env-bound, env present
Config::set(SYNC_ENVCONFIG_KEY, 'hcaptcha'); // env-bound, env absent → seed from config default
expect(runSync())->toBe(0);
expect(ConfigCacheModel::where('k', SYNC_ENVBOUND_KEY)->value('v'))->toBe('us-east-1');
expect(ConfigCacheModel::where('k', SYNC_ENVCONFIG_KEY)->value('v'))->toBe('hcaptcha');
// ADMINONLY keys are never seeded by the sync.
expect(ConfigCacheModel::where('k', SYNC_ADMINONLY_KEY)->exists())->toBeFalse();
// The success marker is written last, on completion.
expect(Cache::get(SYNC_MARKER_KEY))->not->toBeNull();
});
/*
|--------------------------------------------------------------------------
| env overwrite — ENVBOUND / ENVCONFIG-env-valid stale rows
|--------------------------------------------------------------------------
*/
test('sync overwrites a stale ENVBOUND row to match config (8.3, 9.2)', function () {
Config::set(SYNC_ENVBOUND_KEY, 'us-east-1');
// Plant a stale row that disagrees with config.
ConfigCacheService::putRaw(SYNC_ENVBOUND_KEY, 'eu-west-9');
expect(ConfigCacheModel::where('k', SYNC_ENVBOUND_KEY)->value('v'))->toBe('eu-west-9');
// Marker cleared in beforeEach, so the sync will run.
expect(runSync())->toBe(0);
expect(ConfigCacheModel::where('k', SYNC_ENVBOUND_KEY)->value('v'))->toBe('us-east-1');
});
test('sync overwrites a stale ENVCONFIG row when its env var is present + valid (8.3)', function () {
syncSetProcessEnv(SYNC_ENVCONFIG_VAR, 'turnstile');
Config::set(SYNC_ENVCONFIG_KEY, 'turnstile');
// Plant a stale row that disagrees with the env-resolved config value.
ConfigCacheService::putRaw(SYNC_ENVCONFIG_KEY, 'hcaptcha');
expect(runSync())->toBe(0);
expect(ConfigCacheModel::where('k', SYNC_ENVCONFIG_KEY)->value('v'))->toBe('turnstile');
});
/*
|--------------------------------------------------------------------------
| ENVCONFIG-env-absent / ADMINONLY existing rows untouched
|--------------------------------------------------------------------------
*/
test('sync leaves an ENVCONFIG-env-absent existing row untouched (8.4)', function () {
syncSetProcessEnv(SYNC_ENVCONFIG_VAR, null); // env absent → dynamic
Config::set(SYNC_ENVCONFIG_KEY, 'hcaptcha');
// A custom (admin-managed) value that differs from config.
ConfigCacheService::putRaw(SYNC_ENVCONFIG_KEY, 'turnstile');
expect(runSync())->toBe(0);
// Must NOT be overwritten to the config value.
expect(ConfigCacheModel::where('k', SYNC_ENVCONFIG_KEY)->value('v'))->toBe('turnstile');
});
test('sync leaves an ADMINONLY existing row untouched (8.4)', function () {
Config::set(SYNC_ADMINONLY_KEY, '/* default */');
ConfigCacheService::putRaw(SYNC_ADMINONLY_KEY, '.admin { color: red; }');
expect(runSync())->toBe(0);
expect(ConfigCacheModel::where('k', SYNC_ADMINONLY_KEY)->value('v'))->toBe('.admin { color: red; }');
});
/*
|--------------------------------------------------------------------------
| idempotent second run
|--------------------------------------------------------------------------
*/
test('sync is idempotent: a second run makes no further changes (8.5)', function () {
Config::set(SYNC_ENVBOUND_KEY, 'us-east-1');
ConfigCacheService::putRaw(SYNC_ENVBOUND_KEY, 'eu-west-9');
expect(runSync())->toBe(0);
expect(ConfigCacheModel::where('k', SYNC_ENVBOUND_KEY)->value('v'))->toBe('us-east-1');
$updatedAt = ConfigCacheModel::where('k', SYNC_ENVBOUND_KEY)->value('updated_at');
// Second run: nothing governed changed → hash matches marker → no-op.
expect(runSync())->toBe(0);
expect(ConfigCacheModel::where('k', SYNC_ENVBOUND_KEY)->value('v'))->toBe('us-east-1');
expect(ConfigCacheModel::where('k', SYNC_ENVBOUND_KEY)->value('updated_at'))
->toEqual($updatedAt);
});
/*
|--------------------------------------------------------------------------
| change-hash unchanged → no-op (proves the hash gate)
|--------------------------------------------------------------------------
*/
test('change-hash unchanged: a stale row planted after a successful sync is NOT fixed (8.7)', function () {
Config::set(SYNC_ENVBOUND_KEY, 'us-east-1');
// First run writes the marker for the current governed values.
expect(runSync())->toBe(0);
expect(Cache::get(SYNC_MARKER_KEY))->not->toBeNull();
// Now corrupt the row manually WITHOUT changing any governed config value.
ConfigCacheService::putRaw(SYNC_ENVBOUND_KEY, 'eu-west-9');
// Because the hash still matches the marker, the sync returns early and does
// NOT reconcile the stale row. This proves the hash gate is in effect.
expect(runSync())->toBe(0);
expect(ConfigCacheModel::where('k', SYNC_ENVBOUND_KEY)->value('v'))->toBe('eu-west-9');
});
/*
|--------------------------------------------------------------------------
| hash written only on success; marker-absent → sync re-runs
|--------------------------------------------------------------------------
*/
test('marker written on success equals the current change hash (8.7)', function () {
Config::set(SYNC_ENVBOUND_KEY, 'us-east-1');
expect(runSync())->toBe(0);
// Recompute the change hash the same way the service does and compare.
$expected = ConfigCacheService::configHash();
expect(Cache::get(SYNC_MARKER_KEY))->toBe($expected);
});
test('marker absent → the next run performs reconciliation again (crash-safe invariant, 8.7)', function () {
Config::set(SYNC_ENVBOUND_KEY, 'us-east-1');
expect(runSync())->toBe(0);
expect(Cache::get(SYNC_MARKER_KEY))->not->toBeNull();
// Simulate a crash-before-marker (or a fresh container): forget the marker
// and plant a stale row. With no marker, the sync must reconcile again.
Cache::forget(SYNC_MARKER_KEY);
ConfigCacheService::putRaw(SYNC_ENVBOUND_KEY, 'eu-west-9');
expect(runSync())->toBe(0);
expect(ConfigCacheModel::where('k', SYNC_ENVBOUND_KEY)->value('v'))->toBe('us-east-1');
expect(Cache::get(SYNC_MARKER_KEY))->not->toBeNull();
});
/*
|--------------------------------------------------------------------------
| PIXELFED_CONFIG_CACHE_SYNC / --no-sync
|--------------------------------------------------------------------------
*/
test('PIXELFED_CONFIG_CACHE_SYNC unset defaults to true: sync runs (8.9)', function () {
syncSetProcessEnv('PIXELFED_CONFIG_CACHE_SYNC', null);
Config::set(SYNC_ENVBOUND_KEY, 'us-east-1');
expect(runSync())->toBe(0);
// Seeded → default-on behavior confirmed.
expect(ConfigCacheModel::where('k', SYNC_ENVBOUND_KEY)->exists())->toBeTrue();
expect(Cache::get(SYNC_MARKER_KEY))->not->toBeNull();
});
test('PIXELFED_CONFIG_CACHE_SYNC=false skips the sync entirely (8.9)', function () {
syncSetProcessEnv('PIXELFED_CONFIG_CACHE_SYNC', 'false');
Config::set(SYNC_ENVBOUND_KEY, 'us-east-1');
expect(runSync())->toBe(0);
// Nothing seeded, no marker written.
expect(ConfigCacheModel::where('k', SYNC_ENVBOUND_KEY)->exists())->toBeFalse();
expect(Cache::get(SYNC_MARKER_KEY))->toBeNull();
});
test('--no-sync flag skips the sync entirely (8.9)', function () {
Config::set(SYNC_ENVBOUND_KEY, 'us-east-1');
expect(runSync(['--no-sync' => true]))->toBe(0);
expect(ConfigCacheModel::where('k', SYNC_ENVBOUND_KEY)->exists())->toBeFalse();
expect(Cache::get(SYNC_MARKER_KEY))->toBeNull();
});
/*
|--------------------------------------------------------------------------
| lock prevents concurrent run
|--------------------------------------------------------------------------
*/
test('an externally-held lock makes the sync a no-op even when the hash changed (8.8)', function () {
Config::set(SYNC_ENVBOUND_KEY, 'us-east-1');
// Plant a stale row and ensure the hash differs from any marker (marker is
// cleared in beforeEach), so only the lock can stop the reconciliation.
ConfigCacheService::putRaw(SYNC_ENVBOUND_KEY, 'eu-west-9');
// Acquire the lock with a DIFFERENT owner so the command cannot get it.
$lock = Cache::lock(SYNC_LOCK_KEY, 60);
expect($lock->get())->toBeTrue();
try {
expect(runSync())->toBe(0);
// Lock held → no reconciliation → stale row unchanged, no marker.
expect(ConfigCacheModel::where('k', SYNC_ENVBOUND_KEY)->value('v'))->toBe('eu-west-9');
expect(Cache::get(SYNC_MARKER_KEY))->toBeNull();
} finally {
$lock->release();
}
});
/*
|--------------------------------------------------------------------------
| two containers converge (second no-ops on matching hash)
|--------------------------------------------------------------------------
*/
test('two containers with identical env converge: the second run is a no-op (8.9a)', function () {
Config::set(SYNC_ENVBOUND_KEY, 'us-east-1');
ConfigCacheService::putRaw(SYNC_ENVBOUND_KEY, 'eu-west-9');
// Container A reconciles and writes the shared marker.
expect(runSync())->toBe(0);
expect(ConfigCacheModel::where('k', SYNC_ENVBOUND_KEY)->value('v'))->toBe('us-east-1');
$markerAfterA = Cache::get(SYNC_MARKER_KEY);
expect($markerAfterA)->not->toBeNull();
// Container B (same env, shared Redis/DB) sees the matching hash → no-op.
// Corrupt the row first to prove B does NOT re-reconcile (matching marker).
ConfigCacheService::putRaw(SYNC_ENVBOUND_KEY, 'eu-west-9');
expect(runSync())->toBe(0);
expect(ConfigCacheModel::where('k', SYNC_ENVBOUND_KEY)->value('v'))->toBe('eu-west-9');
expect(Cache::get(SYNC_MARKER_KEY))->toBe($markerAfterA);
});
/*
|--------------------------------------------------------------------------
| config:cache CommandFinished listener fires sync, and only for that command
|--------------------------------------------------------------------------
*/
test('config:cache CommandFinished event triggers the sync (8.6)', function () {
Config::set(SYNC_ENVBOUND_KEY, 'us-east-1');
expect(Cache::get(SYNC_MARKER_KEY))->toBeNull();
event(new CommandFinished('config:cache', new ArrayInput([]), new NullOutput, 0));
// The provider's listener called the sync → marker written, row seeded.
expect(Cache::get(SYNC_MARKER_KEY))->not->toBeNull();
expect(ConfigCacheModel::where('k', SYNC_ENVBOUND_KEY)->exists())->toBeTrue();
});
test('a non config:cache CommandFinished event does NOT trigger the sync (8.6)', function () {
Config::set(SYNC_ENVBOUND_KEY, 'us-east-1');
expect(Cache::get(SYNC_MARKER_KEY))->toBeNull();
event(new CommandFinished('config:clear', new ArrayInput([]), new NullOutput, 0));
// Listener is filtered to config:cache only → nothing happened.
expect(Cache::get(SYNC_MARKER_KEY))->toBeNull();
expect(ConfigCacheModel::where('k', SYNC_ENVBOUND_KEY)->exists())->toBeFalse();
});
/*
|--------------------------------------------------------------------------
| pre-migration safety
|--------------------------------------------------------------------------
|
| A true "table missing" run is hard to simulate under LazilyRefreshDatabase
| without dropping config_cache (which would break the shared schema for other
| tests). The command wraps its DB work in try/catch (QueryException) and
| returns 0, so pre-migration safety is covered structurally. We assert the
| observable guarantee that matters: the command always returns exit code 0.
|
*/
test('sync always returns exit code 0 (pre-migration safe by structure, 8.5)', function () {
Config::set(SYNC_ENVBOUND_KEY, 'us-east-1');
expect(runSync())->toBe(0);
});
/*
|--------------------------------------------------------------------------
| env PRESENT but empty → sync prunes the row; env ABSENT → preserve
|--------------------------------------------------------------------------
*/
test('sync prunes a row when the env var is present but its value is empty', function () {
// Env is authoritative and resolves empty → the stale row is meaningless.
syncSetProcessEnv(SYNC_ENVBOUND_VAR, 'us-east-1');
Config::set(SYNC_ENVBOUND_KEY, '');
ConfigCacheModel::where('k', SYNC_ENVBOUND_KEY)->delete();
$row = new ConfigCacheModel;
$row->k = SYNC_ENVBOUND_KEY;
$row->v = 'stale-leftover';
$row->save();
expect(ConfigCacheModel::where('k', SYNC_ENVBOUND_KEY)->exists())->toBeTrue();
expect(runSync())->toBe(0);
expect(ConfigCacheModel::where('k', SYNC_ENVBOUND_KEY)->exists())->toBeFalse();
});
test('sync does not seed a row when the env var is present but its value is empty', function () {
syncSetProcessEnv(SYNC_ENVBOUND_VAR, 'us-east-1');
Config::set(SYNC_ENVBOUND_KEY, '');
ConfigCacheModel::where('k', SYNC_ENVBOUND_KEY)->delete();
expect(runSync())->toBe(0);
expect(ConfigCacheModel::where('k', SYNC_ENVBOUND_KEY)->exists())->toBeFalse();
});
test('sync preserves an existing row when the env var is deleted (DB takes authority)', function () {
// env absent → the DB row holds the last-known .env value and must be KEPT:
// not pruned, seeded, or overwritten from the now-empty config value.
syncSetProcessEnv(SYNC_ENVCONFIG_VAR, null);
Config::set(SYNC_ENVCONFIG_KEY, '');
ConfigCacheModel::where('k', SYNC_ENVCONFIG_KEY)->delete();
$row = new ConfigCacheModel;
$row->k = SYNC_ENVCONFIG_KEY;
$row->v = 'last-known-turnstile';
$row->save();
expect(runSync())->toBe(0);
expect(ConfigCacheModel::where('k', SYNC_ENVCONFIG_KEY)->value('v'))->toBe('last-known-turnstile');
});
test('sync leaves ADMINONLY rows untouched (never seeded, overwritten, or pruned)', function () {
// ADMINONLY has no env var; the sync must never touch its rows.
ConfigCacheService::putRaw(SYNC_ADMINONLY_KEY, '.admin { color: red; }');
// Change the ENVCONFIG key so the sync actually runs (hash flips).
syncSetProcessEnv(SYNC_ENVCONFIG_VAR, null);
Config::set(SYNC_ENVCONFIG_KEY, 'hcaptcha');
expect(runSync())->toBe(0);
// The admin-set value survives regardless of what config() says.
expect(ConfigCacheModel::where('k', SYNC_ADMINONLY_KEY)->value('v'))->toBe('.admin { color: red; }');
});
/*
|--------------------------------------------------------------------------
| --force bypasses the change-hash gate for on-demand cleanup
|--------------------------------------------------------------------------
*/
test('--force reconciles a stale row even when the change-hash is unchanged', function () {
Config::set(SYNC_ENVBOUND_KEY, 'us-east-1');
// First run writes the marker for the current governed values.
expect(runSync())->toBe(0);
expect(Cache::get(SYNC_MARKER_KEY))->not->toBeNull();
// Corrupt the row WITHOUT changing any governed config value, so the hash
// still matches the marker (the gate would normally skip).
ConfigCacheService::putRaw(SYNC_ENVBOUND_KEY, 'eu-west-9');
// --force bypasses the gate and reconciles the stale row back to config.
expect(runSync(['--force' => true]))->toBe(0);
expect(ConfigCacheModel::where('k', SYNC_ENVBOUND_KEY)->value('v'))->toBe('us-east-1');
});
test('--force prunes a stale row when the env value is empty and the hash is unchanged', function () {
// Env present but resolves empty (env-authoritative), so a stale row should
// be pruned. The marker already matches the hash, so a normal run skips.
syncSetProcessEnv(SYNC_ENVBOUND_VAR, 'us-east-1');
Config::set(SYNC_ENVBOUND_KEY, '');
// Seed the marker for the current state so the gate is "closed".
expect(runSync())->toBe(0);
// Plant a stale row after the marker was written.
ConfigCacheModel::where('k', SYNC_ENVBOUND_KEY)->delete();
$row = new ConfigCacheModel;
$row->k = SYNC_ENVBOUND_KEY;
$row->v = 'orphaned-encrypted-value';
$row->save();
// A normal run skips (hash unchanged); --force prunes it.
expect(runSync())->toBe(0);
expect(ConfigCacheModel::where('k', SYNC_ENVBOUND_KEY)->exists())->toBeTrue();
expect(runSync(['--force' => true]))->toBe(0);
expect(ConfigCacheModel::where('k', SYNC_ENVBOUND_KEY)->exists())->toBeFalse();
});
test('--force still respects the single-flight lock', function () {
Config::set(SYNC_ENVBOUND_KEY, 'us-east-1');
// Hold the lock with a different owner so the forced run cannot acquire it.
$lock = Cache::lock(SYNC_LOCK_KEY, 15);
expect($lock->get())->toBeTrue();
try {
ConfigCacheService::putRaw(SYNC_ENVBOUND_KEY, 'eu-west-9');
// Even with --force, a held lock makes the run a no-op.
expect(runSync(['--force' => true]))->toBe(0);
expect(ConfigCacheModel::where('k', SYNC_ENVBOUND_KEY)->value('v'))->toBe('eu-west-9');
} finally {
$lock->release();
}
});
test('--no-sync takes precedence over --force', function () {
Config::set(SYNC_ENVBOUND_KEY, 'us-east-1');
ConfigCacheModel::where('k', SYNC_ENVBOUND_KEY)->delete();
// --no-sync short-circuits before any reconciliation, regardless of --force.
expect(runSync(['--no-sync' => true, '--force' => true]))->toBe(0);
expect(ConfigCacheModel::where('k', SYNC_ENVBOUND_KEY)->exists())->toBeFalse();
});
/*
|--------------------------------------------------------------------------
| a reconcile flushes the cache for EVERY managed key (not just touched)
|--------------------------------------------------------------------------
*/
test('a reconcile flushes the memoized cache for an untouched key so no stale value survives', function () {
// Plant a stale cache entry for an ADMINONLY key that the reconcile will NOT
// touch (its row is left alone). The flush-all must still clear it.
$adminCacheKey = ConfigCacheService::CACHE_KEY.SYNC_ADMINONLY_KEY;
Cache::forever($adminCacheKey, 'STALE-CACHED-VALUE');
expect(Cache::get($adminCacheKey))->toBe('STALE-CACHED-VALUE');
// Force a reconcile to run by changing a governed (ENVBOUND) config value so
// the change-hash differs from any marker.
Config::set(SYNC_ENVBOUND_KEY, 'ap-southeast-2');
expect(runSync())->toBe(0);
// The untouched key's stale cache entry was flushed by flushAll().
expect(Cache::get($adminCacheKey))->toBeNull();
});
test('--force flushes the cache for every managed key even when the hash is unchanged', function () {
Config::set(SYNC_ENVBOUND_KEY, 'us-east-1');
// Seed the marker so a normal run would skip.
expect(runSync())->toBe(0);
$adminCacheKey = ConfigCacheService::CACHE_KEY.SYNC_ADMINONLY_KEY;
Cache::forever($adminCacheKey, 'STALE-CACHED-VALUE');
expect(runSync(['--force' => true]))->toBe(0);
expect(Cache::get($adminCacheKey))->toBeNull();
});