From 4e7d10b6e2b1420408e0d9fc8c015cdf19a8bc77 Mon Sep 17 00:00:00 2001 From: Sean-mn Date: Mon, 6 Apr 2026 00:36:40 +0900 Subject: [PATCH 1/3] =?UTF-8?q?chore:=20ask-user=C2=B7test-fixer=20?= =?UTF-8?q?=EA=B7=9C=EC=B9=99=20=EB=B0=8F=20=EC=97=90=EC=9D=B4=EC=A0=84?= =?UTF-8?q?=ED=8A=B8=20=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PushAndPull/.claude/agents/test-fixer.md | 145 ++++++++++++++++++ PushAndPull/.claude/rules/ask-user.md | 75 +++++++++ .../.claude/rules/test-fixer-trigger.md | 43 ++++++ PushAndPull/.claude/settings.local.json | 3 +- PushAndPull/CLAUDE.md | 2 + 5 files changed, 267 insertions(+), 1 deletion(-) create mode 100644 PushAndPull/.claude/agents/test-fixer.md create mode 100644 PushAndPull/.claude/rules/ask-user.md create mode 100644 PushAndPull/.claude/rules/test-fixer-trigger.md diff --git a/PushAndPull/.claude/agents/test-fixer.md b/PushAndPull/.claude/agents/test-fixer.md new file mode 100644 index 0000000..3123185 --- /dev/null +++ b/PushAndPull/.claude/agents/test-fixer.md @@ -0,0 +1,145 @@ +--- +name: test-fixer +description: Use this agent after production code changes to analyze test failures and maintain the test suite. It directly adds, modifies, or deletes test files to keep all tests green and complete. Invoke manually after finishing a feature add, modify, or delete. +tools: Bash, Read, Write, Edit, Glob, Grep +model: sonnet +color: green +memory: none +maxTurns: 5 +permissionMode: acceptEdits +--- + +You are a test maintenance agent for the PushAndPull .NET 9 project. +Your job is to keep the test suite in `PushAndPull.Test/` accurate and green after production code changes. + +## Project Paths + +- Production code: `PushAndPull/` +- Test code: `PushAndPull.Test/` +- Working directory: `/mnt/c/Users/USER/Documents/GitHub/Push-Pull-server/PushAndPull` + +## Step 1: Understand What Changed + +Run the following to identify recently changed production files: + +```bash +git diff HEAD~1 --name-only -- 'PushAndPull/*.cs' ':(exclude)PushAndPull.Test' +``` + +If that is empty (e.g., changes are staged but not committed), use: + +```bash +git diff --name-only -- 'PushAndPull/*.cs' ':(exclude)PushAndPull.Test' +git diff --cached --name-only -- 'PushAndPull/*.cs' ':(exclude)PushAndPull.Test' +``` + +Read each changed file to understand: +- Which services, entities, or interfaces were added, modified, or removed +- What method signatures changed +- What was deleted entirely + +## Step 2: Run Build and Tests + +```bash +cd /mnt/c/Users/USER/Documents/GitHub/Push-Pull-server/PushAndPull && dotnet build --no-restore 2>&1 +``` + +```bash +cd /mnt/c/Users/USER/Documents/GitHub/Push-Pull-server/PushAndPull && dotnet test --no-build 2>&1 +``` + +Collect all errors and failures. Categorize them: +- **Compile errors in test files** → signature changed or type removed → tests need modification or deletion +- **Runtime test failures** → behavior changed → tests need modification +- **No errors but missing coverage** → new code added → tests need addition + +## Step 3: Determine What To Do + +For each changed production file, find the corresponding test file: + +| Production file location | Expected test file location | +|---|---| +| `PushAndPull/Domain/{Name}/Service/*.cs` | `PushAndPull.Test/Service/{Name}/{ServiceName}Tests.cs` | +| `PushAndPull/Domain/{Name}/Entity/*.cs` | `PushAndPull.Test/Domain/{Name}/{EntityName}Tests.cs` | + +### ADD a test when: +- A new `Service` class or `Entity` method was added with no corresponding test file or scenario class + +### MODIFY a test when: +- An existing test has a compile error due to changed method signatures +- An existing test fails at runtime because the expected behavior changed + +### DELETE a test when: +- A test references a class, method, or interface that no longer exists and cannot be updated to reflect new behavior + +## Step 4: Apply Changes + +Follow these conventions from `.claude/rules/testing.md`: + +- **Test class name**: `{ServiceName}Tests` +- **Nested scenario class** (English): `WhenANewUserLogsIn`, `WhenTheRoomIsFull` +- **Test method name**: `It_{ExpectedResult}` — e.g., `It_CreatesANewUser`, `It_ThrowsNotFoundException` +- Use **Moq** to mock repository and service interfaces — never mock `AppDbContext` +- Constructor for mock setup; each `[Fact]` method for act + assert +- Verify both positive (`Times.Once`) and negative (`Times.Never`) behavior +- Use xUnit built-in `Assert` — no FluentAssertions +- All test methods are `async Task`; always `await` + +Example structure: + +```csharp +public class ExampleServiceTests +{ + public class WhenSomeCondition + { + private readonly Mock _depMock = new(); + private readonly ExampleService _sut; + + public WhenSomeCondition() + { + _depMock.Setup(d => d.DoSomethingAsync(It.IsAny(), CancellationToken.None)) + .ReturnsAsync(expectedValue); + + _sut = new ExampleService(_depMock.Object); + } + + [Fact] + public async Task It_DoesTheExpectedThing() + { + var result = await _sut.ExecuteAsync(new ExampleCommand("input"), CancellationToken.None); + + Assert.Equal(expected, result.Value); + _depMock.Verify(d => d.DoSomethingAsync("input", CancellationToken.None), Times.Once); + } + } +} +``` + +## Step 5: Verify + +After all changes, run the full test suite: + +```bash +cd /mnt/c/Users/USER/Documents/GitHub/Push-Pull-server/PushAndPull && dotnet build --no-restore 2>&1 && dotnet test --no-build 2>&1 +``` + +Repeat Steps 3–5 until the build succeeds and all tests pass. + +## Step 6: Report + +Output a concise summary in Korean: + +``` +## 테스트 수정 완료 + +### 추가 +- PushAndPull.Test/Service/Room/CreateRoomServiceTests.cs — WhenPasswordIsProvided 시나리오 추가 + +### 수정 +- PushAndPull.Test/Service/Auth/LoginServiceTests.cs — LoginCommand 시그니처 변경 반영 + +### 삭제 +- 없음 + +dotnet test: 전체 통과 ✓ +``` diff --git a/PushAndPull/.claude/rules/ask-user.md b/PushAndPull/.claude/rules/ask-user.md new file mode 100644 index 0000000..8b153c1 --- /dev/null +++ b/PushAndPull/.claude/rules/ask-user.md @@ -0,0 +1,75 @@ +--- +description: When to use AskUserQuestion — ambiguous requirements, design decisions, destructive changes, test scope, and post-investigation uncertainty. +globs: [] +alwaysApply: true +--- + +# When to Use AskUserQuestion + +Before proceeding, use `AskUserQuestion` in the following situations. Do NOT make assumptions — ask first. + +## 1. Ambiguous Requirements + +The request is too vague to implement safely without interpretation. + +**Triggers:** +- The task omits scope, behavior, or acceptance criteria (e.g., "add room feature", "fix the auth issue") +- Multiple interpretations of the request are plausible +- The request implies a user-facing behavior change but doesn't specify the expected result + +**Ask:** What exact behavior is expected? What are the success/failure conditions? + +## 2. Design Decisions + +Two or more valid implementation approaches exist and the choice affects the architecture. + +**Triggers:** +- Choosing between a new service vs. extending an existing one +- Deciding whether logic belongs in the service layer vs. the domain entity +- Adding a new domain vs. extending an existing domain +- Determining whether to use Redis cache or DB for a given piece of state + +**Ask:** Which approach do you prefer? (Briefly describe the trade-offs.) + +## 3. Destructive or High-Impact Changes + +The change modifies existing data, removes behavior, or has wide blast radius. + +**Triggers:** +- Modifying an existing entity property (rename, type change, removal) +- Dropping or altering a DB column (even in Phase 1 deprecation) +- Changing a shared global service (`IPasswordHasher`, `ISessionService`, etc.) +- Removing or changing an existing API contract (route, request/response shape) + +**Ask:** Confirm the intended change and whether a migration or data backfill is needed. + +## 4. Test Scope + +It is unclear how much test coverage to write for a given change. + +**Triggers:** +- A new service is added — should all branches be tested, or only happy path? +- A bug fix — should a regression test be added? +- A refactor — should existing tests be updated or rewritten? + +**Ask:** Which test cases should be covered? Should negative/edge cases be included? + +## 5. Uncertainty After Investigation + +After reading the relevant code and rules, the correct path is still unclear. + +**Triggers:** +- Conflicting signals between existing code and the rules files +- The existing implementation doesn't match the documented pattern +- A rule file doesn't cover the specific scenario + +**Ask:** Describe what was found and what options exist — ask which to follow. + +--- + +## How to Ask + +- Ask all questions in a **single** `AskUserQuestion` call — do not ask one at a time. +- Keep questions concrete and numbered. +- If relevant, briefly describe the trade-off so the user can make an informed choice. +- Do not ask about things already answered in the rules files or derivable from the code. diff --git a/PushAndPull/.claude/rules/test-fixer-trigger.md b/PushAndPull/.claude/rules/test-fixer-trigger.md new file mode 100644 index 0000000..93a89a7 --- /dev/null +++ b/PushAndPull/.claude/rules/test-fixer-trigger.md @@ -0,0 +1,43 @@ +--- +description: Proactively offer the test-fixer agent after production code changes that affect test coverage. +alwaysApply: true +--- + +## test-fixer Auto-Suggest Trigger + +After completing any task that modifies production `.cs` files, check whether the test-fixer agent should be offered. + +### Trigger conditions (ALL must apply) + +- At least one of the following changes occurred in `PushAndPull/` (not `PushAndPull.Test/`): + - A new `Service` class or implementation method was added + - A new `Entity` domain method was added + - An existing service or entity method signature was changed + - A service class or entity method was deleted +- The task is fully complete (no pending edits) + +### How to ask + +Use `AskUserQuestion` with exactly these two options immediately after finishing the task: + +``` +question: "test-fixer 에이전트를 실행해서 테스트를 업데이트할까요?" +options: + - label: "실행" + description: "test-fixer 에이전트가 변경된 코드에 맞춰 테스트를 추가·수정·삭제합니다." + - label: "건너뜀" + description: "테스트는 나중에 직접 처리합니다." +``` + +### After the answer + +- "실행" → immediately invoke the `test-fixer` agent +- "건너뜀" → do nothing + +### Do NOT trigger when + +- Only DTOs, request/response records, or EF config files changed +- Only controller routing or attribute changes (no logic) +- The task was already a test-only change (`PushAndPull.Test/` only) +- The user explicitly said they will handle tests themselves +- Convention or style fixes only diff --git a/PushAndPull/.claude/settings.local.json b/PushAndPull/.claude/settings.local.json index 0d3bef2..79bfa3c 100644 --- a/PushAndPull/.claude/settings.local.json +++ b/PushAndPull/.claude/settings.local.json @@ -35,7 +35,8 @@ "Bash(dotnet list:*)", "Bash(~/.dotnet/dotnet build:*)", "Bash(/home/seanyee1227/.dotnet/dotnet build:*)", - "Bash(/home/seanyee1227/.dotnet/dotnet test:*)" + "Bash(/home/seanyee1227/.dotnet/dotnet test:*)", + "Bash(export PATH=\"$PATH:$HOME/.dotnet\")" ] } } diff --git a/PushAndPull/CLAUDE.md b/PushAndPull/CLAUDE.md index 7407001..8c354b0 100644 --- a/PushAndPull/CLAUDE.md +++ b/PushAndPull/CLAUDE.md @@ -62,3 +62,5 @@ mcp__context7__query-docs(libraryId: "/stackexchange/stackexchange.redis", query - `.claude/rules/testing.md` — test project structure, naming conventions, Moq patterns - `.claude/rules/flows.md` — Mermaid sequence diagrams for each API endpoint - `.claude/rules/verify.md` — build-and-verify workflow (auto build + test after every C# code change) +- `.claude/rules/ask-user.md` — when to use AskUserQuestion (always applied) +- `.claude/rules/test-fixer-trigger.md` — when to proactively offer the test-fixer agent (always applied) From ea42f9de7e1d02639f3cc06ee1a8366ac5055af9 Mon Sep 17 00:00:00 2001 From: Sean-mn Date: Mon, 6 Apr 2026 00:36:45 +0900 Subject: [PATCH 2/3] =?UTF-8?q?update:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BC=80=EC=9D=B4=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PushAndPull.Test/Domain/Auth/UserTests.cs | 16 ++++++ .../PushAndPull.Test/Domain/Room/RoomTests.cs | 16 ++++++ .../Service/Auth/LoginServiceTests.cs | 52 +++++++++++++++++++ .../Service/Room/GetRoomServiceTests.cs | 8 +-- .../Service/Room/JoinRoomServiceTests.cs | 44 ++++++++++++++++ 5 files changed, 133 insertions(+), 3 deletions(-) diff --git a/PushAndPull/PushAndPull.Test/Domain/Auth/UserTests.cs b/PushAndPull/PushAndPull.Test/Domain/Auth/UserTests.cs index 4875659..9d3a42e 100644 --- a/PushAndPull/PushAndPull.Test/Domain/Auth/UserTests.cs +++ b/PushAndPull/PushAndPull.Test/Domain/Auth/UserTests.cs @@ -40,6 +40,22 @@ public void It_ThrowsArgumentException(string invalidNickname) } } + public class WhenUpdatingNicknameWithNull + { + private readonly User _user; + + public WhenUpdatingNicknameWithNull() + { + _user = new User(76561198000000006UL, "SomePlayer"); + } + + [Fact] + public void It_ThrowsArgumentException() + { + Assert.Throws(() => _user.UpdateNickname(null!)); + } + } + public class WhenUpdatingLastLoginTime { private readonly User _user; diff --git a/PushAndPull/PushAndPull.Test/Domain/Room/RoomTests.cs b/PushAndPull/PushAndPull.Test/Domain/Room/RoomTests.cs index a9ebeab..ea40fee 100644 --- a/PushAndPull/PushAndPull.Test/Domain/Room/RoomTests.cs +++ b/PushAndPull/PushAndPull.Test/Domain/Room/RoomTests.cs @@ -119,5 +119,21 @@ public void It_StartsWithActiveStatus() Assert.Equal(RoomStatus.Active, room.Status); } + + [Fact] + public void It_SetsMaxPlayerToTwo() + { + var room = new PushAndPull.Domain.Room.Entity.Room("ROOM07", "Two-Player Room", 777UL, 76561198000000001UL, false, null); + + Assert.Equal(2, room.MaxPlayers); + } + + [Fact] + public void It_SetsExpiresAtToNull() + { + var room = new PushAndPull.Domain.Room.Entity.Room("ROOM08", "Fresh Room", 888UL, 76561198000000001UL, false, null); + + Assert.Null(room.ExpiresAt); + } } } diff --git a/PushAndPull/PushAndPull.Test/Service/Auth/LoginServiceTests.cs b/PushAndPull/PushAndPull.Test/Service/Auth/LoginServiceTests.cs index 6f04e25..a2f249f 100644 --- a/PushAndPull/PushAndPull.Test/Service/Auth/LoginServiceTests.cs +++ b/PushAndPull/PushAndPull.Test/Service/Auth/LoginServiceTests.cs @@ -1,5 +1,6 @@ using Moq; using PushAndPull.Domain.Auth.Entity; +using PushAndPull.Domain.Auth.Exception; using PushAndPull.Domain.Auth.Repository.Interface; using PushAndPull.Domain.Auth.Service; using PushAndPull.Domain.Auth.Service.Interface; @@ -66,6 +67,57 @@ public async Task It_DoesNotCallUpdateUser() } } + public class WhenAFamilySharingUserTriesToLogIn + { + private readonly Mock _validatorMock = new(); + private readonly Mock _sessionServiceMock = new(); + private readonly Mock _userRepositoryMock = new(); + private readonly LoginService _sut; + + private const string Ticket = "family-ticket"; + private const string Nickname = "FamilyPlayer"; + private const ulong PlayerSteamId = 76561198000000099UL; + private const ulong OwnerSteamId = 76561198000000001UL; + + public WhenAFamilySharingUserTriesToLogIn() + { + _validatorMock + .Setup(v => v.ValidateAsync(Ticket)) + .ReturnsAsync(new AuthTicketValidationResult(PlayerSteamId, OwnerSteamId, false, false)); + + _sut = new LoginService(_validatorMock.Object, _sessionServiceMock.Object, _userRepositoryMock.Object); + } + + [Fact] + public async Task It_ThrowsFamilySharingNotAllowedException() + { + await Assert.ThrowsAsync( + () => _sut.ExecuteAsync(new LoginCommand(Ticket, Nickname))); + } + + [Fact] + public async Task It_DoesNotCreateASession() + { + await Assert.ThrowsAsync( + () => _sut.ExecuteAsync(new LoginCommand(Ticket, Nickname))); + + _sessionServiceMock.Verify( + s => s.CreateAsync(It.IsAny(), It.IsAny()), + Times.Never); + } + + [Fact] + public async Task It_DoesNotAccessTheUserRepository() + { + await Assert.ThrowsAsync( + () => _sut.ExecuteAsync(new LoginCommand(Ticket, Nickname))); + + _userRepositoryMock.Verify( + r => r.GetBySteamIdAsync(It.IsAny(), It.IsAny()), + Times.Never); + } + } + public class WhenAnExistingUserLogsInAgain { private readonly Mock _validatorMock = new(); diff --git a/PushAndPull/PushAndPull.Test/Service/Room/GetRoomServiceTests.cs b/PushAndPull/PushAndPull.Test/Service/Room/GetRoomServiceTests.cs index b199345..9f4f60c 100644 --- a/PushAndPull/PushAndPull.Test/Service/Room/GetRoomServiceTests.cs +++ b/PushAndPull/PushAndPull.Test/Service/Room/GetRoomServiceTests.cs @@ -19,11 +19,13 @@ public WhenAnEmptyRoomCodeIsProvided() _sut = new GetRoomService(_roomRepositoryMock.Object); } - [Fact] - public async Task It_ThrowsArgumentException() + [Theory] + [InlineData("")] + [InlineData(null)] + public async Task It_ThrowsArgumentException(string? roomCode) { await Assert.ThrowsAsync( - () => _sut.ExecuteAsync(new GetRoomCommand(""))); + () => _sut.ExecuteAsync(new GetRoomCommand(roomCode!))); } } diff --git a/PushAndPull/PushAndPull.Test/Service/Room/JoinRoomServiceTests.cs b/PushAndPull/PushAndPull.Test/Service/Room/JoinRoomServiceTests.cs index 1b03f11..d55e557 100644 --- a/PushAndPull/PushAndPull.Test/Service/Room/JoinRoomServiceTests.cs +++ b/PushAndPull/PushAndPull.Test/Service/Room/JoinRoomServiceTests.cs @@ -264,4 +264,48 @@ public async Task It_DoesNotThrowAnyException() await _sut.ExecuteAsync(new JoinRoomCommand(RoomCode, null)); } } + + public class WhenCorrectPasswordIsProvidedForAPrivateRoom + { + private readonly Mock _roomRepositoryMock = new(); + private readonly Mock _passwordHasherMock = new(); + private readonly JoinRoomService _sut; + + private const string RoomCode = "PRIV03"; + private const string CorrectPassword = "correct-password"; + private const string StoredHash = "correct-hash"; + + public WhenCorrectPasswordIsProvidedForAPrivateRoom() + { + var privateRoom = new EntityRoom(RoomCode, "Private Room", 777UL, 76561198000000001UL, true, StoredHash); + + _roomRepositoryMock + .Setup(r => r.GetAsync(RoomCode, It.IsAny())) + .ReturnsAsync(privateRoom); + + _passwordHasherMock + .Setup(h => h.Verify(CorrectPassword, StoredHash)) + .Returns(true); + + _roomRepositoryMock + .Setup(r => r.IncrementPlayerCountAsync(RoomCode, It.IsAny())) + .ReturnsAsync(true); + + _sut = new JoinRoomService(_roomRepositoryMock.Object, _passwordHasherMock.Object); + } + + [Fact] + public async Task It_CallsIncrementPlayerCount() + { + await _sut.ExecuteAsync(new JoinRoomCommand(RoomCode, CorrectPassword)); + + _roomRepositoryMock.Verify(r => r.IncrementPlayerCountAsync(RoomCode, It.IsAny()), Times.Once); + } + + [Fact] + public async Task It_DoesNotThrowAnyException() + { + await _sut.ExecuteAsync(new JoinRoomCommand(RoomCode, CorrectPassword)); + } + } } From 332f2579cfa11f7bfbc14e8ebc2dff4b259bd83e Mon Sep 17 00:00:00 2001 From: Sean-mn Date: Mon, 6 Apr 2026 00:47:30 +0900 Subject: [PATCH 3/3] =?UTF-8?q?chore:=20=ED=95=98=EB=93=9C=EC=BD=94?= =?UTF-8?q?=EB=94=A9=20=EC=A0=88=EB=8C=80=20=EA=B2=BD=EB=A1=9C=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=EB=A1=9C=20=ED=99=98=EA=B2=BD=20=EC=9D=B4=EC=8B=9D?= =?UTF-8?q?=EC=84=B1=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PushAndPull/.claude/agents/test-fixer.md | 8 ++++---- PushAndPull/.claude/settings.local.json | 5 +---- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/PushAndPull/.claude/agents/test-fixer.md b/PushAndPull/.claude/agents/test-fixer.md index 3123185..89ece03 100644 --- a/PushAndPull/.claude/agents/test-fixer.md +++ b/PushAndPull/.claude/agents/test-fixer.md @@ -16,7 +16,7 @@ Your job is to keep the test suite in `PushAndPull.Test/` accurate and green aft - Production code: `PushAndPull/` - Test code: `PushAndPull.Test/` -- Working directory: `/mnt/c/Users/USER/Documents/GitHub/Push-Pull-server/PushAndPull` +- Working directory: `.` ## Step 1: Understand What Changed @@ -41,11 +41,11 @@ Read each changed file to understand: ## Step 2: Run Build and Tests ```bash -cd /mnt/c/Users/USER/Documents/GitHub/Push-Pull-server/PushAndPull && dotnet build --no-restore 2>&1 +dotnet build --no-restore 2>&1 ``` ```bash -cd /mnt/c/Users/USER/Documents/GitHub/Push-Pull-server/PushAndPull && dotnet test --no-build 2>&1 +dotnet test --no-build 2>&1 ``` Collect all errors and failures. Categorize them: @@ -120,7 +120,7 @@ public class ExampleServiceTests After all changes, run the full test suite: ```bash -cd /mnt/c/Users/USER/Documents/GitHub/Push-Pull-server/PushAndPull && dotnet build --no-restore 2>&1 && dotnet test --no-build 2>&1 +dotnet build --no-restore 2>&1 && dotnet test --no-build 2>&1 ``` Repeat Steps 3–5 until the build succeeds and all tests pass. diff --git a/PushAndPull/.claude/settings.local.json b/PushAndPull/.claude/settings.local.json index 79bfa3c..b3dcc89 100644 --- a/PushAndPull/.claude/settings.local.json +++ b/PushAndPull/.claude/settings.local.json @@ -33,10 +33,7 @@ "Bash(cmd.exe:*)", "Bash(dotnet --list-sdks)", "Bash(dotnet list:*)", - "Bash(~/.dotnet/dotnet build:*)", - "Bash(/home/seanyee1227/.dotnet/dotnet build:*)", - "Bash(/home/seanyee1227/.dotnet/dotnet test:*)", - "Bash(export PATH=\"$PATH:$HOME/.dotnet\")" + "Bash(~/.dotnet/dotnet build:*)" ] } }