Skip to content

Release v0.1.9 — Phase 9: Conversion Job Core (CONV-112–CONV-129)#123

Merged
menvil merged 17 commits into
mainfrom
release/v0.1.9-phase09-conversion-job-core
May 31, 2026
Merged

Release v0.1.9 — Phase 9: Conversion Job Core (CONV-112–CONV-129)#123
menvil merged 17 commits into
mainfrom
release/v0.1.9-phase09-conversion-job-core

Conversation

@menvil

@menvil menvil commented May 31, 2026

Copy link
Copy Markdown
Owner

Release v0.1.9 — Phase 9: Conversion Job Core (CONV-112 → CONV-129)

Release branch release/v0.1.9-phase09-conversion-job-coremain.

main is current with develop, so this PR contains exactly the 16 Phase 9 commits (CONV-113 → CONV-127, plus a Pint formatting fixup). Same commits as PR #122 (feature → develop), targeting main for the release.

Phase 9 (backend conversion-job core)

  • ConversionStatus enum, conversion_jobs table, ConversionJob model + factory + relationships
  • ConversionContext / ConversionResult DTOs
  • ConverterDriver interface, ConverterDriverRegistry, test-only FakeConverterDriver
  • RecordConversionResultFileAction, CreateConversionJobAction
  • ProcessConversionJob queue job: queued → processing → completed and safe → failed paths
  • CONV-112 (queue tables) already satisfied by the default Laravel jobs/failed_jobs migration + QUEUE_CONNECTION=database
  • No real image/PDF conversion (Phase 10), no Convert Now UI (Phase 11), no billing (Phase 14/15)

Verification

  • composer test — green (256 tests / 700 assertions)
  • composer lint (Pint) — clean
  • npm run build — passes

Notes

menvil and others added 16 commits May 31, 2026 22:33
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…126/CONV-128)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…CONV-127/CONV-129)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 31, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@menvil, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 36 minutes and 52 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b87ada0c-8053-4a50-b713-4ba266b5e27a

📥 Commits

Reviewing files that changed from the base of the PR and between c8d717e and 0ebc5cc.

📒 Files selected for processing (4)
  • app/Actions/Conversions/CreateConversionJobAction.php
  • app/Actions/Conversions/RecordConversionResultFileAction.php
  • database/factories/ConversionJobFactory.php
  • tests/Feature/Actions/CreateConversionJobActionTest.php
📝 Walkthrough

Walkthrough

This pull request introduces a complete file conversion pipeline enabling users to convert files between formats asynchronously. The system creates and tracks conversion jobs, dispatches them to a queue, executes conversions via pluggable drivers, and persists results as new file records with checksums and metadata.

Changes

Conversion Pipeline Implementation

