Replace CI/CD documentation with practical guidelines for AI coding agents including module structure, naming conventions, error handling patterns, Oban worker examples, and testing patterns with Mox.
255 lines
5.6 KiB
Markdown
255 lines
5.6 KiB
Markdown
# Agent Guidelines for Pinchflat
|
|
|
|
Guidelines for AI agents working on this Elixir/Phoenix codebase.
|
|
|
|
## Project Overview
|
|
|
|
Pinchflat is a self-hosted media management app using:
|
|
|
|
- **Backend**: Elixir 1.17+, Phoenix 1.7, Ecto with SQLite
|
|
- **Frontend**: Phoenix LiveView, Tailwind CSS, esbuild
|
|
- **Background Jobs**: Oban
|
|
- **Testing**: ExUnit with Mox for mocking
|
|
|
|
## Build/Lint/Test Commands
|
|
|
|
```bash
|
|
# Enter Nix dev shell (required for tooling)
|
|
nix develop
|
|
|
|
# Run all checks (compile, format, credo, tests, sobelow, prettier)
|
|
mix check
|
|
|
|
# Run tests only
|
|
mix test
|
|
|
|
# Run a single test file
|
|
mix test test/pinchflat/media_test.exs
|
|
|
|
# Run a specific test by line number
|
|
mix test test/pinchflat/media_test.exs:42
|
|
|
|
# Run tests matching a pattern
|
|
mix test --only describe:"list_media_items"
|
|
|
|
# Format Elixir code
|
|
mix format
|
|
|
|
# Run Credo linter
|
|
mix credo
|
|
|
|
# Security analysis
|
|
mix sobelow --config
|
|
|
|
# Format JS/CSS/YAML/JSON
|
|
yarn run lint:fix
|
|
```
|
|
|
|
## Code Style Guidelines
|
|
|
|
### Module Structure
|
|
|
|
Follow this order in modules:
|
|
|
|
1. `@moduledoc` - required for all public modules
|
|
2. `use`/`import`/`alias`/`require` statements
|
|
3. Module attributes (`@allowed_fields`, `@impl`, etc.)
|
|
4. Public functions with `@doc`
|
|
5. Private functions
|
|
|
|
```elixir
|
|
defmodule Pinchflat.Media do
|
|
@moduledoc """
|
|
The Media context.
|
|
"""
|
|
|
|
import Ecto.Query, warn: false
|
|
use Pinchflat.Media.MediaQuery
|
|
|
|
alias Pinchflat.Repo
|
|
alias Pinchflat.Media.MediaItem
|
|
|
|
@doc """
|
|
Returns the list of media_items.
|
|
"""
|
|
def list_media_items do
|
|
Repo.all(MediaItem)
|
|
end
|
|
|
|
defp some_private_function do
|
|
# ...
|
|
end
|
|
end
|
|
```
|
|
|
|
### Imports and Aliases
|
|
|
|
- Use `alias` for frequently used modules
|
|
- Prefer explicit imports: `import Ecto.Query, warn: false`
|
|
- Group aliases alphabetically
|
|
- Use `alias __MODULE__` for self-reference in schemas
|
|
|
|
### Naming Conventions
|
|
|
|
- **Modules**: PascalCase matching file path (`Pinchflat.Media.MediaItem`)
|
|
- **Functions**: snake*case with verb prefixes (`get*`, `list*`, `create*`, `update*`, `delete*`)
|
|
- **Predicate functions**: end with `?` (`pending_download?/1`)
|
|
- **Private functions**: prefix with `do_` for wrapper pattern (`do_delete_media_files`)
|
|
- **Workers**: suffix with `Worker` (`MediaDownloadWorker`)
|
|
- **Fixtures**: suffix with `_fixture` (`media_item_fixture`)
|
|
|
|
### Formatting
|
|
|
|
- Line length: 120 characters max
|
|
- Use `mix format` - config in `.formatter.exs`
|
|
- Prettier for JS/CSS/YAML/JSON
|
|
|
|
### Return Types
|
|
|
|
Document returns in `@doc`:
|
|
|
|
```elixir
|
|
@doc """
|
|
Creates a media_item.
|
|
|
|
Returns {:ok, %MediaItem{}} | {:error, %Ecto.Changeset{}}
|
|
"""
|
|
def create_media_item(attrs) do
|
|
# ...
|
|
end
|
|
```
|
|
|
|
### Error Handling
|
|
|
|
- Use `{:ok, result}` / `{:error, reason}` tuples
|
|
- Use `!` suffix for functions that raise (`get_media_item!/1`)
|
|
- Pattern match on error tuples explicitly
|
|
- Rescue specific exceptions when needed:
|
|
|
|
```elixir
|
|
def perform(%Oban.Job{args: %{"id" => id}}) do
|
|
# ...
|
|
rescue
|
|
Ecto.NoResultsError -> Logger.info("Record not found")
|
|
Ecto.StaleEntryError -> Logger.info("Record stale")
|
|
end
|
|
```
|
|
|
|
### Oban Workers
|
|
|
|
```elixir
|
|
defmodule Pinchflat.Downloading.MediaDownloadWorker do
|
|
@moduledoc false
|
|
|
|
use Oban.Worker,
|
|
queue: :media_fetching,
|
|
priority: 5,
|
|
unique: [period: :infinity, states: [:available, :scheduled, :retryable, :executing]],
|
|
tags: ["media_item", "show_in_dashboard"]
|
|
|
|
@impl Oban.Worker
|
|
def perform(%Oban.Job{args: %{"id" => id} = args}) do
|
|
# Return :ok, {:ok, result}, {:error, reason}, or {:snooze, seconds}
|
|
end
|
|
end
|
|
```
|
|
|
|
### Testing Patterns
|
|
|
|
Use `Pinchflat.DataCase` for database tests:
|
|
|
|
```elixir
|
|
defmodule Pinchflat.MediaTest do
|
|
use Pinchflat.DataCase
|
|
|
|
import Pinchflat.MediaFixtures
|
|
import Pinchflat.SourcesFixtures
|
|
|
|
alias Pinchflat.Media
|
|
|
|
describe "list_media_items/0" do
|
|
test "returns all media_items" do
|
|
media_item = media_item_fixture()
|
|
assert Media.list_media_items() == [media_item]
|
|
end
|
|
end
|
|
end
|
|
```
|
|
|
|
### Mocking with Mox
|
|
|
|
Mocks are defined in `test/test_helper.exs`:
|
|
|
|
- `YtDlpRunnerMock` - yt-dlp commands
|
|
- `HTTPClientMock` - HTTP requests
|
|
- `UserScriptRunnerMock` - user scripts
|
|
- `AppriseRunnerMock` - notifications
|
|
|
|
```elixir
|
|
test "calls user script" do
|
|
expect(UserScriptRunnerMock, :run, fn :media_deleted, data ->
|
|
assert data.id == media_item.id
|
|
{:ok, "", 0}
|
|
end)
|
|
|
|
Media.delete_media_item(media_item, delete_files: true)
|
|
end
|
|
```
|
|
|
|
### Test Fixtures
|
|
|
|
Create fixtures in `test/support/fixtures/`:
|
|
|
|
```elixir
|
|
def media_item_fixture(attrs \\ %{}) do
|
|
{:ok, media_item} =
|
|
attrs
|
|
|> Enum.into(%{
|
|
media_id: Faker.String.base64(12),
|
|
title: Faker.Commerce.product_name(),
|
|
source_id: source_fixture().id
|
|
})
|
|
|> Pinchflat.Media.create_media_item()
|
|
|
|
media_item
|
|
end
|
|
```
|
|
|
|
### Test Helpers
|
|
|
|
From `Pinchflat.TestingHelperMethods`:
|
|
|
|
- `now/0` - current UTC datetime
|
|
- `now_minus/2` - datetime in past (`now_minus(5, :days)`)
|
|
- `now_plus/2` - datetime in future
|
|
- `assert_changed/2` - verify state change
|
|
|
|
## Project Structure
|
|
|
|
```
|
|
lib/
|
|
pinchflat/ # Business logic contexts
|
|
media/ # Media context (MediaItem, queries)
|
|
sources/ # Source context
|
|
downloading/ # Download workers and helpers
|
|
yt_dlp/ # yt-dlp integration
|
|
pinchflat_web/ # Phoenix web layer
|
|
test/
|
|
pinchflat/ # Context tests
|
|
pinchflat_web/ # Controller/LiveView tests
|
|
support/
|
|
fixtures/ # Test data factories
|
|
data_case.ex # Database test setup
|
|
conn_case.ex # HTTP test setup
|
|
```
|
|
|
|
## Pre-commit Hooks
|
|
|
|
Lefthook runs on commit:
|
|
|
|
- `prettier` - JS/CSS/YAML/JSON formatting
|
|
- `mix format` - Elixir formatting
|
|
- `typos` - spell checking
|
|
- `actionlint` - GitHub Actions validation
|
|
|
|
Bypass if needed: `git commit --no-verify -m "message"`
|