From 1abc4af51974b9dd11f2aa336107373c571b57f2 Mon Sep 17 00:00:00 2001 From: darbs-claude Date: Mon, 23 Feb 2026 05:18:37 +0000 Subject: [PATCH] test: add PromptVersion model tests Add Feature test covering PromptVersion creation, relationships (prompt, creator), restore() rollback method, and version history tracking. Also add idempotent migration for prompts and prompt_versions tables required by the test suite. Closes #15 --- ...0001_01_01_000004_create_prompt_tables.php | 65 ++++ tests/Feature/PromptVersionTest.php | 279 ++++++++++++++++++ 2 files changed, 344 insertions(+) create mode 100644 Migrations/0001_01_01_000004_create_prompt_tables.php create mode 100644 tests/Feature/PromptVersionTest.php diff --git a/Migrations/0001_01_01_000004_create_prompt_tables.php b/Migrations/0001_01_01_000004_create_prompt_tables.php new file mode 100644 index 0000000..f5eac73 --- /dev/null +++ b/Migrations/0001_01_01_000004_create_prompt_tables.php @@ -0,0 +1,65 @@ +id(); + $table->string('name'); + $table->string('category')->nullable(); + $table->text('description')->nullable(); + $table->text('system_prompt')->nullable(); + $table->text('user_template')->nullable(); + $table->json('variables')->nullable(); + $table->string('model')->nullable(); + $table->json('model_config')->nullable(); + $table->boolean('is_active')->default(true); + $table->timestamps(); + + $table->index('category'); + $table->index('is_active'); + }); + } + + if (! Schema::hasTable('prompt_versions')) { + Schema::create('prompt_versions', function (Blueprint $table) { + $table->id(); + $table->foreignId('prompt_id')->constrained()->cascadeOnDelete(); + $table->unsignedInteger('version'); + $table->text('system_prompt')->nullable(); + $table->text('user_template')->nullable(); + $table->json('variables')->nullable(); + $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete(); + $table->timestamps(); + + $table->index(['prompt_id', 'version']); + }); + } + + Schema::enableForeignKeyConstraints(); + } + + public function down(): void + { + Schema::disableForeignKeyConstraints(); + Schema::dropIfExists('prompt_versions'); + Schema::dropIfExists('prompts'); + Schema::enableForeignKeyConstraints(); + } +}; diff --git a/tests/Feature/PromptVersionTest.php b/tests/Feature/PromptVersionTest.php new file mode 100644 index 0000000..6e4fb96 --- /dev/null +++ b/tests/Feature/PromptVersionTest.php @@ -0,0 +1,279 @@ + 'Test Prompt', + 'system_prompt' => 'You are a helpful assistant.', + 'user_template' => 'Answer this: {{{question}}}', + 'variables' => ['question'], + ]); + + $version = PromptVersion::create([ + 'prompt_id' => $prompt->id, + 'version' => 1, + 'system_prompt' => 'You are a helpful assistant.', + 'user_template' => 'Answer this: {{{question}}}', + 'variables' => ['question'], + ]); + + expect($version->id)->not->toBeNull() + ->and($version->version)->toBe(1) + ->and($version->prompt_id)->toBe($prompt->id); + }); + + it('casts variables as array', function () { + $prompt = Prompt::create(['name' => 'Test Prompt']); + + $version = PromptVersion::create([ + 'prompt_id' => $prompt->id, + 'version' => 1, + 'variables' => ['topic', 'tone'], + ]); + + expect($version->variables) + ->toBeArray() + ->toBe(['topic', 'tone']); + }); + + it('casts version as integer', function () { + $prompt = Prompt::create(['name' => 'Test Prompt']); + + $version = PromptVersion::create([ + 'prompt_id' => $prompt->id, + 'version' => 3, + ]); + + expect($version->version)->toBeInt()->toBe(3); + }); + + it('can be created without optional fields', function () { + $prompt = Prompt::create(['name' => 'Minimal Prompt']); + + $version = PromptVersion::create([ + 'prompt_id' => $prompt->id, + 'version' => 1, + ]); + + expect($version->id)->not->toBeNull() + ->and($version->system_prompt)->toBeNull() + ->and($version->user_template)->toBeNull() + ->and($version->created_by)->toBeNull(); + }); +}); + +// ========================================================================= +// Relationship Tests +// ========================================================================= + +describe('relationships', function () { + it('belongs to a prompt', function () { + $prompt = Prompt::create([ + 'name' => 'Parent Prompt', + 'system_prompt' => 'System text.', + ]); + + $version = PromptVersion::create([ + 'prompt_id' => $prompt->id, + 'version' => 1, + ]); + + expect($version->prompt)->toBeInstanceOf(Prompt::class) + ->and($version->prompt->id)->toBe($prompt->id) + ->and($version->prompt->name)->toBe('Parent Prompt'); + }); + + it('belongs to a creator user', function () { + $user = User::factory()->create(); + $prompt = Prompt::create(['name' => 'Test Prompt']); + + $version = PromptVersion::create([ + 'prompt_id' => $prompt->id, + 'version' => 1, + 'created_by' => $user->id, + ]); + + expect($version->creator)->toBeInstanceOf(User::class) + ->and($version->creator->id)->toBe($user->id); + }); + + it('has null creator when created_by is null', function () { + $prompt = Prompt::create(['name' => 'Test Prompt']); + + $version = PromptVersion::create([ + 'prompt_id' => $prompt->id, + 'version' => 1, + ]); + + expect($version->creator)->toBeNull(); + }); +}); + +// ========================================================================= +// Restore Method Tests +// ========================================================================= + +describe('restore', function () { + it('restores system_prompt and user_template to the parent prompt', function () { + $prompt = Prompt::create([ + 'name' => 'Test Prompt', + 'system_prompt' => 'Original system prompt.', + 'user_template' => 'Original template.', + ]); + + $version = PromptVersion::create([ + 'prompt_id' => $prompt->id, + 'version' => 1, + 'system_prompt' => 'Versioned system prompt.', + 'user_template' => 'Versioned template.', + ]); + + $prompt->update([ + 'system_prompt' => 'Newer system prompt.', + 'user_template' => 'Newer template.', + ]); + + $version->restore(); + + $fresh = $prompt->fresh(); + expect($fresh->system_prompt)->toBe('Versioned system prompt.') + ->and($fresh->user_template)->toBe('Versioned template.'); + }); + + it('restores variables to the parent prompt', function () { + $prompt = Prompt::create([ + 'name' => 'Test Prompt', + 'variables' => ['topic'], + ]); + + $version = PromptVersion::create([ + 'prompt_id' => $prompt->id, + 'version' => 1, + 'variables' => ['topic', 'tone'], + ]); + + $prompt->update(['variables' => ['topic', 'tone', 'length']]); + + $version->restore(); + + expect($prompt->fresh()->variables)->toBe(['topic', 'tone']); + }); + + it('returns the parent prompt instance after restore', function () { + $prompt = Prompt::create([ + 'name' => 'Test Prompt', + 'system_prompt' => 'Old.', + ]); + + $version = PromptVersion::create([ + 'prompt_id' => $prompt->id, + 'version' => 1, + 'system_prompt' => 'Versioned.', + ]); + + $result = $version->restore(); + + expect($result)->toBeInstanceOf(Prompt::class) + ->and($result->id)->toBe($prompt->id); + }); +}); + +// ========================================================================= +// Version History Tests +// ========================================================================= + +describe('version history', function () { + it('prompt tracks multiple versions in descending order', function () { + $prompt = Prompt::create([ + 'name' => 'Evolving Prompt', + 'system_prompt' => 'v1.', + ]); + + PromptVersion::create(['prompt_id' => $prompt->id, 'version' => 1, 'system_prompt' => 'v1.']); + PromptVersion::create(['prompt_id' => $prompt->id, 'version' => 2, 'system_prompt' => 'v2.']); + PromptVersion::create(['prompt_id' => $prompt->id, 'version' => 3, 'system_prompt' => 'v3.']); + + $versions = $prompt->versions()->get(); + + expect($versions)->toHaveCount(3) + ->and($versions->first()->version)->toBe(3) + ->and($versions->last()->version)->toBe(1); + }); + + it('createVersion snapshots current prompt state', function () { + $prompt = Prompt::create([ + 'name' => 'Test Prompt', + 'system_prompt' => 'Original system prompt.', + 'user_template' => 'Original template.', + 'variables' => ['topic'], + ]); + + $version = $prompt->createVersion(); + + expect($version)->toBeInstanceOf(PromptVersion::class) + ->and($version->version)->toBe(1) + ->and($version->system_prompt)->toBe('Original system prompt.') + ->and($version->user_template)->toBe('Original template.') + ->and($version->variables)->toBe(['topic']); + }); + + it('createVersion increments version number', function () { + $prompt = Prompt::create([ + 'name' => 'Test Prompt', + 'system_prompt' => 'v1.', + ]); + + $v1 = $prompt->createVersion(); + $prompt->update(['system_prompt' => 'v2.']); + $v2 = $prompt->createVersion(); + + expect($v1->version)->toBe(1) + ->and($v2->version)->toBe(2); + }); + + it('createVersion records the creator user id', function () { + $user = User::factory()->create(); + $prompt = Prompt::create([ + 'name' => 'Test Prompt', + 'system_prompt' => 'System text.', + ]); + + $version = $prompt->createVersion($user->id); + + expect($version->created_by)->toBe($user->id); + }); + + it('versions are scoped to their parent prompt', function () { + $promptA = Prompt::create(['name' => 'Prompt A']); + $promptB = Prompt::create(['name' => 'Prompt B']); + + PromptVersion::create(['prompt_id' => $promptA->id, 'version' => 1]); + PromptVersion::create(['prompt_id' => $promptA->id, 'version' => 2]); + PromptVersion::create(['prompt_id' => $promptB->id, 'version' => 1]); + + expect($promptA->versions()->count())->toBe(2) + ->and($promptB->versions()->count())->toBe(1); + }); + + it('deleting prompt cascades to versions', function () { + $prompt = Prompt::create(['name' => 'Test Prompt']); + + PromptVersion::create(['prompt_id' => $prompt->id, 'version' => 1]); + PromptVersion::create(['prompt_id' => $prompt->id, 'version' => 2]); + + $promptId = $prompt->id; + $prompt->delete(); + + expect(PromptVersion::where('prompt_id', $promptId)->count())->toBe(0); + }); +}); -- 2.45.3