The regenerate job replaced the media item without carrying its meta, silently dropping the user's alt text (and slide metadata) from the persisted post. Copy meta from the freshly-locked post row (not the pre-render snapshot) inside the transaction, so an alt edit made while the multi-second render runs is kept rather than overwritten.
181 lines
6.1 KiB
PHP
181 lines
6.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Jobs\Ai\RegeneratePostMediaImage;
|
|
use App\Models\Media;
|
|
use App\Models\Post;
|
|
use App\Models\User;
|
|
use App\Models\Workspace;
|
|
use Illuminate\Support\Facades\Storage;
|
|
|
|
test('job fallback source context uses post content when source_meta is missing', function () {
|
|
$user = User::factory()->create();
|
|
$workspace = Workspace::factory()->create([
|
|
'user_id' => $user->id,
|
|
'account_id' => $user->account_id,
|
|
]);
|
|
|
|
$post = Post::factory()->create([
|
|
'workspace_id' => $workspace->id,
|
|
'user_id' => $user->id,
|
|
'content' => "Headline with typo ECP\nBody line for fallback context.",
|
|
'media' => [],
|
|
]);
|
|
|
|
$job = new RegeneratePostMediaImage(
|
|
workspaceId: $workspace->id,
|
|
postId: $post->id,
|
|
userId: $user->id,
|
|
mediaId: 'media-ai-1',
|
|
regenerationId: '0196f5ca-bf2e-7d15-9a22-5709ab10d6c9',
|
|
instruction: 'Fix typo from ECP to ICP.',
|
|
);
|
|
|
|
$method = new ReflectionMethod(RegeneratePostMediaImage::class, 'buildSourceContext');
|
|
$method->setAccessible(true);
|
|
|
|
$context = $method->invoke($job, [], $post, $workspace);
|
|
|
|
expect(data_get($context, 'title'))->toBe('Headline with typo ECP');
|
|
expect((string) data_get($context, 'body'))->toContain('Body line for fallback context.');
|
|
expect(data_get($context, 'keywords'))->toBeArray()->not->toBeEmpty();
|
|
expect(data_get($context, 'width'))->toBe(1080);
|
|
expect(data_get($context, 'height'))->toBe(1350);
|
|
});
|
|
|
|
test('merge structured copy keeps text unchanged for image_only mode', function () {
|
|
$job = new RegeneratePostMediaImage(
|
|
workspaceId: 'workspace-id',
|
|
postId: 'post-id',
|
|
userId: 'user-id',
|
|
mediaId: 'media-id',
|
|
regenerationId: 'regen-id',
|
|
instruction: 'change image only',
|
|
);
|
|
|
|
$method = new ReflectionMethod(RegeneratePostMediaImage::class, 'mergeStructuredCopy');
|
|
$method->setAccessible(true);
|
|
|
|
$baseContext = [
|
|
'title' => 'Keep THIS Text',
|
|
'body' => 'Body should remain exactly the same.',
|
|
'keywords' => ['saas', 'dashboard'],
|
|
'background_path' => 'ai-images/bg.webp',
|
|
'language' => 'en',
|
|
'width' => 1080,
|
|
'height' => 1350,
|
|
];
|
|
|
|
$structured = [
|
|
'change_mode' => 'image_only',
|
|
'title' => 'Model tried to rewrite',
|
|
'body' => 'Model tried to rewrite body',
|
|
'keywords' => ['modern office', 'team'],
|
|
];
|
|
|
|
$copy = $method->invoke($job, $baseContext, $structured);
|
|
|
|
expect(data_get($copy, 'title'))->toBe($baseContext['title'])
|
|
->and(data_get($copy, 'body'))->toBe($baseContext['body'])
|
|
->and(data_get($copy, 'keywords'))->toBe(['modern office', 'team'])
|
|
->and(data_get($copy, 'regenerate_image'))->toBeTrue()
|
|
->and(data_get($copy, 'regenerate_text'))->toBeFalse()
|
|
->and(data_get($copy, 'change_mode'))->toBe('image_only');
|
|
});
|
|
|
|
test('regenerating an image keeps the current saved alt text, not a pre-render snapshot', function () {
|
|
$user = User::factory()->create();
|
|
$workspace = Workspace::factory()->create([
|
|
'user_id' => $user->id,
|
|
'account_id' => $user->account_id,
|
|
]);
|
|
|
|
Storage::fake();
|
|
Storage::put('ai-images/regen.webp', 'fake-image-bytes');
|
|
|
|
$targetMedia = Media::factory()->create([
|
|
'mediable_type' => (new Workspace)->getMorphClass(),
|
|
'mediable_id' => $workspace->id,
|
|
'collection' => 'ai-generated',
|
|
]);
|
|
|
|
$post = Post::factory()->create([
|
|
'workspace_id' => $workspace->id,
|
|
'user_id' => $user->id,
|
|
'media' => [[
|
|
'id' => $targetMedia->id,
|
|
'type' => 'image',
|
|
'source' => 'ai',
|
|
'path' => 'ai-images/old.webp',
|
|
'url' => 'https://cdn.test/old.webp',
|
|
'meta' => ['alt_text' => 'CURRENT alt saved during regen', 'slide_title' => 'Intro'],
|
|
]],
|
|
]);
|
|
|
|
$job = new RegeneratePostMediaImage(
|
|
workspaceId: $workspace->id,
|
|
postId: $post->id,
|
|
userId: $user->id,
|
|
mediaId: $targetMedia->id,
|
|
regenerationId: 'regen-id',
|
|
instruction: 'image only tweak',
|
|
);
|
|
|
|
// Snapshot captured before the async render carries a now-stale alt.
|
|
$staleTarget = [
|
|
'id' => $targetMedia->id,
|
|
'type' => 'image',
|
|
'source' => 'ai',
|
|
'meta' => ['alt_text' => 'STALE alt from before regen'],
|
|
];
|
|
|
|
$rendered = ['path' => 'ai-images/regen.webp', 'source_meta' => []];
|
|
|
|
$method = new ReflectionMethod(RegeneratePostMediaImage::class, 'replaceMediaOnPost');
|
|
$method->setAccessible(true);
|
|
$method->invoke($job, $post, $staleTarget, $workspace, $rendered);
|
|
|
|
expect(data_get($post->fresh()->media, '0.meta.alt_text'))->toBe('CURRENT alt saved during regen')
|
|
->and(data_get($post->fresh()->media, '0.meta.slide_title'))->toBe('Intro');
|
|
});
|
|
|
|
test('merge structured copy keeps keywords unchanged for text_only mode', function () {
|
|
$job = new RegeneratePostMediaImage(
|
|
workspaceId: 'workspace-id',
|
|
postId: 'post-id',
|
|
userId: 'user-id',
|
|
mediaId: 'media-id',
|
|
regenerationId: 'regen-id',
|
|
instruction: 'change text only',
|
|
);
|
|
|
|
$method = new ReflectionMethod(RegeneratePostMediaImage::class, 'mergeStructuredCopy');
|
|
$method->setAccessible(true);
|
|
|
|
$baseContext = [
|
|
'title' => 'Old title',
|
|
'body' => 'Old body',
|
|
'keywords' => ['saas', 'dashboard'],
|
|
'background_path' => 'ai-images/bg.webp',
|
|
'language' => 'en',
|
|
'width' => 1080,
|
|
'height' => 1350,
|
|
];
|
|
|
|
$structured = [
|
|
'change_mode' => 'text_only',
|
|
'title' => 'New title',
|
|
'body' => 'New body',
|
|
'keywords' => ['ignored', 'for', 'text-only'],
|
|
];
|
|
|
|
$copy = $method->invoke($job, $baseContext, $structured);
|
|
|
|
expect(data_get($copy, 'title'))->toBe('New title')
|
|
->and(data_get($copy, 'body'))->toBe('New body')
|
|
->and(data_get($copy, 'keywords'))->toBe($baseContext['keywords'])
|
|
->and(data_get($copy, 'regenerate_image'))->toBeFalse()
|
|
->and(data_get($copy, 'regenerate_text'))->toBeTrue()
|
|
->and(data_get($copy, 'change_mode'))->toBe('text_only');
|
|
});
|