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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .claude/rules/sveltekit.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,12 @@ group them as an object rather than flattening to top-level keys.
Apply default values at this boundary so the page component typically does not need to handle `undefined`.

```typescript
// Bad: flat, scattered across top-level keys
atcoder_username: user?.atCoderAccount?.handle ?? '',
atcoder_validationcode: user?.atCoderAccount?.validationCode ?? '',
is_validated: user?.atCoderAccount?.isValidated ?? false,
// Bad: flat top-level keys (legacy snake_case naming, scattered fields)
atcoder_handle: user?.atCoderAccount?.handle ?? '',
atcoder_validation_code: user?.atCoderAccount?.validationCode ?? '',
atcoder_is_validated: user?.atCoderAccount?.isValidated ?? false,

// Good: grouped by model, defaults absorbed here
// Good: grouped by model with camelCase; defaults absorbed here
atCoderAccount: {
handle: user?.atCoderAccount?.handle ?? '',
validationCode: user?.atCoderAccount?.validationCode ?? '',
Expand Down
6 changes: 3 additions & 3 deletions .claude/rules/testing-e2e.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,10 @@ const toggleInput = page.locator('input[aria-label="<aria-label value>"]');
const toggleLabel = page.locator('label:has(input[aria-label="<aria-label value>"])');

await toggleLabel.click();
await expect(toggleInput).toBeChecked({ checked: true });
await expect(toggleInput).toBeChecked();
```
Comment on lines 85 to 87
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Playwright公式ドキュメントで、expect(locator).toBeChecked() は options.checked を使って true/false の両状態検証をサポートしていますか?推奨されるトグル反転アサーション例も確認してください。

💡 Result:

はい、Playwright公式ドキュメントによると、expect(locator).toBeCheckedはoptions.checkedを使ってtrue/falseの両状態を検証できます。デフォルトではchecked(true)を期待しますが、options.checked: falseを指定するとunchecked状態を検証します。また、indeterminate状態もサポートされていますが、checkedオプションとは併用不可です。 推奨されるトグル反転アサーション例: - チェック状態確認: await expect(locator).toBeChecked; - アンチェック状態確認: await expect(locator).not.toBeChecked; または await expect(locator).toBeChecked({ checked: false }); - トグル例(公式風): await locator.check; // または click await expect(locator).toBeChecked; await locator.uncheck; await expect(locator).not.toBeChecked; 公式ドキュメント(LocatorAssertions.toBeChecked)で明記されており、v1.18以降でoptions.checkedが追加されています。

Citations:


🏁 Script executed:

# Confirm file locations and content
git ls-files | grep -E '(testing-e2e\.md|workbooks_list\.spec\.ts)'

Repository: AtCoder-NoviSteps/AtCoderNoviSteps

Length of output: 133


🏁 Script executed:

# Check line count and read testing-e2e.md around lines 85-87
wc -l .claude/rules/testing-e2e.md
echo "---"
sed -n '80,95p' .claude/rules/testing-e2e.md

Repository: AtCoder-NoviSteps/AtCoderNoviSteps

Length of output: 880


🏁 Script executed:

# Check workbooks_list.spec.ts content around lines 198-200
wc -l e2e/workbooks_list.spec.ts
echo "---"
sed -n '195,205p' e2e/workbooks_list.spec.ts

Repository: AtCoder-NoviSteps/AtCoderNoviSteps

Length of output: 368


トグル検証の一方向性が、実装パターンと乖離しており修正が必要です。

ガイド Line 85-87 の await expect(toggleInput).toBeChecked(); は、初期状態が ON の場合にクリック後 OFF へ遷移する正常系を検証できません。
実装(e2e/workbooks_list.spec.ts Line 198-200)では { checked: !isChecked } で前状態の反転を検証しており、ガイドとの方針が異なります。Playwright 公式ドキュメント(v1.18 以降)も options.checked での両状態検証をサポートしているため、ガイドを実装パターンに合わせて修正してください。

