@atlashub/smartstack-mcp 1.4.1 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -78,14 +78,14 @@ if (envLevel && ["debug", "info", "warn", "error"].includes(envLevel)) {
78
78
  import path2 from "path";
79
79
 
80
80
  // src/utils/fs.ts
81
- import fs from "fs-extra";
81
+ import { stat, mkdir, readFile, writeFile, cp, rm } from "fs/promises";
82
82
  import path from "path";
83
83
  import { glob } from "glob";
84
84
  var FileSystemError = class extends Error {
85
- constructor(message, operation, path14, cause) {
85
+ constructor(message, operation, path18, cause) {
86
86
  super(message);
87
87
  this.operation = operation;
88
- this.path = path14;
88
+ this.path = path18;
89
89
  this.cause = cause;
90
90
  this.name = "FileSystemError";
91
91
  }
@@ -103,26 +103,26 @@ function validatePathSecurity(targetPath, baseDir) {
103
103
  }
104
104
  async function fileExists(filePath) {
105
105
  try {
106
- const stat = await fs.stat(filePath);
107
- return stat.isFile();
106
+ const s = await stat(filePath);
107
+ return s.isFile();
108
108
  } catch {
109
109
  return false;
110
110
  }
111
111
  }
112
112
  async function directoryExists(dirPath) {
113
113
  try {
114
- const stat = await fs.stat(dirPath);
115
- return stat.isDirectory();
114
+ const s = await stat(dirPath);
115
+ return s.isDirectory();
116
116
  } catch {
117
117
  return false;
118
118
  }
119
119
  }
120
120
  async function ensureDirectory(dirPath) {
121
- await fs.ensureDir(dirPath);
121
+ await mkdir(dirPath, { recursive: true });
122
122
  }
123
123
  async function readJson(filePath) {
124
124
  try {
125
- const content = await fs.readFile(filePath, "utf-8");
125
+ const content = await readFile(filePath, "utf-8");
126
126
  try {
127
127
  return JSON.parse(content);
128
128
  } catch (parseError) {
@@ -148,7 +148,7 @@ async function readJson(filePath) {
148
148
  }
149
149
  async function readText(filePath) {
150
150
  try {
151
- return await fs.readFile(filePath, "utf-8");
151
+ return await readFile(filePath, "utf-8");
152
152
  } catch (error) {
153
153
  const err = error instanceof Error ? error : new Error(String(error));
154
154
  throw new FileSystemError(
@@ -161,8 +161,8 @@ async function readText(filePath) {
161
161
  }
162
162
  async function writeText(filePath, content) {
163
163
  try {
164
- await fs.ensureDir(path.dirname(filePath));
165
- await fs.writeFile(filePath, content, "utf-8");
164
+ await mkdir(path.dirname(filePath), { recursive: true });
165
+ await writeFile(filePath, content, "utf-8");
166
166
  } catch (error) {
167
167
  const err = error instanceof Error ? error : new Error(String(error));
168
168
  throw new FileSystemError(
@@ -456,6 +456,41 @@ var ApiDocsInputSchema = z.object({
456
456
  format: z.enum(["markdown", "json", "openapi"]).default("markdown").describe("Output format"),
457
457
  controller: z.string().optional().describe("Filter by controller name")
458
458
  });
459
+ var TestTypeSchema = z.enum(["unit", "integration", "security", "e2e"]);
460
+ var TestTargetSchema = z.enum(["entity", "service", "controller", "validator", "repository", "all"]);
461
+ var ScaffoldTestsInputSchema = z.object({
462
+ target: TestTargetSchema.describe("Type of component to test"),
463
+ name: z.string().min(1).describe('Component name (PascalCase, e.g., "User", "Order")'),
464
+ testTypes: z.array(TestTypeSchema).default(["unit"]).describe("Types of tests to generate"),
465
+ options: z.object({
466
+ includeEdgeCases: z.boolean().default(true).describe("Include edge case tests"),
467
+ includeTenantIsolation: z.boolean().default(true).describe("Include tenant isolation tests"),
468
+ includeSoftDelete: z.boolean().default(true).describe("Include soft delete tests"),
469
+ includeAudit: z.boolean().default(true).describe("Include audit trail tests"),
470
+ includeValidation: z.boolean().default(true).describe("Include validation tests"),
471
+ includeAuthorization: z.boolean().default(false).describe("Include authorization tests"),
472
+ includePerformance: z.boolean().default(false).describe("Include performance tests"),
473
+ entityProperties: z.array(EntityPropertySchema).optional().describe("Entity properties for test generation"),
474
+ isSystemEntity: z.boolean().default(false).describe("If true, entity has no TenantId"),
475
+ dryRun: z.boolean().default(false).describe("Preview without writing files")
476
+ }).optional()
477
+ });
478
+ var AnalyzeTestCoverageInputSchema = z.object({
479
+ path: z.string().optional().describe("Project path to analyze"),
480
+ scope: z.enum(["entity", "service", "controller", "all"]).default("all").describe("Scope of analysis"),
481
+ outputFormat: z.enum(["summary", "detailed", "json"]).default("summary").describe("Output format"),
482
+ includeRecommendations: z.boolean().default(true).describe("Include recommendations for missing tests")
483
+ });
484
+ var ValidateTestConventionsInputSchema = z.object({
485
+ path: z.string().optional().describe("Project path to validate"),
486
+ checks: z.array(z.enum(["naming", "structure", "patterns", "assertions", "mocking", "all"])).default(["all"]).describe("Types of convention checks to perform"),
487
+ autoFix: z.boolean().default(false).describe("Automatically fix minor issues")
488
+ });
489
+ var SuggestTestScenariosInputSchema = z.object({
490
+ target: z.enum(["entity", "service", "controller", "file"]).describe("Type of target to analyze"),
491
+ name: z.string().min(1).describe("Component name or file path"),
492
+ depth: z.enum(["basic", "comprehensive", "security-focused"]).default("comprehensive").describe("Depth of analysis")
493
+ });
459
494
 
460
495
  // src/lib/detector.ts
461
496
  import path4 from "path";
@@ -2398,7 +2433,7 @@ export function use{{name}}(options: Use{{name}}Options = {}) {
2398
2433
  }
2399
2434
  async function scaffoldTest(name, options, structure, config, result, dryRun = false) {
2400
2435
  const isSystemEntity = options?.isSystemEntity || false;
2401
- const serviceTestTemplate = `using System;
2436
+ const serviceTestTemplate2 = `using System;
2402
2437
  using System.Threading;
2403
2438
  using System.Threading.Tasks;
2404
2439
  using Microsoft.Extensions.Logging;
@@ -2505,7 +2540,7 @@ public class {{name}}ServiceTests
2505
2540
  name,
2506
2541
  isSystemEntity
2507
2542
  };
2508
- const testContent = Handlebars.compile(serviceTestTemplate)(context);
2543
+ const testContent = Handlebars.compile(serviceTestTemplate2)(context);
2509
2544
  const testsPath = structure.application ? path7.join(path7.dirname(structure.application), `${path7.basename(structure.application)}.Tests`, "Services") : path7.join(config.smartstack.projectPath, "Application.Tests", "Services");
2510
2545
  const testFilePath = path7.join(testsPath, `${name}ServiceTests.cs`);
2511
2546
  if (!dryRun) {
@@ -3446,175 +3481,3015 @@ function compareVersions2(a, b) {
3446
3481
  return 0;
3447
3482
  }
3448
3483
 
3449
- // src/resources/conventions.ts
3450
- var conventionsResourceTemplate = {
3451
- uri: "smartstack://conventions",
3452
- name: "AtlasHub Conventions",
3453
- description: "Documentation of AtlasHub/SmartStack naming conventions, patterns, and best practices",
3454
- mimeType: "text/markdown"
3484
+ // src/tools/scaffold-tests.ts
3485
+ import Handlebars2 from "handlebars";
3486
+ import path10 from "path";
3487
+ var scaffoldTestsTool = {
3488
+ name: "scaffold_tests",
3489
+ description: "Generate unit, integration, and security tests for SmartStack entities, services, controllers, validators, and repositories. Ensures non-regression and maximum security coverage.",
3490
+ inputSchema: {
3491
+ type: "object",
3492
+ properties: {
3493
+ target: {
3494
+ type: "string",
3495
+ enum: ["entity", "service", "controller", "validator", "repository", "all"],
3496
+ description: "Type of component to test"
3497
+ },
3498
+ name: {
3499
+ type: "string",
3500
+ description: 'Component name (PascalCase, e.g., "User", "Order")'
3501
+ },
3502
+ testTypes: {
3503
+ type: "array",
3504
+ items: {
3505
+ type: "string",
3506
+ enum: ["unit", "integration", "security", "e2e"]
3507
+ },
3508
+ default: ["unit"],
3509
+ description: "Types of tests to generate"
3510
+ },
3511
+ options: {
3512
+ type: "object",
3513
+ properties: {
3514
+ includeEdgeCases: {
3515
+ type: "boolean",
3516
+ default: true,
3517
+ description: "Include edge case tests"
3518
+ },
3519
+ includeTenantIsolation: {
3520
+ type: "boolean",
3521
+ default: true,
3522
+ description: "Include tenant isolation tests"
3523
+ },
3524
+ includeSoftDelete: {
3525
+ type: "boolean",
3526
+ default: true,
3527
+ description: "Include soft delete tests"
3528
+ },
3529
+ includeAudit: {
3530
+ type: "boolean",
3531
+ default: true,
3532
+ description: "Include audit trail tests"
3533
+ },
3534
+ includeValidation: {
3535
+ type: "boolean",
3536
+ default: true,
3537
+ description: "Include validation tests"
3538
+ },
3539
+ includeAuthorization: {
3540
+ type: "boolean",
3541
+ default: false,
3542
+ description: "Include authorization tests"
3543
+ },
3544
+ includePerformance: {
3545
+ type: "boolean",
3546
+ default: false,
3547
+ description: "Include performance tests"
3548
+ },
3549
+ isSystemEntity: {
3550
+ type: "boolean",
3551
+ default: false,
3552
+ description: "If true, entity has no TenantId"
3553
+ },
3554
+ dryRun: {
3555
+ type: "boolean",
3556
+ default: false,
3557
+ description: "Preview without writing files"
3558
+ }
3559
+ }
3560
+ }
3561
+ },
3562
+ required: ["target", "name"]
3563
+ }
3455
3564
  };
3456
- async function getConventionsResource(config) {
3457
- const { schemas, tablePrefixes, codePrefixes, migrationFormat, namespaces, servicePattern } = config.conventions;
3458
- return `# AtlasHub SmartStack Conventions
3459
-
3460
- ## Overview
3461
-
3462
- This document describes the mandatory conventions for extending the SmartStack/AtlasHub platform.
3463
- Following these conventions ensures compatibility and prevents conflicts.
3565
+ Handlebars2.registerHelper("pascalCase", (str) => {
3566
+ if (!str) return "";
3567
+ return str.charAt(0).toUpperCase() + str.slice(1);
3568
+ });
3569
+ Handlebars2.registerHelper("camelCase", (str) => {
3570
+ if (!str) return "";
3571
+ return str.charAt(0).toLowerCase() + str.slice(1);
3572
+ });
3573
+ Handlebars2.registerHelper("unless", function(conditional, options) {
3574
+ if (!conditional) {
3575
+ return options.fn(this);
3576
+ }
3577
+ return options.inverse(this);
3578
+ });
3579
+ var entityTestTemplate = `using System;
3580
+ using FluentAssertions;
3581
+ using Xunit;
3582
+ using {{domainNamespace}};
3464
3583
 
3465
- ---
3584
+ namespace {{testNamespace}}.Unit.Domain;
3466
3585
 
3467
- ## 1. Database Conventions
3586
+ /// <summary>
3587
+ /// Unit tests for {{name}} entity
3588
+ /// Tests: Factory methods, Soft delete, Audit trail, Validation
3589
+ /// </summary>
3590
+ public class {{name}}Tests
3591
+ {
3592
+ private const string ValidCode = "test_code";
3593
+ {{#unless isSystemEntity}}
3594
+ private readonly Guid _tenantId = Guid.NewGuid();
3595
+ {{/unless}}
3468
3596
 
3469
- ### SQL Schemas
3597
+ #region Factory Method Tests
3470
3598
 
3471
- SmartStack uses SQL Server schemas to separate platform tables from client extensions:
3599
+ [Fact]
3600
+ public void Create_WhenValidData_ShouldCreateEntity()
3601
+ {
3602
+ // Arrange
3603
+ var code = ValidCode;
3604
+ {{#unless isSystemEntity}}
3605
+ var tenantId = _tenantId;
3606
+ {{/unless}}
3472
3607
 
3473
- | Schema | Usage | Description |
3474
- |--------|-------|-------------|
3475
- | \`${schemas.platform}\` | SmartStack platform | All native SmartStack tables |
3476
- | \`${schemas.extensions}\` | Client extensions | Custom tables added by clients |
3608
+ // Act
3609
+ var entity = {{name}}.Create({{#unless isSystemEntity}}tenantId, {{/unless}}code);
3477
3610
 
3478
- ### Domain Table Prefixes
3611
+ // Assert
3612
+ entity.Should().NotBeNull();
3613
+ entity.Id.Should().NotBeEmpty();
3614
+ entity.Code.Should().Be(code.ToLowerInvariant());
3615
+ {{#unless isSystemEntity}}
3616
+ entity.TenantId.Should().Be(tenantId);
3617
+ {{/unless}}
3618
+ entity.CreatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
3619
+ entity.IsDeleted.Should().BeFalse();
3620
+ }
3479
3621
 
3480
- Tables are organized by domain using prefixes:
3622
+ {{#unless isSystemEntity}}
3623
+ [Fact]
3624
+ public void Create_WhenEmptyTenantId_ShouldThrow()
3625
+ {
3626
+ // Arrange
3627
+ var tenantId = Guid.Empty;
3628
+ var code = ValidCode;
3481
3629
 
3482
- | Prefix | Domain | Example Tables |
3483
- |--------|--------|----------------|
3484
- | \`auth_\` | Authorization | auth_Users, auth_Roles, auth_Permissions |
3485
- | \`nav_\` | Navigation | nav_Contexts, nav_Applications, nav_Modules |
3486
- | \`usr_\` | User profiles | usr_Profiles, usr_Preferences |
3487
- | \`ai_\` | AI features | ai_Providers, ai_Models, ai_Prompts |
3488
- | \`cfg_\` | Configuration | cfg_Settings |
3489
- | \`wkf_\` | Workflows | wkf_EmailTemplates, wkf_Workflows |
3490
- | \`support_\` | Support | support_Tickets, support_Comments |
3491
- | \`entra_\` | Entra sync | entra_Groups, entra_SyncState |
3492
- | \`ref_\` | References | ref_Companies, ref_Departments |
3493
- | \`loc_\` | Localization | loc_Languages, loc_Translations |
3494
- | \`lic_\` | Licensing | lic_Licenses |
3495
- | \`tenant_\` | Multi-Tenancy | tenant_Tenants, tenant_TenantUsers, tenant_TenantUserRoles |
3630
+ // Act
3631
+ var act = () => {{name}}.Create(tenantId, code);
3496
3632
 
3497
- ### Navigation Code Prefixes
3633
+ // Assert
3634
+ act.Should().Throw<ArgumentException>()
3635
+ .WithParameterName("tenantId");
3636
+ }
3637
+ {{/unless}}
3498
3638
 
3499
- All navigation data (Context, Application, Module, Section, Resource) uses code prefixes to distinguish system data from client extensions:
3639
+ [Theory]
3640
+ [InlineData("")]
3641
+ [InlineData(" ")]
3642
+ public void Create_WhenEmptyCode_ShouldHandleGracefully(string code)
3643
+ {
3644
+ // Arrange
3645
+ {{#unless isSystemEntity}}
3646
+ var tenantId = _tenantId;
3647
+ {{/unless}}
3500
3648
 
3501
- | Origin | Prefix | Usage | Example |
3502
- |--------|--------|-------|---------|
3503
- | SmartStack (system) | \`${codePrefixes.core}\` | Protected, delivered with SmartStack | \`${codePrefixes.core}administration\` |
3504
- | Client (extension) | \`${codePrefixes.extension}\` | Custom, added by clients | \`${codePrefixes.extension}it\` |
3649
+ // Act
3650
+ var entity = {{name}}.Create({{#unless isSystemEntity}}tenantId, {{/unless}}code);
3505
3651
 
3506
- **Navigation Hierarchy (5 levels):**
3652
+ // Assert
3653
+ entity.Should().NotBeNull();
3654
+ entity.Code.Should().BeEmpty();
3655
+ }
3507
3656
 
3508
- \`\`\`
3509
- Context \u2192 Application \u2192 Module \u2192 Section \u2192 Resource
3510
- \`\`\`
3657
+ #endregion
3511
3658
 
3512
- **Examples for each level:**
3659
+ {{#if includeSoftDelete}}
3660
+ #region Soft Delete Tests
3513
3661
 
3514
- | Level | Core Example | Extension Example |
3515
- |-------|--------------|-------------------|
3516
- | Context | \`${codePrefixes.core}administration\` | \`${codePrefixes.extension}it\` |
3517
- | Application | \`${codePrefixes.core}settings\` | \`${codePrefixes.extension}custom_app\` |
3518
- | Module | \`${codePrefixes.core}users\` | \`${codePrefixes.extension}inventory\` |
3519
- | Section | \`${codePrefixes.core}management\` | \`${codePrefixes.extension}reports\` |
3520
- | Resource | \`${codePrefixes.core}user_list\` | \`${codePrefixes.extension}stock_view\` |
3662
+ [Fact]
3663
+ public void SoftDelete_WhenCalled_ShouldSetIsDeletedTrue()
3664
+ {
3665
+ // Arrange
3666
+ var entity = {{name}}.Create({{#unless isSystemEntity}}_tenantId, {{/unless}}ValidCode);
3667
+ var deletedBy = "test_user";
3521
3668
 
3522
- **Rules:**
3523
- 1. \`${codePrefixes.core}*\` codes are **protected** - clients cannot create or modify them
3524
- 2. \`${codePrefixes.extension}*\` codes are **free** - clients can create custom navigation
3525
- 3. SmartStack updates will never overwrite \`${codePrefixes.extension}*\` data
3526
- 4. Codes must be unique within their level (e.g., no two Contexts with same code)
3669
+ // Act
3670
+ entity.SoftDelete(deletedBy);
3527
3671
 
3528
- ### Entity Configuration
3672
+ // Assert
3673
+ entity.IsDeleted.Should().BeTrue();
3674
+ entity.DeletedAt.Should().NotBeNull();
3675
+ entity.DeletedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
3676
+ entity.DeletedBy.Should().Be(deletedBy);
3677
+ }
3529
3678
 
3530
- \`\`\`csharp
3531
- public class MyEntityConfiguration : IEntityTypeConfiguration<MyEntity>
3532
- {
3533
- public void Configure(EntityTypeBuilder<MyEntity> builder)
3679
+ [Fact]
3680
+ public void SoftDelete_WhenAlreadyDeleted_ShouldNotChangeDeletedAt()
3534
3681
  {
3535
- // CORRECT: Use schema + domain prefix
3536
- builder.ToTable("auth_Users", "${schemas.platform}");
3682
+ // Arrange
3683
+ var entity = {{name}}.Create({{#unless isSystemEntity}}_tenantId, {{/unless}}ValidCode);
3684
+ entity.SoftDelete("first_deleter");
3685
+ var originalDeletedAt = entity.DeletedAt;
3537
3686
 
3538
- // WRONG: No schema specified
3539
- // builder.ToTable("auth_Users");
3687
+ // Act
3688
+ entity.SoftDelete("second_deleter");
3540
3689
 
3541
- // WRONG: No domain prefix
3542
- // builder.ToTable("Users", "${schemas.platform}");
3690
+ // Assert
3691
+ entity.DeletedAt.Should().Be(originalDeletedAt);
3543
3692
  }
3544
- }
3545
- \`\`\`
3546
3693
 
3547
- ### Extending Core Entities
3694
+ [Fact]
3695
+ public void Restore_WhenSoftDeleted_ShouldClearDeletedFields()
3696
+ {
3697
+ // Arrange
3698
+ var entity = {{name}}.Create({{#unless isSystemEntity}}_tenantId, {{/unless}}ValidCode);
3699
+ entity.SoftDelete("deleter");
3700
+ var restoredBy = "restorer";
3548
3701
 
3549
- When extending a core entity, use the \`${schemas.extensions}\` schema:
3702
+ // Act
3703
+ entity.Restore(restoredBy);
3550
3704
 
3551
- \`\`\`csharp
3552
- // Client extension for auth_Users
3553
- public class UserExtension
3554
- {
3555
- public Guid UserId { get; set; } // FK to auth_Users
3556
- public User User { get; set; }
3705
+ // Assert
3706
+ entity.IsDeleted.Should().BeFalse();
3707
+ entity.DeletedAt.Should().BeNull();
3708
+ entity.DeletedBy.Should().BeNull();
3709
+ entity.UpdatedBy.Should().Be(restoredBy);
3710
+ }
3557
3711
 
3558
- // Custom properties
3559
- public string CustomField { get; set; }
3560
- }
3712
+ #endregion
3713
+ {{/if}}
3561
3714
 
3562
- // Configuration - use extensions schema
3563
- builder.ToTable("client_UserExtensions", "${schemas.extensions}");
3564
- builder.HasOne(e => e.User)
3565
- .WithOne()
3566
- .HasForeignKey<UserExtension>(e => e.UserId);
3567
- \`\`\`
3715
+ {{#if includeAudit}}
3716
+ #region Audit Trail Tests
3568
3717
 
3569
- ---
3718
+ [Fact]
3719
+ public void Create_ShouldSetCreatedAtToUtcNow()
3720
+ {
3721
+ // Arrange & Act
3722
+ var entity = {{name}}.Create({{#unless isSystemEntity}}_tenantId, {{/unless}}ValidCode);
3570
3723
 
3571
- ## 2. Migration Conventions
3724
+ // Assert
3725
+ entity.CreatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
3726
+ entity.CreatedAt.Kind.Should().Be(DateTimeKind.Utc);
3727
+ }
3572
3728
 
3573
- ### Naming Format
3729
+ [Fact]
3730
+ public void Create_WhenCreatedByProvided_ShouldSetCreatedBy()
3731
+ {
3732
+ // Arrange
3733
+ var createdBy = "test_creator";
3574
3734
 
3575
- Migrations MUST follow this naming pattern:
3735
+ // Act
3736
+ var entity = {{name}}.Create({{#unless isSystemEntity}}_tenantId, {{/unless}}ValidCode, createdBy);
3576
3737
 
3577
- \`\`\`
3578
- ${migrationFormat}
3579
- \`\`\`
3738
+ // Assert
3739
+ entity.CreatedBy.Should().Be(createdBy);
3740
+ }
3580
3741
 
3581
- | Part | Description | Example |
3582
- |------|-------------|---------|
3583
- | \`{context}\` | DbContext name | \`core\`, \`extensions\` |
3584
- | \`{version}\` | Semver version | \`v1.0.0\`, \`v1.2.0\` |
3585
- | \`{sequence}\` | Order in version | \`001\`, \`002\` |
3586
- | \`{Description}\` | Action (PascalCase) | \`CreateAuthUsers\` |
3742
+ [Fact]
3743
+ public void Update_WhenCalled_ShouldUpdateAuditFields()
3744
+ {
3745
+ // Arrange
3746
+ var entity = {{name}}.Create({{#unless isSystemEntity}}_tenantId, {{/unless}}ValidCode);
3747
+ var originalCreatedAt = entity.CreatedAt;
3748
+ var updatedBy = "updater";
3587
3749
 
3588
- **Examples:**
3589
- - \`core_v1.0.0_001_InitialSchema.cs\`
3590
- - \`core_v1.0.0_002_CreateAuthUsers.cs\`
3591
- - \`core_v1.2.0_001_AddUserProfiles.cs\`
3592
- - \`extensions_v1.0.0_001_AddClientFeatures.cs\`
3750
+ // Act
3751
+ entity.Update(updatedBy);
3593
3752
 
3594
- ### Creating Migrations
3753
+ // Assert
3754
+ entity.UpdatedAt.Should().NotBeNull();
3755
+ entity.UpdatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
3756
+ entity.UpdatedBy.Should().Be(updatedBy);
3757
+ entity.CreatedAt.Should().Be(originalCreatedAt);
3758
+ }
3595
3759
 
3596
- \`\`\`bash
3597
- # Create a new migration
3598
- dotnet ef migrations add core_v1.0.0_001_InitialSchema
3760
+ #endregion
3761
+ {{/if}}
3599
3762
 
3600
- # With context specified
3601
- dotnet ef migrations add core_v1.2.0_001_AddUserProfiles --context ApplicationDbContext
3602
- \`\`\`
3763
+ {{#if includeEdgeCases}}
3764
+ #region Edge Cases
3603
3765
 
3604
- ### Migration Rules
3766
+ [Fact]
3767
+ public void Create_WithSpecialCharactersInCode_ShouldNormalize()
3768
+ {
3769
+ // Arrange
3770
+ var code = "Test-Code_123";
3605
3771
 
3606
- 1. **One migration per feature** - Group related changes in a single migration
3607
- 2. **Version-based naming** - Use semver (v1.0.0, v1.2.0) to link migrations to releases
3608
- 3. **Sequence numbers** - Use NNN (001, 002, etc.) for migrations in the same version
3609
- 4. **Context prefix** - Use \`core_\` for platform tables, \`extensions_\` for client extensions
3610
- 5. **Descriptive names** - Use clear PascalCase descriptions (CreateAuthUsers, AddUserProfiles, etc.)
3611
- 6. **Schema must be specified** - All tables must specify their schema in ToTable()
3772
+ // Act
3773
+ var entity = {{name}}.Create({{#unless isSystemEntity}}_tenantId, {{/unless}}code);
3612
3774
 
3613
- ---
3775
+ // Assert
3776
+ entity.Code.Should().Be(code.ToLowerInvariant());
3777
+ }
3614
3778
 
3615
- ## 3. Namespace Conventions
3779
+ [Fact]
3780
+ public void Create_WithMaxLengthCode_ShouldSucceed()
3781
+ {
3782
+ // Arrange
3783
+ var code = new string('a', 100);
3616
3784
 
3617
- ### Layer Structure
3785
+ // Act
3786
+ var entity = {{name}}.Create({{#unless isSystemEntity}}_tenantId, {{/unless}}code);
3787
+
3788
+ // Assert
3789
+ entity.Code.Should().HaveLength(100);
3790
+ }
3791
+
3792
+ #endregion
3793
+ {{/if}}
3794
+ }
3795
+ `;
3796
+ var serviceTestTemplate = `using System;
3797
+ using System.Collections.Generic;
3798
+ using System.Threading;
3799
+ using System.Threading.Tasks;
3800
+ using FluentAssertions;
3801
+ using Microsoft.Extensions.Logging;
3802
+ using Moq;
3803
+ using Xunit;
3804
+ using {{applicationNamespace}}.Services;
3805
+ using {{applicationNamespace}}.Repositories;
3806
+ using {{domainNamespace}};
3807
+
3808
+ namespace {{testNamespace}}.Unit.Services;
3809
+
3810
+ /// <summary>
3811
+ /// Unit tests for {{name}}Service
3812
+ /// Tests: CRUD operations, Business logic, Error handling, Tenant isolation
3813
+ /// </summary>
3814
+ public class {{name}}ServiceTests
3815
+ {
3816
+ private readonly Mock<I{{name}}Repository> _repositoryMock;
3817
+ private readonly Mock<ILogger<{{name}}Service>> _loggerMock;
3818
+ private readonly {{name}}Service _sut;
3819
+ {{#unless isSystemEntity}}
3820
+ private readonly Guid _tenantId = Guid.NewGuid();
3821
+ {{/unless}}
3822
+
3823
+ public {{name}}ServiceTests()
3824
+ {
3825
+ _repositoryMock = new Mock<I{{name}}Repository>();
3826
+ _loggerMock = new Mock<ILogger<{{name}}Service>>();
3827
+ _sut = new {{name}}Service(
3828
+ _repositoryMock.Object,
3829
+ _loggerMock.Object
3830
+ );
3831
+ }
3832
+
3833
+ #region GetByIdAsync Tests
3834
+
3835
+ [Fact]
3836
+ public async Task GetByIdAsync_WhenEntityExists_ShouldReturnEntity()
3837
+ {
3838
+ // Arrange
3839
+ var id = Guid.NewGuid();
3840
+ var entity = {{name}}.Create({{#unless isSystemEntity}}_tenantId, {{/unless}}"test");
3841
+ _repositoryMock
3842
+ .Setup(r => r.GetByIdAsync(id, It.IsAny<CancellationToken>()))
3843
+ .ReturnsAsync(entity);
3844
+
3845
+ // Act
3846
+ var result = await _sut.GetByIdAsync(id);
3847
+
3848
+ // Assert
3849
+ result.Should().NotBeNull();
3850
+ _repositoryMock.Verify(r => r.GetByIdAsync(id, It.IsAny<CancellationToken>()), Times.Once);
3851
+ }
3852
+
3853
+ [Fact]
3854
+ public async Task GetByIdAsync_WhenNotExists_ShouldReturnNull()
3855
+ {
3856
+ // Arrange
3857
+ var id = Guid.NewGuid();
3858
+ _repositoryMock
3859
+ .Setup(r => r.GetByIdAsync(id, It.IsAny<CancellationToken>()))
3860
+ .ReturnsAsync(({{name}}?)null);
3861
+
3862
+ // Act
3863
+ var result = await _sut.GetByIdAsync(id);
3864
+
3865
+ // Assert
3866
+ result.Should().BeNull();
3867
+ }
3868
+
3869
+ [Fact]
3870
+ public async Task GetByIdAsync_WhenCancelled_ShouldThrowOperationCancelledException()
3871
+ {
3872
+ // Arrange
3873
+ var id = Guid.NewGuid();
3874
+ var cts = new CancellationTokenSource();
3875
+ cts.Cancel();
3876
+
3877
+ _repositoryMock
3878
+ .Setup(r => r.GetByIdAsync(id, It.IsAny<CancellationToken>()))
3879
+ .ThrowsAsync(new OperationCanceledException());
3880
+
3881
+ // Act
3882
+ var act = async () => await _sut.GetByIdAsync(id, cts.Token);
3883
+
3884
+ // Assert
3885
+ await act.Should().ThrowAsync<OperationCanceledException>();
3886
+ }
3887
+
3888
+ #endregion
3889
+
3890
+ {{#unless isSystemEntity}}
3891
+ #region Tenant Isolation Tests
3892
+
3893
+ [Fact]
3894
+ public async Task GetAllAsync_ShouldFilterByTenant()
3895
+ {
3896
+ // Arrange
3897
+ var entities = new List<{{name}}>
3898
+ {
3899
+ {{name}}.Create(_tenantId, "entity1"),
3900
+ {{name}}.Create(_tenantId, "entity2"),
3901
+ };
3902
+ _repositoryMock
3903
+ .Setup(r => r.GetAllByTenantAsync(_tenantId, It.IsAny<CancellationToken>()))
3904
+ .ReturnsAsync(entities);
3905
+
3906
+ // Act
3907
+ var result = await _sut.GetAllAsync(_tenantId);
3908
+
3909
+ // Assert
3910
+ result.Should().HaveCount(2);
3911
+ result.Should().OnlyContain(e => e.TenantId == _tenantId);
3912
+ }
3913
+
3914
+ [Fact]
3915
+ public async Task CreateAsync_ShouldSetTenantId()
3916
+ {
3917
+ // Arrange
3918
+ {{name}}? capturedEntity = null;
3919
+ _repositoryMock
3920
+ .Setup(r => r.AddAsync(It.IsAny<{{name}}>(), It.IsAny<CancellationToken>()))
3921
+ .Callback<{{name}}, CancellationToken>((e, _) => capturedEntity = e)
3922
+ .ReturnsAsync(({{name}} e, CancellationToken _) => e);
3923
+
3924
+ // Act
3925
+ await _sut.CreateAsync(_tenantId, "test_code");
3926
+
3927
+ // Assert
3928
+ capturedEntity.Should().NotBeNull();
3929
+ capturedEntity!.TenantId.Should().Be(_tenantId);
3930
+ }
3931
+
3932
+ [Fact]
3933
+ public async Task GetByIdAsync_WhenEntityBelongsToDifferentTenant_ShouldReturnNull()
3934
+ {
3935
+ // Arrange
3936
+ var id = Guid.NewGuid();
3937
+ var otherTenantId = Guid.NewGuid();
3938
+ var entity = {{name}}.Create(otherTenantId, "test");
3939
+
3940
+ _repositoryMock
3941
+ .Setup(r => r.GetByIdAsync(id, It.IsAny<CancellationToken>()))
3942
+ .ReturnsAsync(({{name}}?)null); // Repository should filter by tenant
3943
+
3944
+ // Act
3945
+ var result = await _sut.GetByIdAsync(id, _tenantId);
3946
+
3947
+ // Assert
3948
+ result.Should().BeNull();
3949
+ }
3950
+
3951
+ #endregion
3952
+ {{/unless}}
3953
+
3954
+ #region Error Handling Tests
3955
+
3956
+ [Fact]
3957
+ public async Task UpdateAsync_WhenEntityNotFound_ShouldThrow()
3958
+ {
3959
+ // Arrange
3960
+ var id = Guid.NewGuid();
3961
+ _repositoryMock
3962
+ .Setup(r => r.GetByIdAsync(id, It.IsAny<CancellationToken>()))
3963
+ .ReturnsAsync(({{name}}?)null);
3964
+
3965
+ // Act
3966
+ var act = async () => await _sut.UpdateAsync(id, "new_code");
3967
+
3968
+ // Assert
3969
+ await act.Should().ThrowAsync<InvalidOperationException>()
3970
+ .WithMessage("*not found*");
3971
+ }
3972
+
3973
+ [Fact]
3974
+ public async Task DeleteAsync_WhenEntityNotFound_ShouldReturnFalse()
3975
+ {
3976
+ // Arrange
3977
+ var id = Guid.NewGuid();
3978
+ _repositoryMock
3979
+ .Setup(r => r.GetByIdAsync(id, It.IsAny<CancellationToken>()))
3980
+ .ReturnsAsync(({{name}}?)null);
3981
+
3982
+ // Act
3983
+ var result = await _sut.DeleteAsync(id);
3984
+
3985
+ // Assert
3986
+ result.Should().BeFalse();
3987
+ }
3988
+
3989
+ #endregion
3990
+
3991
+ {{#if includeEdgeCases}}
3992
+ #region Edge Cases
3993
+
3994
+ [Fact]
3995
+ public async Task CreateAsync_WithEmptyCode_ShouldStillCreate()
3996
+ {
3997
+ // Arrange
3998
+ _repositoryMock
3999
+ .Setup(r => r.AddAsync(It.IsAny<{{name}}>(), It.IsAny<CancellationToken>()))
4000
+ .ReturnsAsync(({{name}} e, CancellationToken _) => e);
4001
+
4002
+ // Act
4003
+ var result = await _sut.CreateAsync({{#unless isSystemEntity}}_tenantId, {{/unless}}"");
4004
+
4005
+ // Assert
4006
+ result.Should().NotBeNull();
4007
+ result.Code.Should().BeEmpty();
4008
+ }
4009
+
4010
+ [Fact]
4011
+ public async Task GetAllAsync_WhenNoEntities_ShouldReturnEmptyList()
4012
+ {
4013
+ // Arrange
4014
+ _repositoryMock
4015
+ .Setup(r => r.GetAllAsync(It.IsAny<CancellationToken>()))
4016
+ .ReturnsAsync(new List<{{name}}>());
4017
+
4018
+ // Act
4019
+ var result = await _sut.GetAllAsync();
4020
+
4021
+ // Assert
4022
+ result.Should().BeEmpty();
4023
+ }
4024
+
4025
+ #endregion
4026
+ {{/if}}
4027
+ }
4028
+ `;
4029
+ var controllerTestTemplate = `using System;
4030
+ using System.Collections.Generic;
4031
+ using System.Net;
4032
+ using System.Net.Http;
4033
+ using System.Net.Http.Json;
4034
+ using System.Threading.Tasks;
4035
+ using FluentAssertions;
4036
+ using Microsoft.AspNetCore.Mvc.Testing;
4037
+ using Microsoft.Extensions.DependencyInjection;
4038
+ using Moq;
4039
+ using Xunit;
4040
+ using {{apiNamespace}};
4041
+ using {{applicationNamespace}}.Services;
4042
+ using {{domainNamespace}};
4043
+
4044
+ namespace {{testNamespace}}.Integration.Controllers;
4045
+
4046
+ /// <summary>
4047
+ /// Integration tests for {{name}}Controller
4048
+ /// Tests: HTTP status codes, Authorization, Validation, CRUD operations
4049
+ /// </summary>
4050
+ public class {{name}}ControllerTests : IClassFixture<WebApplicationFactory<Program>>
4051
+ {
4052
+ private readonly WebApplicationFactory<Program> _factory;
4053
+ private readonly HttpClient _client;
4054
+ private readonly Mock<I{{name}}Service> _serviceMock;
4055
+ {{#unless isSystemEntity}}
4056
+ private readonly Guid _tenantId = Guid.NewGuid();
4057
+ {{/unless}}
4058
+
4059
+ public {{name}}ControllerTests(WebApplicationFactory<Program> factory)
4060
+ {
4061
+ _serviceMock = new Mock<I{{name}}Service>();
4062
+
4063
+ _factory = factory.WithWebHostBuilder(builder =>
4064
+ {
4065
+ builder.ConfigureServices(services =>
4066
+ {
4067
+ // Replace the real service with our mock
4068
+ var descriptor = services.SingleOrDefault(
4069
+ d => d.ServiceType == typeof(I{{name}}Service));
4070
+
4071
+ if (descriptor != null)
4072
+ {
4073
+ services.Remove(descriptor);
4074
+ }
4075
+
4076
+ services.AddScoped(_ => _serviceMock.Object);
4077
+ });
4078
+ });
4079
+
4080
+ _client = _factory.CreateClient();
4081
+ {{#unless isSystemEntity}}
4082
+ _client.DefaultRequestHeaders.Add("X-Tenant-Id", _tenantId.ToString());
4083
+ {{/unless}}
4084
+ }
4085
+
4086
+ #region GET Tests
4087
+
4088
+ [Fact]
4089
+ public async Task GetAll_WhenAuthorized_ShouldReturn200()
4090
+ {
4091
+ // Arrange
4092
+ var entities = new List<{{name}}>
4093
+ {
4094
+ {{name}}.Create({{#unless isSystemEntity}}_tenantId, {{/unless}}"entity1"),
4095
+ {{name}}.Create({{#unless isSystemEntity}}_tenantId, {{/unless}}"entity2"),
4096
+ };
4097
+ _serviceMock
4098
+ .Setup(s => s.GetAllAsync({{#unless isSystemEntity}}_tenantId, {{/unless}}default))
4099
+ .ReturnsAsync(entities);
4100
+
4101
+ // Act
4102
+ var response = await _client.GetAsync("/api/{{nameLower}}");
4103
+
4104
+ // Assert
4105
+ response.StatusCode.Should().Be(HttpStatusCode.OK);
4106
+ }
4107
+
4108
+ [Fact]
4109
+ public async Task GetById_WhenEntityExists_ShouldReturn200()
4110
+ {
4111
+ // Arrange
4112
+ var id = Guid.NewGuid();
4113
+ var entity = {{name}}.Create({{#unless isSystemEntity}}_tenantId, {{/unless}}"test");
4114
+ _serviceMock
4115
+ .Setup(s => s.GetByIdAsync(id, {{#unless isSystemEntity}}_tenantId, {{/unless}}default))
4116
+ .ReturnsAsync(entity);
4117
+
4118
+ // Act
4119
+ var response = await _client.GetAsync($"/api/{{nameLower}}/{id}");
4120
+
4121
+ // Assert
4122
+ response.StatusCode.Should().Be(HttpStatusCode.OK);
4123
+ }
4124
+
4125
+ [Fact]
4126
+ public async Task GetById_WhenNotFound_ShouldReturn404()
4127
+ {
4128
+ // Arrange
4129
+ var id = Guid.NewGuid();
4130
+ _serviceMock
4131
+ .Setup(s => s.GetByIdAsync(id, {{#unless isSystemEntity}}_tenantId, {{/unless}}default))
4132
+ .ReturnsAsync(({{name}}?)null);
4133
+
4134
+ // Act
4135
+ var response = await _client.GetAsync($"/api/{{nameLower}}/{id}");
4136
+
4137
+ // Assert
4138
+ response.StatusCode.Should().Be(HttpStatusCode.NotFound);
4139
+ }
4140
+
4141
+ #endregion
4142
+
4143
+ #region POST Tests
4144
+
4145
+ [Fact]
4146
+ public async Task Create_WhenValidData_ShouldReturn201()
4147
+ {
4148
+ // Arrange
4149
+ var request = new { Code = "new_entity" };
4150
+ var entity = {{name}}.Create({{#unless isSystemEntity}}_tenantId, {{/unless}}request.Code);
4151
+ _serviceMock
4152
+ .Setup(s => s.CreateAsync({{#unless isSystemEntity}}_tenantId, {{/unless}}request.Code, default))
4153
+ .ReturnsAsync(entity);
4154
+
4155
+ // Act
4156
+ var response = await _client.PostAsJsonAsync("/api/{{nameLower}}", request);
4157
+
4158
+ // Assert
4159
+ response.StatusCode.Should().Be(HttpStatusCode.Created);
4160
+ }
4161
+
4162
+ {{#if includeValidation}}
4163
+ [Fact]
4164
+ public async Task Create_WhenInvalidData_ShouldReturn400()
4165
+ {
4166
+ // Arrange
4167
+ var request = new { Code = (string?)null };
4168
+
4169
+ // Act
4170
+ var response = await _client.PostAsJsonAsync("/api/{{nameLower}}", request);
4171
+
4172
+ // Assert
4173
+ response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
4174
+ }
4175
+ {{/if}}
4176
+
4177
+ #endregion
4178
+
4179
+ #region PUT Tests
4180
+
4181
+ [Fact]
4182
+ public async Task Update_WhenEntityExists_ShouldReturn200()
4183
+ {
4184
+ // Arrange
4185
+ var id = Guid.NewGuid();
4186
+ var request = new { Code = "updated_code" };
4187
+ var entity = {{name}}.Create({{#unless isSystemEntity}}_tenantId, {{/unless}}"test");
4188
+ _serviceMock
4189
+ .Setup(s => s.GetByIdAsync(id, {{#unless isSystemEntity}}_tenantId, {{/unless}}default))
4190
+ .ReturnsAsync(entity);
4191
+ _serviceMock
4192
+ .Setup(s => s.UpdateAsync(id, request.Code, default))
4193
+ .ReturnsAsync(entity);
4194
+
4195
+ // Act
4196
+ var response = await _client.PutAsJsonAsync($"/api/{{nameLower}}/{id}", request);
4197
+
4198
+ // Assert
4199
+ response.StatusCode.Should().Be(HttpStatusCode.OK);
4200
+ }
4201
+
4202
+ [Fact]
4203
+ public async Task Update_WhenNotFound_ShouldReturn404()
4204
+ {
4205
+ // Arrange
4206
+ var id = Guid.NewGuid();
4207
+ var request = new { Code = "updated_code" };
4208
+ _serviceMock
4209
+ .Setup(s => s.GetByIdAsync(id, {{#unless isSystemEntity}}_tenantId, {{/unless}}default))
4210
+ .ReturnsAsync(({{name}}?)null);
4211
+
4212
+ // Act
4213
+ var response = await _client.PutAsJsonAsync($"/api/{{nameLower}}/{id}", request);
4214
+
4215
+ // Assert
4216
+ response.StatusCode.Should().Be(HttpStatusCode.NotFound);
4217
+ }
4218
+
4219
+ #endregion
4220
+
4221
+ #region DELETE Tests
4222
+
4223
+ [Fact]
4224
+ public async Task Delete_WhenEntityExists_ShouldReturn204()
4225
+ {
4226
+ // Arrange
4227
+ var id = Guid.NewGuid();
4228
+ _serviceMock
4229
+ .Setup(s => s.DeleteAsync(id, {{#unless isSystemEntity}}_tenantId, {{/unless}}default))
4230
+ .ReturnsAsync(true);
4231
+
4232
+ // Act
4233
+ var response = await _client.DeleteAsync($"/api/{{nameLower}}/{id}");
4234
+
4235
+ // Assert
4236
+ response.StatusCode.Should().Be(HttpStatusCode.NoContent);
4237
+ }
4238
+
4239
+ [Fact]
4240
+ public async Task Delete_WhenNotFound_ShouldReturn404()
4241
+ {
4242
+ // Arrange
4243
+ var id = Guid.NewGuid();
4244
+ _serviceMock
4245
+ .Setup(s => s.DeleteAsync(id, {{#unless isSystemEntity}}_tenantId, {{/unless}}default))
4246
+ .ReturnsAsync(false);
4247
+
4248
+ // Act
4249
+ var response = await _client.DeleteAsync($"/api/{{nameLower}}/{id}");
4250
+
4251
+ // Assert
4252
+ response.StatusCode.Should().Be(HttpStatusCode.NotFound);
4253
+ }
4254
+
4255
+ #endregion
4256
+
4257
+ {{#if includeAuthorization}}
4258
+ #region Authorization Tests
4259
+
4260
+ [Fact]
4261
+ public async Task GetAll_WhenUnauthorized_ShouldReturn401()
4262
+ {
4263
+ // Arrange
4264
+ var client = _factory.CreateClient();
4265
+ // Don't add auth header
4266
+
4267
+ // Act
4268
+ var response = await client.GetAsync("/api/{{nameLower}}");
4269
+
4270
+ // Assert
4271
+ response.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
4272
+ }
4273
+
4274
+ {{#unless isSystemEntity}}
4275
+ [Fact]
4276
+ public async Task GetById_WhenDifferentTenant_ShouldReturn403()
4277
+ {
4278
+ // Arrange
4279
+ var id = Guid.NewGuid();
4280
+ var client = _factory.CreateClient();
4281
+ client.DefaultRequestHeaders.Add("X-Tenant-Id", Guid.NewGuid().ToString());
4282
+
4283
+ _serviceMock
4284
+ .Setup(s => s.GetByIdAsync(id, It.IsAny<Guid>(), default))
4285
+ .ReturnsAsync(({{name}}?)null);
4286
+
4287
+ // Act
4288
+ var response = await client.GetAsync($"/api/{{nameLower}}/{id}");
4289
+
4290
+ // Assert
4291
+ response.StatusCode.Should().BeOneOf(HttpStatusCode.Forbidden, HttpStatusCode.NotFound);
4292
+ }
4293
+ {{/unless}}
4294
+
4295
+ #endregion
4296
+ {{/if}}
4297
+ }
4298
+ `;
4299
+ var validatorTestTemplate = `using FluentAssertions;
4300
+ using FluentValidation.TestHelper;
4301
+ using Xunit;
4302
+ using {{applicationNamespace}}.DTOs;
4303
+ using {{applicationNamespace}}.Validators;
4304
+
4305
+ namespace {{testNamespace}}.Unit.Validators;
4306
+
4307
+ /// <summary>
4308
+ /// Unit tests for {{name}} validators
4309
+ /// Tests: Validation rules, Error messages
4310
+ /// </summary>
4311
+ public class {{name}}ValidatorTests
4312
+ {
4313
+ private readonly Create{{name}}DtoValidator _createValidator;
4314
+ private readonly Update{{name}}DtoValidator _updateValidator;
4315
+
4316
+ public {{name}}ValidatorTests()
4317
+ {
4318
+ _createValidator = new Create{{name}}DtoValidator();
4319
+ _updateValidator = new Update{{name}}DtoValidator();
4320
+ }
4321
+
4322
+ #region Create Validator Tests
4323
+
4324
+ [Fact]
4325
+ public void CreateValidator_WhenCodeIsEmpty_ShouldHaveError()
4326
+ {
4327
+ // Arrange
4328
+ var dto = new Create{{name}}Dto { Code = string.Empty };
4329
+
4330
+ // Act
4331
+ var result = _createValidator.TestValidate(dto);
4332
+
4333
+ // Assert
4334
+ result.ShouldHaveValidationErrorFor(x => x.Code)
4335
+ .WithErrorMessage("*required*");
4336
+ }
4337
+
4338
+ [Fact]
4339
+ public void CreateValidator_WhenCodeIsTooLong_ShouldHaveError()
4340
+ {
4341
+ // Arrange
4342
+ var dto = new Create{{name}}Dto { Code = new string('a', 101) };
4343
+
4344
+ // Act
4345
+ var result = _createValidator.TestValidate(dto);
4346
+
4347
+ // Assert
4348
+ result.ShouldHaveValidationErrorFor(x => x.Code)
4349
+ .WithErrorMessage("*100 characters*");
4350
+ }
4351
+
4352
+ [Fact]
4353
+ public void CreateValidator_WhenValidData_ShouldNotHaveErrors()
4354
+ {
4355
+ // Arrange
4356
+ var dto = new Create{{name}}Dto { Code = "valid_code" };
4357
+
4358
+ // Act
4359
+ var result = _createValidator.TestValidate(dto);
4360
+
4361
+ // Assert
4362
+ result.ShouldNotHaveAnyValidationErrors();
4363
+ }
4364
+
4365
+ #endregion
4366
+
4367
+ #region Update Validator Tests
4368
+
4369
+ [Fact]
4370
+ public void UpdateValidator_WhenCodeIsEmpty_ShouldHaveError()
4371
+ {
4372
+ // Arrange
4373
+ var dto = new Update{{name}}Dto { Code = string.Empty };
4374
+
4375
+ // Act
4376
+ var result = _updateValidator.TestValidate(dto);
4377
+
4378
+ // Assert
4379
+ result.ShouldHaveValidationErrorFor(x => x.Code);
4380
+ }
4381
+
4382
+ [Fact]
4383
+ public void UpdateValidator_WhenValidData_ShouldNotHaveErrors()
4384
+ {
4385
+ // Arrange
4386
+ var dto = new Update{{name}}Dto { Code = "updated_code" };
4387
+
4388
+ // Act
4389
+ var result = _updateValidator.TestValidate(dto);
4390
+
4391
+ // Assert
4392
+ result.ShouldNotHaveAnyValidationErrors();
4393
+ }
4394
+
4395
+ #endregion
4396
+
4397
+ {{#if includeEdgeCases}}
4398
+ #region Edge Cases
4399
+
4400
+ [Theory]
4401
+ [InlineData("test-code")]
4402
+ [InlineData("test_code")]
4403
+ [InlineData("test.code")]
4404
+ public void CreateValidator_WhenCodeHasSpecialCharacters_ShouldPass(string code)
4405
+ {
4406
+ // Arrange
4407
+ var dto = new Create{{name}}Dto { Code = code };
4408
+
4409
+ // Act
4410
+ var result = _createValidator.TestValidate(dto);
4411
+
4412
+ // Assert
4413
+ result.ShouldNotHaveValidationErrorFor(x => x.Code);
4414
+ }
4415
+
4416
+ [Theory]
4417
+ [InlineData(" leading")]
4418
+ [InlineData("trailing ")]
4419
+ [InlineData(" both ")]
4420
+ public void CreateValidator_WhenCodeHasWhitespace_ShouldTrimAndValidate(string code)
4421
+ {
4422
+ // Arrange
4423
+ var dto = new Create{{name}}Dto { Code = code };
4424
+
4425
+ // Act
4426
+ var result = _createValidator.TestValidate(dto);
4427
+
4428
+ // Assert
4429
+ // Depending on implementation, may pass or fail
4430
+ // This test documents the expected behavior
4431
+ }
4432
+
4433
+ #endregion
4434
+ {{/if}}
4435
+ }
4436
+ `;
4437
+ var repositoryTestTemplate = `using System;
4438
+ using System.Collections.Generic;
4439
+ using System.Linq;
4440
+ using System.Threading.Tasks;
4441
+ using FluentAssertions;
4442
+ using Microsoft.EntityFrameworkCore;
4443
+ using Xunit;
4444
+ using {{infrastructureNamespace}}.Persistence;
4445
+ using {{infrastructureNamespace}}.Repositories;
4446
+ using {{domainNamespace}};
4447
+
4448
+ namespace {{testNamespace}}.Integration.Repositories;
4449
+
4450
+ /// <summary>
4451
+ /// Integration tests for {{name}}Repository
4452
+ /// Tests: CRUD with DbContext, Queries, Tenant scope
4453
+ /// </summary>
4454
+ public class {{name}}RepositoryTests : IDisposable
4455
+ {
4456
+ private readonly ApplicationDbContext _context;
4457
+ private readonly {{name}}Repository _repository;
4458
+ {{#unless isSystemEntity}}
4459
+ private readonly Guid _tenantId = Guid.NewGuid();
4460
+ {{/unless}}
4461
+
4462
+ public {{name}}RepositoryTests()
4463
+ {
4464
+ var options = new DbContextOptionsBuilder<ApplicationDbContext>()
4465
+ .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
4466
+ .Options;
4467
+
4468
+ _context = new ApplicationDbContext(options);
4469
+ _repository = new {{name}}Repository(_context);
4470
+ }
4471
+
4472
+ public void Dispose()
4473
+ {
4474
+ _context.Dispose();
4475
+ }
4476
+
4477
+ #region GetByIdAsync Tests
4478
+
4479
+ [Fact]
4480
+ public async Task GetByIdAsync_WhenEntityExists_ShouldReturnEntity()
4481
+ {
4482
+ // Arrange
4483
+ var entity = {{name}}.Create({{#unless isSystemEntity}}_tenantId, {{/unless}}"test");
4484
+ await _context.Set<{{name}}>().AddAsync(entity);
4485
+ await _context.SaveChangesAsync();
4486
+
4487
+ // Act
4488
+ var result = await _repository.GetByIdAsync(entity.Id);
4489
+
4490
+ // Assert
4491
+ result.Should().NotBeNull();
4492
+ result!.Id.Should().Be(entity.Id);
4493
+ }
4494
+
4495
+ [Fact]
4496
+ public async Task GetByIdAsync_WhenNotExists_ShouldReturnNull()
4497
+ {
4498
+ // Arrange
4499
+ var id = Guid.NewGuid();
4500
+
4501
+ // Act
4502
+ var result = await _repository.GetByIdAsync(id);
4503
+
4504
+ // Assert
4505
+ result.Should().BeNull();
4506
+ }
4507
+
4508
+ #endregion
4509
+
4510
+ #region AddAsync Tests
4511
+
4512
+ [Fact]
4513
+ public async Task AddAsync_ShouldPersistEntity()
4514
+ {
4515
+ // Arrange
4516
+ var entity = {{name}}.Create({{#unless isSystemEntity}}_tenantId, {{/unless}}"test");
4517
+
4518
+ // Act
4519
+ var result = await _repository.AddAsync(entity);
4520
+ await _context.SaveChangesAsync();
4521
+
4522
+ // Assert
4523
+ result.Should().NotBeNull();
4524
+ var persisted = await _context.Set<{{name}}>().FindAsync(entity.Id);
4525
+ persisted.Should().NotBeNull();
4526
+ }
4527
+
4528
+ #endregion
4529
+
4530
+ #region UpdateAsync Tests
4531
+
4532
+ [Fact]
4533
+ public async Task UpdateAsync_ShouldUpdateEntity()
4534
+ {
4535
+ // Arrange
4536
+ var entity = {{name}}.Create({{#unless isSystemEntity}}_tenantId, {{/unless}}"original");
4537
+ await _context.Set<{{name}}>().AddAsync(entity);
4538
+ await _context.SaveChangesAsync();
4539
+
4540
+ // Act
4541
+ entity.Update("updater");
4542
+ await _repository.UpdateAsync(entity);
4543
+ await _context.SaveChangesAsync();
4544
+
4545
+ // Assert
4546
+ var updated = await _context.Set<{{name}}>().FindAsync(entity.Id);
4547
+ updated!.UpdatedBy.Should().Be("updater");
4548
+ }
4549
+
4550
+ #endregion
4551
+
4552
+ #region DeleteAsync Tests
4553
+
4554
+ [Fact]
4555
+ public async Task DeleteAsync_ShouldRemoveEntity()
4556
+ {
4557
+ // Arrange
4558
+ var entity = {{name}}.Create({{#unless isSystemEntity}}_tenantId, {{/unless}}"test");
4559
+ await _context.Set<{{name}}>().AddAsync(entity);
4560
+ await _context.SaveChangesAsync();
4561
+
4562
+ // Act
4563
+ await _repository.DeleteAsync(entity);
4564
+ await _context.SaveChangesAsync();
4565
+
4566
+ // Assert
4567
+ var deleted = await _context.Set<{{name}}>().FindAsync(entity.Id);
4568
+ deleted.Should().BeNull();
4569
+ }
4570
+
4571
+ #endregion
4572
+
4573
+ {{#unless isSystemEntity}}
4574
+ #region Tenant Isolation Tests
4575
+
4576
+ [Fact]
4577
+ public async Task GetAllByTenantAsync_ShouldOnlyReturnTenantEntities()
4578
+ {
4579
+ // Arrange
4580
+ var otherTenantId = Guid.NewGuid();
4581
+ await _context.Set<{{name}}>().AddRangeAsync(
4582
+ {{name}}.Create(_tenantId, "entity1"),
4583
+ {{name}}.Create(_tenantId, "entity2"),
4584
+ {{name}}.Create(otherTenantId, "other_tenant_entity")
4585
+ );
4586
+ await _context.SaveChangesAsync();
4587
+
4588
+ // Act
4589
+ var result = await _repository.GetAllByTenantAsync(_tenantId);
4590
+
4591
+ // Assert
4592
+ result.Should().HaveCount(2);
4593
+ result.Should().OnlyContain(e => e.TenantId == _tenantId);
4594
+ }
4595
+
4596
+ [Fact]
4597
+ public async Task ExistsAsync_WhenEntityInDifferentTenant_ShouldReturnFalse()
4598
+ {
4599
+ // Arrange
4600
+ var otherTenantId = Guid.NewGuid();
4601
+ var entity = {{name}}.Create(otherTenantId, "test");
4602
+ await _context.Set<{{name}}>().AddAsync(entity);
4603
+ await _context.SaveChangesAsync();
4604
+
4605
+ // Act
4606
+ var result = await _repository.ExistsAsync(entity.Id, _tenantId);
4607
+
4608
+ // Assert
4609
+ result.Should().BeFalse();
4610
+ }
4611
+
4612
+ #endregion
4613
+ {{/unless}}
4614
+
4615
+ {{#if includeSoftDelete}}
4616
+ #region Soft Delete Tests
4617
+
4618
+ [Fact]
4619
+ public async Task GetAllAsync_ShouldExcludeSoftDeletedEntities()
4620
+ {
4621
+ // Arrange
4622
+ var activeEntity = {{name}}.Create({{#unless isSystemEntity}}_tenantId, {{/unless}}"active");
4623
+ var deletedEntity = {{name}}.Create({{#unless isSystemEntity}}_tenantId, {{/unless}}"deleted");
4624
+ deletedEntity.SoftDelete("deleter");
4625
+
4626
+ await _context.Set<{{name}}>().AddRangeAsync(activeEntity, deletedEntity);
4627
+ await _context.SaveChangesAsync();
4628
+
4629
+ // Act
4630
+ var result = await _repository.GetAllAsync();
4631
+
4632
+ // Assert
4633
+ result.Should().ContainSingle();
4634
+ result.First().Code.Should().Be("active");
4635
+ }
4636
+
4637
+ [Fact]
4638
+ public async Task GetAllIncludingDeletedAsync_ShouldIncludeSoftDeletedEntities()
4639
+ {
4640
+ // Arrange
4641
+ var activeEntity = {{name}}.Create({{#unless isSystemEntity}}_tenantId, {{/unless}}"active");
4642
+ var deletedEntity = {{name}}.Create({{#unless isSystemEntity}}_tenantId, {{/unless}}"deleted");
4643
+ deletedEntity.SoftDelete("deleter");
4644
+
4645
+ await _context.Set<{{name}}>().AddRangeAsync(activeEntity, deletedEntity);
4646
+ await _context.SaveChangesAsync();
4647
+
4648
+ // Act
4649
+ var result = await _repository.GetAllIncludingDeletedAsync();
4650
+
4651
+ // Assert
4652
+ result.Should().HaveCount(2);
4653
+ }
4654
+
4655
+ #endregion
4656
+ {{/if}}
4657
+ }
4658
+ `;
4659
+ var securityTestTemplate = `using System;
4660
+ using System.Net;
4661
+ using System.Net.Http;
4662
+ using System.Threading.Tasks;
4663
+ using FluentAssertions;
4664
+ using Microsoft.AspNetCore.Mvc.Testing;
4665
+ using Xunit;
4666
+ using {{apiNamespace}};
4667
+
4668
+ namespace {{testNamespace}}.Security;
4669
+
4670
+ /// <summary>
4671
+ /// Security tests for {{name}}
4672
+ /// Tests: Tenant isolation, Authorization, Input validation, Injection prevention
4673
+ /// </summary>
4674
+ public class {{name}}SecurityTests : IClassFixture<WebApplicationFactory<Program>>
4675
+ {
4676
+ private readonly WebApplicationFactory<Program> _factory;
4677
+ {{#unless isSystemEntity}}
4678
+ private readonly Guid _tenantId = Guid.NewGuid();
4679
+ {{/unless}}
4680
+
4681
+ public {{name}}SecurityTests(WebApplicationFactory<Program> factory)
4682
+ {
4683
+ _factory = factory;
4684
+ }
4685
+
4686
+ {{#unless isSystemEntity}}
4687
+ #region Tenant Isolation Tests
4688
+
4689
+ [Fact]
4690
+ public async Task Request_WithoutTenantHeader_ShouldReturn400()
4691
+ {
4692
+ // Arrange
4693
+ var client = _factory.CreateClient();
4694
+ // Intentionally not adding X-Tenant-Id header
4695
+
4696
+ // Act
4697
+ var response = await client.GetAsync("/api/{{nameLower}}");
4698
+
4699
+ // Assert
4700
+ response.StatusCode.Should().BeOneOf(
4701
+ HttpStatusCode.BadRequest,
4702
+ HttpStatusCode.Unauthorized
4703
+ );
4704
+ }
4705
+
4706
+ [Fact]
4707
+ public async Task Request_WithInvalidTenantId_ShouldReturn400()
4708
+ {
4709
+ // Arrange
4710
+ var client = _factory.CreateClient();
4711
+ client.DefaultRequestHeaders.Add("X-Tenant-Id", "invalid-guid");
4712
+
4713
+ // Act
4714
+ var response = await client.GetAsync("/api/{{nameLower}}");
4715
+
4716
+ // Assert
4717
+ response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
4718
+ }
4719
+
4720
+ [Fact]
4721
+ public async Task Request_WithEmptyTenantId_ShouldReturn400()
4722
+ {
4723
+ // Arrange
4724
+ var client = _factory.CreateClient();
4725
+ client.DefaultRequestHeaders.Add("X-Tenant-Id", Guid.Empty.ToString());
4726
+
4727
+ // Act
4728
+ var response = await client.GetAsync("/api/{{nameLower}}");
4729
+
4730
+ // Assert
4731
+ response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
4732
+ }
4733
+
4734
+ #endregion
4735
+ {{/unless}}
4736
+
4737
+ #region Input Validation Security Tests
4738
+
4739
+ [Theory]
4740
+ [InlineData("<script>alert('xss')</script>")]
4741
+ [InlineData("'; DROP TABLE users; --")]
4742
+ [InlineData("{{'{{'}}constructor.prototype{{'}}'}}")]
4743
+ public async Task Create_WithMaliciousInput_ShouldSanitizeOrReject(string maliciousInput)
4744
+ {
4745
+ // Arrange
4746
+ var client = _factory.CreateClient();
4747
+ {{#unless isSystemEntity}}
4748
+ client.DefaultRequestHeaders.Add("X-Tenant-Id", _tenantId.ToString());
4749
+ {{/unless}}
4750
+ var request = new { Code = maliciousInput };
4751
+
4752
+ // Act
4753
+ var response = await client.PostAsJsonAsync("/api/{{nameLower}}", request);
4754
+
4755
+ // Assert
4756
+ // Should either reject (400) or sanitize (201 with cleaned data)
4757
+ response.StatusCode.Should().BeOneOf(
4758
+ HttpStatusCode.BadRequest,
4759
+ HttpStatusCode.Created,
4760
+ HttpStatusCode.UnprocessableEntity
4761
+ );
4762
+ }
4763
+
4764
+ [Theory]
4765
+ [InlineData("../../../etc/passwd")]
4766
+ [InlineData("..\\\\..\\\\..\\\\windows\\\\system32")]
4767
+ [InlineData("....//....//....//etc/passwd")]
4768
+ public async Task Create_WithPathTraversalAttempt_ShouldReject(string pathTraversal)
4769
+ {
4770
+ // Arrange
4771
+ var client = _factory.CreateClient();
4772
+ {{#unless isSystemEntity}}
4773
+ client.DefaultRequestHeaders.Add("X-Tenant-Id", _tenantId.ToString());
4774
+ {{/unless}}
4775
+ var request = new { Code = pathTraversal };
4776
+
4777
+ // Act
4778
+ var response = await client.PostAsJsonAsync("/api/{{nameLower}}", request);
4779
+
4780
+ // Assert
4781
+ response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
4782
+ }
4783
+
4784
+ #endregion
4785
+
4786
+ #region ID Manipulation Tests
4787
+
4788
+ [Fact]
4789
+ public async Task GetById_WithNegativeId_ShouldReturn400Or404()
4790
+ {
4791
+ // Arrange
4792
+ var client = _factory.CreateClient();
4793
+ {{#unless isSystemEntity}}
4794
+ client.DefaultRequestHeaders.Add("X-Tenant-Id", _tenantId.ToString());
4795
+ {{/unless}}
4796
+
4797
+ // Act
4798
+ var response = await client.GetAsync("/api/{{nameLower}}/00000000-0000-0000-0000-000000000000");
4799
+
4800
+ // Assert
4801
+ response.StatusCode.Should().BeOneOf(
4802
+ HttpStatusCode.BadRequest,
4803
+ HttpStatusCode.NotFound
4804
+ );
4805
+ }
4806
+
4807
+ [Fact]
4808
+ public async Task GetById_WithMalformedGuid_ShouldReturn400()
4809
+ {
4810
+ // Arrange
4811
+ var client = _factory.CreateClient();
4812
+ {{#unless isSystemEntity}}
4813
+ client.DefaultRequestHeaders.Add("X-Tenant-Id", _tenantId.ToString());
4814
+ {{/unless}}
4815
+
4816
+ // Act
4817
+ var response = await client.GetAsync("/api/{{nameLower}}/not-a-guid");
4818
+
4819
+ // Assert
4820
+ response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
4821
+ }
4822
+
4823
+ #endregion
4824
+
4825
+ #region Rate Limiting Tests (if applicable)
4826
+
4827
+ [Fact]
4828
+ public async Task MultipleRapidRequests_ShouldNotCauseDenialOfService()
4829
+ {
4830
+ // Arrange
4831
+ var client = _factory.CreateClient();
4832
+ {{#unless isSystemEntity}}
4833
+ client.DefaultRequestHeaders.Add("X-Tenant-Id", _tenantId.ToString());
4834
+ {{/unless}}
4835
+ var tasks = new List<Task<HttpResponseMessage>>();
4836
+
4837
+ // Act
4838
+ for (int i = 0; i < 10; i++)
4839
+ {
4840
+ tasks.Add(client.GetAsync("/api/{{nameLower}}"));
4841
+ }
4842
+ var responses = await Task.WhenAll(tasks);
4843
+
4844
+ // Assert
4845
+ // Should handle without crashing (may return 429 if rate limiting is enabled)
4846
+ responses.Should().OnlyContain(r =>
4847
+ r.StatusCode == HttpStatusCode.OK ||
4848
+ r.StatusCode == HttpStatusCode.TooManyRequests);
4849
+ }
4850
+
4851
+ #endregion
4852
+ }
4853
+ `;
4854
+ async function handleScaffoldTests(args, config) {
4855
+ const input = ScaffoldTestsInputSchema.parse(args);
4856
+ const dryRun = input.options?.dryRun || false;
4857
+ logger.info("Scaffolding tests", { target: input.target, name: input.name, dryRun });
4858
+ const structure = await findSmartStackStructure(config.smartstack.projectPath);
4859
+ const result = {
4860
+ success: true,
4861
+ files: [],
4862
+ instructions: []
4863
+ };
4864
+ const options = {
4865
+ includeEdgeCases: input.options?.includeEdgeCases ?? true,
4866
+ includeTenantIsolation: input.options?.includeTenantIsolation ?? true,
4867
+ includeSoftDelete: input.options?.includeSoftDelete ?? true,
4868
+ includeAudit: input.options?.includeAudit ?? true,
4869
+ includeValidation: input.options?.includeValidation ?? true,
4870
+ includeAuthorization: input.options?.includeAuthorization ?? false,
4871
+ isSystemEntity: input.options?.isSystemEntity ?? false
4872
+ };
4873
+ const testTypes = input.testTypes || ["unit"];
4874
+ try {
4875
+ switch (input.target) {
4876
+ case "entity":
4877
+ await scaffoldEntityTests(input.name, options, testTypes, structure, config, result, dryRun);
4878
+ break;
4879
+ case "service":
4880
+ await scaffoldServiceTests(input.name, options, testTypes, structure, config, result, dryRun);
4881
+ break;
4882
+ case "controller":
4883
+ await scaffoldControllerTests(input.name, options, testTypes, structure, config, result, dryRun);
4884
+ break;
4885
+ case "validator":
4886
+ await scaffoldValidatorTests(input.name, options, testTypes, structure, config, result, dryRun);
4887
+ break;
4888
+ case "repository":
4889
+ await scaffoldRepositoryTests(input.name, options, testTypes, structure, config, result, dryRun);
4890
+ break;
4891
+ case "all":
4892
+ await scaffoldEntityTests(input.name, options, testTypes, structure, config, result, dryRun);
4893
+ await scaffoldServiceTests(input.name, options, testTypes, structure, config, result, dryRun);
4894
+ await scaffoldControllerTests(input.name, options, testTypes, structure, config, result, dryRun);
4895
+ await scaffoldValidatorTests(input.name, options, testTypes, structure, config, result, dryRun);
4896
+ await scaffoldRepositoryTests(input.name, options, testTypes, structure, config, result, dryRun);
4897
+ break;
4898
+ }
4899
+ } catch (error) {
4900
+ result.success = false;
4901
+ result.instructions.push(`Error: ${error instanceof Error ? error.message : String(error)}`);
4902
+ }
4903
+ return formatTestResult(result, input.target, input.name, dryRun);
4904
+ }
4905
+ async function scaffoldEntityTests(name, options, testTypes, structure, config, result, dryRun) {
4906
+ const testNamespace = `${config.conventions.namespaces.application}.Tests`;
4907
+ const domainNamespace = config.conventions.namespaces.domain;
4908
+ const context = {
4909
+ name,
4910
+ testNamespace,
4911
+ domainNamespace,
4912
+ ...options
4913
+ };
4914
+ if (testTypes.includes("unit")) {
4915
+ const content = Handlebars2.compile(entityTestTemplate)(context);
4916
+ const testPath = path10.join(structure.root, "Tests", "Unit", "Domain", `${name}Tests.cs`);
4917
+ validatePathSecurity(testPath, structure.root);
4918
+ if (!dryRun) {
4919
+ await ensureDirectory(path10.dirname(testPath));
4920
+ await writeText(testPath, content);
4921
+ }
4922
+ result.files.push({
4923
+ path: path10.relative(structure.root, testPath),
4924
+ content,
4925
+ type: "created"
4926
+ });
4927
+ }
4928
+ if (testTypes.includes("security")) {
4929
+ const securityContent = Handlebars2.compile(securityTestTemplate)({
4930
+ ...context,
4931
+ nameLower: name.charAt(0).toLowerCase() + name.slice(1),
4932
+ apiNamespace: config.conventions.namespaces.api
4933
+ });
4934
+ const securityPath = path10.join(structure.root, "Tests", "Security", `${name}SecurityTests.cs`);
4935
+ validatePathSecurity(securityPath, structure.root);
4936
+ if (!dryRun) {
4937
+ await ensureDirectory(path10.dirname(securityPath));
4938
+ await writeText(securityPath, securityContent);
4939
+ }
4940
+ result.files.push({
4941
+ path: path10.relative(structure.root, securityPath),
4942
+ content: securityContent,
4943
+ type: "created"
4944
+ });
4945
+ }
4946
+ result.instructions.push(`Add package reference: <PackageReference Include="FluentAssertions" Version="6.*" />`);
4947
+ result.instructions.push(`Add package reference: <PackageReference Include="xunit" Version="2.*" />`);
4948
+ }
4949
+ async function scaffoldServiceTests(name, options, testTypes, structure, config, result, dryRun) {
4950
+ const testNamespace = `${config.conventions.namespaces.application}.Tests`;
4951
+ const applicationNamespace = config.conventions.namespaces.application;
4952
+ const domainNamespace = config.conventions.namespaces.domain;
4953
+ const context = {
4954
+ name,
4955
+ testNamespace,
4956
+ applicationNamespace,
4957
+ domainNamespace,
4958
+ ...options
4959
+ };
4960
+ if (testTypes.includes("unit")) {
4961
+ const content = Handlebars2.compile(serviceTestTemplate)(context);
4962
+ const testPath = path10.join(structure.root, "Tests", "Unit", "Services", `${name}ServiceTests.cs`);
4963
+ validatePathSecurity(testPath, structure.root);
4964
+ if (!dryRun) {
4965
+ await ensureDirectory(path10.dirname(testPath));
4966
+ await writeText(testPath, content);
4967
+ }
4968
+ result.files.push({
4969
+ path: path10.relative(structure.root, testPath),
4970
+ content,
4971
+ type: "created"
4972
+ });
4973
+ }
4974
+ result.instructions.push(`Add package reference: <PackageReference Include="Moq" Version="4.*" />`);
4975
+ }
4976
+ async function scaffoldControllerTests(name, options, testTypes, structure, config, result, dryRun) {
4977
+ const testNamespace = `${config.conventions.namespaces.application}.Tests`;
4978
+ const applicationNamespace = config.conventions.namespaces.application;
4979
+ const domainNamespace = config.conventions.namespaces.domain;
4980
+ const apiNamespace = config.conventions.namespaces.api;
4981
+ const context = {
4982
+ name,
4983
+ nameLower: name.charAt(0).toLowerCase() + name.slice(1),
4984
+ testNamespace,
4985
+ applicationNamespace,
4986
+ domainNamespace,
4987
+ apiNamespace,
4988
+ ...options
4989
+ };
4990
+ if (testTypes.includes("integration")) {
4991
+ const content = Handlebars2.compile(controllerTestTemplate)(context);
4992
+ const testPath = path10.join(structure.root, "Tests", "Integration", "Controllers", `${name}ControllerTests.cs`);
4993
+ validatePathSecurity(testPath, structure.root);
4994
+ if (!dryRun) {
4995
+ await ensureDirectory(path10.dirname(testPath));
4996
+ await writeText(testPath, content);
4997
+ }
4998
+ result.files.push({
4999
+ path: path10.relative(structure.root, testPath),
5000
+ content,
5001
+ type: "created"
5002
+ });
5003
+ }
5004
+ if (testTypes.includes("security")) {
5005
+ const securityContent = Handlebars2.compile(securityTestTemplate)(context);
5006
+ const securityPath = path10.join(structure.root, "Tests", "Security", `${name}SecurityTests.cs`);
5007
+ validatePathSecurity(securityPath, structure.root);
5008
+ if (!dryRun) {
5009
+ await ensureDirectory(path10.dirname(securityPath));
5010
+ await writeText(securityPath, securityContent);
5011
+ }
5012
+ result.files.push({
5013
+ path: path10.relative(structure.root, securityPath),
5014
+ content: securityContent,
5015
+ type: "created"
5016
+ });
5017
+ }
5018
+ result.instructions.push(`Add package reference: <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.*" />`);
5019
+ }
5020
+ async function scaffoldValidatorTests(name, options, testTypes, structure, config, result, dryRun) {
5021
+ const testNamespace = `${config.conventions.namespaces.application}.Tests`;
5022
+ const applicationNamespace = config.conventions.namespaces.application;
5023
+ const context = {
5024
+ name,
5025
+ testNamespace,
5026
+ applicationNamespace,
5027
+ ...options
5028
+ };
5029
+ if (testTypes.includes("unit")) {
5030
+ const content = Handlebars2.compile(validatorTestTemplate)(context);
5031
+ const testPath = path10.join(structure.root, "Tests", "Unit", "Validators", `${name}ValidatorTests.cs`);
5032
+ validatePathSecurity(testPath, structure.root);
5033
+ if (!dryRun) {
5034
+ await ensureDirectory(path10.dirname(testPath));
5035
+ await writeText(testPath, content);
5036
+ }
5037
+ result.files.push({
5038
+ path: path10.relative(structure.root, testPath),
5039
+ content,
5040
+ type: "created"
5041
+ });
5042
+ }
5043
+ result.instructions.push(`Add package reference: <PackageReference Include="FluentValidation.TestHelper" Version="11.*" />`);
5044
+ }
5045
+ async function scaffoldRepositoryTests(name, options, testTypes, structure, config, result, dryRun) {
5046
+ const testNamespace = `${config.conventions.namespaces.application}.Tests`;
5047
+ const infrastructureNamespace = config.conventions.namespaces.infrastructure;
5048
+ const domainNamespace = config.conventions.namespaces.domain;
5049
+ const context = {
5050
+ name,
5051
+ testNamespace,
5052
+ infrastructureNamespace,
5053
+ domainNamespace,
5054
+ ...options
5055
+ };
5056
+ if (testTypes.includes("integration")) {
5057
+ const content = Handlebars2.compile(repositoryTestTemplate)(context);
5058
+ const testPath = path10.join(structure.root, "Tests", "Integration", "Repositories", `${name}RepositoryTests.cs`);
5059
+ validatePathSecurity(testPath, structure.root);
5060
+ if (!dryRun) {
5061
+ await ensureDirectory(path10.dirname(testPath));
5062
+ await writeText(testPath, content);
5063
+ }
5064
+ result.files.push({
5065
+ path: path10.relative(structure.root, testPath),
5066
+ content,
5067
+ type: "created"
5068
+ });
5069
+ }
5070
+ result.instructions.push(`Add package reference: <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.*" />`);
5071
+ }
5072
+ function formatTestResult(result, _target, name, dryRun) {
5073
+ const lines = [];
5074
+ lines.push(`# Scaffold Tests: ${name}`);
5075
+ lines.push("");
5076
+ if (dryRun) {
5077
+ lines.push("> **DRY RUN MODE** - No files were written");
5078
+ lines.push("");
5079
+ }
5080
+ if (!result.success) {
5081
+ lines.push("## Error");
5082
+ lines.push("");
5083
+ lines.push(result.instructions.join("\n"));
5084
+ return lines.join("\n");
5085
+ }
5086
+ lines.push(`## Generated Test Files (${result.files.length})`);
5087
+ lines.push("");
5088
+ for (const file of result.files) {
5089
+ lines.push(`### \`${file.path}\``);
5090
+ lines.push("");
5091
+ lines.push("```csharp");
5092
+ lines.push(file.content);
5093
+ lines.push("```");
5094
+ lines.push("");
5095
+ }
5096
+ if (result.instructions.length > 0) {
5097
+ lines.push("## Next Steps");
5098
+ lines.push("");
5099
+ for (const instruction of result.instructions) {
5100
+ lines.push(`- ${instruction}`);
5101
+ }
5102
+ lines.push("");
5103
+ }
5104
+ lines.push("## Test Conventions Applied");
5105
+ lines.push("");
5106
+ lines.push("- **Naming**: `{MethodName}_When{Condition}_Should{ExpectedResult}`");
5107
+ lines.push("- **Pattern**: AAA (Arrange-Act-Assert)");
5108
+ lines.push("- **Assertions**: FluentAssertions (`.Should()`)");
5109
+ lines.push("- **Mocking**: Moq (`Mock<T>`)");
5110
+ lines.push("");
5111
+ return lines.join("\n");
5112
+ }
5113
+
5114
+ // src/tools/analyze-test-coverage.ts
5115
+ import path11 from "path";
5116
+ var analyzeTestCoverageTool = {
5117
+ name: "analyze_test_coverage",
5118
+ description: "Analyze test coverage for a SmartStack project. Identifies entities, services, and controllers without tests, calculates coverage ratios, and provides recommendations.",
5119
+ inputSchema: {
5120
+ type: "object",
5121
+ properties: {
5122
+ path: {
5123
+ type: "string",
5124
+ description: "Project path to analyze (default: configured SmartStack path)"
5125
+ },
5126
+ scope: {
5127
+ type: "string",
5128
+ enum: ["entity", "service", "controller", "all"],
5129
+ default: "all",
5130
+ description: "Scope of analysis"
5131
+ },
5132
+ outputFormat: {
5133
+ type: "string",
5134
+ enum: ["summary", "detailed", "json"],
5135
+ default: "summary",
5136
+ description: "Output format"
5137
+ },
5138
+ includeRecommendations: {
5139
+ type: "boolean",
5140
+ default: true,
5141
+ description: "Include recommendations for missing tests"
5142
+ }
5143
+ }
5144
+ }
5145
+ };
5146
+ async function handleAnalyzeTestCoverage(args, config) {
5147
+ const input = AnalyzeTestCoverageInputSchema.parse(args);
5148
+ const projectPath = input.path || config.smartstack.projectPath;
5149
+ logger.info("Analyzing test coverage", { projectPath, scope: input.scope });
5150
+ const structure = await findSmartStackStructure(projectPath);
5151
+ const result = {
5152
+ summary: { total: 0, tested: 0, coverage: 0 },
5153
+ entities: { total: 0, tested: 0, coverage: 0, items: [], missingTests: [] },
5154
+ services: { total: 0, tested: 0, coverage: 0, items: [], missingTests: [] },
5155
+ controllers: { total: 0, tested: 0, coverage: 0, items: [], missingTests: [] },
5156
+ missing: [],
5157
+ recommendations: []
5158
+ };
5159
+ try {
5160
+ if (input.scope === "all" || input.scope === "entity") {
5161
+ await analyzeEntityCoverage(structure, result);
5162
+ }
5163
+ if (input.scope === "all" || input.scope === "service") {
5164
+ await analyzeServiceCoverage(structure, result);
5165
+ }
5166
+ if (input.scope === "all" || input.scope === "controller") {
5167
+ await analyzeControllerCoverage(structure, result);
5168
+ }
5169
+ result.summary.total = result.entities.total + result.services.total + result.controllers.total;
5170
+ result.summary.tested = result.entities.tested + result.services.tested + result.controllers.tested;
5171
+ result.summary.coverage = result.summary.total > 0 ? Math.round(result.summary.tested / result.summary.total * 100) : 0;
5172
+ if (input.includeRecommendations) {
5173
+ generateRecommendations(result);
5174
+ }
5175
+ } catch (error) {
5176
+ logger.error("Error analyzing test coverage", error);
5177
+ return `Error: ${error instanceof Error ? error.message : String(error)}`;
5178
+ }
5179
+ return formatCoverageResult(result, input.outputFormat);
5180
+ }
5181
+ async function analyzeEntityCoverage(structure, result) {
5182
+ if (!structure.domain) {
5183
+ result.recommendations.push("Domain layer not found - cannot analyze entity coverage");
5184
+ return;
5185
+ }
5186
+ const entityFiles = await findFiles("**/*.cs", { cwd: structure.domain });
5187
+ const entityNames = extractComponentNames(entityFiles, ["Entity", "Aggregate"]);
5188
+ const testPath = path11.join(structure.root, "Tests", "Unit", "Domain");
5189
+ let testFiles = [];
5190
+ try {
5191
+ testFiles = await findFiles("**/*Tests.cs", { cwd: testPath });
5192
+ } catch {
5193
+ }
5194
+ const testedEntities = extractTestedNames(testFiles);
5195
+ result.entities.total = entityNames.length;
5196
+ result.entities.items = entityNames;
5197
+ result.entities.tested = entityNames.filter((e) => testedEntities.includes(e)).length;
5198
+ result.entities.coverage = result.entities.total > 0 ? Math.round(result.entities.tested / result.entities.total * 100) : 0;
5199
+ result.entities.missingTests = entityNames.filter((e) => !testedEntities.includes(e));
5200
+ for (const name of result.entities.missingTests) {
5201
+ result.missing.push({
5202
+ name,
5203
+ type: "entity",
5204
+ priority: determinePriority(name, "entity"),
5205
+ recommendation: `Create unit tests for ${name} entity (factory methods, soft delete, audit trail)`,
5206
+ suggestedTestFile: `Tests/Unit/Domain/${name}Tests.cs`
5207
+ });
5208
+ }
5209
+ }
5210
+ async function analyzeServiceCoverage(structure, result) {
5211
+ if (!structure.application) {
5212
+ result.recommendations.push("Application layer not found - cannot analyze service coverage");
5213
+ return;
5214
+ }
5215
+ const serviceFiles = await findFiles("**/I*Service.cs", { cwd: structure.application });
5216
+ const serviceNames = serviceFiles.map((f) => {
5217
+ const basename = path11.basename(f, ".cs");
5218
+ return basename.startsWith("I") ? basename.slice(1) : basename;
5219
+ }).filter((n) => n.endsWith("Service")).map((n) => n.replace(/Service$/, ""));
5220
+ const testPath = path11.join(structure.root, "Tests", "Unit", "Services");
5221
+ let testFiles = [];
5222
+ try {
5223
+ testFiles = await findFiles("**/*ServiceTests.cs", { cwd: testPath });
5224
+ } catch {
5225
+ }
5226
+ const testedServices = testFiles.map((f) => {
5227
+ const basename = path11.basename(f, ".cs");
5228
+ return basename.replace(/ServiceTests$/, "");
5229
+ });
5230
+ result.services.total = serviceNames.length;
5231
+ result.services.items = serviceNames;
5232
+ result.services.tested = serviceNames.filter((s) => testedServices.includes(s)).length;
5233
+ result.services.coverage = result.services.total > 0 ? Math.round(result.services.tested / result.services.total * 100) : 0;
5234
+ result.services.missingTests = serviceNames.filter((s) => !testedServices.includes(s));
5235
+ for (const name of result.services.missingTests) {
5236
+ result.missing.push({
5237
+ name,
5238
+ type: "service",
5239
+ priority: determinePriority(name, "service"),
5240
+ recommendation: `Create unit tests for ${name}Service (CRUD operations, business logic, error handling)`,
5241
+ suggestedTestFile: `Tests/Unit/Services/${name}ServiceTests.cs`
5242
+ });
5243
+ }
5244
+ }
5245
+ async function analyzeControllerCoverage(structure, result) {
5246
+ if (!structure.api) {
5247
+ result.recommendations.push("API layer not found - cannot analyze controller coverage");
5248
+ return;
5249
+ }
5250
+ const controllerFiles = await findFiles("**/*Controller.cs", { cwd: structure.api });
5251
+ const controllerNames = controllerFiles.map((f) => {
5252
+ const basename = path11.basename(f, ".cs");
5253
+ return basename.replace(/Controller$/, "");
5254
+ }).filter((n) => n !== "Base");
5255
+ const testPath = path11.join(structure.root, "Tests", "Integration", "Controllers");
5256
+ let testFiles = [];
5257
+ try {
5258
+ testFiles = await findFiles("**/*ControllerTests.cs", { cwd: testPath });
5259
+ } catch {
5260
+ }
5261
+ const testedControllers = testFiles.map((f) => {
5262
+ const basename = path11.basename(f, ".cs");
5263
+ return basename.replace(/ControllerTests$/, "");
5264
+ });
5265
+ result.controllers.total = controllerNames.length;
5266
+ result.controllers.items = controllerNames;
5267
+ result.controllers.tested = controllerNames.filter((c) => testedControllers.includes(c)).length;
5268
+ result.controllers.coverage = result.controllers.total > 0 ? Math.round(result.controllers.tested / result.controllers.total * 100) : 0;
5269
+ result.controllers.missingTests = controllerNames.filter((c) => !testedControllers.includes(c));
5270
+ for (const name of result.controllers.missingTests) {
5271
+ result.missing.push({
5272
+ name,
5273
+ type: "controller",
5274
+ priority: determinePriority(name, "controller"),
5275
+ recommendation: `Create integration tests for ${name}Controller (HTTP status codes, authorization, validation)`,
5276
+ suggestedTestFile: `Tests/Integration/Controllers/${name}ControllerTests.cs`
5277
+ });
5278
+ }
5279
+ }
5280
+ function extractComponentNames(files, excludeSuffixes = []) {
5281
+ return files.map((f) => path11.basename(f, ".cs")).filter((name) => {
5282
+ const excludePatterns = [
5283
+ "Configuration",
5284
+ "Extensions",
5285
+ "Handler",
5286
+ "Specification",
5287
+ "Repository",
5288
+ "Service",
5289
+ "Controller",
5290
+ "Dto",
5291
+ "Validator",
5292
+ ...excludeSuffixes
5293
+ ];
5294
+ return !excludePatterns.some((pattern) => name.endsWith(pattern));
5295
+ }).filter((name) => !name.startsWith("I")).filter((name) => !name.startsWith("Base"));
5296
+ }
5297
+ function extractTestedNames(testFiles) {
5298
+ return testFiles.map((f) => {
5299
+ const basename = path11.basename(f, ".cs");
5300
+ return basename.replace(/Tests$/, "");
5301
+ });
5302
+ }
5303
+ function determinePriority(name, type) {
5304
+ const criticalPatterns = ["User", "Auth", "Payment", "Order", "Tenant", "Security", "Permission", "Role"];
5305
+ const highPatterns = ["Account", "Invoice", "Transaction", "Session", "Token", "Subscription"];
5306
+ const nameLower = name.toLowerCase();
5307
+ if (criticalPatterns.some((p) => nameLower.includes(p.toLowerCase()))) {
5308
+ return "critical";
5309
+ }
5310
+ if (highPatterns.some((p) => nameLower.includes(p.toLowerCase()))) {
5311
+ return "high";
5312
+ }
5313
+ if (type === "controller") {
5314
+ return "high";
5315
+ }
5316
+ if (type === "service") {
5317
+ return "medium";
5318
+ }
5319
+ return "medium";
5320
+ }
5321
+ function generateRecommendations(result) {
5322
+ if (result.summary.coverage < 50) {
5323
+ result.recommendations.push("CRITICAL: Overall test coverage is below 50%. Prioritize adding tests for critical components.");
5324
+ } else if (result.summary.coverage < 80) {
5325
+ result.recommendations.push("Test coverage is below recommended 80% threshold. Consider adding more tests.");
5326
+ }
5327
+ if (result.entities.coverage < 60) {
5328
+ result.recommendations.push(`Entity coverage (${result.entities.coverage}%) is low. Focus on testing factory methods, soft delete, and audit trails.`);
5329
+ }
5330
+ if (result.services.coverage < 70) {
5331
+ result.recommendations.push(`Service coverage (${result.services.coverage}%) is low. Ensure CRUD operations and business logic are tested.`);
5332
+ }
5333
+ if (result.controllers.coverage < 80) {
5334
+ result.recommendations.push(`Controller coverage (${result.controllers.coverage}%) is low. Add integration tests for HTTP endpoints.`);
5335
+ }
5336
+ const criticalMissing = result.missing.filter((m) => m.priority === "critical");
5337
+ if (criticalMissing.length > 0) {
5338
+ result.recommendations.push(`${criticalMissing.length} CRITICAL component(s) have no tests: ${criticalMissing.map((m) => m.name).join(", ")}`);
5339
+ }
5340
+ const securityComponents = result.missing.filter(
5341
+ (m) => m.name.toLowerCase().includes("auth") || m.name.toLowerCase().includes("security") || m.name.toLowerCase().includes("permission")
5342
+ );
5343
+ if (securityComponents.length > 0) {
5344
+ result.recommendations.push(`Security-sensitive components without tests: ${securityComponents.map((m) => m.name).join(", ")}. Add security tests immediately.`);
5345
+ }
5346
+ }
5347
+ function formatCoverageResult(result, format) {
5348
+ if (format === "json") {
5349
+ return JSON.stringify(result, null, 2);
5350
+ }
5351
+ const lines = [];
5352
+ lines.push("# Test Coverage Analysis");
5353
+ lines.push("");
5354
+ const coverageEmoji = result.summary.coverage >= 80 ? "\u2705" : result.summary.coverage >= 50 ? "\u26A0\uFE0F" : "\u274C";
5355
+ lines.push("## Summary");
5356
+ lines.push("");
5357
+ lines.push(`| Metric | Value |`);
5358
+ lines.push(`|--------|-------|`);
5359
+ lines.push(`| **Overall Coverage** | ${coverageEmoji} ${result.summary.coverage}% (${result.summary.tested}/${result.summary.total}) |`);
5360
+ lines.push(`| Entities | ${formatCoverageCell(result.entities)} |`);
5361
+ lines.push(`| Services | ${formatCoverageCell(result.services)} |`);
5362
+ lines.push(`| Controllers | ${formatCoverageCell(result.controllers)} |`);
5363
+ lines.push("");
5364
+ if (format === "detailed") {
5365
+ if (result.entities.missingTests.length > 0) {
5366
+ lines.push("## Missing Entity Tests");
5367
+ lines.push("");
5368
+ for (const name of result.entities.missingTests) {
5369
+ lines.push(`- [ ] ${name}`);
5370
+ }
5371
+ lines.push("");
5372
+ }
5373
+ if (result.services.missingTests.length > 0) {
5374
+ lines.push("## Missing Service Tests");
5375
+ lines.push("");
5376
+ for (const name of result.services.missingTests) {
5377
+ lines.push(`- [ ] ${name}Service`);
5378
+ }
5379
+ lines.push("");
5380
+ }
5381
+ if (result.controllers.missingTests.length > 0) {
5382
+ lines.push("## Missing Controller Tests");
5383
+ lines.push("");
5384
+ for (const name of result.controllers.missingTests) {
5385
+ lines.push(`- [ ] ${name}Controller`);
5386
+ }
5387
+ lines.push("");
5388
+ }
5389
+ }
5390
+ if (result.missing.length > 0) {
5391
+ lines.push("## Missing Tests (By Priority)");
5392
+ lines.push("");
5393
+ const priorities = ["critical", "high", "medium", "low"];
5394
+ for (const priority of priorities) {
5395
+ const items = result.missing.filter((m) => m.priority === priority);
5396
+ if (items.length > 0) {
5397
+ const emoji = priority === "critical" ? "\u{1F534}" : priority === "high" ? "\u{1F7E0}" : priority === "medium" ? "\u{1F7E1}" : "\u{1F7E2}";
5398
+ lines.push(`### ${emoji} ${priority.charAt(0).toUpperCase() + priority.slice(1)} Priority`);
5399
+ lines.push("");
5400
+ lines.push("| Component | Type | Suggested Test File |");
5401
+ lines.push("|-----------|------|---------------------|");
5402
+ for (const item of items) {
5403
+ lines.push(`| ${item.name} | ${item.type} | \`${item.suggestedTestFile}\` |`);
5404
+ }
5405
+ lines.push("");
5406
+ }
5407
+ }
5408
+ }
5409
+ if (result.recommendations.length > 0) {
5410
+ lines.push("## Recommendations");
5411
+ lines.push("");
5412
+ for (const rec of result.recommendations) {
5413
+ const emoji = rec.includes("CRITICAL") ? "\u{1F534}" : rec.includes("low") ? "\u26A0\uFE0F" : "\u{1F4A1}";
5414
+ lines.push(`${emoji} ${rec}`);
5415
+ lines.push("");
5416
+ }
5417
+ }
5418
+ lines.push("## Quick Actions");
5419
+ lines.push("");
5420
+ lines.push("Generate tests for missing components using:");
5421
+ lines.push("");
5422
+ if (result.missing.length > 0) {
5423
+ const firstMissing = result.missing[0];
5424
+ lines.push("```");
5425
+ lines.push(`scaffold_tests(target: "${firstMissing.type}", name: "${firstMissing.name}")`);
5426
+ lines.push("```");
5427
+ }
5428
+ lines.push("");
5429
+ return lines.join("\n");
5430
+ }
5431
+ function formatCoverageCell(category) {
5432
+ const emoji = category.coverage >= 80 ? "\u2705" : category.coverage >= 50 ? "\u26A0\uFE0F" : "\u274C";
5433
+ return `${emoji} ${category.coverage}% (${category.tested}/${category.total})`;
5434
+ }
5435
+
5436
+ // src/tools/validate-test-conventions.ts
5437
+ import path12 from "path";
5438
+ var validateTestConventionsTool = {
5439
+ name: "validate_test_conventions",
5440
+ description: "Validate that tests in a SmartStack project follow conventions: naming ({Method}_When{Condition}_Should{Result}), structure (Tests/Unit, Tests/Integration), patterns (AAA), assertions (FluentAssertions), and mocking (Moq).",
5441
+ inputSchema: {
5442
+ type: "object",
5443
+ properties: {
5444
+ path: {
5445
+ type: "string",
5446
+ description: "Project path to validate (default: configured SmartStack path)"
5447
+ },
5448
+ checks: {
5449
+ type: "array",
5450
+ items: {
5451
+ type: "string",
5452
+ enum: ["naming", "structure", "patterns", "assertions", "mocking", "all"]
5453
+ },
5454
+ default: ["all"],
5455
+ description: "Types of convention checks to perform"
5456
+ },
5457
+ autoFix: {
5458
+ type: "boolean",
5459
+ default: false,
5460
+ description: "Automatically fix minor issues (naming only)"
5461
+ }
5462
+ }
5463
+ }
5464
+ };
5465
+ var PATTERNS = {
5466
+ // Test method naming: MethodName_WhenCondition_ShouldExpectedResult
5467
+ testMethodNaming: /^(\w+)_When(\w+)_Should(\w+)$/,
5468
+ // Alternative valid patterns
5469
+ testMethodNamingAlt: /^(\w+)_Should(\w+)_When(\w+)$/,
5470
+ // Fact/Theory attributes
5471
+ factAttribute: /\[Fact\]/,
5472
+ theoryAttribute: /\[Theory\]/,
5473
+ // AAA pattern comments
5474
+ arrangeComment: /\/\/\s*Arrange/i,
5475
+ actComment: /\/\/\s*Act/i,
5476
+ assertComment: /\/\/\s*Assert/i,
5477
+ // FluentAssertions
5478
+ fluentAssertions: /\.Should\(\)/,
5479
+ // Moq
5480
+ moqUsage: /new Mock<|Mock<\w+>/,
5481
+ moqSetup: /\.Setup\(|\.Verify\(/,
5482
+ // Test class naming
5483
+ testClassNaming: /public class (\w+)Tests/,
5484
+ // xUnit patterns
5485
+ asyncTestMethod: /public async Task (\w+)/,
5486
+ syncTestMethod: /public void (\w+)/
5487
+ };
5488
+ async function handleValidateTestConventions(args, config) {
5489
+ const input = ValidateTestConventionsInputSchema.parse(args);
5490
+ const projectPath = input.path || config.smartstack.projectPath;
5491
+ const checks = input.checks.includes("all") ? ["naming", "structure", "patterns", "assertions", "mocking"] : input.checks;
5492
+ logger.info("Validating test conventions", { projectPath, checks });
5493
+ const structure = await findSmartStackStructure(projectPath);
5494
+ const result = {
5495
+ valid: true,
5496
+ violations: [],
5497
+ suggestions: [],
5498
+ autoFixedCount: 0
5499
+ };
5500
+ const testsPath = path12.join(structure.root, "Tests");
5501
+ try {
5502
+ let testFiles = [];
5503
+ try {
5504
+ testFiles = await findFiles("**/*Tests.cs", { cwd: testsPath });
5505
+ } catch {
5506
+ result.suggestions.push("No Tests directory found. Create a Tests project with Unit and Integration subdirectories.");
5507
+ return formatValidationResult(result);
5508
+ }
5509
+ if (testFiles.length === 0) {
5510
+ result.suggestions.push("No test files found. Create test files following the pattern {ComponentName}Tests.cs");
5511
+ return formatValidationResult(result);
5512
+ }
5513
+ if (checks.includes("structure")) {
5514
+ await validateStructure(testsPath, testFiles, result);
5515
+ }
5516
+ for (const testFile of testFiles) {
5517
+ const fullPath = path12.join(testsPath, testFile);
5518
+ const content = await readText(fullPath);
5519
+ if (checks.includes("naming")) {
5520
+ validateNaming(testFile, content, result, input.autoFix);
5521
+ }
5522
+ if (checks.includes("patterns")) {
5523
+ validatePatterns(testFile, content, result);
5524
+ }
5525
+ if (checks.includes("assertions")) {
5526
+ validateAssertions(testFile, content, result);
5527
+ }
5528
+ if (checks.includes("mocking")) {
5529
+ validateMocking(testFile, content, result);
5530
+ }
5531
+ }
5532
+ result.valid = result.violations.filter((v) => v.severity === "error").length === 0;
5533
+ } catch (error) {
5534
+ logger.error("Error validating test conventions", error);
5535
+ return `Error: ${error instanceof Error ? error.message : String(error)}`;
5536
+ }
5537
+ return formatValidationResult(result);
5538
+ }
5539
+ async function validateStructure(testsPath, testFiles, result) {
5540
+ const expectedDirs = ["Unit", "Integration"];
5541
+ const foundDirs = /* @__PURE__ */ new Set();
5542
+ for (const file of testFiles) {
5543
+ const parts = file.split(path12.sep);
5544
+ if (parts.length > 1) {
5545
+ foundDirs.add(parts[0]);
5546
+ }
5547
+ }
5548
+ for (const dir of expectedDirs) {
5549
+ if (!foundDirs.has(dir)) {
5550
+ result.violations.push({
5551
+ type: "structure",
5552
+ severity: "warning",
5553
+ file: testsPath,
5554
+ message: `Missing expected directory: Tests/${dir}`,
5555
+ suggestion: `Create Tests/${dir} directory for ${dir.toLowerCase()} tests`,
5556
+ autoFixable: false
5557
+ });
5558
+ }
5559
+ }
5560
+ const hasUnitDomain = testFiles.some((f) => f.includes(path12.join("Unit", "Domain")));
5561
+ const hasIntegrationControllers = testFiles.some((f) => f.includes(path12.join("Integration", "Controllers")));
5562
+ if (!hasUnitDomain && testFiles.some((f) => f.includes("Unit"))) {
5563
+ result.suggestions.push("Consider organizing unit tests into subdirectories: Unit/Domain, Unit/Services, Unit/Validators");
5564
+ }
5565
+ if (!hasIntegrationControllers && testFiles.some((f) => f.includes("Integration"))) {
5566
+ result.suggestions.push("Consider organizing integration tests into subdirectories: Integration/Controllers, Integration/Repositories");
5567
+ }
5568
+ const hasSecurityTests = testFiles.some((f) => f.includes("Security"));
5569
+ if (!hasSecurityTests) {
5570
+ result.suggestions.push("Consider adding a Tests/Security directory for security-focused tests");
5571
+ }
5572
+ }
5573
+ function validateNaming(testFile, content, result, _autoFix) {
5574
+ const classMatch = content.match(/public class (\w+)/);
5575
+ if (classMatch) {
5576
+ const className = classMatch[1];
5577
+ if (!className.endsWith("Tests")) {
5578
+ result.violations.push({
5579
+ type: "naming",
5580
+ severity: "error",
5581
+ file: testFile,
5582
+ message: `Test class '${className}' should end with 'Tests'`,
5583
+ suggestion: `Rename to '${className}Tests'`,
5584
+ autoFixable: true
5585
+ });
5586
+ }
5587
+ }
5588
+ const methodMatches = content.matchAll(/\[(Fact|Theory)\]\s*\n\s*public (?:async Task|void) (\w+)\(/g);
5589
+ for (const match of methodMatches) {
5590
+ const methodName = match[2];
5591
+ const isValidName = PATTERNS.testMethodNaming.test(methodName) || PATTERNS.testMethodNamingAlt.test(methodName);
5592
+ if (!isValidName) {
5593
+ if (methodName.includes("Test") && !methodName.includes("_")) {
5594
+ result.violations.push({
5595
+ type: "naming",
5596
+ severity: "warning",
5597
+ file: testFile,
5598
+ message: `Test method '${methodName}' doesn't follow naming convention`,
5599
+ suggestion: `Rename to format: {Method}_When{Condition}_Should{Result} (e.g., GetById_WhenExists_ShouldReturnEntity)`,
5600
+ autoFixable: false
5601
+ });
5602
+ } else if (!methodName.includes("_")) {
5603
+ result.violations.push({
5604
+ type: "naming",
5605
+ severity: "warning",
5606
+ file: testFile,
5607
+ message: `Test method '${methodName}' should use underscores to separate parts`,
5608
+ suggestion: `Use format: {Method}_When{Condition}_Should{Result}`,
5609
+ autoFixable: false
5610
+ });
5611
+ }
5612
+ }
5613
+ }
5614
+ }
5615
+ function validatePatterns(testFile, content, result) {
5616
+ const methodBlocks = content.matchAll(/\[(Fact|Theory)\][^\[]*?public (?:async Task|void) \w+\([^)]*\)\s*\{([^}]+)\}/gs);
5617
+ for (const match of methodBlocks) {
5618
+ const methodBody = match[2];
5619
+ const hasArrange = PATTERNS.arrangeComment.test(methodBody);
5620
+ const hasAct = PATTERNS.actComment.test(methodBody);
5621
+ const hasAssert = PATTERNS.assertComment.test(methodBody);
5622
+ if (!hasArrange && !hasAct && !hasAssert && methodBody.split("\n").length > 3) {
5623
+ result.violations.push({
5624
+ type: "pattern",
5625
+ severity: "warning",
5626
+ file: testFile,
5627
+ message: "Test method missing AAA (Arrange-Act-Assert) comments",
5628
+ suggestion: "Add // Arrange, // Act, // Assert comments to improve readability",
5629
+ autoFixable: false
5630
+ });
5631
+ break;
5632
+ }
5633
+ }
5634
+ const asyncMethods = content.matchAll(/public async Task (\w+)\([^)]*\)\s*\{([^}]+)\}/gs);
5635
+ for (const match of asyncMethods) {
5636
+ const methodName = match[1];
5637
+ const methodBody = match[2];
5638
+ if (!methodBody.includes("await")) {
5639
+ result.violations.push({
5640
+ type: "pattern",
5641
+ severity: "warning",
5642
+ file: testFile,
5643
+ message: `Async test method '${methodName}' doesn't contain 'await'`,
5644
+ suggestion: "Either add await or change to synchronous void method",
5645
+ autoFixable: false
5646
+ });
5647
+ }
5648
+ }
5649
+ }
5650
+ function validateAssertions(testFile, content, result) {
5651
+ const hasFluentAssertions = PATTERNS.fluentAssertions.test(content);
5652
+ const hasXunitAssert = /Assert\.\w+\(/.test(content);
5653
+ if (hasXunitAssert && !hasFluentAssertions) {
5654
+ result.violations.push({
5655
+ type: "assertion",
5656
+ severity: "warning",
5657
+ file: testFile,
5658
+ message: "Using xUnit Assert instead of FluentAssertions",
5659
+ suggestion: "Replace Assert.Equal(expected, actual) with actual.Should().Be(expected)",
5660
+ autoFixable: false
5661
+ });
5662
+ }
5663
+ if (hasFluentAssertions) {
5664
+ if (content.includes(".Should().NotBe(null)")) {
5665
+ result.violations.push({
5666
+ type: "assertion",
5667
+ severity: "warning",
5668
+ file: testFile,
5669
+ message: "Use .Should().NotBeNull() instead of .Should().NotBe(null)",
5670
+ suggestion: "Replace .Should().NotBe(null) with .Should().NotBeNull()",
5671
+ autoFixable: true
5672
+ });
5673
+ }
5674
+ if (content.includes(".Should().Throw<") && content.includes("async")) {
5675
+ if (!content.includes(".Should().ThrowAsync<")) {
5676
+ result.suggestions.push("Use .Should().ThrowAsync<T>() for async exception testing");
5677
+ }
5678
+ }
5679
+ }
5680
+ const methodBlocks = content.matchAll(/\[(Fact|Theory)\][^\[]*?public (?:async Task|void) (\w+)\([^)]*\)\s*\{([^}]+)\}/gs);
5681
+ for (const match of methodBlocks) {
5682
+ const methodName = match[2];
5683
+ const methodBody = match[3];
5684
+ const hasAssertion = methodBody.includes(".Should()") || methodBody.includes("Assert.") || methodBody.includes("Verify(") || methodBody.includes("VerifyAll()");
5685
+ if (!hasAssertion) {
5686
+ result.violations.push({
5687
+ type: "assertion",
5688
+ severity: "error",
5689
+ file: testFile,
5690
+ message: `Test method '${methodName}' has no assertions`,
5691
+ suggestion: "Add at least one assertion to verify expected behavior",
5692
+ autoFixable: false
5693
+ });
5694
+ }
5695
+ }
5696
+ }
5697
+ function validateMocking(testFile, content, result) {
5698
+ const hasMoq = PATTERNS.moqUsage.test(content);
5699
+ if (hasMoq) {
5700
+ if (content.includes("new Mock<") && !content.includes(".Setup(")) {
5701
+ result.violations.push({
5702
+ type: "mocking",
5703
+ severity: "warning",
5704
+ file: testFile,
5705
+ message: "Mock objects created but no Setup() calls found",
5706
+ suggestion: "Add .Setup() calls to define mock behavior",
5707
+ autoFixable: false
5708
+ });
5709
+ }
5710
+ const hasSetup = content.includes(".Setup(");
5711
+ const hasVerify = content.includes(".Verify(") || content.includes(".VerifyAll()");
5712
+ if (hasSetup && !hasVerify) {
5713
+ result.suggestions.push(`Consider adding .Verify() calls in ${testFile} to verify mock interactions`);
5714
+ }
5715
+ if (content.includes("MockBehavior.Strict")) {
5716
+ result.suggestions.push("Using MockBehavior.Strict - ensure all mock calls are set up");
5717
+ }
5718
+ }
5719
+ if (testFile.includes("Controller")) {
5720
+ if (!hasMoq && !content.includes("WebApplicationFactory")) {
5721
+ result.violations.push({
5722
+ type: "mocking",
5723
+ severity: "warning",
5724
+ file: testFile,
5725
+ message: "Controller tests should use mocking or WebApplicationFactory",
5726
+ suggestion: "Use Moq for unit tests or WebApplicationFactory for integration tests",
5727
+ autoFixable: false
5728
+ });
5729
+ }
5730
+ }
5731
+ }
5732
+ function formatValidationResult(result) {
5733
+ const lines = [];
5734
+ lines.push("# Test Convention Validation");
5735
+ lines.push("");
5736
+ const status = result.valid ? "\u2705 PASSED" : "\u274C FAILED";
5737
+ const errorCount = result.violations.filter((v) => v.severity === "error").length;
5738
+ const warningCount = result.violations.filter((v) => v.severity === "warning").length;
5739
+ lines.push("## Summary");
5740
+ lines.push("");
5741
+ lines.push(`| Status | ${status} |`);
5742
+ lines.push("|--------|----------|");
5743
+ lines.push(`| Errors | ${errorCount} |`);
5744
+ lines.push(`| Warnings | ${warningCount} |`);
5745
+ if (result.autoFixedCount > 0) {
5746
+ lines.push(`| Auto-fixed | ${result.autoFixedCount} |`);
5747
+ }
5748
+ lines.push("");
5749
+ if (result.violations.length > 0) {
5750
+ lines.push("## Violations");
5751
+ lines.push("");
5752
+ const byType = /* @__PURE__ */ new Map();
5753
+ for (const v of result.violations) {
5754
+ const existing = byType.get(v.type) || [];
5755
+ existing.push(v);
5756
+ byType.set(v.type, existing);
5757
+ }
5758
+ for (const [type, violations] of byType) {
5759
+ lines.push(`### ${type.charAt(0).toUpperCase() + type.slice(1)} Violations`);
5760
+ lines.push("");
5761
+ for (const v of violations) {
5762
+ const emoji = v.severity === "error" ? "\u274C" : "\u26A0\uFE0F";
5763
+ lines.push(`${emoji} **${v.file}**`);
5764
+ lines.push(` - ${v.message}`);
5765
+ lines.push(` - \u{1F4A1} ${v.suggestion}`);
5766
+ if (v.autoFixable) {
5767
+ lines.push(` - \u{1F527} Auto-fixable`);
5768
+ }
5769
+ lines.push("");
5770
+ }
5771
+ }
5772
+ }
5773
+ if (result.suggestions.length > 0) {
5774
+ lines.push("## Suggestions");
5775
+ lines.push("");
5776
+ for (const suggestion of result.suggestions) {
5777
+ lines.push(`\u{1F4A1} ${suggestion}`);
5778
+ lines.push("");
5779
+ }
5780
+ }
5781
+ lines.push("## SmartStack Test Conventions");
5782
+ lines.push("");
5783
+ lines.push("### Naming");
5784
+ lines.push("- Files: `{ComponentName}Tests.cs`");
5785
+ lines.push("- Classes: `{ComponentName}Tests`");
5786
+ lines.push("- Methods: `{MethodName}_When{Condition}_Should{ExpectedResult}`");
5787
+ lines.push("");
5788
+ lines.push("### Structure");
5789
+ lines.push("```");
5790
+ lines.push("Tests/");
5791
+ lines.push("\u251C\u2500\u2500 Unit/");
5792
+ lines.push("\u2502 \u251C\u2500\u2500 Domain/");
5793
+ lines.push("\u2502 \u251C\u2500\u2500 Services/");
5794
+ lines.push("\u2502 \u2514\u2500\u2500 Validators/");
5795
+ lines.push("\u251C\u2500\u2500 Integration/");
5796
+ lines.push("\u2502 \u251C\u2500\u2500 Controllers/");
5797
+ lines.push("\u2502 \u2514\u2500\u2500 Repositories/");
5798
+ lines.push("\u2514\u2500\u2500 Security/");
5799
+ lines.push("```");
5800
+ lines.push("");
5801
+ lines.push("### Pattern (AAA)");
5802
+ lines.push("```csharp");
5803
+ lines.push("[Fact]");
5804
+ lines.push("public async Task GetById_WhenExists_ShouldReturnEntity()");
5805
+ lines.push("{");
5806
+ lines.push(" // Arrange");
5807
+ lines.push(" var id = Guid.NewGuid();");
5808
+ lines.push("");
5809
+ lines.push(" // Act");
5810
+ lines.push(" var result = await _sut.GetByIdAsync(id);");
5811
+ lines.push("");
5812
+ lines.push(" // Assert");
5813
+ lines.push(" result.Should().NotBeNull();");
5814
+ lines.push("}");
5815
+ lines.push("```");
5816
+ lines.push("");
5817
+ return lines.join("\n");
5818
+ }
5819
+
5820
+ // src/tools/suggest-test-scenarios.ts
5821
+ import path13 from "path";
5822
+ var suggestTestScenariosTool = {
5823
+ name: "suggest_test_scenarios",
5824
+ description: "Analyze source code and suggest test scenarios based on detected methods, parameters, and patterns. Generates comprehensive test case recommendations for SmartStack components.",
5825
+ inputSchema: {
5826
+ type: "object",
5827
+ properties: {
5828
+ target: {
5829
+ type: "string",
5830
+ enum: ["entity", "service", "controller", "file"],
5831
+ description: "Type of target to analyze"
5832
+ },
5833
+ name: {
5834
+ type: "string",
5835
+ description: "Component name or file path"
5836
+ },
5837
+ depth: {
5838
+ type: "string",
5839
+ enum: ["basic", "comprehensive", "security-focused"],
5840
+ default: "comprehensive",
5841
+ description: "Depth of analysis"
5842
+ }
5843
+ },
5844
+ required: ["target", "name"]
5845
+ }
5846
+ };
5847
+ var PATTERNS2 = {
5848
+ // Method signatures
5849
+ publicMethod: /public\s+(?:async\s+)?(?:Task<)?(\w+)>?\s+(\w+)\s*\(([^)]*)\)/g,
5850
+ // Parameter extraction
5851
+ parameter: /(?:(\w+)\s+)?(\w+)\s+(\w+)(?:\s*=\s*([^,)]+))?/g,
5852
+ // Property
5853
+ property: /public\s+(?:virtual\s+)?(\w+)\??\s+(\w+)\s*\{\s*get;/g,
5854
+ // Validation attributes
5855
+ validationAttribute: /\[(Required|MaxLength|MinLength|Range|EmailAddress|Phone|Url|RegularExpression)\s*(?:\(([^)]*)\))?\]/g,
5856
+ // Entity patterns
5857
+ baseEntity: /:\s*(?:BaseEntity|SystemEntity)/,
5858
+ tenantEntity: /ITenantEntity/,
5859
+ // Factory method
5860
+ factoryMethod: /public\s+static\s+\w+\s+Create\s*\(/,
5861
+ // Soft delete
5862
+ softDelete: /public\s+void\s+SoftDelete/
5863
+ };
5864
+ async function handleSuggestTestScenarios(args, config) {
5865
+ const input = SuggestTestScenariosInputSchema.parse(args);
5866
+ logger.info("Suggesting test scenarios", { target: input.target, name: input.name, depth: input.depth });
5867
+ const structure = await findSmartStackStructure(config.smartstack.projectPath);
5868
+ const result = {
5869
+ target: input.name,
5870
+ targetType: input.target,
5871
+ detectedMethods: [],
5872
+ scenarios: [],
5873
+ coverage: {
5874
+ happyPath: 0,
5875
+ validation: 0,
5876
+ errorHandling: 0,
5877
+ security: 0,
5878
+ edgeCases: 0
5879
+ }
5880
+ };
5881
+ try {
5882
+ const sourceFile = await findSourceFile(input.target, input.name, structure, config);
5883
+ if (!sourceFile) {
5884
+ return `Error: Could not find ${input.target} '${input.name}'`;
5885
+ }
5886
+ const content = await readText(sourceFile);
5887
+ result.detectedMethods = parseSourceCode(content);
5888
+ result.scenarios = generateScenarios(
5889
+ result.detectedMethods,
5890
+ content,
5891
+ input.target,
5892
+ input.depth
5893
+ );
5894
+ calculateCoverageDistribution(result);
5895
+ } catch (error) {
5896
+ logger.error("Error suggesting test scenarios", error);
5897
+ return `Error: ${error instanceof Error ? error.message : String(error)}`;
5898
+ }
5899
+ return formatScenariosResult(result, input.depth);
5900
+ }
5901
+ async function findSourceFile(target, name, structure, _config) {
5902
+ let searchPath;
5903
+ let pattern;
5904
+ switch (target) {
5905
+ case "entity":
5906
+ searchPath = structure.domain || structure.root;
5907
+ pattern = `**/${name}.cs`;
5908
+ break;
5909
+ case "service":
5910
+ searchPath = structure.application || structure.root;
5911
+ pattern = `**/${name}Service.cs`;
5912
+ break;
5913
+ case "controller":
5914
+ searchPath = structure.api || structure.root;
5915
+ pattern = `**/${name}Controller.cs`;
5916
+ break;
5917
+ case "file":
5918
+ return path13.isAbsolute(name) ? name : path13.join(structure.root, name);
5919
+ default:
5920
+ return null;
5921
+ }
5922
+ const files = await findFiles(pattern, { cwd: searchPath });
5923
+ if (files.length === 0) {
5924
+ const altPattern = `**/*${name}*.cs`;
5925
+ const altFiles = await findFiles(altPattern, { cwd: searchPath });
5926
+ if (altFiles.length > 0) {
5927
+ return path13.join(searchPath, altFiles[0]);
5928
+ }
5929
+ return null;
5930
+ }
5931
+ return path13.join(searchPath, files[0]);
5932
+ }
5933
+ function parseSourceCode(content) {
5934
+ const methods = [];
5935
+ PATTERNS2.publicMethod.lastIndex = 0;
5936
+ let match;
5937
+ while ((match = PATTERNS2.publicMethod.exec(content)) !== null) {
5938
+ const returnType = match[1];
5939
+ const methodName = match[2];
5940
+ const parametersStr = match[3];
5941
+ const parameters = [];
5942
+ if (parametersStr.trim()) {
5943
+ const params = parametersStr.split(",");
5944
+ for (const param of params) {
5945
+ const trimmed = param.trim();
5946
+ if (trimmed) {
5947
+ const parts = trimmed.split(/\s+/);
5948
+ if (parts.length >= 2) {
5949
+ parameters.push(`${parts[parts.length - 2]} ${parts[parts.length - 1]}`);
5950
+ }
5951
+ }
5952
+ }
5953
+ }
5954
+ const isAsync = content.substring(Math.max(0, match.index - 20), match.index).includes("async");
5955
+ const isPublic = content.substring(Math.max(0, match.index - 20), match.index).includes("public");
5956
+ if (isPublic && !methodName.startsWith("get_") && !methodName.startsWith("set_")) {
5957
+ methods.push({
5958
+ name: methodName,
5959
+ returnType,
5960
+ parameters,
5961
+ isAsync,
5962
+ visibility: "public"
5963
+ });
5964
+ }
5965
+ }
5966
+ return methods;
5967
+ }
5968
+ function generateScenarios(methods, content, target, depth) {
5969
+ const scenarios = [];
5970
+ const isEntity = PATTERNS2.baseEntity.test(content);
5971
+ const hasTenant = PATTERNS2.tenantEntity.test(content);
5972
+ const hasFactoryMethod = PATTERNS2.factoryMethod.test(content);
5973
+ const hasSoftDelete = PATTERNS2.softDelete.test(content);
5974
+ for (const method of methods) {
5975
+ const methodScenarios = generateMethodScenarios(
5976
+ method,
5977
+ { isEntity, hasTenant, hasFactoryMethod, hasSoftDelete },
5978
+ target,
5979
+ depth
5980
+ );
5981
+ scenarios.push(...methodScenarios);
5982
+ }
5983
+ if (isEntity && target === "entity") {
5984
+ scenarios.push(...generateEntityScenarios(content, hasTenant, hasSoftDelete, depth));
5985
+ }
5986
+ if (depth !== "basic") {
5987
+ scenarios.push(...generateSecurityScenarios(methods, hasTenant, target));
5988
+ }
5989
+ return scenarios;
5990
+ }
5991
+ function generateMethodScenarios(method, patterns, _target, depth) {
5992
+ const scenarios = [];
5993
+ const methodLower = method.name.toLowerCase();
5994
+ scenarios.push({
5995
+ methodName: method.name,
5996
+ scenarioName: "Happy Path",
5997
+ description: `Test ${method.name} with valid data`,
5998
+ type: "happy-path",
5999
+ priority: "critical",
6000
+ testMethodName: `${method.name}_WhenValidData_ShouldSucceed`,
6001
+ assertions: generateAssertions(method, "happy-path")
6002
+ });
6003
+ if (methodLower.includes("getbyid") || methodLower.includes("get")) {
6004
+ scenarios.push({
6005
+ methodName: method.name,
6006
+ scenarioName: "Not Found",
6007
+ description: `Test ${method.name} when entity does not exist`,
6008
+ type: "error-handling",
6009
+ priority: "high",
6010
+ testMethodName: `${method.name}_WhenNotExists_ShouldReturnNull`,
6011
+ assertions: ["result.Should().BeNull()"]
6012
+ });
6013
+ }
6014
+ if (methodLower.includes("create") || methodLower.includes("add")) {
6015
+ scenarios.push({
6016
+ methodName: method.name,
6017
+ scenarioName: "Validation Failure",
6018
+ description: `Test ${method.name} with invalid data`,
6019
+ type: "validation",
6020
+ priority: "high",
6021
+ testMethodName: `${method.name}_WhenInvalidData_ShouldThrow`,
6022
+ assertions: ["act.Should().ThrowAsync<ValidationException>()"]
6023
+ });
6024
+ if (patterns.hasTenant) {
6025
+ scenarios.push({
6026
+ methodName: method.name,
6027
+ scenarioName: "Missing Tenant",
6028
+ description: `Test ${method.name} without tenant context`,
6029
+ type: "security",
6030
+ priority: "critical",
6031
+ testMethodName: `${method.name}_WhenNoTenant_ShouldThrow`,
6032
+ assertions: ["act.Should().ThrowAsync<ArgumentException>()"]
6033
+ });
6034
+ }
6035
+ }
6036
+ if (methodLower.includes("update")) {
6037
+ scenarios.push({
6038
+ methodName: method.name,
6039
+ scenarioName: "Not Found",
6040
+ description: `Test ${method.name} when entity does not exist`,
6041
+ type: "error-handling",
6042
+ priority: "high",
6043
+ testMethodName: `${method.name}_WhenNotExists_ShouldThrow`,
6044
+ assertions: ["act.Should().ThrowAsync<InvalidOperationException>()"]
6045
+ });
6046
+ if (depth !== "basic") {
6047
+ scenarios.push({
6048
+ methodName: method.name,
6049
+ scenarioName: "Concurrent Update",
6050
+ description: `Test ${method.name} with concurrent modification`,
6051
+ type: "edge-case",
6052
+ priority: "medium",
6053
+ testMethodName: `${method.name}_WhenConcurrentUpdate_ShouldHandleConflict`,
6054
+ assertions: ["Should handle DbUpdateConcurrencyException"]
6055
+ });
6056
+ }
6057
+ }
6058
+ if (methodLower.includes("delete")) {
6059
+ scenarios.push({
6060
+ methodName: method.name,
6061
+ scenarioName: "Not Found",
6062
+ description: `Test ${method.name} when entity does not exist`,
6063
+ type: "error-handling",
6064
+ priority: "high",
6065
+ testMethodName: `${method.name}_WhenNotExists_ShouldReturnFalse`,
6066
+ assertions: ["result.Should().BeFalse()"]
6067
+ });
6068
+ if (patterns.hasSoftDelete) {
6069
+ scenarios.push({
6070
+ methodName: method.name,
6071
+ scenarioName: "Soft Delete",
6072
+ description: `Verify ${method.name} performs soft delete`,
6073
+ type: "happy-path",
6074
+ priority: "high",
6075
+ testMethodName: `${method.name}_WhenCalled_ShouldSoftDelete`,
6076
+ assertions: ["entity.IsDeleted.Should().BeTrue()", "entity.DeletedAt.Should().NotBeNull()"]
6077
+ });
6078
+ }
6079
+ }
6080
+ if (method.isAsync && depth !== "basic") {
6081
+ scenarios.push({
6082
+ methodName: method.name,
6083
+ scenarioName: "Cancellation",
6084
+ description: `Test ${method.name} with cancellation token`,
6085
+ type: "edge-case",
6086
+ priority: "low",
6087
+ testMethodName: `${method.name}_WhenCancelled_ShouldThrowOperationCancelled`,
6088
+ assertions: ["act.Should().ThrowAsync<OperationCanceledException>()"]
6089
+ });
6090
+ }
6091
+ return scenarios;
6092
+ }
6093
+ function generateEntityScenarios(content, hasTenant, hasSoftDelete, depth) {
6094
+ const scenarios = [];
6095
+ if (PATTERNS2.factoryMethod.test(content)) {
6096
+ scenarios.push({
6097
+ methodName: "Create",
6098
+ scenarioName: "Factory Method",
6099
+ description: "Test entity creation via factory method",
6100
+ type: "happy-path",
6101
+ priority: "critical",
6102
+ testMethodName: "Create_WhenValidData_ShouldCreateEntity",
6103
+ assertions: [
6104
+ "entity.Should().NotBeNull()",
6105
+ "entity.Id.Should().NotBeEmpty()",
6106
+ "entity.CreatedAt.Should().BeCloseTo(DateTime.UtcNow)"
6107
+ ]
6108
+ });
6109
+ if (hasTenant) {
6110
+ scenarios.push({
6111
+ methodName: "Create",
6112
+ scenarioName: "Invalid Tenant",
6113
+ description: "Test entity creation with empty tenant ID",
6114
+ type: "validation",
6115
+ priority: "critical",
6116
+ testMethodName: "Create_WhenEmptyTenantId_ShouldThrow",
6117
+ assertions: ["act.Should().Throw<ArgumentException>()"]
6118
+ });
6119
+ }
6120
+ }
6121
+ if (hasSoftDelete) {
6122
+ scenarios.push({
6123
+ methodName: "SoftDelete",
6124
+ scenarioName: "Soft Delete",
6125
+ description: "Test soft delete sets IsDeleted and DeletedAt",
6126
+ type: "happy-path",
6127
+ priority: "high",
6128
+ testMethodName: "SoftDelete_WhenCalled_ShouldSetDeletedFields",
6129
+ assertions: [
6130
+ "entity.IsDeleted.Should().BeTrue()",
6131
+ "entity.DeletedAt.Should().NotBeNull()",
6132
+ "entity.DeletedBy.Should().Be(deletedBy)"
6133
+ ]
6134
+ });
6135
+ if (depth !== "basic") {
6136
+ scenarios.push({
6137
+ methodName: "Restore",
6138
+ scenarioName: "Restore After Delete",
6139
+ description: "Test restoring a soft-deleted entity",
6140
+ type: "happy-path",
6141
+ priority: "medium",
6142
+ testMethodName: "Restore_WhenSoftDeleted_ShouldClearDeletedFields",
6143
+ assertions: [
6144
+ "entity.IsDeleted.Should().BeFalse()",
6145
+ "entity.DeletedAt.Should().BeNull()"
6146
+ ]
6147
+ });
6148
+ }
6149
+ }
6150
+ if (depth !== "basic") {
6151
+ scenarios.push({
6152
+ methodName: "Update",
6153
+ scenarioName: "Audit Trail",
6154
+ description: "Test that Update sets audit fields",
6155
+ type: "happy-path",
6156
+ priority: "medium",
6157
+ testMethodName: "Update_WhenCalled_ShouldSetAuditFields",
6158
+ assertions: [
6159
+ "entity.UpdatedAt.Should().NotBeNull()",
6160
+ "entity.UpdatedBy.Should().Be(updatedBy)",
6161
+ "entity.CreatedAt.Should().Be(originalCreatedAt)"
6162
+ ]
6163
+ });
6164
+ }
6165
+ return scenarios;
6166
+ }
6167
+ function generateSecurityScenarios(_methods, hasTenant, _target) {
6168
+ const scenarios = [];
6169
+ if (hasTenant) {
6170
+ scenarios.push({
6171
+ methodName: "TenantIsolation",
6172
+ scenarioName: "Cross-Tenant Access",
6173
+ description: "Test that entities from other tenants are not accessible",
6174
+ type: "security",
6175
+ priority: "critical",
6176
+ testMethodName: "GetById_WhenDifferentTenant_ShouldReturnNull",
6177
+ assertions: ["result.Should().BeNull()"]
6178
+ });
6179
+ }
6180
+ if (_target === "controller") {
6181
+ scenarios.push({
6182
+ methodName: "Authorization",
6183
+ scenarioName: "Unauthorized Access",
6184
+ description: "Test that unauthorized requests are rejected",
6185
+ type: "security",
6186
+ priority: "critical",
6187
+ testMethodName: "GetAll_WhenUnauthorized_ShouldReturn401",
6188
+ assertions: ["response.StatusCode.Should().Be(HttpStatusCode.Unauthorized)"]
6189
+ });
6190
+ scenarios.push({
6191
+ methodName: "InputValidation",
6192
+ scenarioName: "SQL Injection",
6193
+ description: "Test that SQL injection attempts are handled",
6194
+ type: "security",
6195
+ priority: "high",
6196
+ testMethodName: "Create_WhenSqlInjectionAttempt_ShouldSanitize",
6197
+ assertions: ["Should not execute malicious SQL"]
6198
+ });
6199
+ scenarios.push({
6200
+ methodName: "InputValidation",
6201
+ scenarioName: "XSS Prevention",
6202
+ description: "Test that XSS attempts are handled",
6203
+ type: "security",
6204
+ priority: "high",
6205
+ testMethodName: "Create_WhenXssAttempt_ShouldSanitize",
6206
+ assertions: ["Should not store or return script tags"]
6207
+ });
6208
+ }
6209
+ return scenarios;
6210
+ }
6211
+ function generateAssertions(method, type) {
6212
+ const assertions = [];
6213
+ if (type === "happy-path") {
6214
+ if (method.returnType === "void" || method.returnType === "Task") {
6215
+ assertions.push("Should complete without exception");
6216
+ } else if (method.returnType.includes("bool") || method.returnType === "Boolean") {
6217
+ assertions.push("result.Should().BeTrue()");
6218
+ } else {
6219
+ assertions.push("result.Should().NotBeNull()");
6220
+ }
6221
+ }
6222
+ return assertions;
6223
+ }
6224
+ function calculateCoverageDistribution(result) {
6225
+ const total = result.scenarios.length;
6226
+ if (total === 0) return;
6227
+ const counts = {
6228
+ happyPath: result.scenarios.filter((s) => s.type === "happy-path").length,
6229
+ validation: result.scenarios.filter((s) => s.type === "validation").length,
6230
+ errorHandling: result.scenarios.filter((s) => s.type === "error-handling").length,
6231
+ security: result.scenarios.filter((s) => s.type === "security").length,
6232
+ edgeCases: result.scenarios.filter((s) => s.type === "edge-case" || s.type === "performance").length
6233
+ };
6234
+ result.coverage = {
6235
+ happyPath: Math.round(counts.happyPath / total * 100),
6236
+ validation: Math.round(counts.validation / total * 100),
6237
+ errorHandling: Math.round(counts.errorHandling / total * 100),
6238
+ security: Math.round(counts.security / total * 100),
6239
+ edgeCases: Math.round(counts.edgeCases / total * 100)
6240
+ };
6241
+ }
6242
+ function formatScenariosResult(result, depth) {
6243
+ const lines = [];
6244
+ lines.push(`# Test Scenarios for ${result.target}`);
6245
+ lines.push("");
6246
+ lines.push(`> Analysis depth: **${depth}**`);
6247
+ lines.push("");
6248
+ if (result.detectedMethods.length > 0) {
6249
+ lines.push("## Detected Methods");
6250
+ lines.push("");
6251
+ lines.push("| Method | Return Type | Parameters | Async |");
6252
+ lines.push("|--------|-------------|------------|-------|");
6253
+ for (const method of result.detectedMethods) {
6254
+ const params = method.parameters.length > 0 ? method.parameters.join(", ") : "none";
6255
+ lines.push(`| ${method.name} | ${method.returnType} | ${params} | ${method.isAsync ? "Yes" : "No"} |`);
6256
+ }
6257
+ lines.push("");
6258
+ }
6259
+ lines.push("## Scenario Coverage Distribution");
6260
+ lines.push("");
6261
+ lines.push("```");
6262
+ lines.push(`Happy Path: ${"\u2588".repeat(Math.floor(result.coverage.happyPath / 5))}${"\u2591".repeat(20 - Math.floor(result.coverage.happyPath / 5))} ${result.coverage.happyPath}%`);
6263
+ lines.push(`Validation: ${"\u2588".repeat(Math.floor(result.coverage.validation / 5))}${"\u2591".repeat(20 - Math.floor(result.coverage.validation / 5))} ${result.coverage.validation}%`);
6264
+ lines.push(`Error Handling: ${"\u2588".repeat(Math.floor(result.coverage.errorHandling / 5))}${"\u2591".repeat(20 - Math.floor(result.coverage.errorHandling / 5))} ${result.coverage.errorHandling}%`);
6265
+ lines.push(`Security: ${"\u2588".repeat(Math.floor(result.coverage.security / 5))}${"\u2591".repeat(20 - Math.floor(result.coverage.security / 5))} ${result.coverage.security}%`);
6266
+ lines.push(`Edge Cases: ${"\u2588".repeat(Math.floor(result.coverage.edgeCases / 5))}${"\u2591".repeat(20 - Math.floor(result.coverage.edgeCases / 5))} ${result.coverage.edgeCases}%`);
6267
+ lines.push("```");
6268
+ lines.push("");
6269
+ lines.push("## Suggested Test Scenarios");
6270
+ lines.push("");
6271
+ const priorities = ["critical", "high", "medium", "low"];
6272
+ for (const priority of priorities) {
6273
+ const scenarios = result.scenarios.filter((s) => s.priority === priority);
6274
+ if (scenarios.length === 0) continue;
6275
+ const emoji = priority === "critical" ? "\u{1F534}" : priority === "high" ? "\u{1F7E0}" : priority === "medium" ? "\u{1F7E1}" : "\u{1F7E2}";
6276
+ lines.push(`### ${emoji} ${priority.charAt(0).toUpperCase() + priority.slice(1)} Priority`);
6277
+ lines.push("");
6278
+ for (const scenario of scenarios) {
6279
+ const typeEmoji = getTypeEmoji(scenario.type);
6280
+ lines.push(`#### ${typeEmoji} ${scenario.methodName} - ${scenario.scenarioName}`);
6281
+ lines.push("");
6282
+ lines.push(`**Description:** ${scenario.description}`);
6283
+ lines.push("");
6284
+ lines.push(`**Test Method:** \`${scenario.testMethodName}\``);
6285
+ lines.push("");
6286
+ if (scenario.assertions.length > 0) {
6287
+ lines.push("**Assertions:**");
6288
+ for (const assertion of scenario.assertions) {
6289
+ lines.push(`- \`${assertion}\``);
6290
+ }
6291
+ lines.push("");
6292
+ }
6293
+ }
6294
+ }
6295
+ lines.push("## Quick Test Generation");
6296
+ lines.push("");
6297
+ lines.push("Generate tests for this component:");
6298
+ lines.push("");
6299
+ lines.push("```");
6300
+ lines.push(`scaffold_tests(target: "${result.targetType}", name: "${result.target}", testTypes: ["unit", "security"])`);
6301
+ lines.push("```");
6302
+ lines.push("");
6303
+ return lines.join("\n");
6304
+ }
6305
+ function getTypeEmoji(type) {
6306
+ switch (type) {
6307
+ case "happy-path":
6308
+ return "\u2705";
6309
+ case "validation":
6310
+ return "\u{1F50D}";
6311
+ case "error-handling":
6312
+ return "\u26A0\uFE0F";
6313
+ case "security":
6314
+ return "\u{1F512}";
6315
+ case "edge-case":
6316
+ return "\u{1F9EA}";
6317
+ case "performance":
6318
+ return "\u26A1";
6319
+ default:
6320
+ return "\u{1F4CB}";
6321
+ }
6322
+ }
6323
+
6324
+ // src/resources/conventions.ts
6325
+ var conventionsResourceTemplate = {
6326
+ uri: "smartstack://conventions",
6327
+ name: "AtlasHub Conventions",
6328
+ description: "Documentation of AtlasHub/SmartStack naming conventions, patterns, and best practices",
6329
+ mimeType: "text/markdown"
6330
+ };
6331
+ async function getConventionsResource(config) {
6332
+ const { schemas, tablePrefixes, codePrefixes, migrationFormat, namespaces, servicePattern } = config.conventions;
6333
+ return `# AtlasHub SmartStack Conventions
6334
+
6335
+ ## Overview
6336
+
6337
+ This document describes the mandatory conventions for extending the SmartStack/AtlasHub platform.
6338
+ Following these conventions ensures compatibility and prevents conflicts.
6339
+
6340
+ ---
6341
+
6342
+ ## 1. Database Conventions
6343
+
6344
+ ### SQL Schemas
6345
+
6346
+ SmartStack uses SQL Server schemas to separate platform tables from client extensions:
6347
+
6348
+ | Schema | Usage | Description |
6349
+ |--------|-------|-------------|
6350
+ | \`${schemas.platform}\` | SmartStack platform | All native SmartStack tables |
6351
+ | \`${schemas.extensions}\` | Client extensions | Custom tables added by clients |
6352
+
6353
+ ### Domain Table Prefixes
6354
+
6355
+ Tables are organized by domain using prefixes:
6356
+
6357
+ | Prefix | Domain | Example Tables |
6358
+ |--------|--------|----------------|
6359
+ | \`auth_\` | Authorization | auth_Users, auth_Roles, auth_Permissions |
6360
+ | \`nav_\` | Navigation | nav_Contexts, nav_Applications, nav_Modules |
6361
+ | \`usr_\` | User profiles | usr_Profiles, usr_Preferences |
6362
+ | \`ai_\` | AI features | ai_Providers, ai_Models, ai_Prompts |
6363
+ | \`cfg_\` | Configuration | cfg_Settings |
6364
+ | \`wkf_\` | Workflows | wkf_EmailTemplates, wkf_Workflows |
6365
+ | \`support_\` | Support | support_Tickets, support_Comments |
6366
+ | \`entra_\` | Entra sync | entra_Groups, entra_SyncState |
6367
+ | \`ref_\` | References | ref_Companies, ref_Departments |
6368
+ | \`loc_\` | Localization | loc_Languages, loc_Translations |
6369
+ | \`lic_\` | Licensing | lic_Licenses |
6370
+ | \`tenant_\` | Multi-Tenancy | tenant_Tenants, tenant_TenantUsers, tenant_TenantUserRoles |
6371
+
6372
+ ### Navigation Code Prefixes
6373
+
6374
+ All navigation data (Context, Application, Module, Section, Resource) uses code prefixes to distinguish system data from client extensions:
6375
+
6376
+ | Origin | Prefix | Usage | Example |
6377
+ |--------|--------|-------|---------|
6378
+ | SmartStack (system) | \`${codePrefixes.core}\` | Protected, delivered with SmartStack | \`${codePrefixes.core}administration\` |
6379
+ | Client (extension) | \`${codePrefixes.extension}\` | Custom, added by clients | \`${codePrefixes.extension}it\` |
6380
+
6381
+ **Navigation Hierarchy (5 levels):**
6382
+
6383
+ \`\`\`
6384
+ Context \u2192 Application \u2192 Module \u2192 Section \u2192 Resource
6385
+ \`\`\`
6386
+
6387
+ **Examples for each level:**
6388
+
6389
+ | Level | Core Example | Extension Example |
6390
+ |-------|--------------|-------------------|
6391
+ | Context | \`${codePrefixes.core}administration\` | \`${codePrefixes.extension}it\` |
6392
+ | Application | \`${codePrefixes.core}settings\` | \`${codePrefixes.extension}custom_app\` |
6393
+ | Module | \`${codePrefixes.core}users\` | \`${codePrefixes.extension}inventory\` |
6394
+ | Section | \`${codePrefixes.core}management\` | \`${codePrefixes.extension}reports\` |
6395
+ | Resource | \`${codePrefixes.core}user_list\` | \`${codePrefixes.extension}stock_view\` |
6396
+
6397
+ **Rules:**
6398
+ 1. \`${codePrefixes.core}*\` codes are **protected** - clients cannot create or modify them
6399
+ 2. \`${codePrefixes.extension}*\` codes are **free** - clients can create custom navigation
6400
+ 3. SmartStack updates will never overwrite \`${codePrefixes.extension}*\` data
6401
+ 4. Codes must be unique within their level (e.g., no two Contexts with same code)
6402
+
6403
+ ### Entity Configuration
6404
+
6405
+ \`\`\`csharp
6406
+ public class MyEntityConfiguration : IEntityTypeConfiguration<MyEntity>
6407
+ {
6408
+ public void Configure(EntityTypeBuilder<MyEntity> builder)
6409
+ {
6410
+ // CORRECT: Use schema + domain prefix
6411
+ builder.ToTable("auth_Users", "${schemas.platform}");
6412
+
6413
+ // WRONG: No schema specified
6414
+ // builder.ToTable("auth_Users");
6415
+
6416
+ // WRONG: No domain prefix
6417
+ // builder.ToTable("Users", "${schemas.platform}");
6418
+ }
6419
+ }
6420
+ \`\`\`
6421
+
6422
+ ### Extending Core Entities
6423
+
6424
+ When extending a core entity, use the \`${schemas.extensions}\` schema:
6425
+
6426
+ \`\`\`csharp
6427
+ // Client extension for auth_Users
6428
+ public class UserExtension
6429
+ {
6430
+ public Guid UserId { get; set; } // FK to auth_Users
6431
+ public User User { get; set; }
6432
+
6433
+ // Custom properties
6434
+ public string CustomField { get; set; }
6435
+ }
6436
+
6437
+ // Configuration - use extensions schema
6438
+ builder.ToTable("client_UserExtensions", "${schemas.extensions}");
6439
+ builder.HasOne(e => e.User)
6440
+ .WithOne()
6441
+ .HasForeignKey<UserExtension>(e => e.UserId);
6442
+ \`\`\`
6443
+
6444
+ ---
6445
+
6446
+ ## 2. Migration Conventions
6447
+
6448
+ ### Naming Format
6449
+
6450
+ Migrations MUST follow this naming pattern:
6451
+
6452
+ \`\`\`
6453
+ ${migrationFormat}
6454
+ \`\`\`
6455
+
6456
+ | Part | Description | Example |
6457
+ |------|-------------|---------|
6458
+ | \`{context}\` | DbContext name | \`core\`, \`extensions\` |
6459
+ | \`{version}\` | Semver version | \`v1.0.0\`, \`v1.2.0\` |
6460
+ | \`{sequence}\` | Order in version | \`001\`, \`002\` |
6461
+ | \`{Description}\` | Action (PascalCase) | \`CreateAuthUsers\` |
6462
+
6463
+ **Examples:**
6464
+ - \`core_v1.0.0_001_InitialSchema.cs\`
6465
+ - \`core_v1.0.0_002_CreateAuthUsers.cs\`
6466
+ - \`core_v1.2.0_001_AddUserProfiles.cs\`
6467
+ - \`extensions_v1.0.0_001_AddClientFeatures.cs\`
6468
+
6469
+ ### Creating Migrations
6470
+
6471
+ \`\`\`bash
6472
+ # Create a new migration
6473
+ dotnet ef migrations add core_v1.0.0_001_InitialSchema
6474
+
6475
+ # With context specified
6476
+ dotnet ef migrations add core_v1.2.0_001_AddUserProfiles --context ApplicationDbContext
6477
+ \`\`\`
6478
+
6479
+ ### Migration Rules
6480
+
6481
+ 1. **One migration per feature** - Group related changes in a single migration
6482
+ 2. **Version-based naming** - Use semver (v1.0.0, v1.2.0) to link migrations to releases
6483
+ 3. **Sequence numbers** - Use NNN (001, 002, etc.) for migrations in the same version
6484
+ 4. **Context prefix** - Use \`core_\` for platform tables, \`extensions_\` for client extensions
6485
+ 5. **Descriptive names** - Use clear PascalCase descriptions (CreateAuthUsers, AddUserProfiles, etc.)
6486
+ 6. **Schema must be specified** - All tables must specify their schema in ToTable()
6487
+
6488
+ ---
6489
+
6490
+ ## 3. Namespace Conventions
6491
+
6492
+ ### Layer Structure
3618
6493
 
3619
6494
  | Layer | Namespace | Purpose |
3620
6495
  |-------|-----------|---------|
@@ -4432,7 +7307,7 @@ Run specific or all checks:
4432
7307
  }
4433
7308
 
4434
7309
  // src/resources/project-info.ts
4435
- import path10 from "path";
7310
+ import path14 from "path";
4436
7311
  var projectInfoResourceTemplate = {
4437
7312
  uri: "smartstack://project",
4438
7313
  name: "SmartStack Project Info",
@@ -4469,16 +7344,16 @@ async function getProjectInfoResource(config) {
4469
7344
  lines.push("```");
4470
7345
  lines.push(`${projectInfo.name}/`);
4471
7346
  if (structure.domain) {
4472
- lines.push(`\u251C\u2500\u2500 ${path10.basename(structure.domain)}/ # Domain layer (entities)`);
7347
+ lines.push(`\u251C\u2500\u2500 ${path14.basename(structure.domain)}/ # Domain layer (entities)`);
4473
7348
  }
4474
7349
  if (structure.application) {
4475
- lines.push(`\u251C\u2500\u2500 ${path10.basename(structure.application)}/ # Application layer (services)`);
7350
+ lines.push(`\u251C\u2500\u2500 ${path14.basename(structure.application)}/ # Application layer (services)`);
4476
7351
  }
4477
7352
  if (structure.infrastructure) {
4478
- lines.push(`\u251C\u2500\u2500 ${path10.basename(structure.infrastructure)}/ # Infrastructure (EF Core)`);
7353
+ lines.push(`\u251C\u2500\u2500 ${path14.basename(structure.infrastructure)}/ # Infrastructure (EF Core)`);
4479
7354
  }
4480
7355
  if (structure.api) {
4481
- lines.push(`\u251C\u2500\u2500 ${path10.basename(structure.api)}/ # API layer (controllers)`);
7356
+ lines.push(`\u251C\u2500\u2500 ${path14.basename(structure.api)}/ # API layer (controllers)`);
4482
7357
  }
4483
7358
  if (structure.web) {
4484
7359
  lines.push(`\u2514\u2500\u2500 web/smartstack-web/ # React frontend`);
@@ -4491,8 +7366,8 @@ async function getProjectInfoResource(config) {
4491
7366
  lines.push("| Project | Path |");
4492
7367
  lines.push("|---------|------|");
4493
7368
  for (const csproj of projectInfo.csprojFiles) {
4494
- const name = path10.basename(csproj, ".csproj");
4495
- const relativePath = path10.relative(projectPath, csproj);
7369
+ const name = path14.basename(csproj, ".csproj");
7370
+ const relativePath = path14.relative(projectPath, csproj);
4496
7371
  lines.push(`| ${name} | \`${relativePath}\` |`);
4497
7372
  }
4498
7373
  lines.push("");
@@ -4502,10 +7377,10 @@ async function getProjectInfoResource(config) {
4502
7377
  cwd: structure.migrations,
4503
7378
  ignore: ["*.Designer.cs"]
4504
7379
  });
4505
- const migrations = migrationFiles.map((f) => path10.basename(f)).filter((f) => !f.includes("ModelSnapshot") && !f.includes(".Designer.")).sort();
7380
+ const migrations = migrationFiles.map((f) => path14.basename(f)).filter((f) => !f.includes("ModelSnapshot") && !f.includes(".Designer.")).sort();
4506
7381
  lines.push("## EF Core Migrations");
4507
7382
  lines.push("");
4508
- lines.push(`**Location**: \`${path10.relative(projectPath, structure.migrations)}\``);
7383
+ lines.push(`**Location**: \`${path14.relative(projectPath, structure.migrations)}\``);
4509
7384
  lines.push(`**Total Migrations**: ${migrations.length}`);
4510
7385
  lines.push("");
4511
7386
  if (migrations.length > 0) {
@@ -4540,11 +7415,11 @@ async function getProjectInfoResource(config) {
4540
7415
  lines.push("dotnet build");
4541
7416
  lines.push("");
4542
7417
  lines.push("# Run API");
4543
- lines.push(`cd ${structure.api ? path10.relative(projectPath, structure.api) : "SmartStack.Api"}`);
7418
+ lines.push(`cd ${structure.api ? path14.relative(projectPath, structure.api) : "SmartStack.Api"}`);
4544
7419
  lines.push("dotnet run");
4545
7420
  lines.push("");
4546
7421
  lines.push("# Run frontend");
4547
- lines.push(`cd ${structure.web ? path10.relative(projectPath, structure.web) : "web/smartstack-web"}`);
7422
+ lines.push(`cd ${structure.web ? path14.relative(projectPath, structure.web) : "web/smartstack-web"}`);
4548
7423
  lines.push("npm run dev");
4549
7424
  lines.push("");
4550
7425
  lines.push("# Create migration");
@@ -4567,7 +7442,7 @@ async function getProjectInfoResource(config) {
4567
7442
  }
4568
7443
 
4569
7444
  // src/resources/api-endpoints.ts
4570
- import path11 from "path";
7445
+ import path15 from "path";
4571
7446
  var apiEndpointsResourceTemplate = {
4572
7447
  uri: "smartstack://api/",
4573
7448
  name: "SmartStack API Endpoints",
@@ -4592,7 +7467,7 @@ async function getApiEndpointsResource(config, endpointFilter) {
4592
7467
  }
4593
7468
  async function parseController(filePath, _rootPath) {
4594
7469
  const content = await readText(filePath);
4595
- const fileName = path11.basename(filePath, ".cs");
7470
+ const fileName = path15.basename(filePath, ".cs");
4596
7471
  const controllerName = fileName.replace("Controller", "");
4597
7472
  const endpoints = [];
4598
7473
  const routeMatch = content.match(/\[Route\s*\(\s*"([^"]+)"\s*\)\]/);
@@ -4739,7 +7614,7 @@ function getMethodEmoji(method) {
4739
7614
  }
4740
7615
 
4741
7616
  // src/resources/db-schema.ts
4742
- import path12 from "path";
7617
+ import path16 from "path";
4743
7618
  var dbSchemaResourceTemplate = {
4744
7619
  uri: "smartstack://schema/",
4745
7620
  name: "SmartStack Database Schema",
@@ -4829,7 +7704,7 @@ async function parseEntity(filePath, rootPath, _config) {
4829
7704
  tableName,
4830
7705
  properties,
4831
7706
  relationships,
4832
- file: path12.relative(rootPath, filePath)
7707
+ file: path16.relative(rootPath, filePath)
4833
7708
  };
4834
7709
  }
4835
7710
  async function enrichFromConfigurations(entities, infrastructurePath, _config) {
@@ -4975,7 +7850,7 @@ function formatSchema(entities, filter, _config) {
4975
7850
  }
4976
7851
 
4977
7852
  // src/resources/entities.ts
4978
- import path13 from "path";
7853
+ import path17 from "path";
4979
7854
  var entitiesResourceTemplate = {
4980
7855
  uri: "smartstack://entities/",
4981
7856
  name: "SmartStack Entities",
@@ -5035,7 +7910,7 @@ async function parseEntitySummary(filePath, rootPath, config) {
5035
7910
  hasSoftDelete,
5036
7911
  hasRowVersion,
5037
7912
  file: filePath,
5038
- relativePath: path13.relative(rootPath, filePath)
7913
+ relativePath: path17.relative(rootPath, filePath)
5039
7914
  };
5040
7915
  }
5041
7916
  function inferTableInfo(entityName, config) {
@@ -5222,7 +8097,12 @@ async function createServer() {
5222
8097
  checkMigrationsTool,
5223
8098
  scaffoldExtensionTool,
5224
8099
  apiDocsTool,
5225
- suggestMigrationTool
8100
+ suggestMigrationTool,
8101
+ // Test Tools
8102
+ scaffoldTestsTool,
8103
+ analyzeTestCoverageTool,
8104
+ validateTestConventionsTool,
8105
+ suggestTestScenariosTool
5226
8106
  ]
5227
8107
  };
5228
8108
  });
@@ -5248,6 +8128,19 @@ async function createServer() {
5248
8128
  case "suggest_migration":
5249
8129
  result = await handleSuggestMigration(args, config);
5250
8130
  break;
8131
+ // Test Tools
8132
+ case "scaffold_tests":
8133
+ result = await handleScaffoldTests(args, config);
8134
+ break;
8135
+ case "analyze_test_coverage":
8136
+ result = await handleAnalyzeTestCoverage(args, config);
8137
+ break;
8138
+ case "validate_test_conventions":
8139
+ result = await handleValidateTestConventions(args, config);
8140
+ break;
8141
+ case "suggest_test_scenarios":
8142
+ result = await handleSuggestTestScenarios(args, config);
8143
+ break;
5251
8144
  default:
5252
8145
  throw new Error(`Unknown tool: ${name}`);
5253
8146
  }