fix: DesignLock gegen parallele Bestätigungen; confirm ohne design_rev ist ein Eingabefehler
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
fc8ffeba9e
commit
048cd9353d
@@ -6,6 +6,7 @@ namespace LIT\MCP\Tests\Unit\Integrations\Divi\Design;
|
||||
|
||||
use LIT\MCP\Integrations\Divi\Design\DesignArea;
|
||||
use LIT\MCP\Integrations\Divi\Design\DesignChange;
|
||||
use LIT\MCP\Integrations\Divi\Design\DesignLock;
|
||||
use LIT\MCP\Integrations\Divi\Design\DesignPlan;
|
||||
use LIT\MCP\Integrations\Divi\Design\SnapshotStore;
|
||||
use LIT\MCP\Integrations\Divi\Design\SnapshotTooLarge;
|
||||
@@ -17,11 +18,35 @@ use LIT\MCP\Integrations\Divi\Release\ReleaseStatus;
|
||||
use LIT\MCP\Abilities\WriteFailed;
|
||||
use LIT\MCP\Tests\Support\FakeChangeLog;
|
||||
use LIT\MCP\Tests\Unit\TestCase;
|
||||
use Brain\Monkey\Functions;
|
||||
use Mockery;
|
||||
use RuntimeException;
|
||||
|
||||
final class DesignChangeTest extends TestCase
|
||||
{
|
||||
/** @var array<string, mixed> Options as add_option()/delete_option() see them (DesignLock). */
|
||||
private array $options = [];
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->options = [];
|
||||
Functions\when('add_option')->alias(function (string $name, mixed $value = ''): bool {
|
||||
if (array_key_exists($name, $this->options)) {
|
||||
return false;
|
||||
}
|
||||
$this->options[$name] = $value;
|
||||
|
||||
return true;
|
||||
});
|
||||
Functions\when('get_option')->alias(fn (string $name, mixed $default = false): mixed => $this->options[$name] ?? $default);
|
||||
Functions\when('delete_option')->alias(function (string $name): bool {
|
||||
unset($this->options[$name]);
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A DesignArea over an array; the first $failWrites calls of write()/restore() throw, $differences are the
|
||||
* value warnings of differences().
|
||||
@@ -164,6 +189,118 @@ final class DesignChangeTest extends TestCase
|
||||
self::assertSame([], $area->writes);
|
||||
}
|
||||
|
||||
/** Final review M-9: confirm without design_rev is a missing input, not a stale one. */
|
||||
public function testConfirmWithoutDesignRevIsInvalidInput(): void
|
||||
{
|
||||
$stored = ['v' => 1];
|
||||
$area = self::area($stored);
|
||||
$snapshots = Mockery::mock(SnapshotStore::class);
|
||||
$snapshots->shouldNotReceive('save');
|
||||
|
||||
$result = (new DesignChange($snapshots, FakeChangeLog::forbidden()))
|
||||
->run('lit/divi-presets-set', $area, ['confirm' => true], self::plan(['v' => 2]));
|
||||
|
||||
$details = self::assertToolError($result, 'validation_failed');
|
||||
self::assertSame('design_rev fehlt; nichts wurde gespeichert.', $details['message']);
|
||||
self::assertSame('aus dem Probelauf übernehmen', $details['fields']['design_rev']);
|
||||
self::assertSame([], $area->writes);
|
||||
}
|
||||
|
||||
/** Final review I-5: a confirm while another change of the area runs writes nothing. */
|
||||
public function testAHeldLockRefusesTheConfirm(): void
|
||||
{
|
||||
$this->options['lit_mcp_lock_presets'] = time();
|
||||
$stored = ['v' => 1];
|
||||
$area = self::area($stored);
|
||||
$snapshots = Mockery::mock(SnapshotStore::class);
|
||||
$snapshots->shouldNotReceive('save');
|
||||
|
||||
$result = (new DesignChange($snapshots, FakeChangeLog::forbidden(), new DesignLock()))->run(
|
||||
'lit/divi-presets-set',
|
||||
$area,
|
||||
['confirm' => true, 'design_rev' => $area->rev($stored)],
|
||||
self::plan(['v' => 2])
|
||||
);
|
||||
|
||||
self::assertToolError($result, 'conflict');
|
||||
self::assertSame(409, $result->get_error_data()['status']);
|
||||
self::assertStringContainsString('andere Änderung an den Presets', $result->get_error_message());
|
||||
self::assertSame([], $area->writes);
|
||||
self::assertArrayHasKey('lit_mcp_lock_presets', $this->options, 'a foreign lock is not released');
|
||||
}
|
||||
|
||||
public function testTheLockIsHeldWhileWritingAndReleasedAfterSuccess(): void
|
||||
{
|
||||
$stored = ['v' => 1];
|
||||
$area = self::area($stored);
|
||||
$snapshots = Mockery::mock(SnapshotStore::class);
|
||||
$snapshots->expects('save')->andReturnUsing(function (): string {
|
||||
self::assertArrayHasKey('lit_mcp_lock_presets', $this->options, 'snapshot under the lock');
|
||||
|
||||
return 'presets-1';
|
||||
});
|
||||
$snapshots->expects('attachLog')->andReturnUsing(function (): void {
|
||||
self::assertArrayHasKey('lit_mcp_lock_presets', $this->options, 'log under the lock');
|
||||
});
|
||||
|
||||
$result = (new DesignChange($snapshots, new FakeChangeLog(9)))->run(
|
||||
'lit/divi-presets-set',
|
||||
$area,
|
||||
['confirm' => true, 'design_rev' => $area->rev($stored)],
|
||||
self::plan(['v' => 2])
|
||||
);
|
||||
|
||||
self::assertTrue($result['ok']);
|
||||
self::assertArrayNotHasKey('lit_mcp_lock_presets', $this->options);
|
||||
}
|
||||
|
||||
public function testTheLockIsReleasedAfterAFailedWrite(): void
|
||||
{
|
||||
$stored = ['v' => 1];
|
||||
$area = self::area($stored, 1);
|
||||
$snapshots = Mockery::mock(SnapshotStore::class);
|
||||
$snapshots->expects('save')->andReturn('presets-1');
|
||||
|
||||
try {
|
||||
(new DesignChange($snapshots, FakeChangeLog::forbidden()))->run(
|
||||
'lit/divi-presets-set',
|
||||
$area,
|
||||
['confirm' => true, 'design_rev' => $area->rev($stored)],
|
||||
self::plan(['v' => 2])
|
||||
);
|
||||
self::fail('expected WriteFailed');
|
||||
} catch (WriteFailed) {
|
||||
self::assertArrayNotHasKey('lit_mcp_lock_presets', $this->options);
|
||||
}
|
||||
}
|
||||
|
||||
/** The design_rev is checked against the inventory read under the lock, not before it. */
|
||||
public function testTheRevIsCheckedOnTheInventoryReadUnderTheLock(): void
|
||||
{
|
||||
$stored = ['v' => 1];
|
||||
$area = self::area($stored);
|
||||
$rev = $area->rev($stored);
|
||||
$snapshots = Mockery::mock(SnapshotStore::class);
|
||||
$snapshots->shouldNotReceive('save');
|
||||
Functions\when('add_option')->alias(function (string $name, mixed $value = '') use (&$stored): bool {
|
||||
$stored = ['v' => 'parallel'];
|
||||
$this->options[$name] = $value;
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
$result = (new DesignChange($snapshots, FakeChangeLog::forbidden()))->run(
|
||||
'lit/divi-presets-set',
|
||||
$area,
|
||||
['confirm' => true, 'design_rev' => $rev],
|
||||
self::plan(['v' => 2])
|
||||
);
|
||||
|
||||
self::assertToolError($result, 'rev_mismatch');
|
||||
self::assertSame([], $area->writes);
|
||||
self::assertArrayNotHasKey('lit_mcp_lock_presets', $this->options);
|
||||
}
|
||||
|
||||
public function testBlockedUsagesRefuseTheConfirm(): void
|
||||
{
|
||||
$stored = ['v' => 1];
|
||||
|
||||
Reference in New Issue
Block a user