Initial commit: Runware-powered content generator foundation
Laravel app with a cost-tracked Runware.ai integration (image, video, text generation), a hard monthly EUR budget cap with live USD/EUR FX conversion, and a locally-maintained model/pricing table since Runware's model-search API doesn't expose pricing.
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
// use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ExampleTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* A basic test example.
|
||||
*/
|
||||
public function test_the_application_returns_a_successful_response(): void
|
||||
{
|
||||
$response = $this->get('/');
|
||||
|
||||
$response->assertRedirect('/generations');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Exceptions\BudgetExceededException;
|
||||
use App\Models\Generation;
|
||||
use App\Models\RunwareModel;
|
||||
use App\Services\Runware\GenerationService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Tests\TestCase;
|
||||
|
||||
class GenerationServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function service(): GenerationService
|
||||
{
|
||||
return $this->app->make(GenerationService::class);
|
||||
}
|
||||
|
||||
public function test_image_generation_records_actual_cost_and_downloads_file(): void
|
||||
{
|
||||
Storage::fake('public');
|
||||
Http::fake([
|
||||
'https://api.runware.ai/v1' => Http::response(['data' => [[
|
||||
'taskType' => 'imageInference',
|
||||
'taskUUID' => '11111111-1111-1111-1111-111111111111',
|
||||
'imageURL' => 'https://cdn.example.com/out.jpg',
|
||||
'cost' => 0.01,
|
||||
]]]),
|
||||
'https://cdn.example.com/out.jpg' => Http::response('FAKEBYTES'),
|
||||
]);
|
||||
|
||||
RunwareModel::create([
|
||||
'modality' => 'image', 'air_id' => 'a:cheap@1', 'label' => 'Cheap',
|
||||
'unit' => 'per_image', 'price_usd' => 0.01, 'active' => true, 'is_default' => true,
|
||||
]);
|
||||
|
||||
$generation = $this->service()->createImage('a cat wearing a hat');
|
||||
|
||||
$this->assertSame('completed', $generation->status);
|
||||
$this->assertEquals(0.01, $generation->cost_usd);
|
||||
$this->assertNotNull($generation->cost_eur);
|
||||
$this->assertFalse($generation->cost_is_estimated);
|
||||
Storage::disk('public')->assertExists($generation->output_path);
|
||||
}
|
||||
|
||||
public function test_failed_api_call_marks_generation_failed_without_cost(): void
|
||||
{
|
||||
Http::fake([
|
||||
'https://api.runware.ai/v1' => Http::response(['errors' => [['message' => 'boom']]], 400),
|
||||
]);
|
||||
|
||||
RunwareModel::create([
|
||||
'modality' => 'image', 'air_id' => 'a:cheap@1', 'label' => 'Cheap',
|
||||
'unit' => 'per_image', 'price_usd' => 0.01, 'active' => true, 'is_default' => true,
|
||||
]);
|
||||
|
||||
$this->expectException(\RuntimeException::class);
|
||||
|
||||
try {
|
||||
$this->service()->createImage('a cat wearing a hat');
|
||||
} finally {
|
||||
$generation = Generation::first();
|
||||
$this->assertSame('failed', $generation->status);
|
||||
$this->assertNull($generation->cost_usd);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_budget_cap_blocks_generation_that_would_exceed_it(): void
|
||||
{
|
||||
config(['runware.monthly_cap_eur' => 0.001]);
|
||||
|
||||
RunwareModel::create([
|
||||
'modality' => 'image', 'air_id' => 'a:pricey@1', 'label' => 'Pricey',
|
||||
'unit' => 'per_image', 'price_usd' => 10, 'active' => true, 'is_default' => true,
|
||||
]);
|
||||
|
||||
$this->expectException(BudgetExceededException::class);
|
||||
|
||||
$this->service()->createImage('this should never call the API');
|
||||
|
||||
Http::assertNothingSent();
|
||||
}
|
||||
|
||||
public function test_text_generation_falls_back_to_estimated_cost_when_usage_cost_missing(): void
|
||||
{
|
||||
Http::fake([
|
||||
'https://api.runware.ai/v1/chat/completions' => Http::response([
|
||||
'choices' => [['message' => ['content' => 'Hello there']]],
|
||||
'usage' => ['total_tokens' => 100],
|
||||
]),
|
||||
]);
|
||||
|
||||
RunwareModel::create([
|
||||
'modality' => 'text', 'air_id' => 't:cheap@1', 'label' => 'Cheap',
|
||||
'unit' => 'per_million_tokens', 'price_usd' => 1, 'active' => true, 'is_default' => true,
|
||||
]);
|
||||
|
||||
$generation = $this->service()->createText('say hello');
|
||||
|
||||
$this->assertSame('completed', $generation->status);
|
||||
$this->assertSame('Hello there', $generation->output_text);
|
||||
$this->assertTrue($generation->cost_is_estimated);
|
||||
$this->assertEqualsWithDelta(0.0001, $generation->cost_usd, 0.00001);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\RunwareModel;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class RunwareModelDefaultTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_cheapest_active_model_is_marked_default(): void
|
||||
{
|
||||
$expensive = RunwareModel::create([
|
||||
'modality' => 'image', 'air_id' => 'a:expensive@1', 'label' => 'Expensive',
|
||||
'unit' => 'per_image', 'price_usd' => 0.05, 'active' => true,
|
||||
]);
|
||||
|
||||
RunwareModel::recomputeDefault('image');
|
||||
$this->assertTrue($expensive->fresh()->is_default);
|
||||
|
||||
$cheap = RunwareModel::create([
|
||||
'modality' => 'image', 'air_id' => 'a:cheap@1', 'label' => 'Cheap',
|
||||
'unit' => 'per_image', 'price_usd' => 0.01, 'active' => true,
|
||||
]);
|
||||
RunwareModel::recomputeDefault('image');
|
||||
|
||||
$this->assertFalse($expensive->fresh()->is_default);
|
||||
$this->assertTrue($cheap->fresh()->is_default);
|
||||
$this->assertSame($cheap->air_id, RunwareModel::defaultFor('image')->air_id);
|
||||
}
|
||||
|
||||
public function test_inactive_models_are_never_default(): void
|
||||
{
|
||||
$inactiveCheap = RunwareModel::create([
|
||||
'modality' => 'video', 'air_id' => 'v:cheap@1', 'label' => 'Cheap but off',
|
||||
'unit' => 'per_second', 'price_usd' => 0.001, 'active' => false,
|
||||
]);
|
||||
$active = RunwareModel::create([
|
||||
'modality' => 'video', 'air_id' => 'v:active@1', 'label' => 'Active',
|
||||
'unit' => 'per_second', 'price_usd' => 0.05, 'active' => true,
|
||||
]);
|
||||
|
||||
RunwareModel::recomputeDefault('video');
|
||||
|
||||
$this->assertFalse($inactiveCheap->fresh()->is_default);
|
||||
$this->assertTrue($active->fresh()->is_default);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Tests;
|
||||
|
||||
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
||||
|
||||
abstract class TestCase extends BaseTestCase
|
||||
{
|
||||
//
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class ExampleTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* A basic test example.
|
||||
*/
|
||||
public function test_that_true_is_true(): void
|
||||
{
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user