fix: TB dry runs hide uneditable live templates, off-switch despite draft (rulings Q20, Q21)

- ThemeBuilderAccess::editableLiveTemplates() shared by tb-list and the tb-assign/tb-activate dry runs;
  uneditable live templates only as a count (hidden_overlaps, hidden_live)
- tb-activate enabled:false works while the Theme Builder has a draft, with a warning
- possible_overlaps skips identical conditions; reattach records the previous enabled state;
  exclude_from must be a list; tb-activate live from the read-back

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-27 20:55:53 +02:00
co-authored by Claude Opus 5.5
parent 64fc439cb2
commit a31d4340f9
11 changed files with 355 additions and 37 deletions
@@ -176,6 +176,8 @@ final class TbAssignAbilityTest extends TestCase
self::assertTrue($result['ok']);
self::assertSame([1066, 1061, 1062, 1063], $unmarked);
self::assertStringContainsString('wieder zugeordnet', $log->summaries()[0]);
self::assertStringContainsString('vorher aktiviert', $log->summaries()[0]);
self::assertTrue($result['previous']['enabled']);
}
/** Ruling Q8/B10: a layout Divi trashed meanwhile blocks the reattach; nothing is written. */
@@ -306,4 +308,50 @@ final class TbAssignAbilityTest extends TestCase
self::assertToolError(self::ability($store)->permission(self::input($template, ['template_rev' => 'x'])), 'rev_mismatch');
self::assertToolError(self::ability($store)->permission(self::input($template, ['template_id' => 4711])), 'not_found');
}
/** Ruling Q21: live templates the user may not edit appear only as a number in the dry run. */
public function testDryRunHidesLiveTemplatesTheUserMayNotEdit(): void
{
Functions\when('current_user_can')->alias(
static fn (string $cap, mixed ...$args): bool => ! ($cap === 'edit_post' && in_array($args[0] ?? 0, [1071, 1072], true))
);
$template = ThemeBuilderFixtures::template(1066, ['enabled' => true]);
$mine = ThemeBuilderFixtures::template(1070, ['enabled' => true, 'title' => 'Meine', 'use_on' => [self::PERSONS]]);
$secret = ThemeBuilderFixtures::template(1071, ['enabled' => true, 'title' => 'Geheim', 'use_on' => [self::PERSONS]]);
$unrelated = ThemeBuilderFixtures::template(1072, ['enabled' => true, 'title' => 'Geheim 2', 'use_on' => ['404']]);
$store = ThemeBuilderFixtures::store([$template, $mine, $secret, $unrelated]);
$store->shouldReceive('setConditions')->never();
$dry = self::ability($store)->execute(self::input($template));
self::assertToolError($dry, 'confirm_required');
self::assertSame([1070], array_column($dry->get_error_data()['overlaps'], 'id'));
self::assertSame(1, $dry->get_error_data()['hidden_overlaps']);
self::assertStringContainsString('ohne Berechtigung, die sich überschneiden können: 1', self::clientError($dry)['message']);
$all = (string) json_encode([$dry->get_error_data(), self::clientError($dry)], JSON_UNESCAPED_UNICODE);
self::assertStringNotContainsString('1071', $all);
self::assertStringNotContainsString('Geheim', $all);
}
/** Ruling Q20: the draft exemption is for the off-switch only – tb-assign stays refused. */
public function testRefusedWhileTheThemeBuilderHasADraft(): void
{
$template = ThemeBuilderFixtures::template(1066, ['enabled' => true]);
$store = ThemeBuilderFixtures::store([$template], 999);
$store->shouldReceive('setConditions')->never();
self::assertToolError(self::ability($store)->execute(self::input($template, ['confirm' => true])), 'unsupported');
}
public function testExcludeFromMustBeAList(): void
{
$template = ThemeBuilderFixtures::template(1066, ['exclude_from' => ['homepage']]);
$store = ThemeBuilderFixtures::store([$template]);
$store->shouldReceive('setConditions')->never();
$refused = self::ability($store)->execute(self::input($template, ['exclude_from' => null]));
self::assertToolError($refused, 'validation_failed');
self::assertArrayHasKey('exclude_from', self::errorFields($refused));
}
}