*/ private array $options = []; protected function setUp(): void { parent::setUp(); $this->options = []; Functions\when('get_option')->alias(fn (string $name, mixed $default = false): mixed => $this->options[$name] ?? $default); Functions\when('update_option')->alias(function (string $name, mixed $value, mixed $autoload = null): bool { self::assertFalse($autoload, 'snapshots are never autoloaded'); $this->options[$name] = $value; return true; }); Functions\when('get_current_user_id')->justReturn(7); } public function testSavesListsAndReturnsTheNewestFirst(): void { $store = new SnapshotStore(); $first = $store->save('presets', ['module' => ['a' => 1]], 'lit/divi-presets-set'); $second = $store->save('presets', ['module' => ['a' => 2]], 'lit/divi-presets-set'); $store->attachLog('presets', $second, 42); self::assertSame('lit_mcp_snapshots_presets', SnapshotStore::option('presets')); self::assertMatchesRegularExpression('/^presets-\d{8}T\d{6}Z-[0-9a-f]{6}$/', $first); $list = $store->list('presets'); self::assertSame([$second, $first], array_column($list, 'id')); self::assertSame(42, $list[0]['log_id']); self::assertSame(7, $list[0]['user_id']); self::assertArrayNotHasKey('data', $list[0]); self::assertSame(['module' => ['a' => 1]], $store->get('presets', $first)['data']); self::assertNull($store->get('presets', 'presets-unknown')); self::assertSame([], $store->list('colors')); } public function testKeepsAtMostTwentyPerArea(): void { $store = new SnapshotStore(); $ids = []; for ($i = 0; $i < 22; ++$i) { $ids[] = $store->save('presets', ['n' => $i], 'lit/divi-presets-set'); } $list = $store->list('presets'); self::assertCount(SnapshotStore::LIMIT, $list); self::assertSame($ids[21], $list[0]['id']); self::assertNull($store->get('presets', $ids[0])); self::assertNull($store->get('presets', $ids[1])); } /** Final review I-4: the option as a whole stays below TOTAL_BYTES; the oldest entries fall out first. */ public function testKeepsTheOptionBelowTheTotalSize(): void { $store = new SnapshotStore(); $big = ['x' => str_repeat('a', 1900000)]; $ids = []; for ($i = 0; $i < 5; ++$i) { $ids[] = $store->save('presets', $big + ['n' => $i], 'lit/divi-presets-set'); } $list = $store->list('presets'); self::assertSame([$ids[4], $ids[3], $ids[2], $ids[1]], array_column($list, 'id')); self::assertLessThanOrEqual(SnapshotStore::TOTAL_BYTES, array_sum(array_column($list, 'size'))); } public function testOldEntriesOverTheTotalSizeFallOutButTheNewestStays(): void { $store = new SnapshotStore(); $store->save('presets', ['x' => str_repeat('a', 1900000)], 'lit/divi-presets-set'); $this->options['lit_mcp_snapshots_presets'][0]['size'] = SnapshotStore::TOTAL_BYTES + 1; $newest = $store->save('presets', ['n' => 1], 'lit/divi-presets-set'); self::assertSame([$newest], array_column($store->list('presets'), 'id')); } public function testRefusesSnapshotsAboveTheLimit(): void { $this->expectException(SnapshotTooLarge::class); (new SnapshotStore())->save('presets', ['x' => str_repeat('a', SnapshotStore::MAX_BYTES)], 'lit/divi-presets-set'); } public function testUnknownAreaIsAProgrammingError(): void { $this->expectException(InvalidArgumentException::class); SnapshotStore::option('pages'); } public function testAFailedOptionWriteThrows(): void { Functions\when('update_option')->justReturn(false); $this->expectException(RuntimeException::class); $this->expectExceptionMessage('snapshot presets could not be stored'); (new SnapshotStore())->save('presets', ['a' => 1], 'lit/divi-presets-set'); } public function testAnUnencodableSnapshotIsRefusedBeforeAnyWrite(): void { Functions\when('wp_json_encode')->justReturn(false); $written = false; Functions\when('update_option')->alias(function () use (&$written): bool { $written = true; return true; }); try { (new SnapshotStore())->save('presets', ['a' => 1], 'lit/divi-presets-set'); self::fail('an unencodable snapshot must be refused'); } catch (RuntimeException $e) { self::assertSame('snapshot presets could not be encoded', $e->getMessage()); } self::assertFalse($written, 'nothing may be written when the snapshot cannot be encoded'); } public function testCorruptOptionReadsAsEmpty(): void { $this->options['lit_mcp_snapshots_presets'] = 'kaputt'; self::assertSame([], (new SnapshotStore())->list('presets')); } }