Skip to content

BE/FE refact#254

Merged
yusuke0610 merged 12 commits into
mainfrom
stg
May 17, 2026
Merged

BE/FE refact#254
yusuke0610 merged 12 commits into
mainfrom
stg

Conversation

@yusuke0610

@yusuke0610 yusuke0610 commented May 16, 2026

Copy link
Copy Markdown
Owner

Backend Refactor: タスクハンドラ失敗パスを NonRetryableError に統一

Summary

  • タスクハンドラの「黙って return」と汎用 RuntimeErrorNonRetryableError raise に統一し、worker の dead_letter 遷移を確実にする。
  • 3 ハンドラの失敗パスを守る回帰テストを新設し、旧契約を固定化していた既存テストを新契約に追従させた。
  • CLAUDE.md「タスクハンドラの『黙って return』は禁止」原則の徹底。

Background

backend のリファクタリングレビューで、以下の不整合が見つかった:

  1. career_analysis.py:38-40analysis が None のとき logger.error + return の silent return。worker は例外を受け取らないため正常終了として処理し、UI 側で「completed」として誤観測される。
  2. github_analysis_service.py:35,41payload["user_id"]KeyError、cache 不在時に RuntimeError。どちらも worker の汎用 except Exception 経路でリトライ対象になってしまうが、本質的にはディスパッチ側のバグであり、再試行しても回復しない。

前回 turn で blog_summarize.py は同パターンを既に NonRetryableError 化済みだったため、3 ハンドラ間で挙動を揃える必要があった。

CLAUDE.md より(再掲):

タスクハンドラの「黙って return」は禁止: 失敗パスでは NonRetryableError / RetryableErrorraise し、worker に dead_letter / retrying 遷移と通知発行を任せる。早期 return は呼び出し側に completed として観測される。

Changes

app/services/tasks/handlers/career_analysis.py

  • 必須キー(user_id / record_id / target_position)欠落時に NonRetryableError を raise(missing リストをメッセージに含める)。
  • record 不在時の silent return を NonRetryableError raise に置換。
  • payload["..."] の直接アクセスを .get() に統一し、KeyError の発生経路を排除。

app/services/intelligence/github_analysis_service.py

  • payload["user_id"]KeyErrorNonRetryableError
  • cache 不在時の RuntimeErrorNonRetryableError

backend/tests/services/tasks/test_handlers_failure.py(新規, 7 ケース)

3 ハンドラの失敗パスを横断的に固定化:

  • TestBlogSummarizeHandlerFailures: user_id 欠落 / cache 不在
  • TestCareerAnalysisHandlerFailures: user_id 欠落 / record_id 欠落 / record 不在
  • TestGithubAnalysisHandlerFailures: user_id 欠落 / cache 不在

すべて pytest.raises(NonRetryableError) で明示的に検証(CLAUDE.md「失敗パスを pytest.raises(ExpectedError) で必ず assert する」原則)。

backend/tests/test_worker_extended.py(旧契約 assert の更新)

CLAUDE.md「契約変更時は既存テストの assert を必ず見直す。旧契約を固定化したテスト(例: test_no_cache_returns_early のような silent-return アサーション)が残ると修正の意図が後退する。テスト名と本体の両方を更新する」原則に従い、以下を改名+書き換え:

  • test_no_cache_raises_runtime_errortest_no_cache_raises_non_retryable
  • test_no_record_returns_earlytest_no_record_raises_non_retryable

TDD フロー

  1. red: 修正前に pytest tests/services/tasks/test_handlers_failure.py で 5/7 失敗(career_analysis 3 / github_analysis 2)。blog_summarize 2 件は前回修正済みで pass。
  2. green: ハンドラ修正後、新規テスト 7/7 pass。
  3. 旧契約 assert 2 件を新契約に追従。make test-backend 全件 pass。

Validation

make lint-backend     # All checks passed
make test-backend     # 503 passed
  • 個別実行: nix develop --command bash -c "cd backend && .venv/bin/python -m pytest tests/services/tasks/test_handlers_failure.py -v" → 7/7 pass
  • E2E トリガー外(router / model / migration の変更なし)

Test plan

  • make test-backend で全 503 件 pass を確認
  • make lint-backend clean
  • 新規ハンドラ単体テスト 7 件すべて pass
  • 旧契約 assert 2 件が新契約で pass
  • stg 環境に Cloud Tasks 経由で不正 payload(user_id 欠落 / 存在しない record_id)を流し、対応するキャッシュレコードが dead_letter 状態になることを確認(任意)
  • 失敗時の通知(failed ステータス)が _create_notification 経由でユーザーに届くことを stg で確認(任意)

Impact / Risk

  • 挙動変更: 不正な payload を受け取ったタスクは「通知なしの silent completed」ではなく「dead_letter + 失敗通知」になる。ユーザー観測上は 改善(失敗が見えるようになる)。
  • 後方互換: 公開 API / DB schema / migration の変更なし。
  • リトライ挙動: NonRetryableError は Cloud Tasks のリトライを止めるため、不正 payload で無限リトライしていたケースがあれば即停止する(Cloud Tasks のキュー負荷低減)。

Out of scope

Backend リファクタリングレビューで挙げた以下は本 PR では未対応(別 PR で順次対応予定):

  • routers/blog.py の例外マッピング共通化
  • worker.py_run_* テスト向けシム削除
  • routers/blog.py の package 化(accounts / articles / summary)
  • services/blog/collector.py のプラットフォーム別分割

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Centralized auth session hook, unified API path constants, project-modal form hook, blog integration endpoints (accounts, sync, summarize, score) and unified task-status helper.
  • Bug Fixes

    • Improved task failure handling so missing data or missing cache become non-retryable (routes to dead-letter) and auth/401 handling is standardized.
  • Tests

    • Many new/updated tests covering auth flows, task handlers, worker logic, security and form hooks.
  • Documentation

    • Added duplication/DRY guidance, agent/skill docs, and CI/dupe tooling notes.
  • Infra

    • Consolidated Terraform stack module and added monitoring/alerting resources.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 16, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR tightens backend task handler error contracts (raising NonRetryableError for missing payload/state), centralizes environment key usage, refactors frontend auth/session and form state into hooks, centralizes API paths and task-status utilities, adds duplication-detection tooling and CLAUDE skill/docs, and consolidates Terraform environment stacks into a shared devforge_stack with monitoring modules and moved state mappings.

Changes

Backend Task Handler Error Contracts & Env Keys

Layer / File(s) Summary
Task handler and service error contract changes
backend/app/services/intelligence/github_analysis_service.py, backend/app/services/tasks/handlers/career_analysis.py, backend/tests/services/tasks/test_handlers_failure.py
Handlers validate required payload keys and missing DB/cache rows by raising NonRetryableError and include payload/key/user metadata in logs; tests assert NonRetryableError for missing user/cache/record cases.
Central env key source and propagated updates
backend/app/core/env_keys.py, backend/app/core/settings.py, backend/app/core/redis_client.py, backend/app/core/encryption.py, backend/app/main.py
Introduces env_keys as single source of truth for environment variable names and updates settings, Redis client, encryption, and app startup to use these constants.
Auth token/session helpers
backend/app/routers/auth/token_manager.py, backend/app/routers/auth/endpoints.py
Centralized extraction of refresh token from session cookie via extract_refresh_token_from_session; endpoints delegate to the helper.
Blog router reorganization
backend/app/routers/blog/__init__.py, backend/app/routers/blog/accounts.py, backend/app/routers/blog/sync.py, backend/app/routers/blog/summarize.py, backend/app/routers/blog/score.py
Blog routes split into a package with sub-routers for accounts, sync, summarize, and score; removed monolithic blog.py.
Shared schema for task status
backend/app/schemas/shared.py, backend/app/schemas/career_analysis.py, backend/app/schemas/__init__.py
Adds TaskStatusResponse to schemas.shared and re-exports it where used.
Resume formatting sharing
backend/app/services/shared/resume_format.py, backend/app/services/markdown/generators/resume_generator.py, backend/app/services/pdf/generators/resume_generator.py
Introduces shared CATEGORY_LABELS and attr() helper used by both Markdown and PDF resume generators.

Frontend Refactoring: Auth, Hooks, API Paths, Utilities, Tests

