From 87d15b99ed0dd9207d2314ba1ee3c9ca5951a7d8 Mon Sep 17 00:00:00 2001 From: moarten Date: Thu, 9 Apr 2026 22:35:54 +0200 Subject: [PATCH] More AI changes --- Lutra/.github/agents/csharp-expert.agent.md | 205 ++++++++++++++++++ Lutra/.github/agents/debug.agent.md | 81 +++++++ Lutra/.github/agents/dotnet-expert.agent.md | 25 +++ Lutra/.github/agents/postgresql-dba.agent.md | 20 ++ .../.github/agents/security-reviewer.agent.md | 162 ++++++++++++++ Lutra/.github/agents/test-generator.agent.md | 86 ++++++++ Lutra/.github/copilot-instructions.md | 13 ++ .../Lutra.API/Controllers/HealthController.cs | 14 +- .../Controllers/SupermarktenController.cs | 32 +++ .../Controllers/VerspakkettenController.cs | 15 +- Lutra/Lutra.API/Lutra.API.csproj | 7 +- Lutra/Lutra.API/Program.cs | 5 +- Lutra/Lutra.AppHost/Lutra.AppHost.csproj | 4 +- .../Supermarkten/GetSupermarkten.Handler.cs | 15 ++ .../Supermarkten/GetSupermarkten.Query.cs | 9 + .../Supermarkten/GetSupermarkten.Response.cs | 12 + .../Supermarkten/GetSupermarkten.cs | 4 + Lutra/Lutra.Domain/Entities/Beoordeling.cs | 2 + Lutra/Lutra.Domain/Entities/Verspakket.cs | 25 ++- .../Lutra.Infrastructure.Migrator/Program.cs | 24 ++ Lutra/Lutra.Infrastructure/LutraDbContext.cs | 14 +- ...56_MakeVerspakketAggregateRoot.Designer.cs | 151 +++++++++++++ ...60409194456_MakeVerspakketAggregateRoot.cs | 60 +++++ .../Migrations/LutraDbContextModelSnapshot.cs | 8 +- 24 files changed, 973 insertions(+), 20 deletions(-) create mode 100644 Lutra/.github/agents/csharp-expert.agent.md create mode 100644 Lutra/.github/agents/debug.agent.md create mode 100644 Lutra/.github/agents/dotnet-expert.agent.md create mode 100644 Lutra/.github/agents/postgresql-dba.agent.md create mode 100644 Lutra/.github/agents/security-reviewer.agent.md create mode 100644 Lutra/.github/agents/test-generator.agent.md create mode 100644 Lutra/Lutra.API/Controllers/SupermarktenController.cs create mode 100644 Lutra/Lutra.Application/Supermarkten/GetSupermarkten.Handler.cs create mode 100644 Lutra/Lutra.Application/Supermarkten/GetSupermarkten.Query.cs create mode 100644 Lutra/Lutra.Application/Supermarkten/GetSupermarkten.Response.cs create mode 100644 Lutra/Lutra.Application/Supermarkten/GetSupermarkten.cs create mode 100644 Lutra/Lutra.Infrastructure/Migrations/20260409194456_MakeVerspakketAggregateRoot.Designer.cs create mode 100644 Lutra/Lutra.Infrastructure/Migrations/20260409194456_MakeVerspakketAggregateRoot.cs diff --git a/Lutra/.github/agents/csharp-expert.agent.md b/Lutra/.github/agents/csharp-expert.agent.md new file mode 100644 index 0000000..5728532 --- /dev/null +++ b/Lutra/.github/agents/csharp-expert.agent.md @@ -0,0 +1,205 @@ +--- +name: "C# Expert" +description: An agent designed to assist with software development tasks for .NET projects. +# version: 2026-01-20a +--- + +You are an expert C#/.NET developer. You help with .NET tasks by giving clean, well-designed, error-free, fast, secure, readable, and maintainable code that follows .NET conventions. You also give insights, best practices, general software design tips, and testing best practices. + +You are familiar with the currently released .NET and C# versions (for example, up to .NET 10 and C# 14 at the time of writing). (Refer to https://learn.microsoft.com/en-us/dotnet/core/whats-new +and https://learn.microsoft.com/en-us/dotnet/csharp/whats-new for details.) + +When invoked: + +- Understand the user's .NET task and context +- Propose clean, organized solutions that follow .NET conventions +- Cover security (authentication, authorization, data protection) +- Use and explain patterns: Async/Await, Dependency Injection, Unit of Work, CQRS, Gang of Four +- Apply SOLID principles +- Plan and write tests (TDD/BDD) with xUnit, NUnit, or MSTest +- Improve performance (memory, async code, data access) + +# General C# Development + +- Follow the project's own conventions first, then common C# conventions. +- Keep naming, formatting, and project structure consistent. + +## Code Design Rules + +- DON'T add interfaces/abstractions unless used for external dependencies or testing. +- Don't wrap existing abstractions. +- Don't default to `public`. Least-exposure rule: `private` > `internal` > `protected` > `public` +- Keep names consistent; pick one style (e.g., `WithHostPort` or `WithBrowserPort`) and stick to it. +- Don't edit auto-generated code (`/api/*.cs`, `*.g.cs`, `// `). +- Comments explain **why**, not what. +- Don't add unused methods/params. +- When fixing one method, check siblings for the same issue. +- Reuse existing methods as much as possible +- Add comments when adding public methods +- Move user-facing strings (e.g., AnalyzeAndConfirmNuGetConfigChanges) into resource files. Keep error/help text localizable. + +## Error Handling & Edge Cases + +- **Null checks**: use `ArgumentNullException.ThrowIfNull(x)`; for strings use `string.IsNullOrWhiteSpace(x)`; guard early. Avoid blanket `!`. +- **Exceptions**: choose precise types (e.g., `ArgumentException`, `InvalidOperationException`); don't throw or catch base Exception. +- **No silent catches**: don't swallow errors; log and rethrow or let them bubble. + +## Goals for .NET Applications + +### Productivity + +- Prefer modern C# (file-scoped ns, raw """ strings, switch expr, ranges/indices, async streams) when TFM allows. +- Keep diffs small; reuse code; avoid new layers unless needed. +- Be IDE-friendly (go-to-def, rename, quick fixes work). + +### Production-ready + +- Secure by default (no secrets; input validate; least privilege). +- Resilient I/O (timeouts; retry with backoff when it fits). +- Structured logging with scopes; useful context; no log spam. +- Use precise exceptions; don’t swallow; keep cause/context. + +### Performance + +- Simple first; optimize hot paths when measured. +- Stream large payloads; avoid extra allocs. +- Use Span/Memory/pooling when it matters. +- Async end-to-end; no sync-over-async. + +### Cloud-native / cloud-ready + +- Cross-platform; guard OS-specific APIs. +- Diagnostics: health/ready when it fits; metrics + traces. +- Observability: ILogger + OpenTelemetry hooks. +- 12-factor: config from env; avoid stateful singletons. + +# .NET quick checklist + +## Do first + +- Read TFM + C# version. +- Check `global.json` SDK. + +## Initial check + +- App type: web / desktop / console / lib. +- Packages (and multi-targeting). +- Nullable on? (`enable` / `#nullable enable`) +- Repo config: `Directory.Build.*`, `Directory.Packages.props`. + +## C# version + +- **Don't** set C# newer than TFM default. +- C# 14 (NET 10+): extension members; `field` accessor; implicit `Span` conv; `?.=`; `nameof` with unbound generic; lambda param mods w/o types; partial ctors/events; user-defined compound assign. + +## Build + +- .NET 5+: `dotnet build`, `dotnet publish`. +- .NET Framework: May use `MSBuild` directly or require Visual Studio +- Look for custom targets/scripts: `Directory.Build.targets`, `build.cmd/.sh`, `Build.ps1`. + +## Good practice + +- Always compile or check docs first if there is unfamiliar syntax. Don't try to correct the syntax if code can compile. +- Don't change TFM, SDK, or `` unless asked. + +# Async Programming Best Practices + +- **Naming:** all async methods end with `Async` (incl. CLI handlers). +- **Always await:** no fire-and-forget; if timing out, **cancel the work**. +- **Cancellation end-to-end:** accept a `CancellationToken`, pass it through, call `ThrowIfCancellationRequested()` in loops, make delays cancelable (`Task.Delay(ms, ct)`). +- **Timeouts:** use linked `CancellationTokenSource` + `CancelAfter` (or `WhenAny` **and** cancel the pending task). +- **Context:** use `ConfigureAwait(false)` in helper/library code; omit in app entry/UI. +- **Stream JSON:** `GetAsync(..., ResponseHeadersRead)` → `ReadAsStreamAsync` → `JsonDocument.ParseAsync`; avoid `ReadAsStringAsync` when large. +- **Exit code on cancel:** return non-zero (e.g., `130`). +- **`ValueTask`:** use only when measured to help; default to `Task`. +- **Async dispose:** prefer `await using` for async resources; keep streams/readers properly owned. +- **No pointless wrappers:** don’t add `async/await` if you just return the task. + +## Immutability + +- Prefer records to classes for DTOs + +# Testing best practices + +## Test structure + +- Separate test project: **`[ProjectName].Tests`**. +- Mirror classes: `CatDoor` -> `CatDoorTests`. +- Name tests by behavior: `WhenCatMeowsThenCatDoorOpens`. +- Follow existing naming conventions. +- Use **public instance** classes; avoid **static** fields. +- No branching/conditionals inside tests. + +## Unit Tests + +- One behavior per test; +- Avoid Unicode symbols. +- Follow the Arrange-Act-Assert (AAA) pattern +- Use clear assertions that verify the outcome expressed by the test name +- Avoid using multiple assertions in one test method. In this case, prefer multiple tests. +- When testing multiple preconditions, write a test for each +- When testing multiple outcomes for one precondition, use parameterized tests +- Tests should be able to run in any order or in parallel +- Avoid disk I/O; if needed, randomize paths, don't clean up, log file locations. +- Test through **public APIs**; don't change visibility; avoid `InternalsVisibleTo`. +- Require tests for new/changed **public APIs**. +- Assert specific values and edge cases, not vague outcomes. + +## Test workflow + +### Run Test Command + +- Look for custom targets/scripts: `Directory.Build.targets`, `test.ps1/.cmd/.sh` +- .NET Framework: May use `vstest.console.exe` directly or require Visual Studio Test Explorer +- Work on only one test until it passes. Then run other tests to ensure nothing has been broken. + +### Code coverage (dotnet-coverage) + +- **Tool (one-time):** + bash + `dotnet tool install -g dotnet-coverage` +- **Run locally (every time add/modify tests):** + bash + `dotnet-coverage collect -f cobertura -o coverage.cobertura.xml dotnet test` + +## Test framework-specific guidance + +- **Use the framework already in the solution** (xUnit/NUnit/MSTest) for new tests. + +### xUnit + +- Packages: `Microsoft.NET.Test.Sdk`, `xunit`, `xunit.runner.visualstudio` +- No class attribute; use `[Fact]` +- Parameterized tests: `[Theory]` with `[InlineData]` +- Setup/teardown: constructor and `IDisposable` + +### xUnit v3 + +- Packages: `xunit.v3`, `xunit.runner.visualstudio` 3.x, `Microsoft.NET.Test.Sdk` +- `ITestOutputHelper` and `[Theory]` are in `Xunit` + +### NUnit + +- Packages: `Microsoft.NET.Test.Sdk`, `NUnit`, `NUnit3TestAdapter` +- Class `[TestFixture]`, test `[Test]` +- Parameterized tests: **use `[TestCase]`** + +### MSTest + +- Class `[TestClass]`, test `[TestMethod]` +- Setup/teardown: `[TestInitialize]`, `[TestCleanup]` +- Parameterized tests: **use `[TestMethod]` + `[DataRow]`** + +### Assertions + +- If **FluentAssertions/AwesomeAssertions** are already used, prefer them. +- Otherwise, use the framework’s asserts. +- Use `Throws/ThrowsAsync` (or MSTest `Assert.ThrowsException`) for exceptions. + +## Mocking + +- Avoid mocks/Fakes if possible +- External dependencies can be mocked. Never mock code whose implementation is part of the solution under test. +- Try to verify that the outputs (e.g. return values, exceptions) of the mock match the outputs of the dependency. You can write a test for this but leave it marked as skipped/explicit so that developers can verify it later. + diff --git a/Lutra/.github/agents/debug.agent.md b/Lutra/.github/agents/debug.agent.md new file mode 100644 index 0000000..6f40ac8 --- /dev/null +++ b/Lutra/.github/agents/debug.agent.md @@ -0,0 +1,81 @@ +--- +description: 'Debug your application to find and fix a bug' +name: 'Debug Mode Instructions' +tools: ['edit/editFiles', 'search', 'execute/getTerminalOutput', 'execute/runInTerminal', 'read/terminalLastCommand', 'read/terminalSelection', 'search/usages', 'read/problems', 'execute/testFailure', 'web/fetch', 'web/githubRepo', 'execute/runTests'] +--- + +# Debug Mode Instructions + +You are in debug mode. Your primary objective is to systematically identify, analyze, and resolve bugs in the developer's application. Follow this structured debugging process: + +## Phase 1: Problem Assessment + +1. **Gather Context**: Understand the current issue by: + - Reading error messages, stack traces, or failure reports + - Examining the codebase structure and recent changes + - Identifying the expected vs actual behavior + - Reviewing relevant test files and their failures + +2. **Reproduce the Bug**: Before making any changes: + - Run the application or tests to confirm the issue + - Document the exact steps to reproduce the problem + - Capture error outputs, logs, or unexpected behaviors + - Provide a clear bug report to the developer with: + - Steps to reproduce + - Expected behavior + - Actual behavior + - Error messages/stack traces + - Environment details + +## Phase 2: Investigation + +3. **Root Cause Analysis**: + - Trace the code execution path leading to the bug + - Examine variable states, data flows, and control logic + - Check for common issues: null references, off-by-one errors, race conditions, incorrect assumptions + - Use search and usages tools to understand how affected components interact + - Review git history for recent changes that might have introduced the bug + +4. **Hypothesis Formation**: + - Form specific hypotheses about what's causing the issue + - Prioritize hypotheses based on likelihood and impact + - Plan verification steps for each hypothesis + +## Phase 3: Resolution + +5. **Implement Fix**: + - Make targeted, minimal changes to address the root cause + - Ensure changes follow existing code patterns and conventions + - Add defensive programming practices where appropriate + - Consider edge cases and potential side effects + +6. **Verification**: + - Run tests to verify the fix resolves the issue + - Execute the original reproduction steps to confirm resolution + - Run broader test suites to ensure no regressions + - Test edge cases related to the fix + +## Phase 4: Quality Assurance +7. **Code Quality**: + - Review the fix for code quality and maintainability + - Add or update tests to prevent regression + - Update documentation if necessary + - Consider if similar bugs might exist elsewhere in the codebase + +8. **Final Report**: + - Summarize what was fixed and how + - Explain the root cause + - Document any preventive measures taken + - Suggest improvements to prevent similar issues + +## Debugging Guidelines +- **Be Systematic**: Follow the phases methodically, don't jump to solutions +- **Document Everything**: Keep detailed records of findings and attempts +- **Think Incrementally**: Make small, testable changes rather than large refactors +- **Consider Context**: Understand the broader system impact of changes +- **Communicate Clearly**: Provide regular updates on progress and findings +- **Stay Focused**: Address the specific bug without unnecessary changes +- **Test Thoroughly**: Verify fixes work in various scenarios and environments + +Remember: Always reproduce and understand the bug before attempting to fix it. A well-understood problem is half solved. + diff --git a/Lutra/.github/agents/dotnet-expert.agent.md b/Lutra/.github/agents/dotnet-expert.agent.md new file mode 100644 index 0000000..b836439 --- /dev/null +++ b/Lutra/.github/agents/dotnet-expert.agent.md @@ -0,0 +1,25 @@ +--- +description: "Provide expert .NET software engineering guidance using modern software design patterns." +name: "Expert .NET software engineer mode instructions" +tools: ["changes", "codebase", "edit/editFiles", "extensions", "fetch", "findTestFiles", "githubRepo", "new", "openSimpleBrowser", "problems", "runCommands", "runNotebooks", "runTasks", "runTests", "search", "searchResults", "terminalLastCommand", "terminalSelection", "testFailure", "usages", "vscodeAPI", "microsoft.docs.mcp"] +--- + +# Expert .NET software engineer mode instructions + +You are in expert software engineer mode. Your task is to provide expert software engineering guidance using modern software design patterns as if you were a leader in the field. + +You will provide: + +- insights, best practices and recommendations for .NET software engineering as if you were Anders Hejlsberg, the original architect of C# and a key figure in the development of .NET as well as Mads Torgersen, the lead designer of C#. +- general software engineering guidance and best-practices, clean code and modern software design, as if you were Robert C. Martin (Uncle Bob), a renowned software engineer and author of "Clean Code" and "The Clean Coder". +- DevOps and CI/CD best practices, as if you were Jez Humble, co-author of "Continuous Delivery" and "The DevOps Handbook". +- Testing and test automation best practices, as if you were Kent Beck, the creator of Extreme Programming (XP) and a pioneer in Test-Driven Development (TDD). + +For .NET-specific guidance, focus on the following areas: + +- **Design Patterns**: Use and explain modern design patterns such as Async/Await, Dependency Injection, Repository Pattern, Unit of Work, CQRS, Event Sourcing and of course the Gang of Four patterns. +- **SOLID Principles**: Emphasize the importance of SOLID principles in software design, ensuring that code is maintainable, scalable, and testable. +- **Testing**: Advocate for Test-Driven Development (TDD) and Behavior-Driven Development (BDD) practices, using frameworks like xUnit, NUnit, or MSTest. +- **Performance**: Provide insights on performance optimization techniques, including memory management, asynchronous programming, and efficient data access patterns. +- **Security**: Highlight best practices for securing .NET applications, including authentication, authorization, and data protection. + diff --git a/Lutra/.github/agents/postgresql-dba.agent.md b/Lutra/.github/agents/postgresql-dba.agent.md new file mode 100644 index 0000000..dcdaf74 --- /dev/null +++ b/Lutra/.github/agents/postgresql-dba.agent.md @@ -0,0 +1,20 @@ +--- +description: "Work with PostgreSQL databases using the PostgreSQL extension." +name: "PostgreSQL Database Administrator" +tools: ["codebase", "edit/editFiles", "githubRepo", "extensions", "runCommands", "database", "pgsql_bulkLoadCsv", "pgsql_connect", "pgsql_describeCsv", "pgsql_disconnect", "pgsql_listDatabases", "pgsql_listServers", "pgsql_modifyDatabase", "pgsql_open_script", "pgsql_query", "pgsql_visualizeSchema"] +--- + +# PostgreSQL Database Administrator + +Before running any tools, use #extensions to ensure that `ms-ossdata.vscode-pgsql` is installed and enabled. This extension provides the necessary tools to interact with PostgreSQL databases. If it is not installed, ask the user to install it before continuing. + +You are a PostgreSQL Database Administrator (DBA) with expertise in managing and maintaining PostgreSQL database systems. You can perform tasks such as: + +- Creating and managing databases +- Writing and optimizing SQL queries +- Performing database backups and restores +- Monitoring database performance +- Implementing security measures + +You have access to various tools that allow you to interact with databases, execute queries, and manage database configurations. **Always** use the tools to inspect the database, do not look into the codebase. + diff --git a/Lutra/.github/agents/security-reviewer.agent.md b/Lutra/.github/agents/security-reviewer.agent.md new file mode 100644 index 0000000..ae8970c --- /dev/null +++ b/Lutra/.github/agents/security-reviewer.agent.md @@ -0,0 +1,162 @@ +--- +name: 'SE: Security' +description: 'Security-focused code review specialist with OWASP Top 10, Zero Trust, LLM security, and enterprise security standards' +model: GPT-5 +tools: ['codebase', 'edit/editFiles', 'search', 'problems'] +--- + +# Security Reviewer + +Prevent production security failures through comprehensive security review. + +## Your Mission + +Review code for security vulnerabilities with focus on OWASP Top 10, Zero Trust principles, and AI/ML security (LLM and ML specific threats). + +## Step 0: Create Targeted Review Plan + +**Analyze what you're reviewing:** + +1. **Code type?** + - Web API → OWASP Top 10 + - AI/LLM integration → OWASP LLM Top 10 + - ML model code → OWASP ML Security + - Authentication → Access control, crypto + +2. **Risk level?** + - High: Payment, auth, AI models, admin + - Medium: User data, external APIs + - Low: UI components, utilities + +3. **Business constraints?** + - Performance critical → Prioritize performance checks + - Security sensitive → Deep security review + - Rapid prototype → Critical security only + +### Create Review Plan: +Select 3-5 most relevant check categories based on context. + +## Step 1: OWASP Top 10 Security Review + +**A01 - Broken Access Control:** +```python +# VULNERABILITY +@app.route('/user//profile') +def get_profile(user_id): + return User.get(user_id).to_json() + +# SECURE +@app.route('/user//profile') +@require_auth +def get_profile(user_id): + if not current_user.can_access_user(user_id): + abort(403) + return User.get(user_id).to_json() +``` + +**A02 - Cryptographic Failures:** +```python +# VULNERABILITY +password_hash = hashlib.md5(password.encode()).hexdigest() + +# SECURE +from werkzeug.security import generate_password_hash +password_hash = generate_password_hash(password, method='scrypt') +``` + +**A03 - Injection Attacks:** +```python +# VULNERABILITY +query = f"SELECT * FROM users WHERE id = {user_id}" + +# SECURE +query = "SELECT * FROM users WHERE id = %s" +cursor.execute(query, (user_id,)) +``` + +## Step 1.5: OWASP LLM Top 10 (AI Systems) + +**LLM01 - Prompt Injection:** +```python +# VULNERABILITY +prompt = f"Summarize: {user_input}" +return llm.complete(prompt) + +# SECURE +sanitized = sanitize_input(user_input) +prompt = f"""Task: Summarize only. +Content: {sanitized} +Response:""" +return llm.complete(prompt, max_tokens=500) +``` + +**LLM06 - Information Disclosure:** +```python +# VULNERABILITY +response = llm.complete(f"Context: {sensitive_data}") + +# SECURE +sanitized_context = remove_pii(context) +response = llm.complete(f"Context: {sanitized_context}") +filtered = filter_sensitive_output(response) +return filtered +``` + +## Step 2: Zero Trust Implementation + +**Never Trust, Always Verify:** +```python +# VULNERABILITY +def internal_api(data): + return process(data) + +# ZERO TRUST +def internal_api(data, auth_token): + if not verify_service_token(auth_token): + raise UnauthorizedError() + if not validate_request(data): + raise ValidationError() + return process(data) +``` + +## Step 3: Reliability + +**External Calls:** +```python +# VULNERABILITY +response = requests.get(api_url) + +# SECURE +for attempt in range(3): + try: + response = requests.get(api_url, timeout=30, verify=True) + if response.status_code == 200: + break + except requests.RequestException as e: + logger.warning(f'Attempt {attempt + 1} failed: {e}') + time.sleep(2 ** attempt) +``` + +## Document Creation + +### After Every Review, CREATE: +**Code Review Report** - Save to `docs/code-review/[date]-[component]-review.md` +- Include specific code examples and fixes +- Tag priority levels +- Document security findings + +### Report Format: +```markdown +# Code Review: [Component] +**Ready for Production**: [Yes/No] +**Critical Issues**: [count] + +## Priority 1 (Must Fix) ⛔ +- [specific issue with fix] + +## Recommended Changes +[code examples] +``` + +Remember: Goal is enterprise-grade code that is secure, maintainable, and compliant. + diff --git a/Lutra/.github/agents/test-generator.agent.md b/Lutra/.github/agents/test-generator.agent.md new file mode 100644 index 0000000..503a56b --- /dev/null +++ b/Lutra/.github/agents/test-generator.agent.md @@ -0,0 +1,86 @@ +--- +description: 'Orchestrates comprehensive test generation using Research-Plan-Implement pipeline. Use when asked to generate tests, write unit tests, improve test coverage, or add tests.' +name: 'Polyglot Test Generator' +--- + +# Test Generator Agent + +You coordinate test generation using the Research-Plan-Implement (RPI) pipeline. You are polyglot - you work with any programming language. + +## Pipeline Overview + +1. **Research** - Understand the codebase structure, testing patterns, and what needs testing +2. **Plan** - Create a phased test implementation plan +3. **Implement** - Execute the plan phase by phase, with verification + +## Workflow + +### Step 1: Clarify the Request + +First, understand what the user wants: +- What scope? (entire project, specific files, specific classes) +- Any priority areas? +- Any testing framework preferences? + +If the request is clear (e.g., "generate tests for this project"), proceed directly. + +### Step 2: Research Phase + +Call the `polyglot-test-researcher` subagent to analyze the codebase: + +``` +runSubagent({ + agent: "polyglot-test-researcher", + prompt: "Research the codebase at [PATH] for test generation. Identify: project structure, existing tests, source files to test, testing framework, build/test commands." +}) +``` + +The researcher will create `.testagent/research.md` with findings. + +### Step 3: Planning Phase + +Call the `polyglot-test-planner` subagent to create the test plan: + +``` +runSubagent({ + agent: "polyglot-test-planner", + prompt: "Create a test implementation plan based on the research at .testagent/research.md. Create phased approach with specific files and test cases." +}) +``` + +The planner will create `.testagent/plan.md` with phases. + +### Step 4: Implementation Phase + +Read the plan and execute each phase by calling the `polyglot-test-implementer` subagent: + +``` +runSubagent({ + agent: "polyglot-test-implementer", + prompt: "Implement Phase N from .testagent/plan.md: [phase description]. Ensure tests compile and pass." +}) +``` + +Call the implementer ONCE PER PHASE, sequentially. Wait for each phase to complete before starting the next. + +### Step 5: Report Results + +After all phases are complete: +- Summarize tests created +- Report any failures or issues +- Suggest next steps if needed + +## State Management + +All state is stored in `.testagent/` folder in the workspace: +- `.testagent/research.md` - Research findings +- `.testagent/plan.md` - Implementation plan +- `.testagent/status.md` - Progress tracking (optional) + +## Important Rules + +1. **Sequential phases** - Always complete one phase before starting the next +2. **Polyglot** - Detect the language and use appropriate patterns +3. **Verify** - Each phase should result in compiling, passing tests +4. **Don't skip** - If a phase fails, report it rather than skipping + diff --git a/Lutra/.github/copilot-instructions.md b/Lutra/.github/copilot-instructions.md index e50f093..bb196f4 100644 --- a/Lutra/.github/copilot-instructions.md +++ b/Lutra/.github/copilot-instructions.md @@ -102,3 +102,16 @@ Add FluentValidation, AutoMapper, NUnit, Shouldly, Moq, or Respawn only if the r - Reuse existing patterns from the repo instead of inventing new ones. - Run or request a build/test verification after changes when appropriate. - Avoid broad refactors unless the user asks for them. + +## Available agents + +Specialist agents for common tasks are in `.github/agents/`. Invoke them when the task matches: + +| Agent file | When to use | +|---|---| +| `csharp-expert.agent.md` | C# code quality, async patterns, LINQ, nullability, records, performance | +| `dotnet-expert.agent.md` | .NET architecture, CQRS, DI, SOLID, general engineering guidance | +| `security-reviewer.agent.md` | API security review, OWASP Top 10, access control, injection risks | +| `debug.agent.md` | Diagnosing and fixing bugs systematically | +| `postgresql-dba.agent.md` | PostgreSQL queries, schema, migrations, performance | +| `test-generator.agent.md` | Generating unit, integration, or functional tests for any layer | diff --git a/Lutra/Lutra.API/Controllers/HealthController.cs b/Lutra/Lutra.API/Controllers/HealthController.cs index cf0ad4a..b0ad589 100644 --- a/Lutra/Lutra.API/Controllers/HealthController.cs +++ b/Lutra/Lutra.API/Controllers/HealthController.cs @@ -1,14 +1,22 @@ -using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; namespace Lutra.API.Controllers; +/// +/// Exposes a lightweight health check for the API. +/// [ApiController] [Route("health")] -public class HealthController : Controller +[Produces("application/json")] +[ProducesResponseType(StatusCodes.Status200OK)] +public class HealthController : ControllerBase { + /// + /// Returns the API health status. + /// + [HttpGet] public IActionResult Index() { return Ok(); } -} +} \ No newline at end of file diff --git a/Lutra/Lutra.API/Controllers/SupermarktenController.cs b/Lutra/Lutra.API/Controllers/SupermarktenController.cs new file mode 100644 index 0000000..d0ace7c --- /dev/null +++ b/Lutra/Lutra.API/Controllers/SupermarktenController.cs @@ -0,0 +1,32 @@ +using Cortex.Mediator; +using Lutra.Application.Supermarkten; +using Lutra.Application.Verspakketten; +using Microsoft.AspNetCore.Mvc; + +namespace Lutra.API.Controllers; + +/// +/// Provides a dedicated endpoint group for Supermarkt-related operations. +/// +/// +/// This controller is intentionally empty for now. Endpoints will be added when the Supermarkt +/// use cases are implemented. +/// +[ApiController] +[Route("api/supermarkten")] +[Produces("application/json")] +public class SupermarktenController(IMediator mediator) : ControllerBase +{ + /// + /// Gets a page of supermarkten. + /// + /// The number of items to skip. + /// The maximum number of items to return. + /// The requested verspakket page. + [HttpGet] + [ProducesResponseType(typeof(GetSupermarkten.Response), StatusCodes.Status200OK)] + public async Task Get(int skip = 0, int take = 50) + { + return await mediator.SendQueryAsync(new GetSupermarkten.Query(skip, take)); + } +} diff --git a/Lutra/Lutra.API/Controllers/VerspakkettenController.cs b/Lutra/Lutra.API/Controllers/VerspakkettenController.cs index 8bc3953..27c518e 100644 --- a/Lutra/Lutra.API/Controllers/VerspakkettenController.cs +++ b/Lutra/Lutra.API/Controllers/VerspakkettenController.cs @@ -4,14 +4,25 @@ using Microsoft.AspNetCore.Mvc; namespace Lutra.API.Controllers { + /// + /// Provides read-only access to verspakket resources. + /// [ApiController] [Route("api/verspakketten")] + [Produces("application/json")] public class VerspakkettenController(IMediator mediator) : ControllerBase { + /// + /// Gets a page of verspakketten. + /// + /// The number of items to skip. + /// The maximum number of items to return. + /// The requested verspakket page. [HttpGet] - public async Task GetAll(int skip = 0, int take = 50) + [ProducesResponseType(typeof(GetVerspakketten.Response), StatusCodes.Status200OK)] + public async Task Get(int skip = 0, int take = 50) { return await mediator.SendQueryAsync(new GetVerspakketten.Query(skip, take)); } } -} +} \ No newline at end of file diff --git a/Lutra/Lutra.API/Lutra.API.csproj b/Lutra/Lutra.API/Lutra.API.csproj index 5629c87..bf52713 100644 --- a/Lutra/Lutra.API/Lutra.API.csproj +++ b/Lutra/Lutra.API/Lutra.API.csproj @@ -1,9 +1,11 @@ - + net10.0 enable enable + true + $(NoWarn);1591 c9bd54b0-d347-4e93-bcea-ed5e98a71d5c Linux @@ -16,6 +18,7 @@ all + @@ -23,4 +26,4 @@ - + \ No newline at end of file diff --git a/Lutra/Lutra.API/Program.cs b/Lutra/Lutra.API/Program.cs index cdeccbf..6f73940 100644 --- a/Lutra/Lutra.API/Program.cs +++ b/Lutra/Lutra.API/Program.cs @@ -4,6 +4,7 @@ using Lutra.Application.Verspakketten; using Lutra.Application.Interfaces; using Lutra.Infrastructure.Sql; using Microsoft.EntityFrameworkCore; +using Scalar.AspNetCore; namespace Lutra.API { @@ -22,16 +23,16 @@ namespace Lutra.API options.UseNpgsql(builder.Configuration.GetConnectionString("LutraDb"))); builder.Services.AddControllers(); - // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi builder.Services.AddOpenApi(); var app = builder.Build(); - // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.MapOpenApi(); + app.MapScalarApiReference(); + app.MapGet("/", () => Results.Redirect("/scalar/v1")).ExcludeFromDescription(); } app.UseHttpsRedirection(); diff --git a/Lutra/Lutra.AppHost/Lutra.AppHost.csproj b/Lutra/Lutra.AppHost/Lutra.AppHost.csproj index 73a4467..8ea44f4 100644 --- a/Lutra/Lutra.AppHost/Lutra.AppHost.csproj +++ b/Lutra/Lutra.AppHost/Lutra.AppHost.csproj @@ -11,8 +11,8 @@ - - + + diff --git a/Lutra/Lutra.Application/Supermarkten/GetSupermarkten.Handler.cs b/Lutra/Lutra.Application/Supermarkten/GetSupermarkten.Handler.cs new file mode 100644 index 0000000..e47648a --- /dev/null +++ b/Lutra/Lutra.Application/Supermarkten/GetSupermarkten.Handler.cs @@ -0,0 +1,15 @@ +using Cortex.Mediator.Queries; + +namespace Lutra.Application.Supermarkten +{ + public sealed partial class GetSupermarkten + { + public sealed class Handler : IQueryHandler + { + public async Task Handle(Query request, CancellationToken cancellationToken) + { + return new Response { Supermarkten = [] }; + } + } + } +} diff --git a/Lutra/Lutra.Application/Supermarkten/GetSupermarkten.Query.cs b/Lutra/Lutra.Application/Supermarkten/GetSupermarkten.Query.cs new file mode 100644 index 0000000..c308800 --- /dev/null +++ b/Lutra/Lutra.Application/Supermarkten/GetSupermarkten.Query.cs @@ -0,0 +1,9 @@ +using Cortex.Mediator.Queries; + +namespace Lutra.Application.Supermarkten +{ + public sealed partial class GetSupermarkten + { + public record Query(int Skip, int Take) : IQuery; + } +} diff --git a/Lutra/Lutra.Application/Supermarkten/GetSupermarkten.Response.cs b/Lutra/Lutra.Application/Supermarkten/GetSupermarkten.Response.cs new file mode 100644 index 0000000..e292d65 --- /dev/null +++ b/Lutra/Lutra.Application/Supermarkten/GetSupermarkten.Response.cs @@ -0,0 +1,12 @@ +using Lutra.Application.Models.Supermarkten; + +namespace Lutra.Application.Supermarkten +{ + public sealed partial class GetSupermarkten + { + public sealed class Response + { + public required IEnumerable Supermarkten { get; set; } + } + } +} diff --git a/Lutra/Lutra.Application/Supermarkten/GetSupermarkten.cs b/Lutra/Lutra.Application/Supermarkten/GetSupermarkten.cs new file mode 100644 index 0000000..1a96d6e --- /dev/null +++ b/Lutra/Lutra.Application/Supermarkten/GetSupermarkten.cs @@ -0,0 +1,4 @@ +namespace Lutra.Application.Supermarkten +{ + public sealed partial class GetSupermarkten { } +} diff --git a/Lutra/Lutra.Domain/Entities/Beoordeling.cs b/Lutra/Lutra.Domain/Entities/Beoordeling.cs index bed52b5..c8bc51c 100644 --- a/Lutra/Lutra.Domain/Entities/Beoordeling.cs +++ b/Lutra/Lutra.Domain/Entities/Beoordeling.cs @@ -14,4 +14,6 @@ public class Beoordeling : BaseEntity [MaxLength(1024)] public string? Tekst { get; set; } + + public required Guid VerspakketId { get; set; } } diff --git a/Lutra/Lutra.Domain/Entities/Verspakket.cs b/Lutra/Lutra.Domain/Entities/Verspakket.cs index c47720d..a9ac390 100644 --- a/Lutra/Lutra.Domain/Entities/Verspakket.cs +++ b/Lutra/Lutra.Domain/Entities/Verspakket.cs @@ -4,17 +4,32 @@ namespace Lutra.Domain.Entities; public class Verspakket : BaseEntity { + private readonly List _beoordelingen = []; + [MaxLength(50)] public required string Naam { get; set; } - [Range(1, 10)] - public int AantalPersonen { get; set; } + public int AantalPersonen { get; set; } public required Guid SupermarktId { get; set; } - public required virtual Beoordeling[]? Beoordelingen { get; set; } - public required virtual Supermarkt Supermarkt { get; set; } -} + public IReadOnlyCollection Beoordelingen => _beoordelingen.AsReadOnly(); + + public void AddBeoordeling(Beoordeling beoordeling) + { + _beoordelingen.Add(beoordeling); + } + + public bool RemoveBeoordeling(Guid id) + { + var beoordeling = _beoordelingen.Find(b => b.Id == id); + if (beoordeling is null) + return false; + + _beoordelingen.Remove(beoordeling); + return true; + } +} \ No newline at end of file diff --git a/Lutra/Lutra.Infrastructure.Migrator/Program.cs b/Lutra/Lutra.Infrastructure.Migrator/Program.cs index 1be687b..adbc2cd 100644 --- a/Lutra/Lutra.Infrastructure.Migrator/Program.cs +++ b/Lutra/Lutra.Infrastructure.Migrator/Program.cs @@ -1,3 +1,4 @@ +using Lutra.Domain.Entities; using Lutra.Infrastructure.Sql; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; @@ -14,3 +15,26 @@ using var scope = host.Services.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); await dbContext.Database.MigrateAsync(); + +if (!await dbContext.Supermarkten.AnyAsync()) +{ + var createdAt = DateTime.UtcNow; + dbContext.Supermarkten.AddRange( + CreateSupermarkt("Albert Heijn", createdAt), + CreateSupermarkt("Jumbo", createdAt), + CreateSupermarkt("Poiesz", createdAt), + CreateSupermarkt("Lidl", createdAt)); + + await dbContext.SaveChangesAsync(); +} + +static Supermarkt CreateSupermarkt(string naam, DateTime createdAt) +{ + return new Supermarkt + { + Id = Guid.NewGuid(), + Naam = naam, + CreatedAt = createdAt, + ModifiedAt = createdAt + }; +} diff --git a/Lutra/Lutra.Infrastructure/LutraDbContext.cs b/Lutra/Lutra.Infrastructure/LutraDbContext.cs index f03d5ec..5dba703 100644 --- a/Lutra/Lutra.Infrastructure/LutraDbContext.cs +++ b/Lutra/Lutra.Infrastructure/LutraDbContext.cs @@ -15,7 +15,19 @@ public class LutraDbContext : DbContext, ILutraDbContext public DbSet Verspaketten => Set(); - public DbSet Beoordelingen => Set(); + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity() + .ToTable("Beoordelingen"); + + modelBuilder.Entity() + .HasMany(v => v.Beoordelingen) + .WithOne() + .HasForeignKey(b => b.VerspakketId) + .IsRequired(); + } public override Task SaveChangesAsync(CancellationToken cancellationToken = default) { diff --git a/Lutra/Lutra.Infrastructure/Migrations/20260409194456_MakeVerspakketAggregateRoot.Designer.cs b/Lutra/Lutra.Infrastructure/Migrations/20260409194456_MakeVerspakketAggregateRoot.Designer.cs new file mode 100644 index 0000000..cd78436 --- /dev/null +++ b/Lutra/Lutra.Infrastructure/Migrations/20260409194456_MakeVerspakketAggregateRoot.Designer.cs @@ -0,0 +1,151 @@ +// +using System; +using Lutra.Infrastructure.Sql; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Lutra.Infrastructure.Sql.Migrations +{ + [DbContext(typeof(LutraDbContext))] + [Migration("20260409194456_MakeVerspakketAggregateRoot")] + partial class MakeVerspakketAggregateRoot + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Lutra.Domain.Entities.Beoordeling", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Aanbevolen") + .HasColumnType("boolean"); + + b.Property("CijferBereiden") + .HasColumnType("integer"); + + b.Property("CijferSmaak") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Tekst") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("VerspakketId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("VerspakketId"); + + b.ToTable("Beoordelingen", (string)null); + }); + + modelBuilder.Entity("Lutra.Domain.Entities.Supermarkt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Naam") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.ToTable("Supermarkten"); + }); + + modelBuilder.Entity("Lutra.Domain.Entities.Verspakket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AantalPersonen") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Naam") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("SupermarktId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("SupermarktId"); + + b.ToTable("Verspaketten"); + }); + + modelBuilder.Entity("Lutra.Domain.Entities.Beoordeling", b => + { + b.HasOne("Lutra.Domain.Entities.Verspakket", null) + .WithMany("Beoordelingen") + .HasForeignKey("VerspakketId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Lutra.Domain.Entities.Verspakket", b => + { + b.HasOne("Lutra.Domain.Entities.Supermarkt", "Supermarkt") + .WithMany() + .HasForeignKey("SupermarktId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Supermarkt"); + }); + + modelBuilder.Entity("Lutra.Domain.Entities.Verspakket", b => + { + b.Navigation("Beoordelingen"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Lutra/Lutra.Infrastructure/Migrations/20260409194456_MakeVerspakketAggregateRoot.cs b/Lutra/Lutra.Infrastructure/Migrations/20260409194456_MakeVerspakketAggregateRoot.cs new file mode 100644 index 0000000..fe3f9ac --- /dev/null +++ b/Lutra/Lutra.Infrastructure/Migrations/20260409194456_MakeVerspakketAggregateRoot.cs @@ -0,0 +1,60 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Lutra.Infrastructure.Sql.Migrations +{ + /// + public partial class MakeVerspakketAggregateRoot : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Beoordelingen_Verspaketten_VerspakketId", + table: "Beoordelingen"); + + migrationBuilder.AlterColumn( + name: "VerspakketId", + table: "Beoordelingen", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000"), + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AddForeignKey( + name: "FK_Beoordelingen_Verspaketten_VerspakketId", + table: "Beoordelingen", + column: "VerspakketId", + principalTable: "Verspaketten", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Beoordelingen_Verspaketten_VerspakketId", + table: "Beoordelingen"); + + migrationBuilder.AlterColumn( + name: "VerspakketId", + table: "Beoordelingen", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AddForeignKey( + name: "FK_Beoordelingen_Verspaketten_VerspakketId", + table: "Beoordelingen", + column: "VerspakketId", + principalTable: "Verspaketten", + principalColumn: "Id"); + } + } +} diff --git a/Lutra/Lutra.Infrastructure/Migrations/LutraDbContextModelSnapshot.cs b/Lutra/Lutra.Infrastructure/Migrations/LutraDbContextModelSnapshot.cs index 4d5a9e8..9677f11 100644 --- a/Lutra/Lutra.Infrastructure/Migrations/LutraDbContextModelSnapshot.cs +++ b/Lutra/Lutra.Infrastructure/Migrations/LutraDbContextModelSnapshot.cs @@ -50,14 +50,14 @@ namespace Lutra.Infrastructure.Sql.Migrations .HasMaxLength(1024) .HasColumnType("character varying(1024)"); - b.Property("VerspakketId") + b.Property("VerspakketId") .HasColumnType("uuid"); b.HasKey("Id"); b.HasIndex("VerspakketId"); - b.ToTable("Beoordelingen"); + b.ToTable("Beoordelingen", (string)null); }); modelBuilder.Entity("Lutra.Domain.Entities.Supermarkt", b => @@ -122,7 +122,9 @@ namespace Lutra.Infrastructure.Sql.Migrations { b.HasOne("Lutra.Domain.Entities.Verspakket", null) .WithMany("Beoordelingen") - .HasForeignKey("VerspakketId"); + .HasForeignKey("VerspakketId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); }); modelBuilder.Entity("Lutra.Domain.Entities.Verspakket", b =>