Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
44a260f
Merge pull request #731 from otomatty/main
github-actions[bot] Apr 24, 2026
eb1eb08
docs(notes): clarify post-delete redirect intent in NoteSettings (#732)
otomatty Apr 24, 2026
4a54b19
feat: wire hashtag (#name) sync + status refresh end-to-end (issue #7…
otomatty Apr 24, 2026
475572d
chore: align config with develop as default branch (#734) (#735)
otomatty Apr 24, 2026
9e1d66f
feat: Propagate page title renames to WikiLinks, tags, and ghost link…
otomatty Apr 24, 2026
e30dd03
fix: prevent same-title page rewrites by matching targetId (issue #73…
otomatty Apr 25, 2026
9dcf4dc
refactor: extract buildGlobalSearchResults as pure function for testa…
otomatty Apr 25, 2026
328f01d
test: expand test coverage with mutation-killing assertions (#740)
otomatty Apr 25, 2026
5cce218
test(P1): add TDD coverage for src/lib/aiService* (#742) (#750)
otomatty Apr 26, 2026
881009d
test(hooks): add coverage for 6 high-value src/hooks/ modules (#743) …
otomatty Apr 26, 2026
df92f6b
test(P3): cover agentSlashCommands and slashSuggestionPlugin (#744) (…
otomatty Apr 26, 2026
99b3115
test(P4): add coverage for src/stores/ and storage layer (#745) (#753)
otomatty Apr 26, 2026
cd8f3de
test(P5): add server/api security & core service unit tests (#746) (#…
otomatty Apr 26, 2026
8124251
fix(api): add missing 0018 migration for onboarding & pages.kind (#755)
otomatty Apr 26, 2026
9f723ff
test: add comprehensive test suite for API routes and services (#756)
otomatty Apr 26, 2026
6ca7ab8
test: add comprehensive unit tests for query, models, installation, a…
otomatty Apr 26, 2026
68e7de5
fix: address PR #757 review comments (#759)
otomatty Apr 26, 2026
83bb072
test: add comprehensive unit tests for hooks, components, and API cli…
otomatty Apr 26, 2026
f0cf358
ci(mutation-light): expand golden list with 5 stable Phase 1/2 files …
otomatty Apr 26, 2026
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
14 changes: 11 additions & 3 deletions .coderabbit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,21 @@
# https://docs.coderabbit.ai/guides/configure-coderabbit
#
# 自動レビューの対象とする base ブランチを指定する。
# default branch (main) は常に含まれるため、ここでは追加ブランチのみ指定する。
# Configure the base branches for auto review. The default branch (main)
# is always included, so only list additional branches here.
# default branch は常に含まれるため、ここでは追加ブランチのみ指定する。
# Configure the base branches for auto review. The default branch is always
# included, so only list additional branches here.
#
# 移行期間メモ: default branch を main → develop へ切り替える作業(#734)と
# この設定変更は非同期に行われるため、切替完了までは main / develop の両方を
# 明示しておく。切替完了後に `develop` を削除するクリーンアップ PR を出す。
# Transition note: the default-branch switch (#734) is handled outside this
# repo. Keep both `main` and `develop` listed until the switch is complete,
# then drop `develop` (it will be covered implicitly as the default branch).
reviews:
auto_review:
enabled: true
base_branches:
- "main"
- "develop"
- "release/.*"
- "feature/.*"
59 changes: 58 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,26 @@ jobs:

- run: bun run test:coverage

# Workspace package tests are not picked up by the root vitest config,
# so run their dedicated configs explicitly. Keep this list in sync with
# the `test:run` script in root package.json. server/mcp has its own job
# (`mcp-test`); server/hocuspocus is invoked only via `test:run` and not
# duplicated here.
# ルートの vitest 設定はワークスペース配下のテストを含まないため、
# 各パッケージの vitest.config を明示的に実行する。server/mcp は別 job
# (`mcp-test`)で動かすためここには含めない。
- name: Run admin tests
run: bunx vitest run --config admin/vitest.config.ts

- name: Run @zedi/shared tests
run: bunx vitest run --config packages/shared/vitest.config.ts

- name: Run @zedi/ui tests
run: bunx vitest run --config packages/ui/vitest.config.ts

- name: Run @zedi/claude-sidecar tests
run: bunx vitest run --config packages/claude-sidecar/vitest.config.ts

- name: Upload coverage report
if: always()
uses: actions/upload-artifact@v7
Expand Down Expand Up @@ -167,6 +187,36 @@ jobs:
working-directory: server/api
run: bunx tsc --noEmit

drizzle-migration-check:
name: Drizzle Migration Check
# Drizzle TS スキーマを変更したら必ずマイグレーション SQL を追加するルールの強制。
# PR #728 のように TS スキーマだけ更新して `server/api/drizzle/*.sql` を忘れると、
# 本番 DB がスキーマに追いつかず 500 エラーになるため、PR 段階で検出する。
# PR 限定(push にはマージベースの計算対象が無い)。
#
# Enforce: any change under `server/api/src/schema/**` must come with a new
# migration SQL file and an updated `_journal.json`. Detects PR #728-style
# regressions where production drifts from the application schema.
if: github.event_name == 'pull_request' && !github.event.pull_request.draft
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6.0.2
with:
# PR ベースとの diff を取るため履歴を全部取得する。
# Need full history so the script can diff against the PR base.
fetch-depth: 0

- uses: actions/setup-node@v6
with:
node-version-file: ".nvmrc"

- name: Run drizzle migration consistency check
env:
DRIZZLE_DIFF_BASE: origin/${{ github.base_ref }}
PR_TITLE: ${{ github.event.pull_request.title }}
PR_BODY: ${{ github.event.pull_request.body }}
run: node scripts/check-drizzle-migrations.mjs

mcp-test:
name: MCP Server Tests
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
Expand Down Expand Up @@ -219,8 +269,15 @@ jobs:

- run: bun install --frozen-lockfile

# Golden list of files with stable mutation scores (Phase 1/2 of Epic #468).
# 安定したスコアを持つファイルを段階的に追加し、PR で退行を早めに検知する。
# Local scores measured 2026-04-26 with `stryker.config.mutation-changed.mjs`:
# dateUtils.ts 94.03% / useContainerColumns.ts 86.67%
# noteViewHelpers.ts 100.00% / aiChatConversationTitle.ts 100.00%
# aiCostUtils.ts 91.18% / encryption.ts 96.43%
# mcpServerImportHelpers.ts 100.00% / onboardingState.ts 83.82%
- name: Run mutation tests (limited scope)
run: bun run test:mutation -- --mutate "src/lib/dateUtils.ts,src/hooks/useContainerColumns.ts,src/pages/NoteView/noteViewHelpers.ts"
run: bun run test:mutation -- --mutate "src/lib/dateUtils.ts,src/hooks/useContainerColumns.ts,src/pages/NoteView/noteViewHelpers.ts,src/lib/aiChatConversationTitle.ts,src/lib/aiCostUtils.ts,src/lib/encryption.ts,src/lib/mcpServerImportHelpers.ts,src/lib/onboardingState.ts"

- name: Upload mutation report
if: always()
Expand Down
16 changes: 13 additions & 3 deletions .github/workflows/claude-code-review.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
# Claude Code Review - main/develop 向け PR で自動実行
# Claude Code Review - runs automatically on PRs targeting main / develop.
#
# 初回セットアップ時の注意:
# OIDC 検証のため、ワークフローはデフォルトブランチ(main)に存在し同一内容である必要があります。
# 初回の PR では Claude Code Review が失敗しますが、マージ後・develop→main リリース後に
# 以降の PR で正常動作します。continue-on-error により初回失敗時もマージをブロックしません。
# OIDC 検証のため、ワークフローは「現在のデフォルトブランチ」に存在し、
# 対象ブランチ側にも同一内容である必要があります。
# 初回の PR では Claude Code Review が失敗する場合がありますが、
# 必要な反映後の PR で正常動作します。
# continue-on-error により初回失敗時もマージをブロックしません。
#
# Initial setup note:
# For OIDC verification, this workflow must exist on the repository's current
# default branch and its contents must match the workflow on the target branch.
# The first PR after a default-branch change may fail, but subsequent PRs work
# normally once the workflow is propagated. `continue-on-error: true` ensures
# that initial failures do not block merges.
# ref: https://github.com/anthropics/claude-code-action/issues/722
#
name: Claude Code Review
Expand Down
27 changes: 27 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,22 @@ bun run test:run # Vitest 単体テスト
- 既存のディレクトリ構成・命名規則に合わせる。
- Conventional Commits 形式でコミット(`feat:`, `fix:`, `docs:` 等)。

## DB スキーマ変更(必読) / Database schema changes (must read)

- **TS スキーマと SQL マイグレーションは常に対で更新する**。`server/api/src/schema/**/*.ts` を編集したら、必ず `server/api/drizzle/NNNN_*.sql` を新規追加し、`server/api/drizzle/meta/_journal.json` にエントリを追記する。
Always pair TS schema edits with a SQL migration: add a new `server/api/drizzle/NNNN_*.sql` and append an entry to `server/api/drizzle/meta/_journal.json`. Skipping this caused production 500s in PR #728 on `/api/onboarding/status` and `/api/pages`.
- **正本のマイグレーション置き場は `server/api/drizzle/` のみ**。CI (`deploy-{dev,prod}.yml`) は `bunx drizzle-kit migrate` だけを実行するため、ここ以外に SQL を置いても本番には適用されない。
_Source of truth is `server/api/drizzle/`. CI runs only `bunx drizzle-kit migrate`; SQL placed elsewhere is dead code._
- **マイグレーションの書き方**:
- 既存の手書き例(`0017_add_link_type.sql` など)の体裁に合わせ、ステートメント間に `--> statement-breakpoint` を入れる。
- 既存環境で重複適用されても安全になるよう、原則として `IF NOT EXISTS` / `ON CONFLICT DO NOTHING` を使う。
- 必要であればバックフィル(既存行への初期値投入)も同じファイル内で行う。
- `bunx drizzle-kit generate` で雛形を作るときは、過去スナップショットが欠落しているため巨大な diff が出ることがある。その場合は `--name` 指定の出力を手で削減し、既存マイグレーション間で重複しない形に整えてから commit する(snapshot ファイルは生成物のみ、当面コミットしない方針)。
- **CI ガード**: `.github/workflows/ci.yml` の `drizzle-migration-check` ジョブが PR で `server/api/src/schema/**` の変更と新規 `server/api/drizzle/*.sql` がペアになっているかを検証する。例外的に SQL 不要な場合(コメント/JSDoc 修正のみなど)は PR 本文かコミットメッセージに `[skip drizzle-check]` を入れる。
_CI guard `drizzle-migration-check` enforces the schema/migration pairing. Use the `[skip drizzle-check]` marker only for non-DDL edits (comments, JSDoc, type aliases that do not affect SQL)._
- **環境別の自動適用**: `develop` への push → `deploy-dev.yml` が development DB へ migrate。`main` への push → `deploy-prod.yml` が production DB へ migrate。スキーマ追従はこの 2 本だけ。
_Auto-apply: push to `develop` migrates dev DB; push to `main` migrates prod DB. No other path applies migrations._

## ブランチ・PR の命名規則

- **ブランチ**: `feature/説明`、`fix/説明`、`hotfix/説明`、`chore/説明` など(例: `feature/ai-models-ui`, `fix/search-crash`)。Issue 番号から作る場合は `feature/123`。
Expand Down Expand Up @@ -93,6 +109,17 @@ terraform/ # インフラ定義

- ルート `package.json` の `workspaces` は `packages/*` と `admin` のみを含む。`server/api`, `server/hocuspocus`, `server/mcp` は **意図的にルートの Bun workspace から外して**、個別の Bun プロジェクトとして管理する。
_Root `workspaces` covers only `packages/*` and `admin`. The three `server/*` services (`api`, `hocuspocus`, `mcp`) are intentionally kept **outside** the root Bun workspace and managed as standalone Bun projects._

### サーバ/クライアント間で共有する定数 / Sharing constants between server and client

- `packages/shared`(`@zedi/shared`)は、フロント・admin・サーバすべてで共通利用したいピュアな TypeScript 定数を集約するためのワークスペースパッケージ。React や Node 専用 API には依存させない。
_`packages/shared` (`@zedi/shared`) is a workspace package for pure TypeScript constants shared by client, admin, and (logically) server code. Keep it free of React or Node-only dependencies._
- フロント (`src/`) と `admin/` はワークスペース内なので `import { ... } from "@zedi/shared/..."` で直接利用できる。
_Workspace consumers (`src/`, `admin/`) import via `@zedi/shared/...`._
- `server/api` 等のサーバプロジェクトはワークスペース外なので `@zedi/shared` を **直接 import できない**。代わりに同じ値を当該サーバ内に二重定義し、フロント側の vitest が `fs.readFileSync` でサーバファイルを読んで両者の文字列一致を検証するドリフト検知テスト(例: `src/lib/tagCharacterClassSync.test.ts`)を置くことで CI で同期を担保する。
_Server projects (e.g. `server/api`) cannot import `@zedi/shared` because they are intentionally outside the workspace. Duplicate the constant inside the server source and add a client-side vitest (e.g. `src/lib/tagCharacterClassSync.test.ts`) that reads the server file via `fs.readFileSync` and asserts the two literals match. This keeps drift detectable in CI._
- 値を更新する際は **`packages/shared` とサーバ側コピーを同時に編集すること**。ドリフト検知テストが落ちたら、片方しか変更していないサインなのでもう一方も追従させる。
_When updating a shared value, edit `packages/shared` and the server-side copy together. If the drift test fails, the change touched only one side; sync the other._
- 理由 / Rationale:
- Railway の Dockerfile ビルドは「各サービスの Root Directory」を build context に取る (例: `server/mcp`)。ここからルート `bun.lock` を参照するのは面倒で、context をサービス単位に閉じるほうが再現性が高い。
_Railway Dockerfile builds take each service's Root Directory as the build context. Scoping `bun.lock` per service keeps the build self-contained and reproducible._
Expand Down
5 changes: 5 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@
- エラーハンドリングとログが適切か。
- 日本語・英語のコメント・ドキュメントがプロジェクトのトーンに合っているか。

## DB スキーマ変更

- TS スキーマ (`server/api/src/schema/**`) を変更した PR では必ず `server/api/drizzle/NNNN_*.sql` を新規追加し、`server/api/drizzle/meta/_journal.json` にもエントリを追記する。詳細は [AGENTS.md §「DB スキーマ変更」](./AGENTS.md#db-スキーマ変更必読--database-schema-changes-must-read) を参照。
- CI の `drizzle-migration-check` ジョブが PR でスキーマ変更と SQL 追加のペアを強制する。

## その他

- 変更が大きい場合は小さな PR に分けることを推奨する。
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ VITE_REALTIME_URL=ws://localhost:1234 # 本番は wss://realtime.zedi-note.app
| **Visualization** | Recharts / `@xyflow/react` (React Flow) / Mermaid / KaTeX / Tesseract.js (OCR) |
| **Auth** | [Better Auth](https://better-auth.com/) (OAuth / セッション cookie) |
| **API** | `server/api` — Hono on Bun + Drizzle ORM (PostgreSQL) |
| **Database** | PostgreSQL (Drizzle migrations: `db/migrations`, `server/api/drizzle`) / IndexedDB (local・ブラウザ) |
| **Database** | PostgreSQL (Drizzle migrations: `server/api/drizzle/`) / IndexedDB (local・ブラウザ) |
| **Realtime** | `server/hocuspocus` — Hocuspocus (Y.js) によるリアルタイム共同編集 |
| **MCP** | `server/mcp` — Claude Code 連携(stdio / HTTP、詳細は [server/mcp/README.md](server/mcp/README.md)) |
| **Storage** | AWS S3(API 経由でアップロード、`@aws-sdk/client-s3`) |
Expand Down Expand Up @@ -337,7 +337,7 @@ packages/ # Bun workspaces(共有ライブラリ)

admin/ # 管理画面アプリ(別 Vite + React + Tailwind / `@zedi/ui` 利用)
extension/ # ブラウザ拡張(Manifest v3、Web Clipper)
db/migrations/ # PostgreSQL マイグレーション SQL
server/api/drizzle/ # PostgreSQL マイグレーション(drizzle-kit が読む正本 / source of truth)
terraform/cloudflare/ # Cloudflare 関連インフラ定義
e2e/ # Playwright E2E テスト
scripts/ # セットアップ / sidecar ビルド / Stryker / 拡張ビルド等のスクリプト
Expand Down
118 changes: 118 additions & 0 deletions admin/src/api/activity.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/**
* activity API クライアントのテスト(adminFetch をモック)。
* Tests for the activity API client (adminFetch mocked).
*/
import { describe, it, expect, vi, beforeEach } from "vitest";
import { listActivity } from "./activity";

vi.mock("./client", async (importOriginal) => {
const actual = await importOriginal<typeof import("./client")>();
return {
...actual,
adminFetch: vi.fn(),
};
});

const { adminFetch } = await import("./client");

describe("listActivity", () => {
beforeEach(() => {
vi.mocked(adminFetch).mockReset();
});

it("パラメータ無しなら /api/activity を呼ぶ / hits /api/activity without params", async () => {
vi.mocked(adminFetch).mockResolvedValueOnce(
new Response(JSON.stringify({ entries: [], total: 0, limit: 50 }), { status: 200 }),
);
await listActivity();
expect(adminFetch).toHaveBeenCalledWith("/api/activity");
});

it("kind / actor / from / to / limit / offset を querystring に詰める / packs kind/actor/from/to/limit/offset into querystring", async () => {
vi.mocked(adminFetch).mockResolvedValueOnce(
new Response(JSON.stringify({ entries: [], total: 0, limit: 10 }), { status: 200 }),
);

await listActivity({
kind: "lint_run",
actor: "ai",
from: "2026-01-01T00:00:00Z",
to: "2026-02-01T00:00:00Z",
limit: 10,
offset: 20,
});

const calledWith = vi.mocked(adminFetch).mock.calls[0]?.[0];
expect(calledWith).toContain("/api/activity?");
const qs = new URLSearchParams((calledWith as string).split("?")[1] ?? "");
expect(qs.get("kind")).toBe("lint_run");
expect(qs.get("actor")).toBe("ai");
expect(qs.get("from")).toBe("2026-01-01T00:00:00Z");
expect(qs.get("to")).toBe("2026-02-01T00:00:00Z");
expect(qs.get("limit")).toBe("10");
expect(qs.get("offset")).toBe("20");
});

it("limit=0 は数値として扱う(offset 同様)/ accepts numeric 0 for limit and offset", async () => {
vi.mocked(adminFetch).mockResolvedValueOnce(
new Response(JSON.stringify({ entries: [], total: 0, limit: 0 }), { status: 200 }),
);

await listActivity({ limit: 0, offset: 0 });
const calledWith = vi.mocked(adminFetch).mock.calls[0]?.[0] as string;
const qs = new URLSearchParams(calledWith.split("?")[1] ?? "");
expect(qs.get("limit")).toBe("0");
expect(qs.get("offset")).toBe("0");
});

it("undefined のキーは付けない / omits keys when undefined", async () => {
vi.mocked(adminFetch).mockResolvedValueOnce(
new Response(JSON.stringify({ entries: [], total: 0, limit: 0 }), { status: 200 }),
);

await listActivity({ kind: "clip_ingest" });
const calledWith = vi.mocked(adminFetch).mock.calls[0]?.[0] as string;
const qs = new URLSearchParams(calledWith.split("?")[1] ?? "");
expect(qs.get("kind")).toBe("clip_ingest");
expect(qs.get("actor")).toBeNull();
expect(qs.get("from")).toBeNull();
expect(qs.get("limit")).toBeNull();
expect(qs.get("offset")).toBeNull();
});

it("200 なら ActivityListResponse を返す / returns ActivityListResponse on success", async () => {
const body = {
entries: [
{
id: "a1",
kind: "lint_run" as const,
actor: "system" as const,
target_page_ids: [],
detail: null,
created_at: "2026-01-01T00:00:00Z",
},
],
total: 1,
limit: 50,
};
vi.mocked(adminFetch).mockResolvedValueOnce(
new Response(JSON.stringify(body), { status: 200 }),
);

await expect(listActivity()).resolves.toEqual(body);
});

it("!res.ok なら getErrorMessage 由来の Error を投げる / throws when response is not ok", async () => {
vi.mocked(adminFetch).mockResolvedValueOnce(
new Response(JSON.stringify({ message: "Forbidden" }), { status: 403 }),
);
await expect(listActivity()).rejects.toThrow("Forbidden");
});

it("body が空でも fallback メッセージで Error を投げる / throws fallback when body is empty", async () => {
vi.mocked(adminFetch).mockResolvedValueOnce(
new Response(null, { status: 500, statusText: "" }),
);
await expect(listActivity()).rejects.toThrow("Failed to fetch activity entries");
});
});
Loading
Loading