修正案
- await toggleLabel.click();
- await expect(toggleInput).toBeChecked();
+ const wasChecked = await toggleInput.isChecked();
+ await toggleLabel.click();
+ await expect(toggleInput).toBeChecked({ checked: !wasChecked });
📝 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
await toggleLabel.click();
await expect(toggleInput).toBeChecked({ checked: true });
await expect(toggleInput).toBeChecked();
```
const wasChecked = await toggleInput.isChecked();
await toggleLabel.click();
await expect(toggleInput).toBeChecked({ checked: !wasChecked });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/rules/testing-e2e.md around lines 85 - 87, The guide's toggle
assertion is one-sided; update the check to mirror the implementation pattern
used in e2e/workbooks_list.spec.ts by asserting the toggled state with the
previous value inverted: after await toggleLabel.click() replace the
unconditional expect(toggleInput).toBeChecked() with an assertion that uses the
options form to assert the new checked state (e.g., toBeChecked({ checked:
!isChecked })) and ensure the surrounding text references the isChecked variable
and the toggleInput/toggleLabel flow so the guide matches the implementation.


The same pattern applies to any Flowbite component that visually overlays its native input (e.g. `Checkbox`, `Radio`).
The same pattern may apply to similar Flowbite components that visually overlay their native input (e.g. `Checkbox`, `Radio`). Verify the component structure before applying this workaround.

## Strict Mode: Scope Locators to the Content Area

Expand All @@ -97,7 +97,7 @@ When the navbar and page body both contain a link or button with the same text (
await page.getByRole('link', { name: 'グレード投票' }).click();

// Good: scoped to page content only
await page.locator('.container').locator('nav').getByRole('link', { name: 'グレード投票' }).click();
await page.locator('.container nav').getByRole('link', { name: 'グレード投票' }).click();
await page.locator('.container').getByRole('link', { name: 'ログイン' }).click();
```

Expand Down
6 changes: 2 additions & 4 deletions .claude/rules/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ Extract `mockFindUnique`, `mockFindMany`, and `mockCount` as the standard trio f

### Cleanup for Integration Tests and Tests with Real Side Effects

This does not apply to standard service layer unit tests that use Prisma mocks.
> **Note:** This does not apply to standard service layer unit tests that use Prisma mocks.

If a test performs real DB mutations, file system changes, external API calls, or other stateful side effects that persist beyond the test (e.g., integration tests, seed scripts), wrap assertions in `try/finally` — a failing assertion skips cleanup and contaminates later tests:

Expand All @@ -118,8 +118,6 @@ try {
}
```

This is not needed for standard service unit tests that use Prisma mocks.

### File Split for Testability

When a service file mixes DB operations and pure functions, split it into two files:
Expand All @@ -135,7 +133,7 @@ E2E tests are complementary to, not a substitute for, unit tests. Add Vitest uni

You may omit a component-level Vitest test when **both** conditions hold:

1. The component is template-only (no logic beyond prop bindings and simple `{#if}`/`{#each}` blocks that only render — no inline function calls, ternaries with side effects, derived computations, or nested logic)
1. The component is template-only (no logic beyond prop bindings and simple `{#if}`/`{#each}` blocks that only render — no inline function calls, ternaries with complex conditional logic, derived computations, or nested logic)
2. The component's rendering paths are covered by E2E tests

When a component contains extracted logic (e.g. derived values, event handlers, utility calls), add unit tests for that logic in the nearest `utils/` file instead of testing the component directly.
Expand Down
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# AtCoder affiliation confirmation API endpoint (NoviSteps organization crawler)
# WARNING: Do NOT commit the actual endpoint URL. Set it in your local .env file.
# See team documentation for the actual URL.
CONFIRM_API_URL=https://your-confirm-api-endpoint.example.com/confirm
5 changes: 0 additions & 5 deletions e2e/votes.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,11 +131,6 @@ test.describe('vote detail page (/votes/[slug])', () => {
const isUnverified = await unverifiedMessage.isVisible();
test.skip(isUnverified, 'test user is not AtCoder-verified');

// Explicit check: voteForm is already guaranteed visible by the or() wait above,
// but this documents the expected state for verified users.
await expect(voteForm).toBeVisible({
timeout: TIMEOUT,
});
// The grade buttons should include Q11 (11Q)
await expect(page.getByRole('button', { name: '11Q' })).toBeVisible({ timeout: TIMEOUT });
});
Expand Down
8 changes: 7 additions & 1 deletion src/features/votes/actions/vote_actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,13 @@ export const voteAbsoluteGrade = async ({
});
}