Layer / File(s) Summary
Core Enums and Data Transfer Objects
app/Enums/ConversionStatus.php, app/Support/Conversions/DTO/ConversionResult.php, app/Support/Conversions/DTO/ConversionContext.php, tests/Unit/Conversions/ConversionResultTest.php, tests/Unit/Conversions/ConversionContextTest.php
ConversionStatus enum defines job lifecycle states. ConversionResult and ConversionContext are immutable DTOs carrying conversion input/output data. Unit tests validate DTO construction and property access.
ConversionJob Model and Relationships
app/Models/ConversionJob.php, app/Models/User.php, app/Models/FileRecord.php, tests/Feature/Models/ConversionJobTest.php
ConversionJob Eloquent model persists job records with fillable attributes, casts for status/options/progress/timestamps, and BelongsTo relationships to User and FileRecord. Inverse HasMany relationships wire User and FileRecord back to ConversionJob. Feature tests validate model creation, relationships, and bidirectional associations.
Driver Contract and Registry Infrastructure
app/Support/Conversions/Contracts/ConverterDriver.php, app/Support/Conversions/ConverterDriverRegistry.php, app/Support/Conversions/Exceptions/MissingConverterDriverException.php, app/Support/Conversions/Exceptions/UnsupportedConversionException.php, tests/Unit/Conversions/ConverterDriverRegistryTest.php, tests/Unit/Conversions/ConverterDriverTest.php
ConverterDriver interface declares key() and convert(ConversionContext) contract. ConverterDriverRegistry manages driver instances, validates duplicate keys during construction, and provides lookup APIs (find, findOrFail) with exception-based failure. Exception classes handle missing drivers and unsupported conversion pairs. Unit tests verify lookup behavior and exception handling.
Service Container and Driver Registration
app/Providers/ConverterServiceProvider.php
ConverterServiceProvider registers ConverterDriverRegistry as a singleton with empty initialization; runtime drivers are registered elsewhere.
Conversion Job Creation and Queuing
app/Actions/Conversions/CreateConversionJobAction.php, tests/Feature/Actions/CreateConversionJobActionTest.php, tests/Unit/Actions/CreateConversionJobActionSkeletonTest.php
CreateConversionJobAction normalizes source/target formats, resolves a converter, validates options, creates a queued ConversionJob, and dispatches ProcessConversionJob. Feature tests verify successful job creation with correct queued state and metadata, source-format normalization from file extension, rejection of unsupported pairs, and rejection of invalid options. Skeleton test confirms method presence.
Async Job Execution and Result Persistence
app/Jobs/ProcessConversionJob.php, app/Actions/Conversions/RecordConversionResultFileAction.php, tests/Feature/Jobs/ProcessConversionJobTest.php, tests/Feature/Actions/RecordConversionResultFileActionTest.php
ProcessConversionJob executes queued conversions: loads the job, guards execution to Queued status, updates state to Processing, resolves a driver, constructs ConversionContext, invokes convert(), records the result, and updates the job to Completed with progress and timestamps; on any exception, marks the job Failed with derived error code/message. RecordConversionResultFileAction reads result file contents, computes SHA-256 checksum, and creates a FileRecord with FileStatus::Analyzed and expiration timestamp. Feature tests validate successful completion, failure handling with error metadata, idempotency (non-queued jobs ignored), and result file persistence with correct checksum and metadata.
Database Schema and Model Factory
database/migrations/2026_05_31_000001_create_conversion_jobs_table.php, database/factories/ConversionJobFactory.php
Migration creates conversion_jobs table with foreign keys to users (cascade) and files (restrict/null), lifecycle/status/progress fields, error tracking, and indexes for efficient querying. Factory generates default attributes including related models, queued status, zero progress, and sample options JSON.
Test Doubles and Unit Tests
tests/Fakes/Conversions/FakeConverterDriver.php, tests/Unit/Conversions/FakeConverterDriverTest.php, tests/Unit/Enums/ConversionStatusTest.php, tests/Unit/Jobs/ProcessConversionJobSkeletonTest.php
FakeConverterDriver test double implements ConverterDriver, writes deterministic output to local storage, and supports configurable failure. Unit tests validate driver behavior, enum values, and job structure.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant CreateAction as CreateConversionJobAction
  participant ConverterReg as ConverterRegistry
  participant Queue as Queue Dispatcher
  participant Job as ProcessConversionJob
  participant DriverReg as ConverterDriverRegistry
  participant Driver as ConverterDriver
  participant Recorder as RecordConversionResultFileAction
  participant DB as Database

  User->>CreateAction: handle(user, file, target_format, options)
  CreateAction->>ConverterReg: normalize & find converter
  ConverterReg-->>CreateAction: converter instance
  CreateAction->>ConverterReg: validate(options)
  ConverterReg-->>CreateAction: validation ok
  CreateAction->>DB: create ConversionJob (Queued)
  DB-->>CreateAction: job record
  CreateAction->>Queue: push ProcessConversionJob
  Queue-->>CreateAction: queued
  CreateAction-->>User: ConversionJob instance

  Queue->>Job: handle(conversionJobId)
  Job->>DB: load ConversionJob
  DB-->>Job: job record
  Job->>DB: update status to Processing
  Job->>DriverReg: findOrFail(converter_key)
  DriverReg-->>Job: ConverterDriver instance
  Job->>Driver: convert(ConversionContext)
  Driver-->>Job: ConversionResult
  Job->>Recorder: handle(user, result)
  Recorder->>Recorder: compute SHA-256
  Recorder->>DB: create FileRecord (Analyzed)
  DB-->>Recorder: FileRecord instance
  Recorder-->>Job: FileRecord
  Job->>DB: update job (Completed, result_file_id, progress=100)
  alt Conversion Fails
    Job->>DB: update job (Failed, error_code, error_message)
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • menvil/FileConverter#120: Introduces/updates InvalidConverterOptionsException validation behavior coupled to CreateConversionJobAction's option validation flow.
  • menvil/FileConverter#76: Established FileRecord model and file-storage metadata infrastructure that conversion-job actions now build relationships upon.
  • menvil/FileConverter#59: Introduced ConverterRegistry and format-normalization logic for converters that CreateConversionJobAction directly depends on.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.71% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly references Phase 9 of the conversion job core implementation with specific issue range (CONV-112–CONV-129), accurately summarizing the main focus of this substantial release.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch release/v0.1.9-phase09-conversion-job-core

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added the release Triggers AI code review (CodeRabbit, Cubic) label May 31, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
database/factories/ConversionJobFactory.php (1)

23-24: ⚡ Quick win

Keep factory ownership consistent between user_id and source_file_id.

Line 23 and Line 24 currently build unrelated records, so tests can generate a ConversionJob whose source file belongs to a different user. Tie source_file_id to the same user to keep fixtures domain-valid.

Proposed diff
     public function definition(): array
     {
         return [
             'user_id' => User::factory(),
-            'source_file_id' => FileRecord::factory(),
+            'source_file_id' => FileRecord::factory()->state(
+                fn (array $attributes): array => ['user_id' => $attributes['user_id']]
+            ),
             'result_file_id' => null,
             'source_format' => 'png',
             'target_format' => 'jpg',
             'converter_key' => 'png_to_jpg',
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@database/factories/ConversionJobFactory.php` around lines 23 - 24,
ConversionJobFactory currently creates an independent User::factory() for
'user_id' and an unrelated FileRecord::factory() for 'source_file_id', which can
produce a source file owned by a different user; fix this by creating a single
shared user factory instance and use it for both fields (e.g. assign $user =
User::factory() to 'user_id' and make 'source_file_id' use
FileRecord::factory()->state(['user_id' => $user]) or, if FileRecord has a user
relation, FileRecord::factory()->for($user) ), updating the ConversionJobFactory
so 'user_id' and FileRecord::factory reference the same $user.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/Actions/Conversions/CreateConversionJobAction.php`:
- Around line 56-62: CreateConversionJobAction is persisting the converter key
from Converter::key() (e.g. "png:jpg") into ConversionJob.converter_key, but
ProcessConversionJob looks up drivers via ConverterDriverRegistry::findOrFail
which expects driver-style keys (e.g. "png_to_jpg"); change
CreateConversionJobAction to store the registry-compatible key instead of the
raw converter key—e.g. transform $converter->key() into the driver key format
expected by ConverterDriverRegistry (replace ":" with "_" or call a
normalization helper on the registry) before saving (affecting the
'converter_key' assignment), so
ProcessConversionJob::findOrFail($job->converter_key) will resolve correctly.

In `@app/Actions/Conversions/RecordConversionResultFileAction.php`:
- Around line 22-31: The code in RecordConversionResultFileAction currently
casts Storage::disk('local')->get($result->path) to string which masks missing
files and lets you store a checksum of an empty string; remove the (string)
cast, check the raw return value from Storage::disk('local')->get($result->path)
and throw a clear exception (e.g., RuntimeException or a domain-specific
exception) if it is null before computing hash('sha256', ...), then proceed to
call FileRecord::create(...) using the validated contents and existing $result
fields.

In `@app/Jobs/ProcessConversionJob.php`:
- Around line 67-76: The catch-all in ProcessConversionJob (catch (Throwable
$exception)) swallows all failures after updating the conversion via
forceFill(...) and report($exception), preventing Laravel from retrying
transient infra errors; change the error handling in ProcessConversionJob so you
only catch domain/terminal exceptions (e.g. specific ConversionDomainException
types) and handle them by updating the conversion record, but for other
exceptions rethrow the exception or call $this->fail($exception) / release() so
the queue can retry; locate the try/catch in the ProcessConversionJob class and
replace the broad Throwable catch with targeted exception types or add logic to
detect infrastructure errors and rethrow or call $this->fail($exception) after
reporting.

---

Nitpick comments:
In `@database/factories/ConversionJobFactory.php`:
- Around line 23-24: ConversionJobFactory currently creates an independent
User::factory() for 'user_id' and an unrelated FileRecord::factory() for
'source_file_id', which can produce a source file owned by a different user; fix
this by creating a single shared user factory instance and use it for both
fields (e.g. assign $user = User::factory() to 'user_id' and make
'source_file_id' use FileRecord::factory()->state(['user_id' => $user]) or, if
FileRecord has a user relation, FileRecord::factory()->for($user) ), updating
the ConversionJobFactory so 'user_id' and FileRecord::factory reference the same
$user.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: eeab42dc-6594-480a-86c4-00d6763f4e2e

📥 Commits

Reviewing files that changed from the base of the PR and between bc17979 and c8d717e.

📒 Files selected for processing (29)
  • app/Actions/Conversions/CreateConversionJobAction.php
  • app/Actions/Conversions/RecordConversionResultFileAction.php
  • app/Enums/ConversionStatus.php
  • app/Jobs/ProcessConversionJob.php
  • app/Models/ConversionJob.php
  • app/Models/FileRecord.php
  • app/Models/User.php
  • app/Providers/ConverterServiceProvider.php
  • app/Support/Conversions/Contracts/ConverterDriver.php
  • app/Support/Conversions/ConverterDriverRegistry.php
  • app/Support/Conversions/DTO/ConversionContext.php
  • app/Support/Conversions/DTO/ConversionResult.php
  • app/Support/Conversions/Exceptions/MissingConverterDriverException.php
  • app/Support/Conversions/Exceptions/UnsupportedConversionException.php
  • database/factories/ConversionJobFactory.php
  • database/migrations/2026_05_31_000001_create_conversion_jobs_table.php
  • tests/Fakes/Conversions/FakeConverterDriver.php
  • tests/Feature/Actions/CreateConversionJobActionTest.php
  • tests/Feature/Actions/RecordConversionResultFileActionTest.php
  • tests/Feature/Jobs/ProcessConversionJobTest.php
  • tests/Feature/Models/ConversionJobTest.php
  • tests/Unit/Actions/CreateConversionJobActionSkeletonTest.php
  • tests/Unit/Conversions/ConversionContextTest.php
  • tests/Unit/Conversions/ConversionResultTest.php
  • tests/Unit/Conversions/ConverterDriverRegistryTest.php
  • tests/Unit/Conversions/ConverterDriverTest.php
  • tests/Unit/Conversions/FakeConverterDriverTest.php
  • tests/Unit/Enums/ConversionStatusTest.php
  • tests/Unit/Jobs/ProcessConversionJobSkeletonTest.php

Comment thread app/Actions/Conversions/CreateConversionJobAction.php Outdated
Comment thread app/Actions/Conversions/RecordConversionResultFileAction.php Outdated
Comment on lines +67 to +76
} catch (Throwable $exception) {
$job->forceFill([
'status' => ConversionStatus::Failed,
'error_code' => Str::snake(class_basename($exception)),
'error_message' => $exception->getMessage(),
'completed_at' => now(),
])->save();

report($exception);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚖️ Poor tradeoff

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for tries/backoff/timeout/failed() conventions on existing jobs.
fd -e php . app/Jobs 2>/dev/null | xargs rg -nP '\$tries|\$backoff|\bfunction backoff\b|\$timeout|\bfunction failed\b' -C2

Repository: menvil/FileConverter

Length of output: 48


Avoid swallowing Throwable in ProcessConversionJob to allow queue retries

  • In app/Jobs/ProcessConversionJob.php (lines 67-76), catch (Throwable $exception) updates the conversion record, calls report($exception), and then exits without rethrowing or calling $this->fail(), so Laravel will treat the job as handled and won’t retry transient failures.
  • Narrow the catch to terminal domain errors and rethrow (or otherwise fail/release) infrastructure-related exceptions.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Jobs/ProcessConversionJob.php` around lines 67 - 76, The catch-all in
ProcessConversionJob (catch (Throwable $exception)) swallows all failures after
updating the conversion via forceFill(...) and report($exception), preventing
Laravel from retrying transient infra errors; change the error handling in
ProcessConversionJob so you only catch domain/terminal exceptions (e.g. specific
ConversionDomainException types) and handle them by updating the conversion
record, but for other exceptions rethrow the exception or call
$this->fail($exception) / release() so the queue can retry; locate the try/catch
in the ProcessConversionJob class and replace the broad Throwable catch with
targeted exception types or add logic to detect infrastructure errors and
rethrow or call $this->fail($exception) after reporting.

… missing result file, share factory user

- CreateConversionJobAction stores the driver-style converter_key (e.g. png_to_jpg)
  so ProcessConversionJob's ConverterDriverRegistry lookup resolves.
- RecordConversionResultFileAction throws when the result file is missing instead
  of hashing an empty string.
- ConversionJobFactory keeps the source file owned by the same user as the job.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release Triggers AI code review (CodeRabbit, Cubic)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant