Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions .agents/skills/vmemo-e2e-testing/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
---
name: "vmemo-e2e-testing-skill"
description: "Vmemo Playwright e2e: author scoped specs, run in UI mode when possible, enforce local Docker + Phoenix prerequisites, and iterate fix-test until requirements pass."
---

# Vmemo E2E Testing Skill

Use this skill when the user asks for Playwright end-to-end work against Vmemo: **new or updated specs**, **local or prod-like runs**, or a **fix–retest loop** until acceptance criteria pass.

Authoritative package layout: `others/e2e-test` (TypeScript, Playwright, Bun per `README.md` and `package.json`).

## Goals

1. Translate user requirements into **minimal, focused** Playwright tests (reuse patterns under `others/e2e-test/tests/`).
2. **Run only the tests that matter** for the current change (single file, line grep, or project), never the full suite unless the user explicitly asks.
3. Prefer **Playwright UI mode** for execution and debugging when a graphical environment is available.
4. On failure, enter a **fix → rerun scoped tests** loop until the scoped tests pass and the user’s requirements are met.
5. Before any **local** e2e run against the dev server, ensure **Docker Compose is up** and **`mix phx.server` is running** (see Preconditions).

## Preconditions (local dev server target)

Default `E2E_BASE_URL` is `http://localhost:4000` (`playwright.config.ts`).

Before running e2e against local Mix/Phoenix:

1. **Docker Compose** (repo root): dependencies such as Postgres and Typesense must be available to the app. Follow `.agents/skills/vmemo/development/SKILL.md`: e.g. `docker compose up -d` from the repository root (and any project-specific overrides the developer uses).
2. **Phoenix server** (repo root): start the app, e.g. `mix phx.server` (or `iex -S mix phx.server` when interactive debugging is needed). Load `.env` if required for `DATABASE_URL` and related vars (see `docs/guides/development/setup.md`).
3. Confirm the app responds at the chosen base URL (default `http://localhost:4000`).

If the user targets **prod-like Docker** from `others/e2e-test/docker-compose.yml`, follow `others/e2e-test/README.md` instead; still do not run unrelated specs.

**Agent hygiene:** Check terminals or process list for an already-running server; do not start duplicate listeners on the same port without resolving conflicts first.

## Install (once per machine / after dependency changes)

From `others/e2e-test`:

```bash
bun install
bunx playwright install chromium
```

This repo expects **Bun** for this e2e package (not generic `npm`/`pnpm` workflows).

## Writing tests

- Place specs in `others/e2e-test/tests/`; follow existing `*-page.spec.ts` naming and structure.
- `globalSetup` logs in once and writes `storageState` to `/tmp/vmemo-e2e-storage.json`; reuse authenticated state instead of duplicating login in every spec unless the scenario requires a logged-out user.
- Shared test account (see `README.md` / `global-setup.ts`): `test@example.com` / `pass123456`.
- Visual assertions: respect existing `expect().toHaveScreenshot()` conventions and both projects (`iphone-se`, `macbook-13`) when adding page-level visual coverage.
- **UI copy in the app is English-only** in tests (selectors, `getByRole` names); do not assert Chinese/Japanese UI strings in selectors.

## Scoped runs (mandatory default)

Always pass a **narrow** Playwright selector after `--` so only relevant tests run:

| Intent | Example (from `others/e2e-test`) |
|--------|----------------------------------|
| Single file | `bun run e2e:ui -- tests/home-page.spec.ts` |
| Single test title (grep) | `bun run e2e:ui -- tests/home-page.spec.ts -g "landing"` |
| One browser project | `bun run e2e:ui -- tests/home-page.spec.ts --project=macbook-13` |

Same patterns apply without UI:

```bash
bun run e2e -- tests/home-page.spec.ts --project=iphone-se
```

Do **not** run `bun run e2e` or `bun run e2e:ui` with no path/grep unless the user explicitly requests the full suite (e.g. pre-release or CI parity).

## UI mode execution

- Script: `e2e:ui` → `playwright test --ui`.
- Use UI mode for local iteration: **scoped** command as above.
- If there is **no display** (SSH-only, CI): use the same scoped args with `bun run e2e`, and use Playwright artifacts (`playwright-report`, `test-results`, traces) to debug; optionally `bunx playwright show-report playwright-report` after a run.

## Fix–test loop (agent workflow)

Until scoped tests pass and requirements are satisfied:

1. Run the **smallest** scoped command (file + optional `-g` + optional `--project`).
2. On failure: read Playwright output, screenshots under `/tmp` when present, `test-results/`, and HTML report; inspect app/LiveView or backend only as needed.
3. Apply the **minimal** fix (test, app, or seed data).
4. **Rerun the same scoped command**; do not expand scope unless the fix could affect other specs and the user agrees.
5. Repeat until green; then summarize what changed and which command was used for the final pass.

## Environment variable

- `E2E_BASE_URL` overrides the default `http://localhost:4000`.

Example:

```bash
cd others/e2e-test
E2E_BASE_URL=http://localhost:4000 bun run e2e:ui -- tests/login-page.spec.ts
```

## Related docs

- `others/e2e-test/README.md` — prod-like Docker, CI label `run-e2e-test`, snapshots, FAQ.
- `.agents/skills/vmemo/development/SKILL.md` — local Docker and setup flow.
- `.claude/skills/playwright-cli/SKILL.md` or `.codex/skills/playwright/SKILL.md` — optional Playwright CLI deep dives.

## Out of scope

- Do not run the full e2e matrix by default.
- Do not modify **codegen-generated** application code (repository policy).
- CI snapshot update workflows remain in `README.md` / GitHub Actions; this skill focuses on local authoring and targeted runs unless the user expands scope.
6 changes: 4 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,10 @@ jobs:
- name: Compile
run: mix compile

- name: Reset
run: mix reset
- name: Prepare test database
run: |
mix ecto.create || true
mix ecto.migrate

- name: Run tests with ExCoveralls
run: mix coveralls.json
Expand Down
26 changes: 26 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
# Vmemo AGENTS.md

## Project overview

- Vmemo is an Elixir Phoenix application built with Ash Framework.
- Treat this as a Phoenix/Ash codebase, not a typical Node.js web app.
- Backend domain logic lives in `lib/vmemo/**`; the web layer lives in `lib/vmemo_web/**`.
- Keep business logic, persistence, and external API orchestration in the backend.
- Frontend JavaScript should stay focused on rendering and interaction; avoid adding business logic or unnecessary client-side state.
- If unsure where logic belongs, put it in the backend.
- For Ash code, prefer Ash actions/queries/resources over direct Ecto access unless explicitly required.
- Respect Ash conventions first; do not model new behavior Ecto-first.
- Keep module names aligned with file paths and prefer cohesive feature-based placement for tightly coupled modules.
- For JavaScript assets, use `assets/vendor/` vendoring patterns instead of assuming npm-based app tooling.
- Use Bun only where the project already expects it, such as e2e workflows.

## Phoenix / LiveView rules

- Do not create standalone `.heex` files for LiveView; render in `render/1`.
- Use kebab-case for `handle_event/3` event names and `phx-*`.
- Use built-in LiveView uploads.
Expand All @@ -25,6 +28,7 @@
- For non-submit action failures (for example delete/retry), use toast.

## Data / SDK / Infra rules

- Prefer ISO8601 datetime strings for API/JSON/log exchange.
- UI time display must follow user timezone.
- Keep formatting logic in top-level utils (for example `VmemoWeb.Utils.Datetime`).
Expand All @@ -34,19 +38,23 @@
- Fail fast on invalid or missing env values.

## Tooling

- Prefer Tidewave tools for Phoenix-aware discovery and runtime checks.
- `Tidewave`: during development, use available tools as documented in https://github.com/tidewave-ai/tidewave_phoenix#available-tools
- `Playwright`: during UI debugging and testing, use the CLI workflow documented in https://github.com/microsoft/playwright-cli

## Project commands

- Use `mix` for project tasks.
- Do not use Python for ad-hoc project automation.
- Keep Git 2.54+ hook definitions in `.git-hooks.gitconfig`; local config should include that file.

## Communication and language

- UI user-facing copy must support i18n via Gettext with `en`, `zh`, and `ja`.

## Delivery

- Prefer the smallest relevant validation set for the change.
- Avoid full-project checks unless the task truly needs them.
- Run focused checks first, then confirm there are no obvious runtime regressions.
Expand All @@ -62,29 +70,47 @@ The following framework usage references are generated pointers; do not edit the

<!-- usage-rules-start -->
<!-- phoenix:ecto-start -->

## phoenix:ecto usage

[phoenix:ecto usage rules](deps/phoenix/usage-rules/ecto.md)

<!-- phoenix:ecto-end -->
<!-- phoenix:elixir-start -->

## phoenix:elixir usage

[phoenix:elixir usage rules](deps/phoenix/usage-rules/elixir.md)

<!-- phoenix:elixir-end -->
<!-- phoenix:html-start -->

## phoenix:html usage

[phoenix:html usage rules](deps/phoenix/usage-rules/html.md)

<!-- phoenix:html-end -->
<!-- phoenix:liveview-start -->

## phoenix:liveview usage

[phoenix:liveview usage rules](deps/phoenix/usage-rules/liveview.md)

<!-- phoenix:liveview-end -->
<!-- phoenix:phoenix-start -->

## phoenix:phoenix usage

[phoenix:phoenix usage rules](deps/phoenix/usage-rules/phoenix.md)

<!-- phoenix:phoenix-end -->
<!-- ash-start -->

## ash usage

_A declarative, extensible framework for building Elixir applications._

[ash usage rules](deps/ash/usage-rules.md)

<!-- ash-end -->
<!-- usage-rules-end -->
2 changes: 2 additions & 0 deletions docs/guides/coding/elixir.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,8 @@ Use this pattern for long-running work that must continue after page leave:
- If naming migration is needed (for example `photo` -> `image`), do it as a planned refactor: canonical modules/resources/actions first, then call sites, without runtime compatibility wrappers.
- Model business logic in resources/actions/policies, not in web templates.
- Register resources in domains and expose clear interfaces through `code_interface`.
- Inside the **same** Ash resource module, call `code_interface` helpers with an **unqualified** function name (for example `read_storage_base64(id, actor: actor)` inside an action `run` callback). Do not write `__MODULE__.read_storage_base64(...)` for that; it adds noise without changing behavior unless you hit name shadowing or a macro hygiene edge case.
- Use `__MODULE__` when an API needs the **resource module atom** (for example `Ash.get(__MODULE__, id, ...)`, `Ash.create(__MODULE__, attrs, ...)`), not merely to prefix another function defined on the current module.
- Keep action naming business-oriented and consistent.
- Keep validation and lifecycle logic close to the resource.
- Keep web layer as orchestrator for UI state and domain calls.
Expand Down
44 changes: 0 additions & 44 deletions lib/vmemo/ai/image_data.ex

This file was deleted.

5 changes: 2 additions & 3 deletions lib/vmemo/chat/ai_router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ defmodule Vmemo.Chat.AiRouter do
alias SmallSdk.Moondream
alias Vmemo.Account
alias Vmemo.Ai.AshAiVision
alias Vmemo.Ai.ImageData
alias Vmemo.Ai.VisionConfig
alias Vmemo.Memo.Image

Expand Down Expand Up @@ -112,8 +111,8 @@ defmodule Vmemo.Chat.AiRouter do
end

defp run_tool(image_id, tool, prompt, actor) do
with {:ok, image} <- Ash.get(Image, image_id, actor: actor),
{:ok, {image_base64, mime_type}} <- ImageData.fetch_base64_from_url(image.url),
with {:ok, %{base64: image_base64, mime_type: mime_type}} <-
Image.read_storage_base64(image_id, actor: actor),
{:ok, result} <- call_tool(tool, image_base64, mime_type, prompt, actor) do
{:ok, %{provider: provider_for(tool), tool_name: tool, text: normalize_result(result)}}
else
Expand Down
41 changes: 29 additions & 12 deletions lib/vmemo/memo/image.ex
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ defmodule Vmemo.Memo.Image do
define :library_images_count, args: [:user_id]
define :list_similar, args: [:image_id, :user_id]
define :sync_typesense_by_id, args: [:image_id]
define :read_storage_base64, args: [:id]
define :ingest_temp_file_for_similarity_search, args: [:temp_path, :storage_file_id]
define :update_search_engine
define :request_generate_caption
Expand Down Expand Up @@ -376,6 +377,28 @@ defmodule Vmemo.Memo.Image do
end
end