if (!locals.user?.is_validated) {
if (!locals.user) {
return fail(INTERNAL_SERVER_ERROR, {
message: 'ユーザー情報の取得に失敗しました。',
Comment on lines +32 to +33
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

When locals.user is missing, this returns INTERNAL_SERVER_ERROR (500). In practice this can happen when the session is valid but the DB user is gone (e.g., deleted) or lookup failed, which is usually better represented as an auth problem (clear the session and return UNAUTHORIZED, or redirect to login) rather than a 500. Consider invalidating the session (locals.auth.setSession(null)) and returning 401/403 with a re-login message.

Suggested change
return fail(INTERNAL_SERVER_ERROR, {
message: 'ユーザー情報の取得に失敗しました。',
await locals.auth.setSession(null);
return fail(UNAUTHORIZED, {
message: 'ユーザー情報の取得に失敗しました。再度ログインしてください。',

Copilot uses AI. Check for mistakes.
});
}

if (!locals.user.is_validated) {
return fail(FORBIDDEN, {
message: 'AtCoderアカウントの認証が必要です。',
});
Expand Down
2 changes: 1 addition & 1 deletion src/routes/problems/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

let { data } = $props();

let taskResults: TaskResults = $derived(data.taskResults.sort(compareByContestIdAndTaskId));
const taskResults: TaskResults = $derived(data.taskResults.sort(compareByContestIdAndTaskId));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

$derived 内で props 配列を破壊的に変更しています。

Line 23 の sort() は元配列を書き換えるため、副作用を避けるためにコピー後にソートしてください。

修正案(非破壊ソート)
- const taskResults: TaskResults = $derived(data.taskResults.sort(compareByContestIdAndTaskId));
+ const taskResults: TaskResults = $derived(
+   [...data.taskResults].sort(compareByContestIdAndTaskId)
+ );
📝 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
const taskResults: TaskResults = $derived(data.taskResults.sort(compareByContestIdAndTaskId));
const taskResults: TaskResults = $derived(
[...data.taskResults].sort(compareByContestIdAndTaskId)
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/routes/problems/`+page.svelte at line 23, The code is mutating the
incoming props by calling sort() on data.taskResults; change the expression that
produces taskResults so it sorts a copy instead (e.g., clone data.taskResults
via slice() or spread before calling sort) so compareByContestIdAndTaskId is
used on a non-mutating copy when creating const taskResults: TaskResults.

Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

data.taskResults.sort(...) mutates the data.taskResults array in place. Because data comes from load(), mutating it can create hard-to-track side effects (e.g., if the same array reference is reused elsewhere or across reactive updates). Prefer sorting a copy (e.g., copy/toSorted) before passing into $derived.

Suggested change
const taskResults: TaskResults = $derived(data.taskResults.sort(compareByContestIdAndTaskId));
const taskResults: TaskResults = $derived(
data.taskResults.toSorted(compareByContestIdAndTaskId),
);

Copilot uses AI. Check for mistakes.

const isAdmin = $derived(data.isAdmin);
const isLoggedIn = $derived(data.isLoggedIn);
Expand Down
9 changes: 2 additions & 7 deletions src/routes/users/edit/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,17 +136,12 @@ export const actions: Actions = {
try {
await userService.deleteUser(username);
locals.auth.setSession(null); // remove cookie

return {
success: true,
username,
message_type: 'green',
message: 'Successfully deleted.',
};
} catch (error) {
console.error('Failed to delete user account', error);
return fail(INTERNAL_SERVER_ERROR, { message: 'Failed to delete account.' });
}

redirect(302, '/login');
},
};

Expand Down
Loading