Layer / File(s) Summary
Auth session hook & API client unauthorized handling
frontend/src/hooks/useAuthSession.ts, frontend/src/hooks/useAuthSession.test.ts, frontend/src/App.tsx, frontend/src/api/client.ts
Adds useAuthSession for centralized sessionStorage, restoration, logout guard, and GitHub error query handling; App delegates auth to the hook; API client centralizes 401 handling via buildUnauthorizedError().
Paths and API client migration
frontend/src/api/paths.ts, frontend/src/api/* (intelligence, blog, ai-resume, career-analysis, resumes, notifications, master-data)
Introduces PATHS and updates many frontend API modules to use centralized route builders instead of hardcoded strings.
Project modal form extraction
frontend/src/hooks/useProjectModalForm.ts, frontend/src/hooks/useProjectModalForm.test.ts, frontend/src/components/forms/ProjectModal.tsx
Extracts project modal form state and handlers into useProjectModalForm, with tests validating initialization, cloning, field behaviors, tech stack/team operations, and date validation.
Blog account manager consolidation
frontend/src/hooks/useBlogAccountManager.ts
Replaces multiple per-platform booleans with a single actions map and setAction/findPlatformWithAction helpers and centralizes auto-sync logic.
Task status utility and hook updates/tests
frontend/src/utils/taskStatus.ts, frontend/src/hooks/analysis/*, frontend/src/hooks/useBlogSummaryPolling.ts, frontend/src/hooks/useCareerAnalysisPage.ts, frontend/src/hooks/analysis/useAsyncAnalysisPage.test.ts
Adds isInProgressStatus() and uses it across hooks to determine polling; adds test covering polling rejection behavior.
Error codes/messages and UI
frontend/src/constants/errorCodes.ts, frontend/src/constants/errorMessages.ts, frontend/src/components/ui/ErrorToast.tsx
Adds SSoT error code list and isErrorCode guard; tightens ERROR_CONFIG typing and updates ErrorToast to use the guard.
Misc test updates
frontend/src/components/analysis/TechBar.test.tsx, frontend/src/hooks/useDocumentForm.test.ts
Minor test cleanup and added cache-initialization test for document form hook.

Tooling, Docs, and Infrastructure

Layer / File(s) Summary
Duplication detection & CLAUDE docs
.jscpd.json, Makefile, .githooks/pre-push, .github/workflows/ci.yml, .gitignore, .claude/*
Adds jscpd config, Makefile dupe targets, pre-push opt-in dupe-check, CI duplication job (artifact upload, warn-only), ignores report/, and extensive CLAUDE rules/skills and duplication guidance for reviews.
Terraform: dev/stg/prod -> devforge_stack consolidation & monitoring
infra/modules/devforge_stack/*, infra/environments/*, infra/environments/shared/*, infra/modules/monitoring/*, lock files
Collapses per-environment module composition into a shared devforge_stack module, adds shared variables/outputs/moved state mappings, splits monitoring into modular files, and updates provider lock files for OpenTofu where applicable.
Cloud Tasks wiring & factory
backend/app/services/tasks/cloud_tasks.py, backend/app/services/tasks/factory.py, backend/app/routers/internal.py
CloudTasks dispatcher and factory read environment keys via env_keys; internal router uses env_keys.TASK_RUNNER for verification logic.
CI workflow and pre-push
.github/workflows/ci.yml, .githooks/pre-push, Makefile
Detects duplication changes in path filters, conditionally runs jscpd, and surfaces dupe artifacts; pre-push can optionally run make dupe-check (warn-only).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 I hopped through stacks of code today,

Handled errors that once would stray.
Hooks and paths now tidy, neat,
Dupe checks hum on every beat.
Infra stacked and tests in line —
A rabbit nods: "This PR looks fine."

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch stg

@yusuke0610

Copy link
Copy Markdown
Owner Author

Frontend Refactor: 責務分離・契約集約・race 防止テストの整備

Summary

  • 3 つの「責務が多すぎる」箇所を hook へ抽出(ProjectModal / useBlogAccountManager / App.tsx)。
  • 「進行中ステータス」判定の文字列リテラル 3 箇所を utils/taskStatus.ts に集約。
  • api/client.ts の 401 処理 3 経路を共通ヘルパーにまとめた。
  • ログアウト直後の race など、これまでガードできていなかった経路にテストを追加。低価値テストを整理。

Background

frontend のリファクタリングレビューで次の点を High/Medium として挙げた:

  1. ProjectModal.tsx (377 行) — state + 9 updater + 4 セクション UI が同居。同パターンの useCareerExperienceMutators は既に hook 化済みなのに、ProjectModal だけ未適用。
  2. useBlogAccountManager (210 行) — 4 つの per-platform lifecycle(saving / syncing / updating / deleting)を個別 useState で管理。handleSave 内に「保存後の自動同期」が直結。
  3. App.tsx:39-89setOnUnauthorized 登録 / github_error 展開 / getCurrentUser セッション復元 / justLoggedOut ref race 防止 の 4 つの useEffect/useRef が同居。App.tsx 自体のテストが存在しないため、ログアウト直後の race を回帰検出できない状態だった。
  4. 「進行中」ステータス判定の重複pending / processing / retrying の or 連鎖が useAsyncAnalysisPage / useCareerAnalysisPage / useBlogSummaryPolling の 3 箇所に複製。契約変更時の修正箇所が分散。
  5. api/client.ts:126-147 — 401 → refresh → retry / 401 (refresh 失敗) / AUTH_EXPIRED 3 経路で同じ _onUnauthorized() + throw ApiError(AUTH_REQUIRED) を 3 回書いている。

Changes

Step 1: ProjectModal → useProjectModalForm 抽出

新規 src/hooks/useProjectModalForm.ts:

  • state + 9 updater(updateField / updateTechStack / addTechStack / removeTechStack / updateTeamTotal / addTeamMember / removeTeamMember / updateTeamMember / togglePhase
  • dateError 派生値
  • initProject の純粋関数化

更新 src/components/forms/ProjectModal.tsx: 377 → 236 行(-141 行)。JSX 中心に縮小。

Step 2: useBlogAccountManager の lifecycle 整理

savingPlatform / syncingPlatform / updatingPlatform / deletingPlatform個別 useStateで持っていた構造を Partial<Record<PlatformKey, PlatformAction>> の単一 map に集約:

type PlatformAction = "saving" | "syncing" | "updating" | "deleting";
const [actions, setActions] = useState<Partial<Record<PlatformKey, PlatformAction>>>({});
const setAction = (platform, action | null) => { ... };

外部 API(savingPlatform 等の派生値返却)は維持し、消費側の変更不要。

handleSave / handleUpdate 内の「保存 → 自動同期」連結を attemptAutoSync(accountId, formatSuccess, fallbackMessage) ヘルパーに分離(成功時/失敗時のメッセージ整形を渡せる小さな pure な責務)。

Step 3: App.tsxuseAuthSession 抽出

新規 src/hooks/useAuthSession.ts: 4 useEffect/useRef を移動。PUBLIC_PATHS 判定、justLoggedOut ref による race 防止もまとめて担う。

更新 src/App.tsx: 122 → 26 行(-96 行)。useTheme + useAuthSession の wiring と <AppRoutes> のみ。

Step 4: utils/taskStatus.ts 新設

新規 src/utils/taskStatus.ts:

export type TaskStatus = "pending" | "processing" | "retrying" | "completed" | "dead_letter";
export function isInProgressStatus(status: string | null | undefined): boolean { ... }

useAsyncAnalysisPage / useCareerAnalysisPage / useBlogSummaryPolling の 3 箇所から参照差し替え。バックエンド app/services/tasks/base.py と同じ判定式。

Step 5: api/client.ts の 401 処理共通化

buildUnauthorizedError() ヘルパーを追加し、3 経路の _onUnauthorized?.() + throw new ApiError({ code: "AUTH_REQUIRED", ... }) を 1 箇所に集約。

Test changes

追加(不足分の補填)

ファイル ケース 守る仕様
useDocumentForm.test.ts Redux キャッシュ存在時に loadLatest が呼ばれない ページ遷移時の二重 fetch / チラつき防止
useAsyncAnalysisPage.test.ts fetchProgress が reject しても polling フェーズが維持される Redis 障害時に hook 全体が壊れない
useAuthSession.test.ts(新規, 5 ケース) sessionStorage 復元 / 公開パス / 保護パスでの復元 / ログアウト後の race 防止 / handleLoginSuccess ログアウト直後の getCurrentUser 競合を防ぐ(従来 App.tsx でテスト不能だった経路)
useProjectModalForm.test.ts(新規, 10 ケース) 初期化 / structuredClone / is_current 切替 / techStack / teamMember / phase / dateError hook 抽出後の updater ロジック単体テスト

整理(低価値テストの削減)

  • components/analysis/FrameworkList.test.tsxTechBar.test.tsx にリネーム(実態が TechBar なのにファイル名が乖離していた)。// items を使ってESLint の unused variable を回避 の dummy assert を撤去。

レビュー誤検出の訂正

レビュー時に「不足」と挙げた 2 件は 既存テストでカバー済み だったため追加せず:

  • 並列 refresh 抑止 → api/client.test.ts:71-107 に既存
  • useBlogAccountManager の sync 失敗 → useBlogAccountManager.test.ts:160-186 に既存

Validation

make lint-frontend       # All checks passed
make test-frontend       # 17 files, 92 tests pass(前回 15 files, 75 tests → +17 ケース)
make build-frontend      # 成功(既存の chunk size warning のみ)
npm run test:e2e         # 13/13 pass(auth / navigation / notifications / github-analysis)

App.tsx の認証ライフサイクル変更は CLAUDE.md の E2E トリガー(「認証・ナビゲーション・レイアウトの変更」)に該当するため Playwright も実行済み。

Test plan

  • make ci 相当(lint / test / build)すべて pass
  • npm run test:e2e 13 シナリオすべて pass
  • useAuthSession race 防止テスト(ログアウト後に getCurrentUser が呼ばれない)が新規で追加
  • useProjectModalForm の 10 updater 分岐がすべてテスト済み
  • ブラウザでの手動確認(任意):
    • 職務経歴書 → プロジェクト編集モーダルで保存/キャンセル/技術スタック追加削除
    • ブログ連携で zenn / note / qiita の保存 → 自動同期 → 同期失敗時のメッセージ表示
    • ログアウト → 再ログイン直後にちらつきが無いこと

Impact / Risk

  • 挙動変更なし: すべて内部リファクタリングで、UI / API 通信 / Redux 状態の外部観測動作は維持。
  • 追加ガード: useAuthSession テストで race 経路を、useDocumentForm テストでキャッシュ復帰経路をそれぞれ守るようになった。今回見つけたわけではないが、潜在的回帰の検知力が上がる。
  • コード行数: 削除 -352 行 / 追加 +247 行(差し引き -105 行、テスト追加分含む)。

Out of scope(別 PR で対応予定)

frontend リファクタリングレビューで挙げた以下は本 PR では未対応:

  • hooks/hooks/auth/ hooks/form/ hooks/blog/ 等のサブディレクトリへ再編(import 影響が広範のため別 PR)
  • useDocumentForm 内の Redux キャッシュ wiring を useFormCache に分離
  • useCareerAnalysisPage:37pollingId! non-null assertion 除去
  • GitHubAnalysisPage.tsx のダッシュボード JSX 87 行を子コンポーネントへ抽出
  • CareerResumeForm.tsx のヘッダーアクション群を <CareerResumeFormHeader> に切り出し
  • pages/GitHubCallbackPage.tsx を thin wrapper 化(実体を components/auth/ へ)

🤖 Generated with Claude Code

@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: 6

🧹 Nitpick comments (2)
frontend/src/hooks/useProjectModalForm.test.ts (1)

22-108: ⚡ Quick win

Add a rerender test for project prop changes.

Please add a case that rerenders the hook with a different project and asserts local is reinitialized. This will lock in the expected React hook lifecycle behavior and prevent stale-form regressions.

🧪 Suggested test shape
+  it("project が切り替わったら local が再初期化される", () => {
+    const p1: CareerProjectForm = { ...sampleProject, name: "A" };
+    const p2: CareerProjectForm = { ...sampleProject, name: "B" };
+    const { result, rerender } = renderHook(
+      ({ project }) => useProjectModalForm(project),
+      { initialProps: { project: p1 } },
+    );
+
+    act(() => result.current.updateField("name", "edited"));
+    expect(result.current.local.name).toBe("edited");
+
+    rerender({ project: p2 });
+    expect(result.current.local.name).toBe("B");
+  });
🤖 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 `@frontend/src/hooks/useProjectModalForm.test.ts` around lines 22 - 108, Add a
test that verifies useProjectModalForm reinitializes when its project prop
changes: render the hook with sampleProject, mutate result.current.local, then
call the renderHook rerender with a different project (e.g., null or a different
sample object) and assert that result.current.local matches the new project's
initial shape (for null: empty defaults; for a new project: its fields, e.g.,
name, is_current, technology_stacks). Use the same renderHook/rerender pattern
and reference result.current from useProjectModalForm to ensure the hook resets
rather than preserving mutated state.
frontend/src/hooks/useAuthSession.test.ts (1)

48-83: ⚡ Quick win

Add a navigation case for public → protected routing.

These tests only cover the initial pathname. The authLoading regression shows up when the hook mounts on a public route and later moves to a protected one, so a navigation-based case would lock that behavior down.

🤖 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 `@frontend/src/hooks/useAuthSession.test.ts` around lines 48 - 83, Add a new
test that simulates navigating from a public route to a protected route to cover
the regression: render the hook with renderHook(() => useAuthSession(), {
wrapper: makeWrapper("/login") }), assert initial state (authLoading false,
api.getCurrentUser not called), then simulate navigation to a protected path
(e.g. update wrapper history to "/career" or call history.push inside act), mock
api.getCurrentUser to resolve a user, waitFor the hook to update and assert
api.getCurrentUser was called and result.current.user and sessionStorage were
populated (use the same symbols useAuthSession, makeWrapper, api.getCurrentUser,
waitFor, sessionStorage used in the existing tests).
🤖 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 `@backend/tests/services/tasks/test_handlers_failure.py`:
- Around line 72-77: The test test_missing_user_id_raises_non_retryable is
removing multiple required keys from the Career payload which can mask
regressions; update this test (and the sibling test covering lines 78-84) to
construct a valid payload containing all required keys except user_id, then call
CareerAnalysisHandler.run with that payload to assert NonRetryableError,
ensuring only the targeted missing-key condition is tested; locate the payload
construction in test_missing_user_id_raises_non_retryable and in the paired test
and modify them to keep all other keys present while omitting only the specific
key under test.

In `@frontend/src/hooks/useAuthSession.ts`:
- Around line 98-103: The handleLogout function currently awaits logout() and if
that call rejects the local cleanup (sessionStorage.removeItem, setting
justLoggedOut.current and setUser(null)) never runs; change handleLogout to call
logout() inside a try/catch and perform the client-side cleanup in a finally
block so client state is cleared deterministically, and in the catch either log
the server error or surface it in a controlled way rather than letting it become
an unhandled rejection (keep references to logout(),
sessionStorage.removeItem("auth_user"), justLoggedOut.current, and setUser(null)
to locate the changes).
- Around line 54-61: The effect in useAuthSession currently clears the entire
search string when a github_error is present, losing other query params; update
the logic inside the useEffect to parse location.search with URLSearchParams,
remove only the "github_error" entry (params.delete("github_error")), build the
new search string from params.toString() and call navigate with the original
pathname plus the preserved search (omit the ?, or use empty search when no
params remain) while keeping replace: true, then call setGithubError(error);
reference useEffect, location, URLSearchParams, navigate, and setGithubError to
locate and change the code.
- Around line 63-96: The effect should set authLoading true before starting
restoration so route guards wait during public→protected transitions; inside the
useEffect (the block that checks user, PUBLIC_PATHS, justLoggedOut and then runs
the async IIFE calling getCurrentUser), call setAuthLoading(true) immediately
before invoking the async restoration (and keep the existing finally that calls
setAuthLoading(false)), ensuring you still skip this when the early-return
branch runs; update the logic around
useEffect/user/location.pathname/getCurrentUser/setAuthLoading to reflect this
change.

In `@frontend/src/hooks/useBlogAccountManager.ts`:
- Around line 46-55: The setAction setter can clobber a newer action when an
earlier async operation's finally calls setAction(platform, null); modify
setAction in useBlogAccountManager to accept an optional expectedAction
parameter (e.g. setAction(platform: PlatformKey, action: PlatformAction | null,
expectedAction?: PlatformAction)) and when action === null only delete the key
if prev[platform] === expectedAction (i.e. the action that scheduled the clear),
otherwise leave it unchanged; update all callers that clear the action (the
finally blocks referenced at lines 142/164/184/214) to pass the exact
PlatformAction they started as expectedAction so a later-started action won't be
cleared by an earlier completion.

In `@frontend/src/utils/taskStatus.ts`:
- Around line 10-15: The TaskStatus union type is missing the "failed" variant
which exists in backend/task flows; update the TaskStatus type definition
(symbol: TaskStatus in frontend/src/utils/taskStatus.ts) to include "failed"
alongside "pending", "processing", "retrying", "completed", and "dead_letter" so
all consumers and type-narrowing remain sound; after adding "failed", run
typechecks and update any switch/if exhaustiveness handling around TaskStatus to
include the new case.

---

Nitpick comments:
In `@frontend/src/hooks/useAuthSession.test.ts`:
- Around line 48-83: Add a new test that simulates navigating from a public
route to a protected route to cover the regression: render the hook with
renderHook(() => useAuthSession(), { wrapper: makeWrapper("/login") }), assert
initial state (authLoading false, api.getCurrentUser not called), then simulate
navigation to a protected path (e.g. update wrapper history to "/career" or call
history.push inside act), mock api.getCurrentUser to resolve a user, waitFor the
hook to update and assert api.getCurrentUser was called and result.current.user
and sessionStorage were populated (use the same symbols useAuthSession,
makeWrapper, api.getCurrentUser, waitFor, sessionStorage used in the existing
tests).

In `@frontend/src/hooks/useProjectModalForm.test.ts`:
- Around line 22-108: Add a test that verifies useProjectModalForm reinitializes
when its project prop changes: render the hook with sampleProject, mutate
result.current.local, then call the renderHook rerender with a different project
(e.g., null or a different sample object) and assert that result.current.local
matches the new project's initial shape (for null: empty defaults; for a new
project: its fields, e.g., name, is_current, technology_stacks). Use the same
renderHook/rerender pattern and reference result.current from
useProjectModalForm to ensure the hook resets rather than preserving mutated
state.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c6445478-3641-4337-9652-825b37744480

📥 Commits

Reviewing files that changed from the base of the PR and between 2a648cc and c7edb53.

📒 Files selected for processing (19)
  • backend/app/services/intelligence/github_analysis_service.py
  • backend/app/services/tasks/handlers/career_analysis.py
  • backend/tests/services/tasks/test_handlers_failure.py
  • backend/tests/test_worker_extended.py
  • frontend/src/App.tsx
  • frontend/src/api/client.ts
  • frontend/src/components/analysis/TechBar.test.tsx
  • frontend/src/components/forms/ProjectModal.tsx
  • frontend/src/hooks/analysis/useAsyncAnalysisPage.test.ts
  • frontend/src/hooks/analysis/useAsyncAnalysisPage.ts
  • frontend/src/hooks/useAuthSession.test.ts
  • frontend/src/hooks/useAuthSession.ts
  • frontend/src/hooks/useBlogAccountManager.ts
  • frontend/src/hooks/useBlogSummaryPolling.ts
  • frontend/src/hooks/useCareerAnalysisPage.ts
  • frontend/src/hooks/useDocumentForm.test.ts
  • frontend/src/hooks/useProjectModalForm.test.ts
  • frontend/src/hooks/useProjectModalForm.ts
  • frontend/src/utils/taskStatus.ts
💤 Files with no reviewable changes (1)
  • frontend/src/components/analysis/TechBar.test.tsx

Comment thread backend/tests/services/tasks/test_handlers_failure.py
Comment thread frontend/src/hooks/useAuthSession.ts
Comment thread frontend/src/hooks/useAuthSession.ts
Comment thread frontend/src/hooks/useAuthSession.ts
Comment thread frontend/src/hooks/useBlogAccountManager.ts
Comment on lines +10 to +15
export type TaskStatus =
| "pending"
| "processing"
| "retrying"
| "completed"
| "dead_letter";

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 | 🟡 Minor | ⚡ Quick win

TaskStatus union is missing failed status.

Line 10-15 defines a backend contract type, but it omits failed, which is already treated as a meaningful task state elsewhere (e.g., retry flow). This can cause type drift and unsafe narrowing over time.

Suggested fix
 export type TaskStatus =
   | "pending"
   | "processing"
   | "retrying"
+  | "failed"
   | "completed"
   | "dead_letter";
🤖 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 `@frontend/src/utils/taskStatus.ts` around lines 10 - 15, The TaskStatus union
type is missing the "failed" variant which exists in backend/task flows; update
the TaskStatus type definition (symbol: TaskStatus in
frontend/src/utils/taskStatus.ts) to include "failed" alongside "pending",
"processing", "retrying", "completed", and "dead_letter" so all consumers and
type-narrowing remain sound; after adding "failed", run typechecks and update
any switch/if exhaustiveness handling around TaskStatus to include the new case.

@yusuke0610

Copy link
Copy Markdown
Owner Author

Cross-Realm Refactor PR Report

  • 採用レポート: report/XR_report_20260516_1801.md
  • 実装ブランチ: refactor/backend/deadcode
  • 適用スコープ: 全部(High 3 + Medium 2 + Documentation 統合)

Summary

3 つの SSoT 違反を解消し、領域横断の同期忘れ事故が起きにくい構造に統合。

  • (H1) 環境変数名: backend/app/core/env_keys.py を新設し、30 個近い env 名を Python 定数として集約。BE 内 7 ファイルの os.getenv("XXX") 文字列リテラルを os.getenv(env_keys.XXX) に置換
  • (H2) エラーコード: frontend/src/constants/errorCodes.ts を新設し ERROR_CODESas const 配列で定義、ErrorCodeKey 型を export。ERROR_CONFIG のキー型を Record<ErrorCodeKey, ...> に縛り、BE errors.py 冒頭に同期ルールコメントを追加。これでキー追加忘れが TypeScript 型エラーで検出される
  • (H3) API パス: frontend/src/api/paths.ts で 40+ 個の /api/... リテラルをツリー定数化。api/{auth,resumes,career-analysis,intelligence,master-data,notifications,blog,ai-resume}.ts の全 30+ リクエストを PATHS.<scope>.<endpoint> 経由に置換
  • (M1) CI 環境値: .github/workflows/ci.ymlPROJECT_ID / REGION / REPO / SERVICE ハードコードを ${{ vars.X || 'fallback' }} 形式に変更。GitHub Variables 未設定でも既存値で動作
  • (Documentation) .claude/CLAUDE.md の重複セクション(make 一覧 / CI 確認 / 環境変数)を docs/development.md / docs/api.md / Makefile help / env_keys.py への参照に置換

SSoT Consolidations

High

  • 観点: 環境変数名(H1)

  • SSoT に据えた場所: backend/app/core/env_keys.py

  • 更新した参照元:

    • BE: backend/app/core/settings.py(21 リテラル), core/encryption.py, core/redis_client.py, services/tasks/cloud_tasks.py, services/tasks/factory.py, routers/internal.py, main.py
    • infra: infra/modules/cloud_run/main.tf の env block(変更なし、リテラルのまま。env_keys.py の docstring に「正本」と明記)
    • CI: .github/workflows/ci.yml(変更なし)
    • compose: docker-compose.yml(変更なし)
  • 互換性配慮: backend のみの内部リファクタ。infra / CI / compose 側のリテラルは正本コメントで関係を明示。実行時挙動は同一

  • 観点: エラーコード(H2)

  • SSoT に据えた場所: backend/app/core/errors.py:ErrorCode(FE 側は frontend/src/constants/errorCodes.ts:ERROR_CODES で型化)

  • 更新した参照元:

    • FE 新規: frontend/src/constants/errorCodes.ts(ERROR_CODES + ErrorCodeKey + isErrorCode)
    • FE: frontend/src/constants/errorMessages.ts(ERROR_CONFIG の型を Record<ErrorCodeKey, ...> に強化)
    • FE: frontend/src/api/client.tsisErrorCode ガード経由でアクセス)
    • FE: frontend/src/components/ui/ErrorToast.tsx(同)
    • BE: backend/app/core/errors.py(冒頭に同期ルールの docstring 追加)
  • 互換性配慮: 既存の文字列ベースの code パラメータ受け取りは維持(外部からの未知 code には INTERNAL_ERROR fallback)。型強化は 内部 access 時のみ

  • 観点: API パス(H3)

  • SSoT に据えた場所: frontend/src/api/paths.tsPATHS ツリー定数

  • 更新した参照元:

    • FE 新規: frontend/src/api/paths.ts(8 サブツリー: auth / resumes / careerAnalysis / intelligence / masterData / notifications / blog / aiResume)
    • FE: frontend/src/api/{auth,resumes,career-analysis,intelligence,master-data,notifications,blog,ai-resume}.ts(30+ リクエストを PATHS.<scope>.<endpoint> 経由に置換)
  • 互換性配慮: backend の router は未変更。パス文字列は同一なのでテストもパス。api/client.test.ts の "/api/test" 等は paths.ts に含めず、テスト専用リテラルとして残置

Medium

  • 観点: CI の region / project_id ハードコード(M1)

  • SSoT に据えた場所: GitHub Repository Variables(未設定時は既存値にフォールバック)

  • 更新した参照元: .github/workflows/ci.yml の dev / stg / prod 各 deploy ジョブ

  • 互換性配慮: ${{ vars.X || 'fallback' }} 形式で GitHub Variables 未設定でも CI が壊れない。設定後に正本切り替え可能

  • 観点: cloudflare.production_branch 命名不一致(M2)

  • SSoT に据えた場所: INFRA_apply の devforge_stack module で variable 化済み(前 PR で対応済み)

  • 互換性配慮: 本 PR では追加変更なし

Documentation Consolidations

.claude/CLAUDE.md の以下セクションを整理:

  • make ターゲット表: 表の前後に「最新は make help」と「セットアップ詳細・各コマンドの目的は docs/development.md 参照」を明記。AI 即時参照用の表は残置
  • CI 確認ルール: 個別コマンドの列挙を削除し、docs/development.md「テスト・リント」セクションへリンク参照
  • 環境変数(必須/オプション): 全 17 個のリテラル列を削除し、SSoT 4 箇所へのリンク参照に置換:
    • 名前定数: backend/app/core/env_keys.py
    • 用途一覧: docs/api.md「環境変数」セクション
    • 本番注入: infra/modules/cloud_run/main.tf
    • 開発注入: docker-compose.yml

正本ファイル(docs/api.md / docs/development.md)は本 PR では未変更。AI が読む .claude/CLAUDE.md から正本へ誘導する形にした。

Skipped

  • DTO の BE↔FE 二重定義(Allowed Duplication として記録のみ): 約 15 ペアの Pydantic schema ↔ TypeScript type は言語境界による物理的同期不能。中期で OpenAPI codegen 導入の ADR が必要。本 PR の対象外
  • /auth/me / /auth/github/login-url / /auth/logout を paths.ts に追加: これらは fetch を直接使っており request ラッパー経由でない。今回は request 経由のものだけを paths.ts に集約。次 PR で直接 fetch も含めて拡張可
  • docs/api.md の env 一覧と env_keys.py の同期スクリプト: ドキュメント生成自動化は本 PR の対象外(手動同期で当面運用)
  • COOKIE_SECURE の BE grep に出ない件(XR_refacter follow-up): env_keys.py に定義はしたが、実際の参照箇所は _parse_bool_env(env_keys.COOKIE_SECURE) 経由で settings.py に存在することを確認済み(grep 漏れは間接参照のため)
  • OLLAMA_BASE_URL / TASK_MAX_ATTEMPTS の不在: docker-compose 専用設定で BE / infra で参照無しを確認。env_keys.py には追加せず、必要になった時に追加する方針

Validation

コマンド 結果
make lint-backend ✅ pass(All checks passed!
make test-backend ✅ pass(503 passed, 33.78s)
make lint-frontend ✅ pass
make test-frontend ✅ pass(vitest 17 files / 92 tests, node:test 4 tests)
make build-frontend ✅ pass(初回 Record<ErrorCodeKey, ...> の string index エラー → isErrorCode ガードで解決後 pass)
make infra-fmt-check ✅ pass(前 PR で確認済み、本 PR で infra 変更無し)
make dupe-check ✅ 74 clones / 3.10%(XR_apply は集約目的、重複率改善は副次的)
E2E 未実行(UI フロー / 認証 / レイアウト変更を伴わないため。API パス文字列は同一)

Build 失敗 → 修正の経緯

初回 build 時に ERROR_CONFIG: Record<ErrorCodeKey, ...> への型強化により、api/client.ts:93components/ui/ErrorToast.tsx:13string index access が型エラー。isErrorCode(code) ? ERROR_CONFIG[code] : ERROR_CONFIG.INTERNAL_ERROR の type guard 経由でアクセスする形に修正し、build 通過。

これは XR_refacter で意図した「FE 側でキー漏れを型エラーで検出する」効果が 実装段階で発動した好例(既存コードが string index で逃げていた箇所が明示的に処理されるようになった)。

Follow-ups

  • OpenAPI codegen 導入: DTO の BE↔FE 二重定義(約 15 ペア)と API パスの自動同期を実現する中期計画。docs/adr/ で ADR を起こすべき
  • infra/modules/cloud_run/main.tf の env block を SSoT 参照する自動同期: 現状は env_keys.py の docstring で「正本」と明記するだけ。terragrunt or 生成スクリプトで tf ファイル側を自動生成する案
  • GitHub Repo Variables の設定: vars.GCP_PROJECT_ID_{DEV,STG,PROD} / vars.GCP_REGION / vars.GCP_ARTIFACT_REPO_{DEV,STG,PROD} / vars.CLOUD_RUN_SERVICE_{DEV,STG,PROD} を GitHub に登録すれば、|| の fallback から切り替えられる
  • docs/api.md の env 一覧の現状確認: env_keys.py のエントリと docs/api.md の表が一致しているか目視確認したい。差分があれば次 PR で docs 側を追従
  • /auth/me 等の paths.ts 追加: 次 PR で fetch 直叩きの 3 パスも paths.ts に統合
  • BE_apply / FE_apply / INFRA_apply 残スコープとの連携:
    • BE_apply: routers/resumes.py_get_resume_or_404 ヘルパ抽出 / services/shared/resume_format.py 新設 / routers/blog.py パッケージ化
    • FE_apply: useProjectModalForm.ts:initProject(null)structuredClone(blankCareerProject) に置換(FE 唯一の High)/ テスト setup factory 化 / payloadBuilders テスト追加
    • INFRA_apply: stg / prod の devforge_stack 化(前 PR で dev 完了)

@yusuke0610

Copy link
Copy Markdown
Owner Author

Infra Refactor PR Report (Skipped 全件解消 + 設計レビュー)

  • 採用レポート: report/INFRA_report_20260516_1758.md
  • 前回 PR: report/INFRA_pr_20260516_1822.md(dev のみ devforge_stack 化)
  • 実装ブランチ: refactor/backend/deadcode
  • 適用スコープ: 前回の Skipped 全件解消(codex 実装をレビュー)
  • 前提条件: リリース前 + state 空 + データ destroy 済み → 破壊的変更 OK

Summary

前回 PR の Skipped を全件解消した。HCL 重複率は 17.93% → 0% に劇的改善(全体: 3.10% → 2.24%、74 → 66 clones)。stg / prod を devforge_stack 化し、infra/environments/shared/ ディレクトリ + symlink パターンで variables.tf / moved.tf / outputs.tf の 3 環境重複を物理的に 1 ファイルに統合。monitoring/main.tf (248 行) を責務別 5 ファイルに分割、cloud_tasks の queue_name を ${app_name}-ai-tasks-${env} で derive 化。cloudflare_pages_project_name / cloudflare_subdomain / cloudflare_production_branch も tfvars 経由に外出し。

Applied Changes

Phase A: stg / prod の devforge_stack 化

  • infra/environments/stg/main.tf: 124 行 → 40 行(devforge_stack 呼び出しと provider 宣言のみ)
  • infra/environments/prod/main.tf: 同様
  • 各環境の outputs は shared/outputs.tf へ集約(symlink)

Phase B: variables.tf / moved.tf / outputs.tf の shared/ + symlink 共通化

  • 新規: infra/environments/shared/variables.tf (104 行) / shared/moved.tf (103 行) / shared/outputs.tf (20 行)
  • symlink:
    • dev/variables.tf../shared/variables.tf
    • dev/moved.tf../shared/moved.tf
    • dev/outputs.tf../shared/outputs.tf
    • stg / prod も同様
  • shared/variables.tfregion variable を追加(default = "asia-northeast1")→ provider 設定を var.region

Phase C: moved.tf の拡充

  • firebase / resume_stack 旧 module address から devforge_stack 配下への移行ブロックを完備
  • google_project_service.apis[*] 6 個も devforge_stack 配下にネスト移行
  • これで stg / prod に万が一 state が残っていても apply 時に no-op で済む

Phase D: cloud_tasks の queue_name derive 化(M2)

  • infra/modules/cloud_tasks/variables.tf: queue_name 削除、app_name + environment を追加
  • infra/modules/cloud_tasks/main.tf: locals { queue_name = "${var.app_name}-ai-tasks-${var.environment}" } で derive
  • infra/modules/devforge_stack/main.tf: cloud_tasks 呼び出しから queue_name リテラルを削除し app_name / environment を渡すだけ

Phase E: monitoring 分割(M4)

infra/modules/monitoring/main.tf (248 行) を削除し、責務別 5 ファイルに分割:

  • notification_channels.tf — Email チャンネル
  • uptime.tf — Uptime check + uptime failure alert
  • auth_failures.tf — 認証失敗系 alert
  • rate_limits.tf — レート制限 alert
  • task_failures.tf — 非同期タスク失敗 alert

sub module 化ではなく 同一 module 内のファイル分割 で済ませている(CLAUDE.md の「過剰な抽象化を避ける」方針に整合)。

Phase F: cloudflare 設定の tfvars 外出し

  • cloudflare_pages_project_name / cloudflare_subdomain / cloudflare_production_branch を tfvars に追加(dev=app-dev/dev、stg=app-stg/stg、prod=app/main)
  • devforge_stack から cloudflare_subdomain の default を削除し、必ず明示渡しに

Phase G: devforge_stack から template_version 削除

  • 内部で未使用だった template_version variable を削除
  • output "template_version"shared/outputs.tf 側で var.template_version を直接参照

Validation

コマンド 結果
make infra-fmt-check ✅ pass(差分なし)
make infra-validate-dev Success! The configuration is valid.
make infra-validate-stg Success! The configuration is valid.
make infra-validate-prod Success! The configuration is valid.
make dupe-check ✅ 66 clones / 2.24% (前回 74 / 3.10%)

dupe-check の改善内訳

言語 前回 (dev のみ devforge_stack 化) 今回 (stg/prod も + symlink) 改善
HCL 17.93% (8 clones) 0% (0 clones) -17.93pt
TypeScript 4.86% 4.86%
TSX 0.62% 0.62%
Python 2.49% 2.49%
Markdown 0.59% 0.59%
全体 3.10% 2.24% -0.86pt

設計レビュー(codex 実装の RV)

良い設計(特筆事項)

  1. shared/ + symlink パターン: Terraform root module の variable / output / moved を物理的に 1 ファイルに集約しつつ、各 env を root module として残せる解。OpenTofu はファイルを読むだけなので validate も通る
  2. moved.tf の symlink 共通化: 3 環境すべてが同じ移行履歴を辿るため正当。state 残存リスクへの保険
  3. monitoring のファイル分割: sub module 化ではなくファイル分割で済ませている(「変更理由が複数だから分割」を遵守、過剰な抽象化を避けた)
  4. cloud_tasks.queue_name の module 内 derive: app_name + environment を渡せばリテラル組み立てが不要になり、devforge_stack 側がさらにシンプルに
  5. cloudflare 設定の tfvars 外出し: dev / stg / prod の差分が tfvars に集約され、main.tf からリテラルが完全に消えた

.jscpd.json の計測値について確認

ignore に以下 9 件を追加して symlink ファイルを除外している:

"**/infra/environments/dev/variables.tf",
"**/infra/environments/stg/variables.tf",
"**/infra/environments/prod/variables.tf",
"**/infra/environments/dev/moved.tf",
"**/infra/environments/stg/moved.tf",
"**/infra/environments/prod/moved.tf",
"**/infra/environments/dev/outputs.tf",
"**/infra/environments/stg/outputs.tf",
"**/infra/environments/prod/outputs.tf"