action :read_storage_base64, :map do
description "Load image bytes from app storage as base64 with a detected MIME type."

argument :id, :uuid, allow_nil?: false

run fn input, context ->
id = Ash.ActionInput.get_argument(input, :id)
actor = Map.get(context, :actor)

with {:ok, image} <- Ash.get(__MODULE__, id, actor: actor),
{:ok, {base64_data, mime_type}} <- read_image_as_base64(image.url) do
{:ok, %{base64: base64_data, mime_type: mime_type}}
else
{:error, :file_not_found} ->
{:error, "Failed to read image: file not found"}

{:error, reason} ->
{:error, reason}
end
end
end

action :ingest_temp_file_for_similarity_search, :uuid do
description """
Copy a temp upload into storage, create a image row (no Oban), sync Typesense inline,
Expand Down Expand Up @@ -888,21 +911,15 @@ defmodule Vmemo.Memo.Image do
if is_nil(id) do
{:error, "Image id is required"}
else
case Ash.get(__MODULE__, id, actor: actor) do
{:ok, image} ->
normalized_photo = normalize_image_url_for_api(image)

case read_image_as_base64(normalized_photo.url) do
{:ok, {base64_data, mime_type}} ->
# Return data URL format: data:image/jpeg;base64,<base64_data>
{:ok, "data:#{mime_type};base64,#{base64_data}"}
case read_storage_base64(id, actor: actor) do
{:ok, %{base64: base64_data, mime_type: mime_type}} ->
{:ok, "data:#{mime_type};base64,#{base64_data}"}

{:error, reason} ->
{:error, "Failed to read image: #{inspect(reason)}"}
end
{:error, reason} when is_binary(reason) ->
{:error, reason}

{:error, reason} ->
{:error, reason}
{:error, "Failed to read image: #{inspect(reason)}"}
end
end
end
Expand Down
8 changes: 4 additions & 4 deletions test/vmemo/ai/caption_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ defmodule Vmemo.Ai.CaptionTest do
{:ok, _profile} = Account.upsert_user_profile(user, %{name: "Tester", language: "zh"})

with_mock Vmemo.Ai.VisionConfig, resolve: fn -> %{model: "openrouter:test"} end do
with_mock Vmemo.Ai.AshAiVision,
with_mock Vmemo.Ai.AshAiVision, [:passthrough],
caption: fn _image_base64, opts ->
send(self(), {:caption_opts, opts})
{:ok, "中文描述"}
Expand All @@ -33,7 +33,7 @@ defmodule Vmemo.Ai.CaptionTest do

test "generate_caption falls back to en when user_id is missing" do
with_mock Vmemo.Ai.VisionConfig, resolve: fn -> %{model: "openrouter:test"} end do
with_mock Vmemo.Ai.AshAiVision,
with_mock Vmemo.Ai.AshAiVision, [:passthrough],
caption: fn _image_base64, opts ->
send(self(), {:caption_opts, opts})
{:ok, "English description"}
Expand All @@ -51,7 +51,7 @@ defmodule Vmemo.Ai.CaptionTest do

test "generate_caption returns plain caption text without appending tags" do
with_mock Vmemo.Ai.VisionConfig, resolve: fn -> %{model: "openrouter:test"} end do
with_mock Vmemo.Ai.AshAiVision,
with_mock Vmemo.Ai.AshAiVision, [:passthrough],
caption: fn _image_base64, _opts -> {:ok, "A cat on desk"} end do
with_mock ReqLLM, generate_text: fn _model, _messages, _opts -> {:ok, :ok} end do
with_mock ReqLLM.Response, text: fn :ok -> ~s(["Pets", "Indoor Scene"]) end do
Expand All @@ -74,7 +74,7 @@ defmodule Vmemo.Ai.CaptionTest do

test "generate_caption_and_tags returns structured caption and tags" do
with_mock Vmemo.Ai.VisionConfig, resolve: fn -> %{model: "openrouter:test"} end do
with_mock Vmemo.Ai.AshAiVision,
with_mock Vmemo.Ai.AshAiVision, [:passthrough],
generate_object: fn _image_base64, _prompt, _schema, _opts ->
{:ok, %{object: %{caption: "A cat on desk", tags: [" Pets ", "Indoor Scene", ""]}}}
end do
Expand Down
Loading
Loading