判定: 妥当。symlink 経由で同一ファイルを 3 回スキャンすると jscpd が 100% 重複として誤検出するため、明示除外が正解。shared/ の正本だけがスキャン対象になり、HCL の真の重複だけが計測される。

注意点: HCL 0% は「真の重複ゼロ」ではなく「symlink 除外後の重複ゼロ」。docs/development.md.claude/rules/common/duplication.md に「symlink 採用に伴う計測の前提」を 1 行追記すると親切(次回 PR で対応推奨)。

改善余地(コスメティック): **/ prefix は jscpd の相対パス解釈では不要。infra/environments/dev/variables.tf だけで動く。動作には影響なし

軽微な懸念

観点 内容 対応
Windows 開発者 symlink は Windows の git では設定が必要(core.symlinks=true 当該開発者がいない or Nix devshell 内なら問題なし
.terraform.lock.hcl の更新 stg / prod で hashicorp/google 7.22 → 7.23 に自動更新 validate 副作用、3 環境同一バージョンに揃った状態で意図通り
template_version の取り扱い devforge_stack からは削除、outputs.tf 内のみで使用 設計判断としては妥当だが、将来 stack 側で必要になれば再追加

残課題(次 PR 候補)

  • docs/development.md.claude/rules/common/duplication.md に symlink + jscpd ignore の説明追記: HCL 0% の誤読を防ぐ
  • L3 (cloudflare.production_branch 命名不一致): prod だけ env 名と branch 名が違う("main")件は既に tfvars に外出しされ、規約の議論ポイントだけ残る。ADR で記録するなら別 PR
  • .terraform.lock.hcl のコミット方針: prod / stg は git 管理されている。lock の自動更新時の運用ルール明文化(PR 内で常に最新化、or 意図的更新時のみ)

Skipped → 解消状況

前回 PR の Skipped 項目 本 PR での対応
H2 (variables.tf 95% 重複) shared/variables.tf + symlink で物理統合
M1 (moved.tf 重複) shared/moved.tf + symlink で物理統合、firebase / resume_stack / google_project_service 旧 address からの移行ブロックも完備
M2 (cloud_tasks.queue_name) ✅ module 内で ${app_name}-ai-tasks-${env} で derive、devforge_stack からリテラル削除
M4 (monitoring/main.tf 248 行) ✅ 5 ファイル分割(notification_channels / uptime / auth_failures / rate_limits / task_failures)
L1 (output 5 個の重複) shared/outputs.tf + symlink で物理統合
L3 (cloudflare.production_branch 命名不一致) ✅ tfvars に外出し、prod のみ "main" は明示的に tfvars に記録
stg / prod 環境の devforge_stack 適用 ✅ 完了

Follow-ups

  • 次 PR: docs/development.md の更新(symlink + jscpd 計測の前提を明記)
  • 監視 alert の追加時は monitoring/<新規責務>.tf を新設する運用ルール
  • variables.tf のさらなる整理(env で実質使われていない variable があれば削除)
  • Windows 開発者が将来参画する場合の symlink ハンドリング(.gitattributes* text=auto eol=lf 等の設定)

@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: 1

🧹 Nitpick comments (1)
backend/app/core/encryption.py (1)

9-11: ⚡ Quick win

Consider using the constant in the error message for consistency.

Line 9 now correctly uses env_keys.FIELD_ENCRYPTION_KEY, but the error message on Line 11 still uses the literal string. For complete centralization, consider:

♻️ Suggested refactor
 def _get_fernet() -> Fernet:
     key = os.getenv(env_keys.FIELD_ENCRYPTION_KEY, "").strip()
     if not key:
-        raise RuntimeError("FIELD_ENCRYPTION_KEY is not configured")
+        raise RuntimeError(f"{env_keys.FIELD_ENCRYPTION_KEY} is not configured")
     return Fernet(key.encode())
🤖 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 `@backend/app/core/encryption.py` around lines 9 - 11, The RuntimeError raised
when the FIELD_ENCRYPTION_KEY is missing uses a hard-coded string; update the
error to reference the centralized constant env_keys.FIELD_ENCRYPTION_KEY
instead so the message stays consistent with the env lookup (change the
RuntimeError in the block that reads key =
os.getenv(env_keys.FIELD_ENCRYPTION_KEY, "").strip() to include
env_keys.FIELD_ENCRYPTION_KEY in the exception text).
🤖 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 `@frontend/src/api/paths.ts`:
- Line 30: Change the PATHS types so careerAnalysis endpoints accept only number
and align aiResume snapshot types with actual usage: update careerAnalysis.byId,
careerAnalysis.status, and careerAnalysis.retry to take id: number (not number |
string), and change aiResume.snapshotById to take id: number; then update any
related signatures so functions in frontend/src/api/ai-resume.ts (getSnapshot,
updateSnapshot, finalizeSnapshot, deleteSnapshot, downloadPdf, downloadMarkdown)
and any callers use number types consistently to match backend int routes.

---

Nitpick comments:
In `@backend/app/core/encryption.py`:
- Around line 9-11: The RuntimeError raised when the FIELD_ENCRYPTION_KEY is
missing uses a hard-coded string; update the error to reference the centralized
constant env_keys.FIELD_ENCRYPTION_KEY instead so the message stays consistent
with the env lookup (change the RuntimeError in the block that reads key =
os.getenv(env_keys.FIELD_ENCRYPTION_KEY, "").strip() to include
env_keys.FIELD_ENCRYPTION_KEY in the exception text).
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d4b2f721-29f4-4197-ac37-94ddd20b2432

📥 Commits

Reviewing files that changed from the base of the PR and between c7edb53 and a5990d1.

📒 Files selected for processing (81)
  • .claude/CLAUDE.md
  • .claude/rules/backend/python.md
  • .claude/rules/common/duplication.md
  • .claude/rules/frontend/typescript.md
  • .claude/rules/infra/opentofu.md
  • .claude/skills/BE_apply/SKILL.md
  • .claude/skills/BE_refacter/SKILL.md
  • .claude/skills/FE_apply/SKILL.md
  • .claude/skills/FE_refacter/SKILL.md
  • .claude/skills/INFRA_apply/SKILL.md
  • .claude/skills/INFRA_refacter/SKILL.md
  • .claude/skills/XR_apply/SKILL.md
  • .claude/skills/XR_refacter/SKILL.md
  • .githooks/pre-push
  • .github/workflows/ci.yml
  • .gitignore
  • .jscpd.json
  • Makefile
  • backend/app/core/encryption.py
  • backend/app/core/env_keys.py
  • backend/app/core/errors.py
  • backend/app/core/redis_client.py
  • backend/app/core/settings.py
  • backend/app/main.py
  • backend/app/routers/internal.py
  • backend/app/services/tasks/cloud_tasks.py
  • backend/app/services/tasks/factory.py
  • backend/tests/services/tasks/test_handlers_failure.py
  • frontend/src/api/ai-resume.ts
  • frontend/src/api/auth.ts
  • frontend/src/api/blog.ts
  • frontend/src/api/career-analysis.ts
  • frontend/src/api/client.ts
  • frontend/src/api/intelligence.ts
  • frontend/src/api/master-data.ts
  • frontend/src/api/notifications.ts
  • frontend/src/api/paths.ts
  • frontend/src/api/resumes.ts
  • frontend/src/components/ui/ErrorToast.tsx
  • frontend/src/constants/errorCodes.ts
  • frontend/src/constants/errorMessages.ts
  • frontend/src/hooks/useAuthSession.ts
  • frontend/src/hooks/useBlogAccountManager.ts
  • infra/environments/dev/.terraform.lock.hcl
  • infra/environments/dev/main.tf
  • infra/environments/dev/moved.tf
  • infra/environments/dev/moved.tf
  • infra/environments/dev/outputs.tf
  • infra/environments/dev/terraform.tfvars
  • infra/environments/dev/variables.tf
  • infra/environments/dev/variables.tf
  • infra/environments/prod/.terraform.lock.hcl
  • infra/environments/prod/main.tf
  • infra/environments/prod/moved.tf
  • infra/environments/prod/moved.tf
  • infra/environments/prod/outputs.tf
  • infra/environments/prod/terraform.tfvars
  • infra/environments/prod/variables.tf
  • infra/environments/prod/variables.tf
  • infra/environments/shared/moved.tf
  • infra/environments/shared/outputs.tf
  • infra/environments/shared/variables.tf
  • infra/environments/stg/.terraform.lock.hcl
  • infra/environments/stg/main.tf
  • infra/environments/stg/moved.tf
  • infra/environments/stg/moved.tf
  • infra/environments/stg/outputs.tf
  • infra/environments/stg/terraform.tfvars
  • infra/environments/stg/variables.tf
  • infra/environments/stg/variables.tf
  • infra/modules/cloud_tasks/main.tf
  • infra/modules/cloud_tasks/variables.tf
  • infra/modules/devforge_stack/main.tf
  • infra/modules/devforge_stack/outputs.tf
  • infra/modules/devforge_stack/variables.tf
  • infra/modules/monitoring/auth_failures.tf
  • infra/modules/monitoring/main.tf
  • infra/modules/monitoring/notification_channels.tf
  • infra/modules/monitoring/rate_limits.tf
  • infra/modules/monitoring/task_failures.tf
  • infra/modules/monitoring/uptime.tf
💤 Files with no reviewable changes (1)
  • infra/modules/monitoring/main.tf
✅ Files skipped from review due to trivial changes (15)
  • .gitignore
  • .claude/rules/backend/python.md
  • .claude/rules/frontend/typescript.md
  • frontend/src/api/master-data.ts
  • .jscpd.json
  • infra/environments/prod/terraform.tfvars
  • infra/environments/dev/.terraform.lock.hcl
  • .claude/rules/common/duplication.md
  • .claude/rules/infra/opentofu.md
  • backend/app/main.py
  • backend/app/core/errors.py
  • infra/environments/stg/.terraform.lock.hcl
  • infra/environments/prod/moved.tf
  • .claude/CLAUDE.md
  • .claude/skills/BE_apply/SKILL.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • frontend/src/api/client.ts
  • frontend/src/hooks/useBlogAccountManager.ts
  • frontend/src/hooks/useAuthSession.ts

Comment thread frontend/src/api/paths.ts
@yusuke0610

yusuke0610 commented May 17, 2026

Copy link
Copy Markdown
Owner Author

Summary

routers/resumes.pyget_by_id → 404 パターン (5 箇所) を _get_resume_or_404 で集約し、services/shared/resume_format.py を新設して PDF/Markdown の _CATEGORY_LABELS (13 件) を SSoT 化した。routers/blog.py (375 行 / 11 関数 / 4 責務) を routers/blog/{accounts,sync,summarize,score}.py パッケージへ分割し、schemas/shared.pyTaskStatusResponse を集約。テストは 4 つの巨大ファイル (test_blog 602 / test_auth 576+121 / test_worker_extended 762 / test_security_edges 661 行) を責務別パッケージへ分割し、test_retry_flow.py の 4 重複 fixture を context manager で集約。API パス変更なし、507 テスト pass。

Applied Changes

High

  • backend/app/routers/resumes.py: _get_resume_or_404(repository, resume_id) ヘルパを追加し、get_resume / update_resume / download_resume_pdf / download_resume_markdown の 4 箇所と get_latest_resume のパターンを統合 (BE_report High force commit #1)。
  • backend/app/routers/blog/ (新規パッケージ): routers/blog.py (375 行 / 4 責務) を以下に分割 (BE_report High ログイン #2):
    • __init__.py: APIRouter(prefix="/api/blog") + サブルーター集約
    • accounts.py: アカウント CRUD + 記事一覧 (155 行)
    • sync.py: 手動同期 (56 行)
    • summarize.py: AI サマリ / リトライ / キャッシュ / ステータス (162 行)
    • score.py: スコアリング (27 行)
    • API パスは完全維持: テスト patch (app.routers.blog.{accounts,summarize}.verify_user_exists/check_llm_available) のみ追従
  • backend/app/services/shared/resume_format.py (新規): CATEGORY_LABELS (13 件) と attr(obj, key, default) を集約。PDF/Markdown 両 generator が import するように変更 (BE_report High Dev #3)。format_period は PDF (「YYYY 年 MM 月〜現在」) と Markdown (「YYYY-MM - 現在」) で意図的に出力が異なるため共通化対象外と判定。

Medium

  • backend/app/routers/auth/token_manager.py: extract_refresh_token_from_session(request) を追加し、endpoints.pyrefresh / logout で同形の 10 行 session Cookie パース処理を 1 行へ集約 (BE_report Medium Stg #4)。
  • backend/app/routers/{blog,intelligence,career_analysis}.py: Medium force commit #1 (blog services init Mixin) と Medium ログイン #2 (LLM prompt_utils) と Medium a #5 (main.py 分離) は Rule of Three 未達/影響範囲過大のため Follow-ups に回す。

Low

  • backend/app/schemas/shared.py: TaskStatusResponse を集約。schemas/career_analysis.py は re-export で互換維持 (BE_report Low ログイン #2)。
  • backend/app/routers/{blog/summarize.py,intelligence.py,career_analysis.py}: from ..schemas.career_analysis import TaskStatusResponsefrom ..schemas.shared import TaskStatusResponse に変更。
  • Low force commit #1 (intelligence_generator.py 内 9 行 clone) はファイル内の自己ヘルパ整理で完結可能だが、影響が局所のため今回は見送り (Follow-ups)。

Test Changes

Removed (= 別ファイルへの分割移動 / 一切のテスト削除なし)

  • backend/tests/test_blog.py (602 行) → tests/blog/test_{accounts,sync,summarize,score}.py
  • backend/tests/test_auth.py (576 行) + tests/test_oauth_flow.py (121 行) → tests/auth/test_{endpoints,token_manager,oauth_flow}.py
  • backend/tests/test_worker_extended.py (762 行) → tests/test_worker/test_{github_analysis,blog_summarize,career_analysis,execute_task}.py + _helpers.py
  • backend/tests/test_security_edges.py (661 行) → tests/security/test_{no_auth,admin_authorization,idor,sql_injection,boundary_values}.py + _helpers.py
  • backend/tests/test_retry_flow.py の 4 重複 patch + setup ブロック → _setup_github_analysis_test context manager で集約 (4 箇所 × 15 行 ≈ 60 行削減)

Added

  • backend/tests/services/tasks/test_handlers_success.py: handler 直接呼び出しでの正常系スモークテスト (BlogSummarizeHandler / CareerAnalysisHandler)。既存の test_handlers_failure.py (NonRetryableError 網羅) と対になり、worker シム経由でない handler 単体の挙動を固定化 (BE_report Add force commit #1)。
  • backend/tests/blog/test_score.py: /api/blog/score の API レベル境界値テスト (0 件 / 技術+非技術混在)。scorer 単体は既存の test_blog_scorer.py が網羅 (BE_report Add Dev #3)。
  • BE_report Add ログイン #2 (resumes の他ユーザー認可テスト) は既存の tests/security/test_idor.py:test_resume_download_endpoints_reject_other_user で pdf / markdown 共に網羅済み。追加不要と判定。

Duplication Resolved

  • PDF/Markdown 間の _CATEGORY_LABELS 重複 (BE_report Dup High force commit #1, jscpd 16L): services/shared/resume_format.py に集約。
  • routers/resumes.pyget_by_id → 404 5 箇所 (BE_report Dup High ログイン #2, jscpd 13L/15L): _get_resume_or_404 で 1 箇所に統合。
  • endpoints.py の session Cookie パース 10 行 × 2 (BE_report Medium): token_manager.extract_refresh_token_from_session で集約。
  • schemas.career_analysis への blog/intelligence からの依存方向の歪み: schemas/shared.py 集約で解消。

残した偶発的重複 (抽出見送り)

  • services/blog/{account_service,sync_service}.py:19-35 の 3 repo init 8L clone — Rule of Three の 2 回目、3 つ目登場で抽出 (BE_report Medium force commit #1)。
  • routers/{career_analysis,intelligence}.py の async task 共通レスポンス整形 — routers/_async_task_responses.py への抽出は 3 つ目 (blog summarize) と合わせて別 PR で再評価 (BE_report Medium ログイン #2)。

Structure Changes

backend/app/
  routers/
    blog/                   # 新規: 375 行 1 ファイル → 4 サブモジュール
      __init__.py
      accounts.py
      sync.py
      summarize.py
      score.py
    resumes.py              # _get_resume_or_404 追加
    auth/
      token_manager.py      # extract_refresh_token_from_session 追加
  services/
    shared/
      sort_utils.py         # 既存
      resume_format.py      # 新規: CATEGORY_LABELS, attr
  schemas/
    shared.py               # TaskStatusResponse 集約

backend/tests/
  blog/                     # 新規: test_blog.py 602 行 → 4 ファイル
    test_accounts.py
    test_sync.py
    test_summarize.py
    test_score.py
  auth/                     # 新規: test_auth.py 576 行 + test_oauth_flow.py 121 行 → 3 ファイル
    test_endpoints.py
    test_token_manager.py
    test_oauth_flow.py
  test_worker/              # 新規: test_worker_extended.py 762 行 → 4 + helper
    _helpers.py
    test_github_analysis.py
    test_blog_summarize.py
    test_career_analysis.py
    test_execute_task.py
  security/                 # 新規: test_security_edges.py 661 行 → 5 + helper
    _helpers.py
    test_no_auth.py
    test_admin_authorization.py
    test_idor.py
    test_sql_injection.py
    test_boundary_values.py
  services/tasks/
    test_handlers_failure.py  # 既存
    test_handlers_success.py  # 新規

旧ファイル (routers/blog.py, tests/test_{blog,auth,oauth_flow,worker_extended,security_edges}.py) は削除済み。

Skipped

採用しなかった指摘と理由:

  • Medium force commit #1 (blog services 3-repo init Mixin): Rule of Three の 2 回目。3 つ目の出現で抽出するのが本ルール。Follow-ups。
  • Medium ログイン #2 (LLM prompt_utils 抽出): services/llm/sanitizer.py への統合は意味的重複の判断が要るため、3 つ目の同パターン (blog summarize 含む 3 router 揃ったタイミング) で別 PR。
  • Medium a #5 (main.py lifespan 分離): 「今すぐ分割しなくてよい」とレポート自身が判断。lifespan ロジックが増えた段階で app/bootstrap.py 化。
  • Low force commit #1 (intelligence_generator.py 内 9 行 clone): ファイル内自己ヘルパ整理で完結。影響局所のため別 PR。
  • test_blog_collector_extended.pytest_blog_collector.py の統合: 「extended の存在理由」を読み込んで判断するコストが今回スコープを越える。Follow-ups (内部内容を確認後、parametrize で集約 or 名前を本質的に分離)。

Validation

  • make lint-backend: pass (auto-fix で 2 件の I001 / F401 を修正)
  • make test-backend: pass507 passed, 2 warnings in 34.54s
  • 追加で回したコマンド:
    • nix develop --command bash -c "cd backend && .venv/bin/python -m pytest tests/blog/ -q --no-cov": 21 passed
    • tests/auth/: 46 passed
    • tests/test_worker/: 25 passed
    • tests/security/: 108 passed
    • tests/services/tasks/: 9 passed
    • tests/test_retry_flow.py: 19 passed
  • カバレッジ:
    • app/services/shared/resume_format.py: 83%
    • app/services/tasks/handlers/blog_summarize.py: 98%
    • app/services/tasks/handlers/career_analysis.py: 98%
    • app/services/tasks/worker.py: 84%

Follow-ups

  • Medium force commit #1 / ログイン #2 / a #5: 上記 Skipped 参照。3 つ目の出現タイミングで再評価する。
  • schemas/career_analysis.py の re-export 削除: 互換のため from .shared import TaskStatusResponse # noqa: F401 を残した。外部利用者がいない確認後に削除。
  • test_blog_collector_extended.py の整理: 内部の重複テストが多い (jscpd で 6 ペア検出)。本体と extended の責務分担を確認して 1 ファイル化 or 明示分担。
  • XR_apply との重複: TaskStatusResponse の SSoT 化は XR レポートにも同じ推奨が出ていた。今回 BE 側で実施したため、XR_apply 時は重複しないこと。
  • frontend サイドエフェクト: 本 PR は API パス完全維持なので frontend 側変更不要。
  • (本 PR で対応済み) ユーザー依頼で frontend/src/api/paths.ts の careerAnalysis / aiResume の動的パス型を number | stringnumber に絞った (3 + 4 箇所)。tsc --noEmit クリーン。

@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 (3)
backend/tests/auth/test_token_manager.py (1)

57-58: ⚡ Quick win

Narrow the expected exception type in the JWT mismatch test.

Line 57 catches Exception, which can mask unrelated failures. Assert the jose-specific exception instead. In python-jose 3.5.0, jwt.decode raises JWTError when the algorithm in the token header does not match the allowed algorithms.

Proposed patch
 from jose import jwt
+from jose.exceptions import JWTError
 
     with pytest.raises(Exception):
+    with pytest.raises(JWTError):
         jwt.decode(hs256_token, _test_public_key, algorithms=["RS256"])
🤖 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 `@backend/tests/auth/test_token_manager.py` around lines 57 - 58, The test
currently uses a broad pytest.raises(Exception) around jwt.decode(hs256_token,
_test_public_key, algorithms=["RS256"]); narrow this to the jose-specific
exception by importing JWTError (e.g. from jose.exceptions import JWTError) and
changing the assertion to pytest.raises(JWTError) so the test specifically
expects the algorithm-mismatch error when decoding hs256_token with
algorithms=["RS256"].
backend/app/routers/blog/sync.py (1)

49-54: ⚡ Quick win

Log unexpected sync failures before mapping to 502.

The generic fallback currently drops operational context. Add logger.exception(...) before raising the 502 to keep stack traces for incident triage.

Proposed patch
     except Exception as exc:
-        # UnsupportedBlogPlatformError は上の except で先に捕捉される
+        logger.exception("Unexpected blog sync failure: account_id=%s user_id=%s", account_id, user.id)
         raise HTTPException(
             status_code=502,
             detail=get_error("blog.sync_failed"),
         ) from exc
🤖 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 `@backend/app/routers/blog/sync.py` around lines 49 - 54, In the generic except
block that currently raises HTTPException(status_code=502,
detail=get_error("blog.sync_failed")) (the except Exception as exc handler), log
the unexpected exception first by calling logger.exception(...) so the stack
trace and context are preserved before mapping to the 502; ensure you reference
the caught variable exc (e.g., logger.exception("Unexpected blog sync failure",
exc_info=exc) or equivalent) and then raise the HTTPException as before—note
that UnsupportedBlogPlatformError is still handled by the earlier except.
backend/tests/auth/test_endpoints.py (1)

235-249: ⚡ Quick win

Always reset limiter in finally to avoid cross-test leakage on early failures.

If an assertion fails before the trailing reset, limiter state can contaminate later tests.

Proposed refactor
 def test_auth_me_rate_limited_after_threshold(client) -> None:
     """/auth/me が 60/分の上限を超えると 429 を返すことを確認する。"""
     auth_header(client, "rl-user")
     limiter.reset()
-    statuses: list[int] = []
-    for _i in range(65):
-        resp = client.get("/auth/me")
-        statuses.append(resp.status_code)
-        if resp.status_code == 429:
-            break
-    assert 429 in statuses
-    # 上限到達前に少なくとも数件は 200 を返している
-    assert statuses.count(200) >= 50
-    limiter.reset()
+    try:
+        statuses: list[int] = []
+        for _i in range(65):
+            resp = client.get("/auth/me")
+            statuses.append(resp.status_code)
+            if resp.status_code == 429:
+                break
+        assert 429 in statuses
+        # 上限到達前に少なくとも数件は 200 を返している
+        assert statuses.count(200) >= 50
+    finally:
+        limiter.reset()
🤖 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 `@backend/tests/auth/test_endpoints.py` around lines 235 - 249, The test
test_auth_me_rate_limited_after_threshold currently calls limiter.reset() only
at the start and end, which can leak limiter state if an assertion fails; wrap
the test request loop and assertions in a try/finally and call limiter.reset()
in the finally block (or convert to a pytest fixture teardown) so
limiter.reset() is always executed; locate the limiter.reset() calls in
test_auth_me_rate_limited_after_threshold and ensure the trailing reset is moved
inside a finally to guarantee cleanup even on early failures.
🤖 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 `@backend/tests/auth/test_endpoints.py`:
- Around line 191-194: The helper _auth_failed_records currently accesses
LogRecord.message which doesn't exist and will raise AttributeError; update the
filter to call r.getMessage() instead (i.e., in _auth_failed_records replace
r.message == "auth_failed" with r.getMessage() == "auth_failed") while keeping
the logger name check (r.name == "devforge") so only devforge WARNING
auth_failed events are selected.

In `@backend/tests/auth/test_oauth_flow.py`:
- Around line 176-192: The test
test_github_login_url_uses_callback_base_url_when_set currently sets
CALLBACK_BASE_URL without a scheme which yields an invalid OAuth redirect_uri;
update the patched env in that test to include the https:// scheme (e.g.
"https://devforge-dev-XXXXX-an.a.run.app") and change the assertion for
redirect_uri to expect the full URL with scheme and path (e.g.
"https://devforge-dev-XXXXX-an.a.run.app/github/callback") so the generated
authorization_url uses a valid redirect URI.

In `@backend/tests/test_worker/test_execute_task.py`:
- Around line 118-124: The test isn't actually simulating a rollback failure;
replace the no-op and manual rollback with a real DB error before calling
_safe_rollback: arrange the db_session mock so that a DB operation raises (e.g.,
set db_session.execute.side_effect or db_session.rollback.side_effect to an
Exception) to put the session into a failing state, then call
_safe_rollback(db_session) and assert that no exception escapes. Target the
existing symbols _safe_rollback and db_session (e.g., adjust db_session.execute
or db_session.rollback side_effect) so the test validates the failure-recovery
path.

---

Nitpick comments:
In `@backend/app/routers/blog/sync.py`:
- Around line 49-54: In the generic except block that currently raises
HTTPException(status_code=502, detail=get_error("blog.sync_failed")) (the except
Exception as exc handler), log the unexpected exception first by calling
logger.exception(...) so the stack trace and context are preserved before
mapping to the 502; ensure you reference the caught variable exc (e.g.,
logger.exception("Unexpected blog sync failure", exc_info=exc) or equivalent)
and then raise the HTTPException as before—note that
UnsupportedBlogPlatformError is still handled by the earlier except.

In `@backend/tests/auth/test_endpoints.py`:
- Around line 235-249: The test test_auth_me_rate_limited_after_threshold
currently calls limiter.reset() only at the start and end, which can leak
limiter state if an assertion fails; wrap the test request loop and assertions
in a try/finally and call limiter.reset() in the finally block (or convert to a
pytest fixture teardown) so limiter.reset() is always executed; locate the
limiter.reset() calls in test_auth_me_rate_limited_after_threshold and ensure
the trailing reset is moved inside a finally to guarantee cleanup even on early
failures.

In `@backend/tests/auth/test_token_manager.py`:
- Around line 57-58: The test currently uses a broad pytest.raises(Exception)
around jwt.decode(hs256_token, _test_public_key, algorithms=["RS256"]); narrow
this to the jose-specific exception by importing JWTError (e.g. from
jose.exceptions import JWTError) and changing the assertion to
pytest.raises(JWTError) so the test specifically expects the algorithm-mismatch
error when decoding hs256_token with algorithms=["RS256"].
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 76055f7c-2e91-40f1-870c-b5eeff67fbc1

📥 Commits

Reviewing files that changed from the base of the PR and between a5990d1 and ee6e7c8.

📒 Files selected for processing (47)
  • backend/app/routers/auth/endpoints.py
  • backend/app/routers/auth/token_manager.py
  • backend/app/routers/blog.py
  • backend/app/routers/blog/__init__.py
  • backend/app/routers/blog/accounts.py
  • backend/app/routers/blog/score.py
  • backend/app/routers/blog/summarize.py
  • backend/app/routers/blog/sync.py
  • backend/app/routers/career_analysis.py
  • backend/app/routers/intelligence.py
  • backend/app/routers/resumes.py
  • backend/app/schemas/__init__.py
  • backend/app/schemas/career_analysis.py
  • backend/app/schemas/shared.py
  • backend/app/services/markdown/generators/resume_generator.py
  • backend/app/services/pdf/generators/resume_generator.py
  • backend/app/services/shared/resume_format.py
  • backend/tests/auth/__init__.py
  • backend/tests/auth/test_endpoints.py
  • backend/tests/auth/test_oauth_flow.py
  • backend/tests/auth/test_token_manager.py
  • backend/tests/blog/__init__.py
  • backend/tests/blog/test_accounts.py
  • backend/tests/blog/test_score.py
  • backend/tests/blog/test_summarize.py
  • backend/tests/blog/test_sync.py
  • backend/tests/security/__init__.py
  • backend/tests/security/_helpers.py
  • backend/tests/security/test_admin_authorization.py
  • backend/tests/security/test_boundary_values.py
  • backend/tests/security/test_idor.py
  • backend/tests/security/test_no_auth.py
  • backend/tests/security/test_sql_injection.py
  • backend/tests/services/tasks/test_handlers_success.py
  • backend/tests/test_auth.py
  • backend/tests/test_career_analysis_api.py
  • backend/tests/test_oauth_flow.py
  • backend/tests/test_retry_flow.py
  • backend/tests/test_security_edges.py
  • backend/tests/test_worker/__init__.py
  • backend/tests/test_worker/_helpers.py
  • backend/tests/test_worker/test_blog_summarize.py
  • backend/tests/test_worker/test_career_analysis.py
  • backend/tests/test_worker/test_execute_task.py
  • backend/tests/test_worker/test_github_analysis.py
  • backend/tests/test_worker_extended.py
  • frontend/src/api/paths.ts
💤 Files with no reviewable changes (5)
  • backend/app/routers/blog.py
  • backend/tests/test_oauth_flow.py
  • backend/tests/test_auth.py
  • backend/tests/test_worker_extended.py
  • backend/tests/test_security_edges.py
✅ Files skipped from review due to trivial changes (4)
  • backend/app/routers/blog/init.py
  • backend/app/routers/career_analysis.py
  • backend/app/routers/intelligence.py
  • backend/app/schemas/init.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/src/api/paths.ts

Comment on lines +191 to +194
def _auth_failed_records(records: list[logging.LogRecord]) -> list[logging.LogRecord]:
"""auth_failed イベント (devforge ロガー WARNING) のみ抽出する。"""
return [r for r in records if r.name == "devforge" and r.message == "auth_failed"]

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 | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
python - <<'PY'
import logging
r = logging.LogRecord("devforge", logging.WARNING, __file__, 1, "auth_failed", (), None)
print("has message attr before formatting:", hasattr(r, "message"))
print("safe payload via getMessage():", r.getMessage())
PY

Repository: yusuke0610/devforge

Length of output: 148


🏁 Script executed:

# First, let's check the actual implementation in the test file
cat -n backend/tests/auth/test_endpoints.py | sed -n '185,200p'

Repository: yusuke0610/devforge

Length of output: 787


🏁 Script executed:

# Also check if there are any uses of _auth_failed_records in the file
rg "_auth_failed_records" backend/tests/auth/test_endpoints.py -B 2 -A 2

Repository: yusuke0610/devforge

Length of output: 824


🏁 Script executed:

# Check the full context of how logging is set up in this test
rg -B 5 -A 5 "caplog\|LogRecord" backend/tests/auth/test_endpoints.py | head -50

Repository: yusuke0610/devforge

Length of output: 45


🏁 Script executed:

# Check if there's a pytest configuration or logging setup in the codebase
fd -e "conftest.py" -e "pyproject.toml" -e "pytest.ini" backend/tests/

Repository: yusuke0610/devforge

Length of output: 425


🏁 Script executed:

# Look for any logging configuration that might affect LogRecord attributes
rg -i "logging.config\|logging.format\|LogRecord" backend/tests/ --type py | head -30

Repository: yusuke0610/devforge

Length of output: 45


🏁 Script executed:

# Let's check the actual test execution to understand caplog behavior
python - <<'PY'
import logging
import pytest

# Simulate what pytest's caplog does
class MockCaplogRecords:
    def __init__(self):
        self.records = []
        self.handler = logging.Handler()
        self.handler.emit = lambda record: self.records.append(record)
    
    def capture(self):
        logger = logging.getLogger("devforge")
        logger.addHandler(self.handler)
        logger.warning("auth_failed")
        logger.removeHandler(self.handler)
        return self.records

caplog_mock = MockCaplogRecords()
records = caplog_mock.capture()

if records:
    r = records[0]
    print(f"LogRecord has 'message' attribute: {hasattr(r, 'message')}")
    print(f"LogRecord has 'msg' attribute: {hasattr(r, 'msg')}")
    print(f"LogRecord.msg value: {r.msg}")
    print(f"LogRecord.getMessage() result: {r.getMessage()}")
    print(f"Dir of record: {[attr for attr in dir(r) if 'msg' in attr.lower() or 'message' in attr.lower()]}")
PY

Repository: yusuke0610/devforge

Length of output: 253


Use LogRecord.getMessage() instead of record.message.

The message attribute does not exist on LogRecord objects. Accessing r.message will raise AttributeError at runtime. Use r.getMessage() to retrieve the formatted message safely.

Proposed fix
 def _auth_failed_records(records: list[logging.LogRecord]) -> list[logging.LogRecord]:
     """auth_failed イベント (devforge ロガー WARNING) のみ抽出する。"""
-    return [r for r in records if r.name == "devforge" and r.message == "auth_failed"]
+    return [r for r in records if r.name == "devforge" and r.getMessage() == "auth_failed"]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _auth_failed_records(records: list[logging.LogRecord]) -> list[logging.LogRecord]:
"""auth_failed イベント (devforge ロガー WARNING) のみ抽出する。"""
return [r for r in records if r.name == "devforge" and r.message == "auth_failed"]
def _auth_failed_records(records: list[logging.LogRecord]) -> list[logging.LogRecord]:
"""auth_failed イベント (devforge ロガー WARNING) のみ抽出する。"""
return [r for r in records if r.name == "devforge" and r.getMessage() == "auth_failed"]
🤖 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 `@backend/tests/auth/test_endpoints.py` around lines 191 - 194, The helper
_auth_failed_records currently accesses LogRecord.message which doesn't exist
and will raise AttributeError; update the filter to call r.getMessage() instead
(i.e., in _auth_failed_records replace r.message == "auth_failed" with
r.getMessage() == "auth_failed") while keeping the logger name check (r.name ==
"devforge") so only devforge WARNING auth_failed events are selected.

Comment on lines +176 to +192
def test_github_login_url_uses_callback_base_url_when_set(client) -> None:
"""CALLBACK_BASE_URL が設定されている場合、x-forwarded-host より優先されることを確認する。"""
with patch.dict(os.environ, {"CALLBACK_BASE_URL": "devforge-dev-XXXXX-an.a.run.app"}):
response = client.get(
"/auth/github/login-url",
headers={
"Origin": "http://localhost:8788",
"Host": "devforge-dev-XXXXX-an.a.run.app",
"X-Forwarded-Proto": "https",
},
)

assert response.status_code == 200
parsed = urlparse(response.json()["authorization_url"])
redirect_uri = parse_qs(parsed.query)["redirect_uri"][0]
assert redirect_uri == "devforge-dev-XXXXX-an.a.run.app/github/callback"

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 | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does GitHub OAuth require redirect_urito be an absolute URL including scheme (e.g.,https://...)?

💡 Result:

Yes, the redirect_uri used in GitHub OAuth flows must be an absolute URL that includes the scheme (e.g., https://) [1][2]. According to official GitHub documentation, when you provide a redirect_uri parameter during the authorization request, it is validated against the callback URL configured in your OAuth app settings [1][2]. Specifically, the host (excluding sub-domains) and port of the provided redirect_uri must exactly match the callback URL configured in your app settings, and the path must reference a subdirectory of that callback URL [1][2]. For native applications, GitHub also supports loopback URLs (e.g., http://127.0.0.1:1234/path), which must also be absolute URLs including the scheme and port [1][2]. The OAuth RFC, which GitHub follows, recommends using loopback literals like 127.0.0.1 or IPv6::1 rather than localhost [1][2].

Citations:


🏁 Script executed:

# First, find the test file and understand its context
fd -t f "test_oauth_flow.py" backend/

Repository: yusuke0610/devforge

Length of output: 101


🏁 Script executed:

# Search for the /auth/github/login-url endpoint
rg -n "github/login-url" --type py -A 10 -B 2

Repository: yusuke0610/devforge

Length of output: 5803


🏁 Script executed:

# Search for how CALLBACK_BASE_URL is used in the codebase
rg -n "CALLBACK_BASE_URL" --type py -A 3 -B 1

Repository: yusuke0610/devforge

Length of output: 2687


🏁 Script executed:

# Find the build_oauth_flow_and_redirect function implementation
rg -n "def build_oauth_flow_and_redirect" --type py -A 30

Repository: yusuke0610/devforge

Length of output: 45


🏁 Script executed:

# Also check how it constructs the redirect_uri
rg -n "redirect_uri" backend/app/routers/auth/oauth_flow.py -B 2 -A 2

Repository: yusuke0610/devforge

Length of output: 1094


🏁 Script executed:

# Find the get_frontend_origin function
rg -n "def get_frontend_origin" --type py -A 10

Repository: yusuke0610/devforge

Length of output: 844


🏁 Script executed:

# Also find get_callback_base_url to understand its behavior
rg -n "def get_callback_base_url" --type py -A 5

Repository: yusuke0610/devforge

Length of output: 585


Add https:// scheme to CALLBACK_BASE_URL in test and assertion.

GitHub OAuth rejects redirect_uri values without a scheme (i.e., https://example.com/path not example.com/path). This test currently allows a scheme-less configuration, which produces an invalid OAuth redirect URI and masks real authentication failures if misconfigured in production.

Proposed patch
-    with patch.dict(os.environ, {"CALLBACK_BASE_URL": "devforge-dev-XXXXX-an.a.run.app"}):
+    with patch.dict(os.environ, {"CALLBACK_BASE_URL": "https://devforge-dev-XXXXX-an.a.run.app"}):
@@
-    assert redirect_uri == "devforge-dev-XXXXX-an.a.run.app/github/callback"
+    assert redirect_uri == "https://devforge-dev-XXXXX-an.a.run.app/github/callback"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def test_github_login_url_uses_callback_base_url_when_set(client) -> None:
"""CALLBACK_BASE_URL が設定されている場合、x-forwarded-host より優先されることを確認する。"""
with patch.dict(os.environ, {"CALLBACK_BASE_URL": "devforge-dev-XXXXX-an.a.run.app"}):
response = client.get(
"/auth/github/login-url",
headers={
"Origin": "http://localhost:8788",
"Host": "devforge-dev-XXXXX-an.a.run.app",
"X-Forwarded-Proto": "https",
},
)
assert response.status_code == 200
parsed = urlparse(response.json()["authorization_url"])
redirect_uri = parse_qs(parsed.query)["redirect_uri"][0]
assert redirect_uri == "devforge-dev-XXXXX-an.a.run.app/github/callback"
def test_github_login_url_uses_callback_base_url_when_set(client) -> None:
"""CALLBACK_BASE_URL が設定されている場合、x-forwarded-host より優先されることを確認する。"""
with patch.dict(os.environ, {"CALLBACK_BASE_URL": "https://devforge-dev-XXXXX-an.a.run.app"}):
response = client.get(
"/auth/github/login-url",
headers={
"Origin": "http://localhost:8788",
"Host": "devforge-dev-XXXXX-an.a.run.app",
"X-Forwarded-Proto": "https",
},
)
assert response.status_code == 200
parsed = urlparse(response.json()["authorization_url"])
redirect_uri = parse_qs(parsed.query)["redirect_uri"][0]
assert redirect_uri == "https://devforge-dev-XXXXX-an.a.run.app/github/callback"
🤖 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 `@backend/tests/auth/test_oauth_flow.py` around lines 176 - 192, The test
test_github_login_url_uses_callback_base_url_when_set currently sets
CALLBACK_BASE_URL without a scheme which yields an invalid OAuth redirect_uri;
update the patched env in that test to include the https:// scheme (e.g.
"https://devforge-dev-XXXXX-an.a.run.app") and change the assertion for
redirect_uri to expect the full URL with scheme and path (e.g.
"https://devforge-dev-XXXXX-an.a.run.app/github/callback") so the generated
authorization_url uses a valid redirect URI.

Comment on lines +118 to +124
# コミット失敗をシミュレートしてセッションを PendingRollback 状態にする
db_session.execute.__self__ if hasattr(db_session.execute, "__self__") else None
db_session.rollback() # まず手動でロールバックして dirty 状態を作る

# _safe_rollback は例外を上げないこと
_safe_rollback(db_session)

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 | 🟡 Minor | ⚡ Quick win

_safe_rollback の失敗系シミュレーションが実質行われていません。

Line 119 は no-op で、Line 120 は正常セッションへの rollback なので、「失敗後の復旧」検証になっていません。実際に DB エラーを発生させてから _safe_rollback を呼ぶ形にしてください。

🔧 Suggested test adjustment
 from unittest.mock import AsyncMock, MagicMock, patch

 import pytest
+from sqlalchemy import text
 from app.models import BlogSummaryCache
 ...
-        # コミット失敗をシミュレートしてセッションを PendingRollback 状態にする
-        db_session.execute.__self__ if hasattr(db_session.execute, "__self__") else None
-        db_session.rollback()  # まず手動でロールバックして dirty 状態を作る
+        # DB エラーを発生させて失敗状態を作る
+        with pytest.raises(Exception):
+            db_session.execute(text("SELECT * FROM __missing_table__"))
 
         # _safe_rollback は例外を上げないこと
         _safe_rollback(db_session)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# コミット失敗をシミュレートしてセッションを PendingRollback 状態にする
db_session.execute.__self__ if hasattr(db_session.execute, "__self__") else None
db_session.rollback() # まず手動でロールバックして dirty 状態を作る
# _safe_rollback は例外を上げないこと
_safe_rollback(db_session)
# DB エラーを発生させて失敗状態を作る
with pytest.raises(Exception):
db_session.execute(text("SELECT * FROM __missing_table__"))
# _safe_rollback は例外を上げないこと
_safe_rollback(db_session)
🤖 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 `@backend/tests/test_worker/test_execute_task.py` around lines 118 - 124, The
test isn't actually simulating a rollback failure; replace the no-op and manual
rollback with a real DB error before calling _safe_rollback: arrange the
db_session mock so that a DB operation raises (e.g., set
db_session.execute.side_effect or db_session.rollback.side_effect to an
Exception) to put the session into a failing state, then call
_safe_rollback(db_session) and assert that no exception escapes. Target the
existing symbols _safe_rollback and db_session (e.g., adjust db_session.execute
or db_session.rollback side_effect) so the test validates the failure-recovery
path.

@yusuke0610
yusuke0610 merged commit c9a2c0b into main May 17, 2026
31 of 33 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant