@atlashub/smartstack-cli 2.9.0 → 3.1.0

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.
Files changed (90) hide show
  1. package/.documentation/agents.html +1 -371
  2. package/.documentation/business-analyse.html +81 -17
  3. package/.documentation/cli-commands.html +1 -1
  4. package/.documentation/commands.html +1 -1
  5. package/.documentation/efcore.html +1 -1
  6. package/.documentation/gitflow.html +1 -1
  7. package/.documentation/hooks.html +27 -66
  8. package/.documentation/index.html +166 -166
  9. package/.documentation/init.html +6 -7
  10. package/.documentation/installation.html +1 -1
  11. package/.documentation/ralph-loop.html +1 -9
  12. package/.documentation/test-web.html +15 -39
  13. package/dist/index.js +23 -16
  14. package/dist/index.js.map +1 -1
  15. package/dist/mcp-entry.mjs +1302 -223
  16. package/dist/mcp-entry.mjs.map +1 -1
  17. package/package.json +1 -1
  18. package/templates/agents/efcore/db-deploy.md +1 -1
  19. package/templates/agents/efcore/migration.md +26 -10
  20. package/templates/agents/efcore/rebase-snapshot.md +24 -7
  21. package/templates/agents/efcore/squash.md +73 -57
  22. package/templates/agents/gitflow/commit.md +138 -18
  23. package/templates/agents/gitflow/exec.md +1 -1
  24. package/templates/agents/gitflow/finish.md +79 -62
  25. package/templates/agents/gitflow/init-clone.md +186 -0
  26. package/templates/agents/gitflow/init-detect.md +137 -0
  27. package/templates/agents/gitflow/init-validate.md +210 -0
  28. package/templates/agents/gitflow/init.md +231 -74
  29. package/templates/agents/gitflow/merge.md +115 -33
  30. package/templates/agents/gitflow/pr.md +151 -46
  31. package/templates/agents/gitflow/start.md +76 -33
  32. package/templates/agents/gitflow/status.md +41 -71
  33. package/templates/hooks/appsettings-guard.sh +76 -0
  34. package/templates/hooks/ef-migration-check.md +1 -1
  35. package/templates/hooks/hooks.json +9 -0
  36. package/templates/project/appsettings.json.template +8 -2
  37. package/templates/project/test-frontend/msw/handlers.ts +58 -0
  38. package/templates/project/test-frontend/msw/server.ts +25 -0
  39. package/templates/project/test-frontend/setup.ts +16 -0
  40. package/templates/project/test-frontend/test-utils.tsx +59 -0
  41. package/templates/project/test-frontend/vitest.config.ts +31 -0
  42. package/templates/skills/_resources/config-safety.md +61 -0
  43. package/templates/skills/_resources/formatting-guide.md +2 -2
  44. package/templates/skills/application/SKILL.md +12 -3
  45. package/templates/skills/application/steps/step-04-backend.md +21 -0
  46. package/templates/skills/application/steps/step-07-tests.md +259 -120
  47. package/templates/skills/business-analyse/SKILL.md +57 -28
  48. package/templates/skills/business-analyse/_shared.md +70 -39
  49. package/templates/skills/business-analyse/html/ba-interactive.html +2596 -0
  50. package/templates/skills/business-analyse/questionnaire/00-application.md +123 -131
  51. package/templates/skills/business-analyse/questionnaire/01-context.md +173 -24
  52. package/templates/skills/business-analyse/questionnaire/02-stakeholders.md +170 -50
  53. package/templates/skills/business-analyse/questionnaire/03-scope.md +154 -48
  54. package/templates/skills/business-analyse/questionnaire/10-documentation.md +1 -1
  55. package/templates/skills/business-analyse/questionnaire/14-risk-assumptions.md +135 -0
  56. package/templates/skills/business-analyse/questionnaire/15-success-metrics.md +136 -0
  57. package/templates/skills/business-analyse/questionnaire.md +55 -46
  58. package/templates/skills/business-analyse/steps/step-00-init.md +24 -2
  59. package/templates/skills/business-analyse/steps/step-01-cadrage.md +31 -20
  60. package/templates/skills/business-analyse/steps/step-03-specify.md +58 -0
  61. package/templates/skills/business-analyse/steps/step-05-handoff.md +301 -1
  62. package/templates/skills/business-analyse/steps/step-06-extract.md +518 -0
  63. package/templates/skills/check-version/SKILL.md +1 -1
  64. package/templates/skills/efcore/steps/db/step-deploy.md +22 -3
  65. package/templates/skills/efcore/steps/db/step-reset.md +27 -4
  66. package/templates/skills/efcore/steps/db/step-seed.md +46 -2
  67. package/templates/skills/efcore/steps/db/step-status.md +14 -0
  68. package/templates/skills/efcore/steps/migration/step-01-check.md +31 -5
  69. package/templates/skills/efcore/steps/migration/step-02-create.md +20 -4
  70. package/templates/skills/efcore/steps/rebase-snapshot/step-03-create.md +60 -0
  71. package/templates/skills/efcore/steps/shared/step-00-init.md +47 -8
  72. package/templates/skills/efcore/steps/squash/step-03-create.md +27 -5
  73. package/templates/skills/gitflow/SKILL.md +91 -29
  74. package/templates/skills/gitflow/_shared.md +144 -2
  75. package/templates/skills/gitflow/phases/status.md +11 -1
  76. package/templates/skills/gitflow/steps/step-commit.md +1 -1
  77. package/templates/skills/gitflow/steps/step-init.md +202 -39
  78. package/templates/skills/gitflow/steps/step-pr.md +17 -5
  79. package/templates/skills/gitflow/templates/config.json +10 -1
  80. package/templates/skills/ralph-loop/SKILL.md +22 -15
  81. package/templates/skills/ralph-loop/steps/step-01-task.md +89 -4
  82. package/templates/skills/ralph-loop/steps/step-02-execute.md +408 -23
  83. package/templates/skills/ralph-loop/steps/step-03-commit.md +84 -2
  84. package/templates/skills/ralph-loop/steps/step-04-check.md +235 -6
  85. package/templates/skills/ralph-loop/steps/step-05-report.md +115 -0
  86. package/templates/skills/validate-feature/SKILL.md +83 -0
  87. package/templates/skills/validate-feature/steps/step-01-compile.md +38 -0
  88. package/templates/skills/validate-feature/steps/step-02-unit-tests.md +45 -0
  89. package/templates/skills/validate-feature/steps/step-03-integration-tests.md +53 -0
  90. package/templates/skills/validate-feature/steps/step-04-api-smoke.md +157 -0
@@ -471,8 +471,8 @@ var init_parseUtil = __esm({
471
471
  init_errors();
472
472
  init_en();
473
473
  makeIssue = (params) => {
474
- const { data, path: path29, errorMaps, issueData } = params;
475
- const fullPath = [...path29, ...issueData.path || []];
474
+ const { data, path: path30, errorMaps, issueData } = params;
475
+ const fullPath = [...path30, ...issueData.path || []];
476
476
  const fullIssue = {
477
477
  ...issueData,
478
478
  path: fullPath
@@ -786,11 +786,11 @@ var init_types = __esm({
786
786
  init_parseUtil();
787
787
  init_util();
788
788
  ParseInputLazyPath = class {
789
- constructor(parent, value, path29, key) {
789
+ constructor(parent, value, path30, key) {
790
790
  this._cachedPath = [];
791
791
  this.parent = parent;
792
792
  this.data = value;
793
- this._path = path29;
793
+ this._path = path30;
794
794
  this._key = key;
795
795
  }
796
796
  get path() {
@@ -4367,10 +4367,10 @@ function assignProp(target, prop, value) {
4367
4367
  configurable: true
4368
4368
  });
4369
4369
  }
4370
- function getElementAtPath(obj, path29) {
4371
- if (!path29)
4370
+ function getElementAtPath(obj, path30) {
4371
+ if (!path30)
4372
4372
  return obj;
4373
- return path29.reduce((acc, key) => acc?.[key], obj);
4373
+ return path30.reduce((acc, key) => acc?.[key], obj);
4374
4374
  }
4375
4375
  function promiseAllObject(promisesObj) {
4376
4376
  const keys = Object.keys(promisesObj);
@@ -4619,11 +4619,11 @@ function aborted(x, startIndex = 0) {
4619
4619
  }
4620
4620
  return false;
4621
4621
  }
4622
- function prefixIssues(path29, issues) {
4622
+ function prefixIssues(path30, issues) {
4623
4623
  return issues.map((iss) => {
4624
4624
  var _a;
4625
4625
  (_a = iss).path ?? (_a.path = []);
4626
- iss.path.unshift(path29);
4626
+ iss.path.unshift(path30);
4627
4627
  return iss;
4628
4628
  });
4629
4629
  }
@@ -14435,8 +14435,8 @@ var require_utils = __commonJS({
14435
14435
  }
14436
14436
  return ind;
14437
14437
  }
14438
- function removeDotSegments(path29) {
14439
- let input = path29;
14438
+ function removeDotSegments(path30) {
14439
+ let input = path30;
14440
14440
  const output = [];
14441
14441
  let nextSlash = -1;
14442
14442
  let len = 0;
@@ -14636,8 +14636,8 @@ var require_schemes = __commonJS({
14636
14636
  wsComponent.secure = void 0;
14637
14637
  }
14638
14638
  if (wsComponent.resourceName) {
14639
- const [path29, query] = wsComponent.resourceName.split("?");
14640
- wsComponent.path = path29 && path29 !== "/" ? path29 : void 0;
14639
+ const [path30, query] = wsComponent.resourceName.split("?");
14640
+ wsComponent.path = path30 && path30 !== "/" ? path30 : void 0;
14641
14641
  wsComponent.query = query;
14642
14642
  wsComponent.resourceName = void 0;
14643
14643
  }
@@ -28777,12 +28777,12 @@ var init_esm7 = __esm({
28777
28777
  /**
28778
28778
  * Get the Path object referenced by the string path, resolved from this Path
28779
28779
  */
28780
- resolve(path29) {
28781
- if (!path29) {
28780
+ resolve(path30) {
28781
+ if (!path30) {
28782
28782
  return this;
28783
28783
  }
28784
- const rootPath = this.getRootString(path29);
28785
- const dir = path29.substring(rootPath.length);
28784
+ const rootPath = this.getRootString(path30);
28785
+ const dir = path30.substring(rootPath.length);
28786
28786
  const dirParts = dir.split(this.splitSep);
28787
28787
  const result = rootPath ? this.getRoot(rootPath).#resolveParts(dirParts) : this.#resolveParts(dirParts);
28788
28788
  return result;
@@ -29534,8 +29534,8 @@ var init_esm7 = __esm({
29534
29534
  /**
29535
29535
  * @internal
29536
29536
  */
29537
- getRootString(path29) {
29538
- return win32.parse(path29).root;
29537
+ getRootString(path30) {
29538
+ return win32.parse(path30).root;
29539
29539
  }
29540
29540
  /**
29541
29541
  * @internal
@@ -29581,8 +29581,8 @@ var init_esm7 = __esm({
29581
29581
  /**
29582
29582
  * @internal
29583
29583
  */
29584
- getRootString(path29) {
29585
- return path29.startsWith("/") ? "/" : "";
29584
+ getRootString(path30) {
29585
+ return path30.startsWith("/") ? "/" : "";
29586
29586
  }
29587
29587
  /**
29588
29588
  * @internal
@@ -29671,11 +29671,11 @@ var init_esm7 = __esm({
29671
29671
  /**
29672
29672
  * Get the depth of a provided path, string, or the cwd
29673
29673
  */
29674
- depth(path29 = this.cwd) {
29675
- if (typeof path29 === "string") {
29676
- path29 = this.cwd.resolve(path29);
29674
+ depth(path30 = this.cwd) {
29675
+ if (typeof path30 === "string") {
29676
+ path30 = this.cwd.resolve(path30);
29677
29677
  }
29678
- return path29.depth();
29678
+ return path30.depth();
29679
29679
  }
29680
29680
  /**
29681
29681
  * Return the cache of child entries. Exposed so subclasses can create
@@ -30162,9 +30162,9 @@ var init_esm7 = __esm({
30162
30162
  process3();
30163
30163
  return results;
30164
30164
  }
30165
- chdir(path29 = this.cwd) {
30165
+ chdir(path30 = this.cwd) {
30166
30166
  const oldCwd = this.cwd;
30167
- this.cwd = typeof path29 === "string" ? this.cwd.resolve(path29) : path29;
30167
+ this.cwd = typeof path30 === "string" ? this.cwd.resolve(path30) : path30;
30168
30168
  this.cwd[setAsCwd](oldCwd);
30169
30169
  }
30170
30170
  };
@@ -30545,8 +30545,8 @@ var init_processor = __esm({
30545
30545
  }
30546
30546
  // match, absolute, ifdir
30547
30547
  entries() {
30548
- return [...this.store.entries()].map(([path29, n]) => [
30549
- path29,
30548
+ return [...this.store.entries()].map(([path30, n]) => [
30549
+ path30,
30550
30550
  !!(n & 2),
30551
30551
  !!(n & 1)
30552
30552
  ]);
@@ -30761,9 +30761,9 @@ var init_walker = __esm({
30761
30761
  signal;
30762
30762
  maxDepth;
30763
30763
  includeChildMatches;
30764
- constructor(patterns, path29, opts) {
30764
+ constructor(patterns, path30, opts) {
30765
30765
  this.patterns = patterns;
30766
- this.path = path29;
30766
+ this.path = path30;
30767
30767
  this.opts = opts;
30768
30768
  this.#sep = !opts.posix && opts.platform === "win32" ? "\\" : "/";
30769
30769
  this.includeChildMatches = opts.includeChildMatches !== false;
@@ -30782,11 +30782,11 @@ var init_walker = __esm({
30782
30782
  });
30783
30783
  }
30784
30784
  }
30785
- #ignored(path29) {
30786
- return this.seen.has(path29) || !!this.#ignore?.ignored?.(path29);
30785
+ #ignored(path30) {
30786
+ return this.seen.has(path30) || !!this.#ignore?.ignored?.(path30);
30787
30787
  }
30788
- #childrenIgnored(path29) {
30789
- return !!this.#ignore?.childrenIgnored?.(path29);
30788
+ #childrenIgnored(path30) {
30789
+ return !!this.#ignore?.childrenIgnored?.(path30);
30790
30790
  }
30791
30791
  // backpressure mechanism
30792
30792
  pause() {
@@ -31001,8 +31001,8 @@ var init_walker = __esm({
31001
31001
  };
31002
31002
  GlobWalker = class extends GlobUtil {
31003
31003
  matches = /* @__PURE__ */ new Set();
31004
- constructor(patterns, path29, opts) {
31005
- super(patterns, path29, opts);
31004
+ constructor(patterns, path30, opts) {
31005
+ super(patterns, path30, opts);
31006
31006
  }
31007
31007
  matchEmit(e) {
31008
31008
  this.matches.add(e);
@@ -31039,8 +31039,8 @@ var init_walker = __esm({
31039
31039
  };
31040
31040
  GlobStream = class extends GlobUtil {
31041
31041
  results;
31042
- constructor(patterns, path29, opts) {
31043
- super(patterns, path29, opts);
31042
+ constructor(patterns, path30, opts) {
31043
+ super(patterns, path30, opts);
31044
31044
  this.results = new Minipass({
31045
31045
  signal: this.signal,
31046
31046
  objectMode: true
@@ -31477,10 +31477,10 @@ var init_fs = __esm({
31477
31477
  init_esm_shims();
31478
31478
  init_esm8();
31479
31479
  FileSystemError = class extends Error {
31480
- constructor(message, operation, path29, cause) {
31480
+ constructor(message, operation, path30, cause) {
31481
31481
  super(message);
31482
31482
  this.operation = operation;
31483
- this.path = path29;
31483
+ this.path = path30;
31484
31484
  this.cause = cause;
31485
31485
  this.name = "FileSystemError";
31486
31486
  }
@@ -31641,7 +31641,7 @@ var init_types3 = __esm({
31641
31641
  dryRun: external_exports.boolean().default(false).describe("Preview without writing files or creating migration")
31642
31642
  });
31643
31643
  TestTypeSchema = external_exports.enum(["unit", "integration", "security", "e2e"]);
31644
- TestTargetSchema = external_exports.enum(["entity", "service", "controller", "validator", "repository", "all"]);
31644
+ TestTargetSchema = external_exports.enum(["entity", "service", "controller", "validator", "repository", "infrastructure", "all"]);
31645
31645
  ScaffoldTestsInputSchema = external_exports.object({
31646
31646
  target: TestTargetSchema.describe("Type of component to test"),
31647
31647
  name: external_exports.string().min(1).describe('Component name (PascalCase, e.g., "User", "Order")'),
@@ -33365,6 +33365,37 @@ var init_validate_conventions = __esm({
33365
33365
  }
33366
33366
  });
33367
33367
 
33368
+ // src/mcp/utils/semver.ts
33369
+ function parseSemver(version2) {
33370
+ const match2 = version2.match(/^(\d+)\.(\d+)\.(\d+)$/);
33371
+ if (!match2) {
33372
+ return null;
33373
+ }
33374
+ return [
33375
+ parseInt(match2[1], 10),
33376
+ parseInt(match2[2], 10),
33377
+ parseInt(match2[3], 10)
33378
+ ];
33379
+ }
33380
+ function compareVersions(a, b) {
33381
+ const partsA = parseSemver(a);
33382
+ const partsB = parseSemver(b);
33383
+ if (!partsA && !partsB) return 0;
33384
+ if (!partsA) return 1;
33385
+ if (!partsB) return -1;
33386
+ for (let i = 0; i < 3; i++) {
33387
+ if (partsA[i] > partsB[i]) return 1;
33388
+ if (partsA[i] < partsB[i]) return -1;
33389
+ }
33390
+ return 0;
33391
+ }
33392
+ var init_semver = __esm({
33393
+ "src/mcp/utils/semver.ts"() {
33394
+ "use strict";
33395
+ init_esm_shims();
33396
+ }
33397
+ });
33398
+
33368
33399
  // src/mcp/tools/check-migrations.ts
33369
33400
  import path9 from "path";
33370
33401
  async function handleCheckMigrations(args, config2) {
@@ -33441,29 +33472,6 @@ async function parseMigrations(migrationsPath, rootPath) {
33441
33472
  return a.sequence.localeCompare(b.sequence);
33442
33473
  });
33443
33474
  }
33444
- function parseSemver(version2) {
33445
- const match2 = version2.match(/^(\d+)\.(\d+)\.(\d+)$/);
33446
- if (!match2) {
33447
- return null;
33448
- }
33449
- return [
33450
- parseInt(match2[1], 10),
33451
- parseInt(match2[2], 10),
33452
- parseInt(match2[3], 10)
33453
- ];
33454
- }
33455
- function compareVersions(a, b) {
33456
- const partsA = parseSemver(a);
33457
- const partsB = parseSemver(b);
33458
- if (!partsA && !partsB) return 0;
33459
- if (!partsA) return 1;
33460
- if (!partsB) return -1;
33461
- for (let i = 0; i < 3; i++) {
33462
- if (partsA[i] > partsB[i]) return 1;
33463
- if (partsA[i] < partsB[i]) return -1;
33464
- }
33465
- return 0;
33466
- }
33467
33475
  function checkNamingConventions(result, _config) {
33468
33476
  for (const migration of result.migrations) {
33469
33477
  if (migration.context === "Unknown") {
@@ -33656,6 +33664,7 @@ var init_check_migrations = __esm({
33656
33664
  init_fs();
33657
33665
  init_git();
33658
33666
  init_detector();
33667
+ init_semver();
33659
33668
  init_logger();
33660
33669
  checkMigrationsTool = {
33661
33670
  name: "check_migrations",
@@ -34774,7 +34783,7 @@ var require_no_conflict = __commonJS({
34774
34783
  "use strict";
34775
34784
  init_esm_shims();
34776
34785
  exports.__esModule = true;
34777
- exports["default"] = function(Handlebars3) {
34786
+ exports["default"] = function(Handlebars4) {
34778
34787
  (function() {
34779
34788
  if (typeof globalThis === "object") return;
34780
34789
  Object.prototype.__defineGetter__("__magic__", function() {
@@ -34784,11 +34793,11 @@ var require_no_conflict = __commonJS({
34784
34793
  delete Object.prototype.__magic__;
34785
34794
  })();
34786
34795
  var $Handlebars = globalThis.Handlebars;
34787
- Handlebars3.noConflict = function() {
34788
- if (globalThis.Handlebars === Handlebars3) {
34796
+ Handlebars4.noConflict = function() {
34797
+ if (globalThis.Handlebars === Handlebars4) {
34789
34798
  globalThis.Handlebars = $Handlebars;
34790
34799
  }
34791
- return Handlebars3;
34800
+ return Handlebars4;
34792
34801
  };
34793
34802
  };
34794
34803
  module.exports = exports["default"];
@@ -34867,13 +34876,13 @@ var require_ast = __commonJS({
34867
34876
  helperExpression: function helperExpression(node) {
34868
34877
  return node.type === "SubExpression" || (node.type === "MustacheStatement" || node.type === "BlockStatement") && !!(node.params && node.params.length || node.hash);
34869
34878
  },
34870
- scopedId: function scopedId(path29) {
34871
- return /^\.|this\b/.test(path29.original);
34879
+ scopedId: function scopedId(path30) {
34880
+ return /^\.|this\b/.test(path30.original);
34872
34881
  },
34873
34882
  // an ID is simple if it only has one part, and that part is not
34874
34883
  // `..` or `this`.
34875
- simpleId: function simpleId(path29) {
34876
- return path29.parts.length === 1 && !AST2.helpers.scopedId(path29) && !path29.depth;
34884
+ simpleId: function simpleId(path30) {
34885
+ return path30.parts.length === 1 && !AST2.helpers.scopedId(path30) && !path30.depth;
34877
34886
  }
34878
34887
  }
34879
34888
  };
@@ -35947,12 +35956,12 @@ var require_helpers2 = __commonJS({
35947
35956
  loc
35948
35957
  };
35949
35958
  }
35950
- function prepareMustache(path29, params, hash, open, strip, locInfo) {
35959
+ function prepareMustache(path30, params, hash, open, strip, locInfo) {
35951
35960
  var escapeFlag = open.charAt(3) || open.charAt(2), escaped = escapeFlag !== "{" && escapeFlag !== "&";
35952
35961
  var decorator = /\*/.test(open);
35953
35962
  return {
35954
35963
  type: decorator ? "Decorator" : "MustacheStatement",
35955
- path: path29,
35964
+ path: path30,
35956
35965
  params,
35957
35966
  hash,
35958
35967
  escaped,
@@ -36224,9 +36233,9 @@ var require_compiler = __commonJS({
36224
36233
  },
36225
36234
  DecoratorBlock: function DecoratorBlock(decorator) {
36226
36235
  var program = decorator.program && this.compileProgram(decorator.program);
36227
- var params = this.setupFullMustacheParams(decorator, program, void 0), path29 = decorator.path;
36236
+ var params = this.setupFullMustacheParams(decorator, program, void 0), path30 = decorator.path;
36228
36237
  this.useDecorators = true;
36229
- this.opcode("registerDecorator", params.length, path29.original);
36238
+ this.opcode("registerDecorator", params.length, path30.original);
36230
36239
  },
36231
36240
  PartialStatement: function PartialStatement(partial2) {
36232
36241
  this.usePartial = true;
@@ -36290,46 +36299,46 @@ var require_compiler = __commonJS({
36290
36299
  }
36291
36300
  },
36292
36301
  ambiguousSexpr: function ambiguousSexpr(sexpr, program, inverse) {
36293
- var path29 = sexpr.path, name = path29.parts[0], isBlock = program != null || inverse != null;
36294
- this.opcode("getContext", path29.depth);
36302
+ var path30 = sexpr.path, name = path30.parts[0], isBlock = program != null || inverse != null;
36303
+ this.opcode("getContext", path30.depth);
36295
36304
  this.opcode("pushProgram", program);
36296
36305
  this.opcode("pushProgram", inverse);
36297
- path29.strict = true;
36298
- this.accept(path29);
36306
+ path30.strict = true;
36307
+ this.accept(path30);
36299
36308
  this.opcode("invokeAmbiguous", name, isBlock);
36300
36309
  },
36301
36310
  simpleSexpr: function simpleSexpr(sexpr) {
36302
- var path29 = sexpr.path;
36303
- path29.strict = true;
36304
- this.accept(path29);
36311
+ var path30 = sexpr.path;
36312
+ path30.strict = true;
36313
+ this.accept(path30);
36305
36314
  this.opcode("resolvePossibleLambda");
36306
36315
  },
36307
36316
  helperSexpr: function helperSexpr(sexpr, program, inverse) {
36308
- var params = this.setupFullMustacheParams(sexpr, program, inverse), path29 = sexpr.path, name = path29.parts[0];
36317
+ var params = this.setupFullMustacheParams(sexpr, program, inverse), path30 = sexpr.path, name = path30.parts[0];
36309
36318
  if (this.options.knownHelpers[name]) {
36310
36319
  this.opcode("invokeKnownHelper", params.length, name);
36311
36320
  } else if (this.options.knownHelpersOnly) {
36312
36321
  throw new _exception2["default"]("You specified knownHelpersOnly, but used the unknown helper " + name, sexpr);
36313
36322
  } else {
36314
- path29.strict = true;
36315
- path29.falsy = true;
36316
- this.accept(path29);
36317
- this.opcode("invokeHelper", params.length, path29.original, _ast2["default"].helpers.simpleId(path29));
36323
+ path30.strict = true;
36324
+ path30.falsy = true;
36325
+ this.accept(path30);
36326
+ this.opcode("invokeHelper", params.length, path30.original, _ast2["default"].helpers.simpleId(path30));
36318
36327
  }
36319
36328
  },
36320
- PathExpression: function PathExpression(path29) {
36321
- this.addDepth(path29.depth);
36322
- this.opcode("getContext", path29.depth);
36323
- var name = path29.parts[0], scoped = _ast2["default"].helpers.scopedId(path29), blockParamId = !path29.depth && !scoped && this.blockParamIndex(name);
36329
+ PathExpression: function PathExpression(path30) {
36330
+ this.addDepth(path30.depth);
36331
+ this.opcode("getContext", path30.depth);
36332
+ var name = path30.parts[0], scoped = _ast2["default"].helpers.scopedId(path30), blockParamId = !path30.depth && !scoped && this.blockParamIndex(name);
36324
36333
  if (blockParamId) {
36325
- this.opcode("lookupBlockParam", blockParamId, path29.parts);
36334
+ this.opcode("lookupBlockParam", blockParamId, path30.parts);
36326
36335
  } else if (!name) {
36327
36336
  this.opcode("pushContext");
36328
- } else if (path29.data) {
36337
+ } else if (path30.data) {
36329
36338
  this.options.data = true;
36330
- this.opcode("lookupData", path29.depth, path29.parts, path29.strict);
36339
+ this.opcode("lookupData", path30.depth, path30.parts, path30.strict);
36331
36340
  } else {
36332
- this.opcode("lookupOnContext", path29.parts, path29.falsy, path29.strict, scoped);
36341
+ this.opcode("lookupOnContext", path30.parts, path30.falsy, path30.strict, scoped);
36333
36342
  }
36334
36343
  },
36335
36344
  StringLiteral: function StringLiteral(string3) {
@@ -36685,16 +36694,16 @@ var require_util3 = __commonJS({
36685
36694
  }
36686
36695
  exports.urlGenerate = urlGenerate;
36687
36696
  function normalize2(aPath) {
36688
- var path29 = aPath;
36697
+ var path30 = aPath;
36689
36698
  var url2 = urlParse(aPath);
36690
36699
  if (url2) {
36691
36700
  if (!url2.path) {
36692
36701
  return aPath;
36693
36702
  }
36694
- path29 = url2.path;
36703
+ path30 = url2.path;
36695
36704
  }
36696
- var isAbsolute = exports.isAbsolute(path29);
36697
- var parts = path29.split(/\/+/);
36705
+ var isAbsolute = exports.isAbsolute(path30);
36706
+ var parts = path30.split(/\/+/);
36698
36707
  for (var part, up = 0, i = parts.length - 1; i >= 0; i--) {
36699
36708
  part = parts[i];
36700
36709
  if (part === ".") {
@@ -36711,15 +36720,15 @@ var require_util3 = __commonJS({
36711
36720
  }
36712
36721
  }
36713
36722
  }
36714
- path29 = parts.join("/");
36715
- if (path29 === "") {
36716
- path29 = isAbsolute ? "/" : ".";
36723
+ path30 = parts.join("/");
36724
+ if (path30 === "") {
36725
+ path30 = isAbsolute ? "/" : ".";
36717
36726
  }
36718
36727
  if (url2) {
36719
- url2.path = path29;
36728
+ url2.path = path30;
36720
36729
  return urlGenerate(url2);
36721
36730
  }
36722
- return path29;
36731
+ return path30;
36723
36732
  }
36724
36733
  exports.normalize = normalize2;
36725
36734
  function join2(aRoot, aPath) {
@@ -39520,8 +39529,8 @@ var require_printer = __commonJS({
39520
39529
  return this.accept(sexpr.path) + " " + params + hash;
39521
39530
  };
39522
39531
  PrintVisitor.prototype.PathExpression = function(id) {
39523
- var path29 = id.parts.join("/");
39524
- return (id.data ? "@" : "") + "PATH:" + path29;
39532
+ var path30 = id.parts.join("/");
39533
+ return (id.data ? "@" : "") + "PATH:" + path30;
39525
39534
  };
39526
39535
  PrintVisitor.prototype.StringLiteral = function(string3) {
39527
39536
  return '"' + string3.value + '"';
@@ -50738,11 +50747,11 @@ var require_mime_types = __commonJS({
50738
50747
  }
50739
50748
  return exts[0];
50740
50749
  }
50741
- function lookup(path29) {
50742
- if (!path29 || typeof path29 !== "string") {
50750
+ function lookup(path30) {
50751
+ if (!path30 || typeof path30 !== "string") {
50743
50752
  return false;
50744
50753
  }
50745
- var extension2 = extname("x." + path29).toLowerCase().substr(1);
50754
+ var extension2 = extname("x." + path30).toLowerCase().substr(1);
50746
50755
  if (!extension2) {
50747
50756
  return false;
50748
50757
  }
@@ -51905,7 +51914,7 @@ var require_form_data = __commonJS({
51905
51914
  init_esm_shims();
51906
51915
  var CombinedStream = require_combined_stream();
51907
51916
  var util4 = __require("util");
51908
- var path29 = __require("path");
51917
+ var path30 = __require("path");
51909
51918
  var http3 = __require("http");
51910
51919
  var https2 = __require("https");
51911
51920
  var parseUrl = __require("url").parse;
@@ -52033,11 +52042,11 @@ var require_form_data = __commonJS({
52033
52042
  FormData3.prototype._getContentDisposition = function(value, options) {
52034
52043
  var filename;
52035
52044
  if (typeof options.filepath === "string") {
52036
- filename = path29.normalize(options.filepath).replace(/\\/g, "/");
52045
+ filename = path30.normalize(options.filepath).replace(/\\/g, "/");
52037
52046
  } else if (options.filename || value && (value.name || value.path)) {
52038
- filename = path29.basename(options.filename || value && (value.name || value.path));
52047
+ filename = path30.basename(options.filename || value && (value.name || value.path));
52039
52048
  } else if (value && value.readable && hasOwn(value, "httpVersion")) {
52040
- filename = path29.basename(value.client._httpMessage.path || "");
52049
+ filename = path30.basename(value.client._httpMessage.path || "");
52041
52050
  }
52042
52051
  if (filename) {
52043
52052
  return 'filename="' + filename + '"';
@@ -52236,9 +52245,9 @@ function isVisitable(thing) {
52236
52245
  function removeBrackets(key) {
52237
52246
  return utils_default.endsWith(key, "[]") ? key.slice(0, -2) : key;
52238
52247
  }
52239
- function renderKey(path29, key, dots) {
52240
- if (!path29) return key;
52241
- return path29.concat(key).map(function each(token, i) {
52248
+ function renderKey(path30, key, dots) {
52249
+ if (!path30) return key;
52250
+ return path30.concat(key).map(function each(token, i) {
52242
52251
  token = removeBrackets(token);
52243
52252
  return !dots && i ? "[" + token + "]" : token;
52244
52253
  }).join(dots ? "." : "");
@@ -52283,9 +52292,9 @@ function toFormData(obj, formData, options) {
52283
52292
  }
52284
52293
  return value;
52285
52294
  }
52286
- function defaultVisitor(value, key, path29) {
52295
+ function defaultVisitor(value, key, path30) {
52287
52296
  let arr = value;
52288
- if (value && !path29 && typeof value === "object") {
52297
+ if (value && !path30 && typeof value === "object") {
52289
52298
  if (utils_default.endsWith(key, "{}")) {
52290
52299
  key = metaTokens ? key : key.slice(0, -2);
52291
52300
  value = JSON.stringify(value);
@@ -52304,7 +52313,7 @@ function toFormData(obj, formData, options) {
52304
52313
  if (isVisitable(value)) {
52305
52314
  return true;
52306
52315
  }
52307
- formData.append(renderKey(path29, key, dots), convertValue(value));
52316
+ formData.append(renderKey(path30, key, dots), convertValue(value));
52308
52317
  return false;
52309
52318
  }
52310
52319
  const stack = [];
@@ -52313,10 +52322,10 @@ function toFormData(obj, formData, options) {
52313
52322
  convertValue,
52314
52323
  isVisitable
52315
52324
  });
52316
- function build(value, path29) {
52325
+ function build(value, path30) {
52317
52326
  if (utils_default.isUndefined(value)) return;
52318
52327
  if (stack.indexOf(value) !== -1) {
52319
- throw Error("Circular reference detected in " + path29.join("."));
52328
+ throw Error("Circular reference detected in " + path30.join("."));
52320
52329
  }
52321
52330
  stack.push(value);
52322
52331
  utils_default.forEach(value, function each(el, key) {
@@ -52324,11 +52333,11 @@ function toFormData(obj, formData, options) {
52324
52333
  formData,
52325
52334
  el,
52326
52335
  utils_default.isString(key) ? key.trim() : key,
52327
- path29,
52336
+ path30,
52328
52337
  exposedHelpers
52329
52338
  );
52330
52339
  if (result === true) {
52331
- build(el, path29 ? path29.concat(key) : [key]);
52340
+ build(el, path30 ? path30.concat(key) : [key]);
52332
52341
  }
52333
52342
  });
52334
52343
  stack.pop();
@@ -52613,7 +52622,7 @@ var init_platform = __esm({
52613
52622
  // node_modules/axios/lib/helpers/toURLEncodedForm.js
52614
52623
  function toURLEncodedForm(data, options) {
52615
52624
  return toFormData_default(data, new platform_default.classes.URLSearchParams(), {
52616
- visitor: function(value, key, path29, helpers) {
52625
+ visitor: function(value, key, path30, helpers) {
52617
52626
  if (platform_default.isNode && utils_default.isBuffer(value)) {
52618
52627
  this.append(key, value.toString("base64"));
52619
52628
  return false;
@@ -52652,11 +52661,11 @@ function arrayToObject(arr) {
52652
52661
  return obj;
52653
52662
  }
52654
52663
  function formDataToJSON(formData) {
52655
- function buildPath(path29, value, target, index) {
52656
- let name = path29[index++];
52664
+ function buildPath(path30, value, target, index) {
52665
+ let name = path30[index++];
52657
52666
  if (name === "__proto__") return true;
52658
52667
  const isNumericKey = Number.isFinite(+name);
52659
- const isLast = index >= path29.length;
52668
+ const isLast = index >= path30.length;
52660
52669
  name = !name && utils_default.isArray(target) ? target.length : name;
52661
52670
  if (isLast) {
52662
52671
  if (utils_default.hasOwnProp(target, name)) {
@@ -52669,7 +52678,7 @@ function formDataToJSON(formData) {
52669
52678
  if (!target[name] || !utils_default.isObject(target[name])) {
52670
52679
  target[name] = [];
52671
52680
  }
52672
- const result = buildPath(path29, value, target[name], index);
52681
+ const result = buildPath(path30, value, target[name], index);
52673
52682
  if (result && utils_default.isArray(target[name])) {
52674
52683
  target[name] = arrayToObject(target[name]);
52675
52684
  }
@@ -55566,9 +55575,9 @@ var init_http = __esm({
55566
55575
  auth = urlUsername + ":" + urlPassword;
55567
55576
  }
55568
55577
  auth && headers.delete("authorization");
55569
- let path29;
55578
+ let path30;
55570
55579
  try {
55571
- path29 = buildURL(
55580
+ path30 = buildURL(
55572
55581
  parsed.pathname + parsed.search,
55573
55582
  config2.params,
55574
55583
  config2.paramsSerializer
@@ -55586,7 +55595,7 @@ var init_http = __esm({
55586
55595
  false
55587
55596
  );
55588
55597
  const options = {
55589
- path: path29,
55598
+ path: path30,
55590
55599
  method,
55591
55600
  headers: headers.toJSON(),
55592
55601
  agents: { http: config2.httpAgent, https: config2.httpsAgent },
@@ -55839,14 +55848,14 @@ var init_cookies = __esm({
55839
55848
  cookies_default = platform_default.hasStandardBrowserEnv ? (
55840
55849
  // Standard browser envs support document.cookie
55841
55850
  {
55842
- write(name, value, expires, path29, domain, secure, sameSite) {
55851
+ write(name, value, expires, path30, domain, secure, sameSite) {
55843
55852
  if (typeof document === "undefined") return;
55844
55853
  const cookie = [`${name}=${encodeURIComponent(value)}`];
55845
55854
  if (utils_default.isNumber(expires)) {
55846
55855
  cookie.push(`expires=${new Date(expires).toUTCString()}`);
55847
55856
  }
55848
- if (utils_default.isString(path29)) {
55849
- cookie.push(`path=${path29}`);
55857
+ if (utils_default.isString(path30)) {
55858
+ cookie.push(`path=${path30}`);
55850
55859
  }
55851
55860
  if (utils_default.isString(domain)) {
55852
55861
  cookie.push(`domain=${domain}`);
@@ -57600,8 +57609,12 @@ var init_api_docs = __esm({
57600
57609
  import path12 from "path";
57601
57610
  async function handleSuggestMigration(args, config2) {
57602
57611
  const input = SuggestMigrationInputSchema2.parse(args);
57612
+ const sanitizedDescription = input.description.replace(/[^a-zA-Z0-9\s\-_]/g, "").trim();
57613
+ if (sanitizedDescription.length < 3) {
57614
+ throw new Error("Migration description must contain at least 3 alphanumeric characters after sanitization");
57615
+ }
57603
57616
  const context = input.context || config2.defaultDbContext || "core";
57604
- logger.info("Suggesting migration name", { description: input.description, context });
57617
+ logger.info("Suggesting migration name", { description: sanitizedDescription, context });
57605
57618
  const structure = await findSmartStackStructure(config2.smartstack.projectPath);
57606
57619
  const existingMigrations = await findExistingMigrations(structure, config2, context);
57607
57620
  let version2 = input.version;
@@ -57619,7 +57632,10 @@ async function handleSuggestMigration(args, config2) {
57619
57632
  } else {
57620
57633
  version2 = version2 || "1.0.0";
57621
57634
  }
57622
- const pascalDescription = toPascalCase(input.description);
57635
+ const pascalDescription = toPascalCase(sanitizedDescription);
57636
+ if (!pascalDescription || !/^[A-Z][a-zA-Z0-9]*$/.test(pascalDescription)) {
57637
+ throw new Error(`Invalid migration description after PascalCase conversion: "${pascalDescription}"`);
57638
+ }
57623
57639
  const sequenceStr = sequence.toString().padStart(3, "0");
57624
57640
  const migrationName = `${context}_v${version2}_${sequenceStr}_${pascalDescription}`;
57625
57641
  const dbContextName = context === "core" ? "CoreDbContext" : "ExtensionsDbContext";
@@ -57690,27 +57706,23 @@ async function findExistingMigrations(structure, config2, context) {
57690
57706
  }
57691
57707
  }
57692
57708
  migrations.sort((a, b) => {
57693
- const verCompare = compareVersions2(a.version, b.version);
57709
+ const verCompare = compareVersions(a.version, b.version);
57694
57710
  if (verCompare !== 0) return verCompare;
57695
57711
  return a.sequence - b.sequence;
57696
57712
  });
57697
- } catch {
57698
- logger.debug("No migrations folder found");
57713
+ } catch (error2) {
57714
+ const err = error2 instanceof Error ? error2 : new Error(String(error2));
57715
+ if (err.message.includes("ENOENT") || err.message.includes("no such file")) {
57716
+ logger.debug("Migrations folder not found (expected for new projects)", { path: migrationsPath });
57717
+ } else {
57718
+ logger.warn("Failed to read migrations folder", { path: migrationsPath, error: err.message });
57719
+ }
57699
57720
  }
57700
57721
  return migrations;
57701
57722
  }
57702
57723
  function toPascalCase(str) {
57703
57724
  return str.replace(/[^a-zA-Z0-9\s]/g, "").split(/\s+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
57704
57725
  }
57705
- function compareVersions2(a, b) {
57706
- const aParts = a.split(".").map(Number);
57707
- const bParts = b.split(".").map(Number);
57708
- for (let i = 0; i < 3; i++) {
57709
- if (aParts[i] > bParts[i]) return 1;
57710
- if (aParts[i] < bParts[i]) return -1;
57711
- }
57712
- return 0;
57713
- }
57714
57726
  var suggestMigrationTool, SuggestMigrationInputSchema2;
57715
57727
  var init_suggest_migration = __esm({
57716
57728
  "src/mcp/tools/suggest-migration.ts"() {
@@ -57719,6 +57731,7 @@ var init_suggest_migration = __esm({
57719
57731
  init_zod();
57720
57732
  init_detector();
57721
57733
  init_fs();
57734
+ init_semver();
57722
57735
  init_logger();
57723
57736
  suggestMigrationTool = {
57724
57737
  name: "suggest_migration",
@@ -57744,9 +57757,9 @@ var init_suggest_migration = __esm({
57744
57757
  }
57745
57758
  };
57746
57759
  SuggestMigrationInputSchema2 = external_exports.object({
57747
- description: external_exports.string().describe("Description of what the migration does"),
57760
+ description: external_exports.string().min(3, "Migration description must be at least 3 characters").max(100, "Migration description must be at most 100 characters").describe("Description of what the migration does"),
57748
57761
  context: external_exports.enum(["core", "extensions"]).optional().describe("DbContext name (default: auto-detected from project config)"),
57749
- version: external_exports.string().optional().describe('Semver version (e.g., "1.0.0")')
57762
+ version: external_exports.string().regex(/^\d+\.\d+\.\d+$/, 'Version must be semver format (e.g., "1.0.0")').optional().describe('Semver version (e.g., "1.0.0")')
57750
57763
  });
57751
57764
  }
57752
57765
  });
@@ -58146,6 +58159,9 @@ async function handleScaffoldTests(args, config2) {
58146
58159
  case "repository":
58147
58160
  await scaffoldRepositoryTests(input.name, options, testTypes, structure, config2, result, dryRun);
58148
58161
  break;
58162
+ case "infrastructure":
58163
+ await scaffoldInfrastructureTests(input.name, options, structure, config2, result, dryRun);
58164
+ break;
58149
58165
  case "all":
58150
58166
  await scaffoldEntityTests(input.name, options, testTypes, structure, config2, result, dryRun);
58151
58167
  await scaffoldServiceTests(input.name, options, testTypes, structure, config2, result, dryRun);
@@ -58183,25 +58199,7 @@ async function scaffoldEntityTests(name, options, testTypes, structure, config2,
58183
58199
  type: "created"
58184
58200
  });
58185
58201
  }
58186
- if (testTypes.includes("security")) {
58187
- const securityContent = import_handlebars2.default.compile(securityTestTemplate)({
58188
- ...context,
58189
- nameLower: name.charAt(0).toLowerCase() + name.slice(1),
58190
- apiNamespace: config2.conventions.namespaces.api
58191
- });
58192
- const securityPath = path14.join(structure.root, "Tests", "Security", `${name}SecurityTests.cs`);
58193
- validatePathSecurity(securityPath, structure.root);
58194
- if (!dryRun) {
58195
- await ensureDirectory(path14.dirname(securityPath));
58196
- await writeText(securityPath, securityContent);
58197
- }
58198
- result.files.push({
58199
- path: path14.relative(structure.root, securityPath),
58200
- content: securityContent,
58201
- type: "created"
58202
- });
58203
- }
58204
- result.instructions.push(`Add package reference: <PackageReference Include="FluentAssertions" Version="6.*" />`);
58202
+ result.instructions.push(`Add package reference: <PackageReference Include="FluentAssertions" Version="8.*" />`);
58205
58203
  result.instructions.push(`Add package reference: <PackageReference Include="xunit" Version="2.*" />`);
58206
58204
  }
58207
58205
  async function scaffoldServiceTests(name, options, testTypes, structure, config2, result, dryRun) {
@@ -58258,6 +58256,18 @@ async function scaffoldControllerTests(name, options, testTypes, structure, conf
58258
58256
  content,
58259
58257
  type: "created"
58260
58258
  });
58259
+ const realContent = import_handlebars2.default.compile(controllerIntegrationTestTemplate)(context);
58260
+ const realTestPath = path14.join(structure.root, "Tests", "Integration", "Controllers", `${name}ControllerIntegrationTests.cs`);
58261
+ validatePathSecurity(realTestPath, structure.root);
58262
+ if (!dryRun) {
58263
+ await ensureDirectory(path14.dirname(realTestPath));
58264
+ await writeText(realTestPath, realContent);
58265
+ }
58266
+ result.files.push({
58267
+ path: path14.relative(structure.root, realTestPath),
58268
+ content: realContent,
58269
+ type: "created"
58270
+ });
58261
58271
  }
58262
58272
  if (testTypes.includes("security")) {
58263
58273
  const securityContent = import_handlebars2.default.compile(securityTestTemplate)(context);
@@ -58273,7 +58283,8 @@ async function scaffoldControllerTests(name, options, testTypes, structure, conf
58273
58283
  type: "created"
58274
58284
  });
58275
58285
  }
58276
- result.instructions.push(`Add package reference: <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.*" />`);
58286
+ result.instructions.push(`Add package reference: <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.*" />`);
58287
+ result.instructions.push(`Add package reference: <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.*" />`);
58277
58288
  }
58278
58289
  async function scaffoldValidatorTests(name, options, testTypes, structure, config2, result, dryRun) {
58279
58290
  const testNamespace = `${config2.conventions.namespaces.application}.Tests`;
@@ -58327,6 +58338,41 @@ async function scaffoldRepositoryTests(name, options, testTypes, structure, conf
58327
58338
  }
58328
58339
  result.instructions.push(`Add package reference: <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.*" />`);
58329
58340
  }
58341
+ async function scaffoldInfrastructureTests(name, options, structure, config2, result, dryRun) {
58342
+ const testNamespace = `${config2.conventions.namespaces.application}.Tests`;
58343
+ const infrastructureNamespace = config2.conventions.namespaces.infrastructure;
58344
+ const context = {
58345
+ name,
58346
+ testNamespace,
58347
+ infrastructureNamespace,
58348
+ ...options
58349
+ };
58350
+ const templates = [
58351
+ { template: testFactoryTemplate, filename: "SmartStackTestFactory.cs" },
58352
+ { template: testAuthHandlerTemplate, filename: "TestAuthHandler.cs" },
58353
+ { template: integrationTestBaseTemplate, filename: "IntegrationTestBase.cs" },
58354
+ { template: testDataSeederTemplate, filename: "TestDataSeeder.cs" }
58355
+ ];
58356
+ for (const { template, filename } of templates) {
58357
+ const content = import_handlebars2.default.compile(template)(context);
58358
+ const testPath = path14.join(structure.root, "Tests", "Integration", filename);
58359
+ validatePathSecurity(testPath, structure.root);
58360
+ if (!dryRun) {
58361
+ await ensureDirectory(path14.dirname(testPath));
58362
+ await writeText(testPath, content);
58363
+ }
58364
+ result.files.push({
58365
+ path: path14.relative(structure.root, testPath),
58366
+ content,
58367
+ type: "created"
58368
+ });
58369
+ }
58370
+ result.instructions.push(`Add package reference: <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.*" />`);
58371
+ result.instructions.push(`Add package reference: <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.*" />`);
58372
+ result.instructions.push(`Add package reference: <PackageReference Include="Microsoft.Data.Sqlite" Version="9.*" />`);
58373
+ result.instructions.push(`Add package reference: <PackageReference Include="FluentAssertions" Version="8.*" />`);
58374
+ result.instructions.push(`IMPORTANT: Add to your API .csproj: <InternalsVisibleTo Include="$(AssemblyName).Tests" /> or add 'public partial class Program { }' to Program.cs`);
58375
+ }
58330
58376
  function formatTestResult(result, _target, name, dryRun) {
58331
58377
  const lines = [];
58332
58378
  lines.push(`# Scaffold Tests: ${name}`);
@@ -58368,7 +58414,7 @@ function formatTestResult(result, _target, name, dryRun) {
58368
58414
  lines.push("");
58369
58415
  return lines.join("\n");
58370
58416
  }
58371
- var import_handlebars2, scaffoldTestsTool, entityTestTemplate, serviceTestTemplate, controllerTestTemplate, validatorTestTemplate, repositoryTestTemplate, securityTestTemplate;
58417
+ var import_handlebars2, scaffoldTestsTool, entityTestTemplate, serviceTestTemplate, controllerTestTemplate, validatorTestTemplate, repositoryTestTemplate, securityTestTemplate, testFactoryTemplate, testAuthHandlerTemplate, integrationTestBaseTemplate, testDataSeederTemplate, controllerIntegrationTestTemplate;
58372
58418
  var init_scaffold_tests = __esm({
58373
58419
  "src/mcp/tools/scaffold-tests.ts"() {
58374
58420
  "use strict";
@@ -58386,8 +58432,8 @@ var init_scaffold_tests = __esm({
58386
58432
  properties: {
58387
58433
  target: {
58388
58434
  type: "string",
58389
- enum: ["entity", "service", "controller", "validator", "repository", "all"],
58390
- description: "Type of component to test"
58435
+ enum: ["entity", "service", "controller", "validator", "repository", "infrastructure", "all"],
58436
+ description: 'Type of component to test. Use "infrastructure" to generate shared test base classes (WebApplicationFactory, IntegrationTestBase, TestAuthHandler).'
58391
58437
  },
58392
58438
  name: {
58393
58439
  type: "string",
@@ -59744,6 +59790,547 @@ public class {{name}}SecurityTests : IClassFixture<WebApplicationFactory<Program
59744
59790
 
59745
59791
  #endregion
59746
59792
  }
59793
+ `;
59794
+ testFactoryTemplate = `using System;
59795
+ using System.Data.Common;
59796
+ using System.Linq;
59797
+ using Microsoft.AspNetCore.Authentication;
59798
+ using Microsoft.AspNetCore.Hosting;
59799
+ using Microsoft.AspNetCore.Mvc.Testing;
59800
+ using Microsoft.Data.Sqlite;
59801
+ using Microsoft.EntityFrameworkCore;
59802
+ using Microsoft.Extensions.DependencyInjection;
59803
+ using {{infrastructureNamespace}}.Persistence;
59804
+
59805
+ namespace {{testNamespace}}.Integration;
59806
+
59807
+ /// <summary>
59808
+ /// Shared WebApplicationFactory for integration tests.
59809
+ /// Uses SQLite in-memory for real SQL behavior.
59810
+ /// Replaces JWT auth with TestAuthHandler.
59811
+ /// </summary>
59812
+ public class SmartStackTestFactory : WebApplicationFactory<Program>, IAsyncLifetime
59813
+ {
59814
+ private DbConnection _connection = null!;
59815
+
59816
+ protected override void ConfigureWebHost(IWebHostBuilder builder)
59817
+ {
59818
+ builder.ConfigureServices(services =>
59819
+ {
59820
+ // Remove existing DbContext registration
59821
+ var dbDescriptor = services.SingleOrDefault(
59822
+ d => d.ServiceType == typeof(DbContextOptions<ApplicationDbContext>));
59823
+ if (dbDescriptor != null)
59824
+ services.Remove(dbDescriptor);
59825
+
59826
+ // Remove existing DbConnection if registered
59827
+ var connDescriptor = services.SingleOrDefault(
59828
+ d => d.ServiceType == typeof(DbConnection));
59829
+ if (connDescriptor != null)
59830
+ services.Remove(connDescriptor);
59831
+
59832
+ // Create and open a shared SQLite in-memory connection
59833
+ _connection = new SqliteConnection("DataSource=:memory:");
59834
+ _connection.Open();
59835
+
59836
+ services.AddSingleton(_connection);
59837
+ services.AddDbContext<ApplicationDbContext>((sp, options) =>
59838
+ {
59839
+ options.UseSqlite(sp.GetRequiredService<DbConnection>());
59840
+ });
59841
+
59842
+ // Replace authentication with test handler
59843
+ services.AddAuthentication("Test")
59844
+ .AddScheme<AuthenticationSchemeOptions, TestAuthHandler>("Test", null);
59845
+ });
59846
+
59847
+ builder.UseEnvironment("Testing");
59848
+ }
59849
+
59850
+ public async Task InitializeAsync()
59851
+ {
59852
+ // Ensure database is created with current schema
59853
+ using var scope = Services.CreateScope();
59854
+ var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
59855
+ await db.Database.EnsureCreatedAsync();
59856
+
59857
+ // Seed minimal test data (tenant, admin user, base permissions)
59858
+ var seeder = new TestDataSeeder(db);
59859
+ await seeder.SeedAsync();
59860
+ }
59861
+
59862
+ async Task IAsyncLifetime.DisposeAsync()
59863
+ {
59864
+ if (_connection is not null)
59865
+ {
59866
+ await _connection.CloseAsync();
59867
+ await _connection.DisposeAsync();
59868
+ }
59869
+ }
59870
+
59871
+ /// <summary>
59872
+ /// Resets the database between test classes for isolation.
59873
+ /// </summary>
59874
+ public async Task ResetDatabaseAsync()
59875
+ {
59876
+ using var scope = Services.CreateScope();
59877
+ var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
59878
+ await db.Database.EnsureDeletedAsync();
59879
+ await db.Database.EnsureCreatedAsync();
59880
+
59881
+ var seeder = new TestDataSeeder(db);
59882
+ await seeder.SeedAsync();
59883
+ }
59884
+ }
59885
+ `;
59886
+ testAuthHandlerTemplate = `using System.Security.Claims;
59887
+ using System.Text.Encodings.Web;
59888
+ using Microsoft.AspNetCore.Authentication;
59889
+ using Microsoft.Extensions.Logging;
59890
+ using Microsoft.Extensions.Options;
59891
+
59892
+ namespace {{testNamespace}}.Integration;
59893
+
59894
+ /// <summary>
59895
+ /// Test authentication handler that creates a fake authenticated user.
59896
+ /// Bypasses JWT validation for integration tests.
59897
+ /// </summary>
59898
+ public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
59899
+ {
59900
+ public static Guid DefaultTenantId { get; set; } = Guid.Parse("11111111-1111-1111-1111-111111111111");
59901
+ public static string DefaultUserId { get; set; } = "test-user-id";
59902
+ public static string DefaultUserName { get; set; } = "test-admin@smartstack.io";
59903
+ public static string DefaultRole { get; set; } = "Administrator";
59904
+
59905
+ public TestAuthHandler(
59906
+ IOptionsMonitor<AuthenticationSchemeOptions> options,
59907
+ ILoggerFactory logger,
59908
+ UrlEncoder encoder)
59909
+ : base(options, logger, encoder)
59910
+ {
59911
+ }
59912
+
59913
+ protected override Task<AuthenticateResult> HandleAuthenticateAsync()
59914
+ {
59915
+ // Check if the request explicitly opts out of auth (for 401 tests)
59916
+ if (Request.Headers.ContainsKey("X-Test-Anonymous"))
59917
+ {
59918
+ return Task.FromResult(AuthenticateResult.Fail("Anonymous request"));
59919
+ }
59920
+
59921
+ var tenantId = DefaultTenantId.ToString();
59922
+ if (Request.Headers.TryGetValue("X-Tenant-Id", out var tenantHeader))
59923
+ {
59924
+ tenantId = tenantHeader.ToString();
59925
+ }
59926
+
59927
+ var claims = new[]
59928
+ {
59929
+ new Claim(ClaimTypes.NameIdentifier, DefaultUserId),
59930
+ new Claim(ClaimTypes.Name, DefaultUserName),
59931
+ new Claim(ClaimTypes.Email, DefaultUserName),
59932
+ new Claim(ClaimTypes.Role, DefaultRole),
59933
+ new Claim("tenant_id", tenantId),
59934
+ // Add all permissions for test user (admin has full access)
59935
+ new Claim("permissions", "*"),
59936
+ };
59937
+
59938
+ var identity = new ClaimsIdentity(claims, "Test");
59939
+ var principal = new ClaimsPrincipal(identity);
59940
+ var ticket = new AuthenticationTicket(principal, "Test");
59941
+
59942
+ return Task.FromResult(AuthenticateResult.Success(ticket));
59943
+ }
59944
+ }
59945
+ `;
59946
+ integrationTestBaseTemplate = `using System;
59947
+ using System.Net.Http;
59948
+ using System.Net.Http.Headers;
59949
+ using System.Net.Http.Json;
59950
+ using System.Threading.Tasks;
59951
+ using FluentAssertions;
59952
+ using Microsoft.Extensions.DependencyInjection;
59953
+ using {{infrastructureNamespace}}.Persistence;
59954
+ using Xunit;
59955
+
59956
+ namespace {{testNamespace}}.Integration;
59957
+
59958
+ /// <summary>
59959
+ /// Base class for integration tests.
59960
+ /// Provides configured HttpClient with auth and tenant headers.
59961
+ /// </summary>
59962
+ public abstract class IntegrationTestBase : IClassFixture<SmartStackTestFactory>, IAsyncLifetime
59963
+ {
59964
+ protected readonly SmartStackTestFactory Factory;
59965
+ protected readonly HttpClient Client;
59966
+ protected readonly Guid TestTenantId = TestAuthHandler.DefaultTenantId;
59967
+
59968
+ protected IntegrationTestBase(SmartStackTestFactory factory)
59969
+ {
59970
+ Factory = factory;
59971
+ Client = factory.CreateClient();
59972
+ Client.DefaultRequestHeaders.Add("X-Tenant-Id", TestTenantId.ToString());
59973
+ }
59974
+
59975
+ public virtual async Task InitializeAsync()
59976
+ {
59977
+ // Reset database for each test class to ensure isolation
59978
+ await Factory.ResetDatabaseAsync();
59979
+ }
59980
+
59981
+ public virtual Task DisposeAsync() => Task.CompletedTask;
59982
+
59983
+ /// <summary>
59984
+ /// Creates an HttpClient without authentication (for 401 tests).
59985
+ /// </summary>
59986
+ protected HttpClient CreateAnonymousClient()
59987
+ {
59988
+ var client = Factory.CreateClient();
59989
+ client.DefaultRequestHeaders.Add("X-Test-Anonymous", "true");
59990
+ client.DefaultRequestHeaders.Add("X-Tenant-Id", TestTenantId.ToString());
59991
+ return client;
59992
+ }
59993
+
59994
+ /// <summary>
59995
+ /// Creates an HttpClient for a different tenant (for isolation tests).
59996
+ /// </summary>
59997
+ protected HttpClient CreateClientForTenant(Guid tenantId)
59998
+ {
59999
+ var client = Factory.CreateClient();
60000
+ client.DefaultRequestHeaders.Add("X-Tenant-Id", tenantId.ToString());
60001
+ return client;
60002
+ }
60003
+
60004
+ /// <summary>
60005
+ /// Gets direct access to the DbContext for verification queries.
60006
+ /// </summary>
60007
+ protected async Task<T> WithDbContextAsync<T>(Func<ApplicationDbContext, Task<T>> action)
60008
+ {
60009
+ using var scope = Factory.Services.CreateScope();
60010
+ var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
60011
+ return await action(db);
60012
+ }
60013
+ }
60014
+ `;
60015
+ testDataSeederTemplate = `using System;
60016
+ using System.Threading.Tasks;
60017
+ using {{infrastructureNamespace}}.Persistence;
60018
+
60019
+ namespace {{testNamespace}}.Integration;
60020
+
60021
+ /// <summary>
60022
+ /// Seeds minimal test data required for integration tests.
60023
+ /// Creates a test tenant, admin user, and base navigation/permissions.
60024
+ /// </summary>
60025
+ public class TestDataSeeder
60026
+ {
60027
+ private readonly ApplicationDbContext _db;
60028
+
60029
+ public static readonly Guid TestTenantId = Guid.Parse("11111111-1111-1111-1111-111111111111");
60030
+ public static readonly Guid TestUserId = Guid.Parse("22222222-2222-2222-2222-222222222222");
60031
+
60032
+ public TestDataSeeder(ApplicationDbContext db)
60033
+ {
60034
+ _db = db;
60035
+ }
60036
+
60037
+ public async Task SeedAsync()
60038
+ {
60039
+ // NOTE: Add your tenant and user seeding logic here.
60040
+ // This depends on your specific entity model.
60041
+ // Example:
60042
+ // var tenant = Tenant.Create("test-tenant", "Test Tenant");
60043
+ // _db.Set<Tenant>().Add(tenant);
60044
+ // await _db.SaveChangesAsync();
60045
+
60046
+ await Task.CompletedTask;
60047
+ }
60048
+ }
60049
+ `;
60050
+ controllerIntegrationTestTemplate = `using System;
60051
+ using System.Collections.Generic;
60052
+ using System.Net;
60053
+ using System.Net.Http;
60054
+ using System.Net.Http.Json;
60055
+ using System.Threading.Tasks;
60056
+ using FluentAssertions;
60057
+ using Xunit;
60058
+ using {{testNamespace}}.Integration;
60059
+
60060
+ namespace {{testNamespace}}.Integration.Controllers;
60061
+
60062
+ /// <summary>
60063
+ /// REAL integration tests for {{name}}Controller.
60064
+ /// Uses actual DI pipeline, real services, SQLite database.
60065
+ /// No mocks - verifies the complete request/response cycle.
60066
+ /// </summary>
60067
+ public class {{name}}ControllerIntegrationTests : IntegrationTestBase
60068
+ {
60069
+ {{#unless isSystemEntity}}
60070
+ private readonly Guid _tenantId;
60071
+ {{/unless}}
60072
+
60073
+ public {{name}}ControllerIntegrationTests(SmartStackTestFactory factory)
60074
+ : base(factory)
60075
+ {
60076
+ {{#unless isSystemEntity}}
60077
+ _tenantId = TestTenantId;
60078
+ {{/unless}}
60079
+ }
60080
+
60081
+ #region GET Tests
60082
+
60083
+ [Fact]
60084
+ public async Task GetAll_WhenAuthenticated_ShouldReturn200()
60085
+ {
60086
+ // Act
60087
+ var response = await Client.GetAsync("/api/{{nameLower}}");
60088
+
60089
+ // Assert
60090
+ response.StatusCode.Should().Be(HttpStatusCode.OK);
60091
+ }
60092
+
60093
+ [Fact]
60094
+ public async Task GetById_WhenEntityExists_ShouldReturn200WithData()
60095
+ {
60096
+ // Arrange - Create an entity first
60097
+ var createRequest = new { Code = "get-test-01", Name = "Get Test Entity" };
60098
+ var createResponse = await Client.PostAsJsonAsync("/api/{{nameLower}}", createRequest);
60099
+ createResponse.StatusCode.Should().BeOneOf(HttpStatusCode.Created, HttpStatusCode.OK);
60100
+
60101
+ var created = await createResponse.Content.ReadFromJsonAsync<Dictionary<string, object>>();
60102
+ created.Should().NotBeNull();
60103
+ var id = created!["id"]?.ToString();
60104
+ id.Should().NotBeNullOrEmpty();
60105
+
60106
+ // Act
60107
+ var response = await Client.GetAsync($"/api/{{nameLower}}/{id}");
60108
+
60109
+ // Assert
60110
+ response.StatusCode.Should().Be(HttpStatusCode.OK);
60111
+ var entity = await response.Content.ReadFromJsonAsync<Dictionary<string, object>>();
60112
+ entity.Should().NotBeNull();
60113
+ }
60114
+
60115
+ [Fact]
60116
+ public async Task GetById_WhenNotExists_ShouldReturn404()
60117
+ {
60118
+ // Arrange
60119
+ var nonExistentId = Guid.NewGuid();
60120
+
60121
+ // Act
60122
+ var response = await Client.GetAsync($"/api/{{nameLower}}/{nonExistentId}");
60123
+
60124
+ // Assert
60125
+ response.StatusCode.Should().Be(HttpStatusCode.NotFound);
60126
+ }
60127
+
60128
+ #endregion
60129
+
60130
+ #region POST Tests
60131
+
60132
+ [Fact]
60133
+ public async Task Create_WhenValidData_ShouldPersistAndReturn201()
60134
+ {
60135
+ // Arrange
60136
+ var request = new { Code = "create-test-01", Name = "Created Entity" };
60137
+
60138
+ // Act
60139
+ var response = await Client.PostAsJsonAsync("/api/{{nameLower}}", request);
60140
+
60141
+ // Assert
60142
+ response.StatusCode.Should().BeOneOf(HttpStatusCode.Created, HttpStatusCode.OK);
60143
+ var created = await response.Content.ReadFromJsonAsync<Dictionary<string, object>>();
60144
+ created.Should().NotBeNull();
60145
+ var id = created!["id"]?.ToString();
60146
+ id.Should().NotBeNullOrEmpty();
60147
+
60148
+ // Verify REAL persistence - read back from DB
60149
+ var getResponse = await Client.GetAsync($"/api/{{nameLower}}/{id}");
60150
+ getResponse.StatusCode.Should().Be(HttpStatusCode.OK);
60151
+ var persisted = await getResponse.Content.ReadFromJsonAsync<Dictionary<string, object>>();
60152
+ persisted.Should().NotBeNull();
60153
+ }
60154
+
60155
+ {{#if includeValidation}}
60156
+ [Fact]
60157
+ public async Task Create_WhenInvalidData_ShouldReturn400()
60158
+ {
60159
+ // Arrange
60160
+ var request = new { Code = (string?)null, Name = (string?)null };
60161
+
60162
+ // Act
60163
+ var response = await Client.PostAsJsonAsync("/api/{{nameLower}}", request);
60164
+
60165
+ // Assert
60166
+ response.StatusCode.Should().BeOneOf(HttpStatusCode.BadRequest, HttpStatusCode.UnprocessableEntity);
60167
+ }
60168
+ {{/if}}
60169
+
60170
+ [Fact]
60171
+ public async Task Create_WhenDuplicateCode_ShouldReturn409()
60172
+ {
60173
+ // Arrange
60174
+ var request = new { Code = "duplicate-test", Name = "First Entity" };
60175
+ var firstResponse = await Client.PostAsJsonAsync("/api/{{nameLower}}", request);
60176
+ firstResponse.StatusCode.Should().BeOneOf(HttpStatusCode.Created, HttpStatusCode.OK);
60177
+
60178
+ // Act - Try to create with same code
60179
+ var duplicateRequest = new { Code = "duplicate-test", Name = "Duplicate Entity" };
60180
+ var response = await Client.PostAsJsonAsync("/api/{{nameLower}}", duplicateRequest);
60181
+
60182
+ // Assert
60183
+ response.StatusCode.Should().BeOneOf(HttpStatusCode.Conflict, HttpStatusCode.BadRequest);
60184
+ }
60185
+
60186
+ #endregion
60187
+
60188
+ #region PUT Tests
60189
+
60190
+ [Fact]
60191
+ public async Task Update_WhenEntityExists_ShouldPersistChanges()
60192
+ {
60193
+ // Arrange - Create an entity first
60194
+ var createRequest = new { Code = "update-test-01", Name = "Original Name" };
60195
+ var createResponse = await Client.PostAsJsonAsync("/api/{{nameLower}}", createRequest);
60196
+ var created = await createResponse.Content.ReadFromJsonAsync<Dictionary<string, object>>();
60197
+ var id = created!["id"]?.ToString();
60198
+
60199
+ // Act
60200
+ var updateRequest = new { Code = "update-test-01", Name = "Updated Name" };
60201
+ var response = await Client.PutAsJsonAsync($"/api/{{nameLower}}/{id}", updateRequest);
60202
+
60203
+ // Assert
60204
+ response.StatusCode.Should().Be(HttpStatusCode.OK);
60205
+
60206
+ // Verify persistence
60207
+ var getResponse = await Client.GetAsync($"/api/{{nameLower}}/{id}");
60208
+ var updated = await getResponse.Content.ReadFromJsonAsync<Dictionary<string, object>>();
60209
+ updated.Should().NotBeNull();
60210
+ }
60211
+
60212
+ [Fact]
60213
+ public async Task Update_WhenNotExists_ShouldReturn404()
60214
+ {
60215
+ // Arrange
60216
+ var nonExistentId = Guid.NewGuid();
60217
+ var request = new { Code = "ghost", Name = "Ghost Entity" };
60218
+
60219
+ // Act
60220
+ var response = await Client.PutAsJsonAsync($"/api/{{nameLower}}/{nonExistentId}", request);
60221
+
60222
+ // Assert
60223
+ response.StatusCode.Should().Be(HttpStatusCode.NotFound);
60224
+ }
60225
+
60226
+ #endregion
60227
+
60228
+ #region DELETE Tests
60229
+
60230
+ [Fact]
60231
+ public async Task Delete_WhenEntityExists_ShouldReturn204()
60232
+ {
60233
+ // Arrange - Create an entity first
60234
+ var createRequest = new { Code = "delete-test-01", Name = "To Delete" };
60235
+ var createResponse = await Client.PostAsJsonAsync("/api/{{nameLower}}", createRequest);
60236
+ var created = await createResponse.Content.ReadFromJsonAsync<Dictionary<string, object>>();
60237
+ var id = created!["id"]?.ToString();
60238
+
60239
+ // Act
60240
+ var response = await Client.DeleteAsync($"/api/{{nameLower}}/{id}");
60241
+
60242
+ // Assert
60243
+ response.StatusCode.Should().BeOneOf(HttpStatusCode.NoContent, HttpStatusCode.OK);
60244
+ }
60245
+
60246
+ [Fact]
60247
+ public async Task Delete_WhenNotExists_ShouldReturn404()
60248
+ {
60249
+ // Arrange
60250
+ var nonExistentId = Guid.NewGuid();
60251
+
60252
+ // Act
60253
+ var response = await Client.DeleteAsync($"/api/{{nameLower}}/{nonExistentId}");
60254
+
60255
+ // Assert
60256
+ response.StatusCode.Should().Be(HttpStatusCode.NotFound);
60257
+ }
60258
+
60259
+ #endregion
60260
+
60261
+ {{#unless isSystemEntity}}
60262
+ #region Tenant Isolation Tests (REAL)
60263
+
60264
+ [Fact]
60265
+ public async Task Create_ThenGetFromOtherTenant_ShouldReturn404()
60266
+ {
60267
+ // Arrange - Create entity in default tenant
60268
+ var request = new { Code = "tenant-iso-01", Name = "Tenant A Entity" };
60269
+ var createResponse = await Client.PostAsJsonAsync("/api/{{nameLower}}", request);
60270
+ var created = await createResponse.Content.ReadFromJsonAsync<Dictionary<string, object>>();
60271
+ var id = created!["id"]?.ToString();
60272
+
60273
+ // Act - Try to access from a different tenant
60274
+ var otherTenantClient = CreateClientForTenant(Guid.NewGuid());
60275
+ var response = await otherTenantClient.GetAsync($"/api/{{nameLower}}/{id}");
60276
+
60277
+ // Assert - Should not see other tenant's data
60278
+ response.StatusCode.Should().BeOneOf(HttpStatusCode.NotFound, HttpStatusCode.Forbidden);
60279
+ }
60280
+
60281
+ [Fact]
60282
+ public async Task GetAll_ShouldOnlyReturnCurrentTenantData()
60283
+ {
60284
+ // Arrange - Create entity in default tenant
60285
+ var request = new { Code = "tenant-list-01", Name = "My Tenant Entity" };
60286
+ await Client.PostAsJsonAsync("/api/{{nameLower}}", request);
60287
+
60288
+ // Act - Get all from a different tenant
60289
+ var otherTenantClient = CreateClientForTenant(Guid.NewGuid());
60290
+ var response = await otherTenantClient.GetAsync("/api/{{nameLower}}");
60291
+
60292
+ // Assert
60293
+ response.StatusCode.Should().Be(HttpStatusCode.OK);
60294
+ var content = await response.Content.ReadAsStringAsync();
60295
+ content.Should().NotContain("tenant-list-01");
60296
+ }
60297
+
60298
+ #endregion
60299
+ {{/unless}}
60300
+
60301
+ {{#if includeAuthorization}}
60302
+ #region Authorization Tests (REAL)
60303
+
60304
+ [Fact]
60305
+ public async Task GetAll_WhenNotAuthenticated_ShouldReturn401()
60306
+ {
60307
+ // Arrange
60308
+ var anonClient = CreateAnonymousClient();
60309
+
60310
+ // Act
60311
+ var response = await anonClient.GetAsync("/api/{{nameLower}}");
60312
+
60313
+ // Assert
60314
+ response.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
60315
+ }
60316
+
60317
+ [Fact]
60318
+ public async Task Create_WhenNotAuthenticated_ShouldReturn401()
60319
+ {
60320
+ // Arrange
60321
+ var anonClient = CreateAnonymousClient();
60322
+ var request = new { Code = "unauth-test", Name = "Should Fail" };
60323
+
60324
+ // Act
60325
+ var response = await anonClient.PostAsJsonAsync("/api/{{nameLower}}", request);
60326
+
60327
+ // Assert
60328
+ response.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
60329
+ }
60330
+
60331
+ #endregion
60332
+ {{/if}}
60333
+ }
59747
60334
  `;
59748
60335
  }
59749
60336
  });
@@ -63685,8 +64272,493 @@ Use this before scaffold_frontend_extension to understand what slots to add.`,
63685
64272
  }
63686
64273
  });
63687
64274
 
63688
- // src/mcp/tools/validate-security.ts
64275
+ // src/mcp/tools/scaffold-frontend-tests.ts
63689
64276
  import path22 from "path";
64277
+ async function handleScaffoldFrontendTests(args, config2) {
64278
+ const input = ScaffoldFrontendTestsInputSchema.parse(args);
64279
+ const dryRun = input.options?.dryRun || false;
64280
+ const name = input.name;
64281
+ const nameLower = name.charAt(0).toLowerCase() + name.slice(1);
64282
+ const apiRoute = input.apiRoute || `/api/${nameLower}`;
64283
+ logger.info("Scaffolding frontend tests", { name, components: input.components, dryRun });
64284
+ const structure = await findSmartStackStructure(config2.smartstack.projectPath);
64285
+ const result = {
64286
+ success: true,
64287
+ files: [],
64288
+ instructions: []
64289
+ };
64290
+ const context = { name, nameLower, apiRoute };
64291
+ const components = input.components.includes("all") ? ["page", "list", "detail", "hooks", "api"] : input.components;
64292
+ const webRoot = input.options?.outputPath || path22.join(structure.root, "web", "smartstack-web");
64293
+ const templateMap = {
64294
+ page: { template: pageTestTemplate, filename: `${name}Page.test.tsx` },
64295
+ list: { template: listTestTemplate, filename: `${name}ListView.test.tsx` },
64296
+ detail: { template: detailTestTemplate, filename: `${name}DetailPage.test.tsx` },
64297
+ hooks: { template: hooksTestTemplate, filename: `use${name}Preferences.test.ts` },
64298
+ api: { template: apiTestTemplate, filename: `${nameLower}Api.test.ts` }
64299
+ };
64300
+ try {
64301
+ for (const component of components) {
64302
+ const entry = templateMap[component];
64303
+ if (!entry) continue;
64304
+ const content = import_handlebars3.default.compile(entry.template)(context);
64305
+ let testDir;
64306
+ if (component === "hooks") {
64307
+ testDir = path22.join(webRoot, "src", "hooks", "__tests__");
64308
+ } else if (component === "api") {
64309
+ testDir = path22.join(webRoot, "src", "services", "api", "__tests__");
64310
+ } else {
64311
+ testDir = path22.join(webRoot, "src", "pages", nameLower, "__tests__");
64312
+ }
64313
+ const testPath = path22.join(testDir, entry.filename);
64314
+ validatePathSecurity(testPath, structure.root);
64315
+ if (!dryRun) {
64316
+ await ensureDirectory(testDir);
64317
+ await writeText(testPath, content);
64318
+ }
64319
+ result.files.push({
64320
+ path: path22.relative(structure.root, testPath),
64321
+ content,
64322
+ type: "created"
64323
+ });
64324
+ }
64325
+ } catch (error2) {
64326
+ result.success = false;
64327
+ result.instructions.push(`Error: ${error2 instanceof Error ? error2.message : String(error2)}`);
64328
+ }
64329
+ result.instructions.push("Install test dependencies: npm install -D vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom msw");
64330
+ result.instructions.push("Copy test infrastructure from templates/project/test-frontend/ to your web project src/test/");
64331
+ result.instructions.push('Add "test": "vitest run" and "test:watch": "vitest" to package.json scripts');
64332
+ return formatResult8(result, name, dryRun);
64333
+ }
64334
+ function formatResult8(result, name, dryRun) {
64335
+ const lines = [];
64336
+ lines.push(`# Scaffold Frontend Tests: ${name}`);
64337
+ lines.push("");
64338
+ if (dryRun) {
64339
+ lines.push("> **DRY RUN MODE** - No files were written");
64340
+ lines.push("");
64341
+ }
64342
+ if (!result.success) {
64343
+ lines.push("## Error");
64344
+ lines.push("");
64345
+ lines.push(result.instructions.join("\n"));
64346
+ return lines.join("\n");
64347
+ }
64348
+ lines.push(`## Generated Test Files (${result.files.length})`);
64349
+ lines.push("");
64350
+ for (const file of result.files) {
64351
+ lines.push(`### \`${file.path}\``);
64352
+ lines.push("");
64353
+ lines.push("```tsx");
64354
+ lines.push(file.content);
64355
+ lines.push("```");
64356
+ lines.push("");
64357
+ }
64358
+ if (result.instructions.length > 0) {
64359
+ lines.push("## Next Steps");
64360
+ lines.push("");
64361
+ for (const instruction of result.instructions) {
64362
+ lines.push(`- ${instruction}`);
64363
+ }
64364
+ lines.push("");
64365
+ }
64366
+ lines.push("## Test Conventions Applied");
64367
+ lines.push("");
64368
+ lines.push("- **Framework**: Vitest + @testing-library/react");
64369
+ lines.push("- **API Mocking**: MSW (Mock Service Worker)");
64370
+ lines.push("- **Pattern**: Arrange (setup MSW handlers) -> Act (render component) -> Assert (check DOM)");
64371
+ lines.push("- **Providers**: BrowserRouter, I18nextProvider, AuthContext");
64372
+ lines.push("");
64373
+ return lines.join("\n");
64374
+ }
64375
+ var import_handlebars3, ScaffoldFrontendTestsInputSchema, scaffoldFrontendTestsTool, pageTestTemplate, listTestTemplate, detailTestTemplate, hooksTestTemplate, apiTestTemplate;
64376
+ var init_scaffold_frontend_tests = __esm({
64377
+ "src/mcp/tools/scaffold-frontend-tests.ts"() {
64378
+ "use strict";
64379
+ init_esm_shims();
64380
+ import_handlebars3 = __toESM(require_lib());
64381
+ init_zod();
64382
+ init_fs();
64383
+ init_detector();
64384
+ init_logger();
64385
+ ScaffoldFrontendTestsInputSchema = external_exports.object({
64386
+ name: external_exports.string().min(1).describe('Entity name in PascalCase (e.g., "Product", "Order")'),
64387
+ components: external_exports.array(external_exports.enum(["page", "list", "form", "detail", "hooks", "api", "all"])).default(["all"]).describe("Components to generate tests for"),
64388
+ apiRoute: external_exports.string().optional().describe('API route path (e.g., "/api/products")'),
64389
+ options: external_exports.object({
64390
+ outputPath: external_exports.string().optional().describe("Custom output path for generated test files"),
64391
+ dryRun: external_exports.boolean().default(false).describe("Preview without writing files")
64392
+ }).optional()
64393
+ });
64394
+ scaffoldFrontendTestsTool = {
64395
+ name: "scaffold_frontend_tests",
64396
+ description: "Generate React component tests (Vitest + Testing Library + MSW) for SmartStack frontend pages, lists, forms, and hooks.",
64397
+ inputSchema: {
64398
+ type: "object",
64399
+ properties: {
64400
+ name: {
64401
+ type: "string",
64402
+ description: 'Entity name in PascalCase (e.g., "Product", "Order")'
64403
+ },
64404
+ components: {
64405
+ type: "array",
64406
+ items: {
64407
+ type: "string",
64408
+ enum: ["page", "list", "form", "detail", "hooks", "api", "all"]
64409
+ },
64410
+ default: ["all"],
64411
+ description: "Components to generate tests for"
64412
+ },
64413
+ apiRoute: {
64414
+ type: "string",
64415
+ description: 'API route path (e.g., "/api/products")'
64416
+ },
64417
+ options: {
64418
+ type: "object",
64419
+ properties: {
64420
+ outputPath: { type: "string" },
64421
+ dryRun: { type: "boolean", default: false }
64422
+ }
64423
+ }
64424
+ },
64425
+ required: ["name"]
64426
+ }
64427
+ };
64428
+ pageTestTemplate = `import { describe, it, expect, vi } from 'vitest';
64429
+ import { renderWithProviders, screen, waitFor } from '@/test/test-utils';
64430
+ import { server } from '@/test/msw/server';
64431
+ import { http, HttpResponse } from 'msw';
64432
+
64433
+ // Adjust import path to your actual page component
64434
+ // import { {{name}}Page } from '@/pages/{{nameLower}}/{{name}}Page';
64435
+
64436
+ describe('{{name}}Page', () => {
64437
+ it('should render the page title', async () => {
64438
+ // TODO: Uncomment and adjust import path
64439
+ // renderWithProviders(<{{name}}Page />);
64440
+ // await waitFor(() => {
64441
+ // expect(screen.getByRole('heading')).toBeInTheDocument();
64442
+ // });
64443
+ expect(true).toBe(true); // Placeholder - replace with actual test
64444
+ });
64445
+
64446
+ it('should show loading state initially', () => {
64447
+ // renderWithProviders(<{{name}}Page />);
64448
+ // expect(screen.getByTestId('page-loader')).toBeInTheDocument();
64449
+ expect(true).toBe(true);
64450
+ });
64451
+
64452
+ it('should display data after loading', async () => {
64453
+ server.use(
64454
+ http.get('{{apiRoute}}', () =>
64455
+ HttpResponse.json({
64456
+ items: [
64457
+ { id: '1', code: 'test-01', name: 'Test {{name}}' },
64458
+ ],
64459
+ totalCount: 1,
64460
+ pageNumber: 1,
64461
+ pageSize: 10,
64462
+ })
64463
+ )
64464
+ );
64465
+
64466
+ // renderWithProviders(<{{name}}Page />);
64467
+ // await waitFor(() => {
64468
+ // expect(screen.getByText('Test {{name}}')).toBeInTheDocument();
64469
+ // });
64470
+ expect(true).toBe(true);
64471
+ });
64472
+
64473
+ it('should show empty state when no data', async () => {
64474
+ server.use(
64475
+ http.get('{{apiRoute}}', () =>
64476
+ HttpResponse.json({
64477
+ items: [],
64478
+ totalCount: 0,
64479
+ pageNumber: 1,
64480
+ pageSize: 10,
64481
+ })
64482
+ )
64483
+ );
64484
+
64485
+ // renderWithProviders(<{{name}}Page />);
64486
+ // await waitFor(() => {
64487
+ // expect(screen.getByText(/empty|no data|aucun/i)).toBeInTheDocument();
64488
+ // });
64489
+ expect(true).toBe(true);
64490
+ });
64491
+
64492
+ it('should show error state on API failure', async () => {
64493
+ server.use(
64494
+ http.get('{{apiRoute}}', () =>
64495
+ HttpResponse.error()
64496
+ )
64497
+ );
64498
+
64499
+ // renderWithProviders(<{{name}}Page />);
64500
+ // await waitFor(() => {
64501
+ // expect(screen.getByText(/error|erreur/i)).toBeInTheDocument();
64502
+ // });
64503
+ expect(true).toBe(true);
64504
+ });
64505
+ });
64506
+ `;
64507
+ listTestTemplate = `import { describe, it, expect } from 'vitest';
64508
+ import { renderWithProviders, screen, within } from '@/test/test-utils';
64509
+ import { server } from '@/test/msw/server';
64510
+ import { http, HttpResponse } from 'msw';
64511
+
64512
+ // Adjust import path
64513
+ // import { {{name}}ListView } from '@/components/{{nameLower}}/{{name}}ListView';
64514
+
64515
+ const mockItems = [
64516
+ { id: '1', code: 'item-01', name: 'First {{name}}' },
64517
+ { id: '2', code: 'item-02', name: 'Second {{name}}' },
64518
+ { id: '3', code: 'item-03', name: 'Third {{name}}' },
64519
+ ];
64520
+
64521
+ describe('{{name}}ListView', () => {
64522
+ it('should render a list of items', () => {
64523
+ // renderWithProviders(<{{name}}ListView items={mockItems} />);
64524
+ // expect(screen.getByText('First {{name}}')).toBeInTheDocument();
64525
+ // expect(screen.getByText('Second {{name}}')).toBeInTheDocument();
64526
+ expect(true).toBe(true);
64527
+ });
64528
+
64529
+ it('should render EntityCard for each item', () => {
64530
+ // renderWithProviders(<{{name}}ListView items={mockItems} />);
64531
+ // const cards = screen.getAllByTestId('entity-card');
64532
+ // expect(cards).toHaveLength(3);
64533
+ expect(true).toBe(true);
64534
+ });
64535
+
64536
+ it('should show empty state when no items', () => {
64537
+ // renderWithProviders(<{{name}}ListView items={[]} />);
64538
+ // expect(screen.getByText(/empty|no data|aucun/i)).toBeInTheDocument();
64539
+ expect(true).toBe(true);
64540
+ });
64541
+
64542
+ it('should support view toggle (table/cards)', () => {
64543
+ // renderWithProviders(<{{name}}ListView items={mockItems} />);
64544
+ // const viewToggle = screen.getByTestId('view-toggle');
64545
+ // expect(viewToggle).toBeInTheDocument();
64546
+ expect(true).toBe(true);
64547
+ });
64548
+
64549
+ it('should handle pagination', () => {
64550
+ // renderWithProviders(
64551
+ // <{{name}}ListView
64552
+ // items={mockItems}
64553
+ // totalCount={30}
64554
+ // pageSize={10}
64555
+ // currentPage={1}
64556
+ // />
64557
+ // );
64558
+ // expect(screen.getByTestId('pagination')).toBeInTheDocument();
64559
+ expect(true).toBe(true);
64560
+ });
64561
+ });
64562
+ `;
64563
+ detailTestTemplate = `import { describe, it, expect } from 'vitest';
64564
+ import { renderWithProviders, screen, waitFor } from '@/test/test-utils';
64565
+ import { server } from '@/test/msw/server';
64566
+ import { http, HttpResponse } from 'msw';
64567
+
64568
+ // Adjust import path
64569
+ // import { {{name}}DetailPage } from '@/pages/{{nameLower}}/{{name}}DetailPage';
64570
+
64571
+ const mockEntity = {
64572
+ id: '1',
64573
+ code: 'test-01',
64574
+ name: 'Test {{name}}',
64575
+ createdAt: '2024-01-01T00:00:00Z',
64576
+ createdBy: 'admin',
64577
+ };
64578
+
64579
+ describe('{{name}}DetailPage', () => {
64580
+ it('should render entity details', async () => {
64581
+ server.use(
64582
+ http.get('{{apiRoute}}/:id', () =>
64583
+ HttpResponse.json(mockEntity)
64584
+ )
64585
+ );
64586
+
64587
+ // renderWithProviders(<{{name}}DetailPage />);
64588
+ // await waitFor(() => {
64589
+ // expect(screen.getByText('Test {{name}}')).toBeInTheDocument();
64590
+ // });
64591
+ expect(true).toBe(true);
64592
+ });
64593
+
64594
+ it('should show loading state', () => {
64595
+ // renderWithProviders(<{{name}}DetailPage />);
64596
+ // expect(screen.getByTestId('page-loader')).toBeInTheDocument();
64597
+ expect(true).toBe(true);
64598
+ });
64599
+
64600
+ it('should show 404 when entity not found', async () => {
64601
+ server.use(
64602
+ http.get('{{apiRoute}}/:id', () =>
64603
+ new HttpResponse(null, { status: 404 })
64604
+ )
64605
+ );
64606
+
64607
+ // renderWithProviders(<{{name}}DetailPage />);
64608
+ // await waitFor(() => {
64609
+ // expect(screen.getByText(/not found|introuvable/i)).toBeInTheDocument();
64610
+ // });
64611
+ expect(true).toBe(true);
64612
+ });
64613
+
64614
+ it('should have a back button', () => {
64615
+ // renderWithProviders(<{{name}}DetailPage />);
64616
+ // expect(screen.getByRole('link', { name: /back|retour/i })).toBeInTheDocument();
64617
+ expect(true).toBe(true);
64618
+ });
64619
+ });
64620
+ `;
64621
+ hooksTestTemplate = `import { describe, it, expect } from 'vitest';
64622
+ import { renderHook, act } from '@testing-library/react';
64623
+
64624
+ // Adjust import path
64625
+ // import { use{{name}}Preferences } from '@/hooks/use{{name}}Preferences';
64626
+
64627
+ describe('use{{name}}Preferences', () => {
64628
+ it('should return default pageSize', () => {
64629
+ // const { result } = renderHook(() => use{{name}}Preferences());
64630
+ // expect(result.current.pageSize).toBe(10);
64631
+ expect(true).toBe(true);
64632
+ });
64633
+
64634
+ it('should return default viewMode', () => {
64635
+ // const { result } = renderHook(() => use{{name}}Preferences());
64636
+ // expect(result.current.viewMode).toBe('table');
64637
+ expect(true).toBe(true);
64638
+ });
64639
+
64640
+ it('should update pageSize', () => {
64641
+ // const { result } = renderHook(() => use{{name}}Preferences());
64642
+ // act(() => {
64643
+ // result.current.setPageSize(25);
64644
+ // });
64645
+ // expect(result.current.pageSize).toBe(25);
64646
+ expect(true).toBe(true);
64647
+ });
64648
+
64649
+ it('should update viewMode', () => {
64650
+ // const { result } = renderHook(() => use{{name}}Preferences());
64651
+ // act(() => {
64652
+ // result.current.setViewMode('cards');
64653
+ // });
64654
+ // expect(result.current.viewMode).toBe('cards');
64655
+ expect(true).toBe(true);
64656
+ });
64657
+ });
64658
+ `;
64659
+ apiTestTemplate = `import { describe, it, expect, beforeEach } from 'vitest';
64660
+ import { server } from '@/test/msw/server';
64661
+ import { http, HttpResponse } from 'msw';
64662
+
64663
+ // Adjust import path
64664
+ // import { {{nameLower}}Api } from '@/services/api/{{nameLower}}Api';
64665
+
64666
+ describe('{{nameLower}}Api', () => {
64667
+ describe('getAll', () => {
64668
+ it('should fetch paginated list', async () => {
64669
+ server.use(
64670
+ http.get('{{apiRoute}}', () =>
64671
+ HttpResponse.json({
64672
+ items: [{ id: '1', code: 'test', name: 'Test' }],
64673
+ totalCount: 1,
64674
+ pageNumber: 1,
64675
+ pageSize: 10,
64676
+ })
64677
+ )
64678
+ );
64679
+
64680
+ // const result = await {{nameLower}}Api.getAll();
64681
+ // expect(result.items).toHaveLength(1);
64682
+ // expect(result.totalCount).toBe(1);
64683
+ expect(true).toBe(true);
64684
+ });
64685
+ });
64686
+
64687
+ describe('getById', () => {
64688
+ it('should fetch entity by ID', async () => {
64689
+ server.use(
64690
+ http.get('{{apiRoute}}/:id', () =>
64691
+ HttpResponse.json({ id: '1', code: 'test', name: 'Test' })
64692
+ )
64693
+ );
64694
+
64695
+ // const result = await {{nameLower}}Api.getById('1');
64696
+ // expect(result.id).toBe('1');
64697
+ expect(true).toBe(true);
64698
+ });
64699
+
64700
+ it('should throw on 404', async () => {
64701
+ server.use(
64702
+ http.get('{{apiRoute}}/:id', () =>
64703
+ new HttpResponse(null, { status: 404 })
64704
+ )
64705
+ );
64706
+
64707
+ // await expect({{nameLower}}Api.getById('999')).rejects.toThrow();
64708
+ expect(true).toBe(true);
64709
+ });
64710
+ });
64711
+
64712
+ describe('create', () => {
64713
+ it('should create entity and return it', async () => {
64714
+ server.use(
64715
+ http.post('{{apiRoute}}', () =>
64716
+ HttpResponse.json(
64717
+ { id: '1', code: 'new-test', name: 'New Test' },
64718
+ { status: 201 }
64719
+ )
64720
+ )
64721
+ );
64722
+
64723
+ // const result = await {{nameLower}}Api.create({ code: 'new-test', name: 'New Test' });
64724
+ // expect(result.id).toBe('1');
64725
+ expect(true).toBe(true);
64726
+ });
64727
+ });
64728
+
64729
+ describe('update', () => {
64730
+ it('should update entity', async () => {
64731
+ server.use(
64732
+ http.put('{{apiRoute}}/:id', () =>
64733
+ HttpResponse.json({ id: '1', code: 'updated', name: 'Updated' })
64734
+ )
64735
+ );
64736
+
64737
+ // const result = await {{nameLower}}Api.update('1', { code: 'updated', name: 'Updated' });
64738
+ // expect(result.code).toBe('updated');
64739
+ expect(true).toBe(true);
64740
+ });
64741
+ });
64742
+
64743
+ describe('delete', () => {
64744
+ it('should delete entity', async () => {
64745
+ server.use(
64746
+ http.delete('{{apiRoute}}/:id', () =>
64747
+ new HttpResponse(null, { status: 204 })
64748
+ )
64749
+ );
64750
+
64751
+ // await expect({{nameLower}}Api.delete('1')).resolves.not.toThrow();
64752
+ expect(true).toBe(true);
64753
+ });
64754
+ });
64755
+ });
64756
+ `;
64757
+ }
64758
+ });
64759
+
64760
+ // src/mcp/tools/validate-security.ts
64761
+ import path23 from "path";
63690
64762
  async function handleValidateSecurity(args, config2) {
63691
64763
  const input = ValidateSecurityInputSchema.parse(args);
63692
64764
  const projectPath = input.path || config2.smartstack.projectPath;
@@ -63768,7 +64840,7 @@ async function checkHardcodedSecrets(structure, result) {
63768
64840
  severity: "blocking",
63769
64841
  category: "hardcoded-secrets",
63770
64842
  message: `Hardcoded ${name} detected`,
63771
- file: path22.relative(structure.root, file),
64843
+ file: path23.relative(structure.root, file),
63772
64844
  line: lineNumber,
63773
64845
  code: truncateCode(lineContent),
63774
64846
  suggestion: `Move ${name} to configuration or environment variables`,
@@ -63803,7 +64875,7 @@ async function checkSqlInjection(structure, result) {
63803
64875
  severity: "blocking",
63804
64876
  category: "sql-injection",
63805
64877
  message: `Potential SQL injection: ${name}`,
63806
- file: path22.relative(structure.root, file),
64878
+ file: path23.relative(structure.root, file),
63807
64879
  line: lineNumber,
63808
64880
  code: truncateCode(lineContent),
63809
64881
  suggestion: 'Use parameterized queries: FromSqlRaw("SELECT * FROM x WHERE id = {0}", id) or FromSqlInterpolated',
@@ -63843,7 +64915,7 @@ async function checkTenantIsolation(structure, result) {
63843
64915
  severity: "blocking",
63844
64916
  category: "tenant-isolation",
63845
64917
  message: "Create method missing tenantId parameter for tenant entity",
63846
- file: path22.relative(structure.root, file),
64918
+ file: path23.relative(structure.root, file),
63847
64919
  line: lineNumber,
63848
64920
  code: truncateCode(match2[0]),
63849
64921
  suggestion: "Add Guid tenantId as first parameter: Create(Guid tenantId, ...)",
@@ -63866,7 +64938,7 @@ async function checkTenantIsolation(structure, result) {
63866
64938
  severity: "critical",
63867
64939
  category: "tenant-isolation",
63868
64940
  message: "Direct DbContext access without tenant filtering",
63869
- file: path22.relative(structure.root, file),
64941
+ file: path23.relative(structure.root, file),
63870
64942
  line: lineNumber,
63871
64943
  code: truncateCode(match2[0]),
63872
64944
  suggestion: "Use repository with global tenant filter or add explicit TenantId filter",
@@ -63903,7 +64975,7 @@ async function checkAuthorization(structure, result) {
63903
64975
  severity: "blocking",
63904
64976
  category: "authorization",
63905
64977
  message: `Controller ${controllerName} missing authorization attribute`,
63906
- file: path22.relative(structure.root, file),
64978
+ file: path23.relative(structure.root, file),
63907
64979
  line: lineNumber,
63908
64980
  suggestion: 'Add [NavRoute("context.application.module")] or [Authorize] attribute to the controller class',
63909
64981
  cweId: "CWE-862"
@@ -63951,7 +65023,7 @@ async function checkPatterns(file, content, patterns, structure, result) {
63951
65023
  severity: "critical",
63952
65024
  category: "dangerous-functions",
63953
65025
  message: `Dangerous function: ${name}`,
63954
- file: path22.relative(structure.root, file),
65026
+ file: path23.relative(structure.root, file),
63955
65027
  line: lineNumber,
63956
65028
  code: truncateCode(lineContent),
63957
65029
  suggestion: "Avoid using dangerous functions with user-controlled input. Use safe alternatives.",
@@ -64160,7 +65232,7 @@ var init_validate_security = __esm({
64160
65232
  });
64161
65233
 
64162
65234
  // src/mcp/tools/analyze-code-quality.ts
64163
- import path23 from "path";
65235
+ import path24 from "path";
64164
65236
  async function handleAnalyzeCodeQuality(args, config2) {
64165
65237
  const input = AnalyzeCodeQualityInputSchema.parse(args);
64166
65238
  const projectPath = input.path || config2.smartstack.projectPath;
@@ -64179,7 +65251,7 @@ async function handleAnalyzeCodeQuality(args, config2) {
64179
65251
  const filteredCsFiles = csFiles.filter((f) => !isExcludedPath(f));
64180
65252
  for (const file of filteredCsFiles) {
64181
65253
  const content = await readText(file);
64182
- const relPath = path23.relative(structure.root, file);
65254
+ const relPath = path24.relative(structure.root, file);
64183
65255
  const lineCount = content.split("\n").length;
64184
65256
  const functions = extractCSharpFunctions(content, relPath);
64185
65257
  allFunctionMetrics.push(...functions);
@@ -64196,7 +65268,7 @@ async function handleAnalyzeCodeQuality(args, config2) {
64196
65268
  const filteredTsFiles = tsFiles.filter((f) => !isExcludedPath(f));
64197
65269
  for (const file of filteredTsFiles) {
64198
65270
  const content = await readText(file);
64199
- const relPath = path23.relative(structure.root, file);
65271
+ const relPath = path24.relative(structure.root, file);
64200
65272
  const lineCount = content.split("\n").length;
64201
65273
  const functions = extractTypeScriptFunctions(content, relPath);
64202
65274
  allFunctionMetrics.push(...functions);
@@ -64812,7 +65884,7 @@ var init_analyze_code_quality = __esm({
64812
65884
  });
64813
65885
 
64814
65886
  // src/mcp/tools/analyze-hierarchy-patterns.ts
64815
- import path24 from "path";
65887
+ import path25 from "path";
64816
65888
  async function handleAnalyzeHierarchyPatterns(args, config2) {
64817
65889
  const input = AnalyzeHierarchyPatternsInputSchema.parse(args);
64818
65890
  const projectPath = input.path || config2.smartstack.projectPath;
@@ -64835,7 +65907,7 @@ async function handleAnalyzeHierarchyPatterns(args, config2) {
64835
65907
  recommendations,
64836
65908
  circularDependencies: detectCircularDependencies(entityGraph)
64837
65909
  };
64838
- return formatResult8(result, outputFormat, structure.root);
65910
+ return formatResult9(result, outputFormat, structure.root);
64839
65911
  }
64840
65912
  async function buildEntityGraph(domainPath, filter3) {
64841
65913
  const entityFiles = await findFiles("**/*.cs", { cwd: domainPath });
@@ -64891,7 +65963,7 @@ async function buildEntityGraph(domainPath, filter3) {
64891
65963
  return entityMap;
64892
65964
  }
64893
65965
  function parseEntity(content, file, domainPath) {
64894
- const fileName = path24.basename(file, ".cs");
65966
+ const fileName = path25.basename(file, ".cs");
64895
65967
  if (fileName.endsWith("Dto") || fileName.endsWith("Command") || fileName.endsWith("Query") || fileName.endsWith("Handler") || fileName.endsWith("Validator") || fileName.endsWith("Exception") || fileName.startsWith("I")) {
64896
65968
  return null;
64897
65969
  }
@@ -64935,7 +66007,7 @@ function parseEntity(content, file, domainPath) {
64935
66007
  const parentEntity = foreignKeys.length > 0 ? foreignKeys[0].referencedEntity : void 0;
64936
66008
  return {
64937
66009
  name: entityName,
64938
- file: path24.relative(domainPath, file),
66010
+ file: path25.relative(domainPath, file),
64939
66011
  baseClass: hasSystemEntity ? "SystemEntity" : "BaseEntity",
64940
66012
  isTenantAware: hasITenantEntity,
64941
66013
  isSystemEntity: hasSystemEntity,
@@ -64966,24 +66038,24 @@ function detectCircularDependencies(entityMap) {
64966
66038
  const cycles = [];
64967
66039
  for (const [name] of entityMap) {
64968
66040
  let dfs2 = function(current) {
64969
- if (path29.includes(current)) {
64970
- const cycleStart = path29.indexOf(current);
64971
- cycles.push([...path29.slice(cycleStart), current]);
66041
+ if (path30.includes(current)) {
66042
+ const cycleStart = path30.indexOf(current);
66043
+ cycles.push([...path30.slice(cycleStart), current]);
64972
66044
  return true;
64973
66045
  }
64974
66046
  if (visited.has(current)) return false;
64975
66047
  visited.add(current);
64976
- path29.push(current);
66048
+ path30.push(current);
64977
66049
  const node = entityMap.get(current);
64978
66050
  if (node?.parent) {
64979
66051
  dfs2(node.parent);
64980
66052
  }
64981
- path29.pop();
66053
+ path30.pop();
64982
66054
  return false;
64983
66055
  };
64984
66056
  var dfs = dfs2;
64985
66057
  const visited = /* @__PURE__ */ new Set();
64986
- const path29 = [];
66058
+ const path30 = [];
64987
66059
  dfs2(name);
64988
66060
  }
64989
66061
  const unique = /* @__PURE__ */ new Map();
@@ -65190,7 +66262,7 @@ function getImplementationSteps(pattern, node) {
65190
66262
  return [];
65191
66263
  }
65192
66264
  }
65193
- function formatResult8(result, format, _rootPath) {
66265
+ function formatResult9(result, format, _rootPath) {
65194
66266
  if (format === "json") {
65195
66267
  return JSON.stringify(result, null, 2);
65196
66268
  }
@@ -68928,7 +70000,7 @@ var init_conventions = __esm({
68928
70000
  });
68929
70001
 
68930
70002
  // src/mcp/resources/project-info.ts
68931
- import path25 from "path";
70003
+ import path26 from "path";
68932
70004
  async function getProjectInfoResource(config2) {
68933
70005
  const projectPath = config2.smartstack.projectPath;
68934
70006
  const projectInfo = await detectProject(projectPath);
@@ -68959,16 +70031,16 @@ async function getProjectInfoResource(config2) {
68959
70031
  lines.push("```");
68960
70032
  lines.push(`${projectInfo.name}/`);
68961
70033
  if (structure.domain) {
68962
- lines.push(`\u251C\u2500\u2500 ${path25.basename(structure.domain)}/ # Domain layer (entities)`);
70034
+ lines.push(`\u251C\u2500\u2500 ${path26.basename(structure.domain)}/ # Domain layer (entities)`);
68963
70035
  }
68964
70036
  if (structure.application) {
68965
- lines.push(`\u251C\u2500\u2500 ${path25.basename(structure.application)}/ # Application layer (services)`);
70037
+ lines.push(`\u251C\u2500\u2500 ${path26.basename(structure.application)}/ # Application layer (services)`);
68966
70038
  }
68967
70039
  if (structure.infrastructure) {
68968
- lines.push(`\u251C\u2500\u2500 ${path25.basename(structure.infrastructure)}/ # Infrastructure (EF Core)`);
70040
+ lines.push(`\u251C\u2500\u2500 ${path26.basename(structure.infrastructure)}/ # Infrastructure (EF Core)`);
68969
70041
  }
68970
70042
  if (structure.api) {
68971
- lines.push(`\u251C\u2500\u2500 ${path25.basename(structure.api)}/ # API layer (controllers)`);
70043
+ lines.push(`\u251C\u2500\u2500 ${path26.basename(structure.api)}/ # API layer (controllers)`);
68972
70044
  }
68973
70045
  if (structure.web) {
68974
70046
  lines.push(`\u2514\u2500\u2500 web/smartstack-web/ # React frontend`);
@@ -68981,8 +70053,8 @@ async function getProjectInfoResource(config2) {
68981
70053
  lines.push("| Project | Path |");
68982
70054
  lines.push("|---------|------|");
68983
70055
  for (const csproj of projectInfo.csprojFiles) {
68984
- const name = path25.basename(csproj, ".csproj");
68985
- const relativePath = path25.relative(projectPath, csproj);
70056
+ const name = path26.basename(csproj, ".csproj");
70057
+ const relativePath = path26.relative(projectPath, csproj);
68986
70058
  lines.push(`| ${name} | \`${relativePath}\` |`);
68987
70059
  }
68988
70060
  lines.push("");
@@ -68992,10 +70064,10 @@ async function getProjectInfoResource(config2) {
68992
70064
  cwd: structure.migrations,
68993
70065
  ignore: ["*.Designer.cs"]
68994
70066
  });
68995
- const migrations = migrationFiles.map((f) => path25.basename(f)).filter((f) => !f.includes("ModelSnapshot") && !f.includes(".Designer.")).sort();
70067
+ const migrations = migrationFiles.map((f) => path26.basename(f)).filter((f) => !f.includes("ModelSnapshot") && !f.includes(".Designer.")).sort();
68996
70068
  lines.push("## EF Core Migrations");
68997
70069
  lines.push("");
68998
- lines.push(`**Location**: \`${path25.relative(projectPath, structure.migrations)}\``);
70070
+ lines.push(`**Location**: \`${path26.relative(projectPath, structure.migrations)}\``);
68999
70071
  lines.push(`**Total Migrations**: ${migrations.length}`);
69000
70072
  lines.push("");
69001
70073
  if (migrations.length > 0) {
@@ -69030,11 +70102,11 @@ async function getProjectInfoResource(config2) {
69030
70102
  lines.push("dotnet build");
69031
70103
  lines.push("");
69032
70104
  lines.push("# Run API");
69033
- lines.push(`cd ${structure.api ? path25.relative(projectPath, structure.api) : "src/Api"}`);
70105
+ lines.push(`cd ${structure.api ? path26.relative(projectPath, structure.api) : "src/Api"}`);
69034
70106
  lines.push("dotnet run");
69035
70107
  lines.push("");
69036
70108
  lines.push("# Run frontend");
69037
- lines.push(`cd ${structure.web ? path25.relative(projectPath, structure.web) : "web"}`);
70109
+ lines.push(`cd ${structure.web ? path26.relative(projectPath, structure.web) : "web"}`);
69038
70110
  lines.push("npm run dev");
69039
70111
  lines.push("");
69040
70112
  lines.push("# Create migration");
@@ -69072,7 +70144,7 @@ var init_project_info = __esm({
69072
70144
  });
69073
70145
 
69074
70146
  // src/mcp/resources/api-endpoints.ts
69075
- import path26 from "path";
70147
+ import path27 from "path";
69076
70148
  async function getApiEndpointsResource(config2, endpointFilter) {
69077
70149
  const structure = await findSmartStackStructure(config2.smartstack.projectPath);
69078
70150
  if (!structure.api) {
@@ -69091,7 +70163,7 @@ async function getApiEndpointsResource(config2, endpointFilter) {
69091
70163
  }
69092
70164
  async function parseController(filePath, _rootPath) {
69093
70165
  const content = await readText(filePath);
69094
- const fileName = path26.basename(filePath, ".cs");
70166
+ const fileName = path27.basename(filePath, ".cs");
69095
70167
  const controllerName = fileName.replace("Controller", "");
69096
70168
  const endpoints = [];
69097
70169
  const routeMatch = content.match(/\[Route\s*\(\s*"([^"]+)"\s*\)\]/);
@@ -69253,7 +70325,7 @@ var init_api_endpoints = __esm({
69253
70325
  });
69254
70326
 
69255
70327
  // src/mcp/resources/db-schema.ts
69256
- import path27 from "path";
70328
+ import path28 from "path";
69257
70329
  async function getDbSchemaResource(config2, tableFilter) {
69258
70330
  const structure = await findSmartStackStructure(config2.smartstack.projectPath);
69259
70331
  if (!structure.domain && !structure.infrastructure) {
@@ -69337,7 +70409,7 @@ async function parseEntity2(filePath, rootPath, _config) {
69337
70409
  tableName,
69338
70410
  properties,
69339
70411
  relationships,
69340
- file: path27.relative(rootPath, filePath)
70412
+ file: path28.relative(rootPath, filePath)
69341
70413
  };
69342
70414
  }
69343
70415
  async function enrichFromConfigurations(entities, infrastructurePath, _config) {
@@ -69498,7 +70570,7 @@ var init_db_schema = __esm({
69498
70570
  });
69499
70571
 
69500
70572
  // src/mcp/resources/entities.ts
69501
- import path28 from "path";
70573
+ import path29 from "path";
69502
70574
  async function getEntitiesResource(config2, entityFilter) {
69503
70575
  const structure = await findSmartStackStructure(config2.smartstack.projectPath);
69504
70576
  if (!structure.domain) {
@@ -69552,7 +70624,7 @@ async function parseEntitySummary(filePath, rootPath, config2) {
69552
70624
  hasSoftDelete,
69553
70625
  hasRowVersion,
69554
70626
  file: filePath,
69555
- relativePath: path28.relative(rootPath, filePath)
70627
+ relativePath: path29.relative(rootPath, filePath)
69556
70628
  };
69557
70629
  }
69558
70630
  function inferTableInfo(entityName, config2) {
@@ -69761,6 +70833,8 @@ async function createServer() {
69761
70833
  analyzeTestCoverageTool,
69762
70834
  validateTestConventionsTool,
69763
70835
  suggestTestScenariosTool,
70836
+ // Frontend Test Tools
70837
+ scaffoldFrontendTestsTool,
69764
70838
  // Frontend Route Tools
69765
70839
  scaffoldApiClientTool,
69766
70840
  scaffoldRoutesTool,
@@ -69786,35 +70860,39 @@ async function createServer() {
69786
70860
  let result;
69787
70861
  switch (name) {
69788
70862
  case "validate_conventions":
69789
- result = await handleValidateConventions(args, config2);
70863
+ result = await handleValidateConventions(args ?? {}, config2);
69790
70864
  break;
69791
70865
  case "check_migrations":
69792
- result = await handleCheckMigrations(args, config2);
70866
+ result = await handleCheckMigrations(args ?? {}, config2);
69793
70867
  break;
69794
70868
  case "scaffold_extension":
69795
- result = await handleScaffoldExtension(args, config2);
70869
+ result = await handleScaffoldExtension(args ?? {}, config2);
69796
70870
  break;
69797
70871
  case "api_docs":
69798
- result = await handleApiDocs(args, config2);
70872
+ result = await handleApiDocs(args ?? {}, config2);
69799
70873
  break;
69800
70874
  case "suggest_migration":
69801
- result = await handleSuggestMigration(args, config2);
70875
+ result = await handleSuggestMigration(args ?? {}, config2);
69802
70876
  break;
69803
70877
  case "generate_permissions":
69804
- result = await handleGeneratePermissions(args, config2);
70878
+ result = await handleGeneratePermissions(args ?? {}, config2);
69805
70879
  break;
69806
70880
  // Test Tools
69807
70881
  case "scaffold_tests":
69808
- result = await handleScaffoldTests(args, config2);
70882
+ result = await handleScaffoldTests(args ?? {}, config2);
69809
70883
  break;
69810
70884
  case "analyze_test_coverage":
69811
- result = await handleAnalyzeTestCoverage(args, config2);
70885
+ result = await handleAnalyzeTestCoverage(args ?? {}, config2);
69812
70886
  break;
69813
70887
  case "validate_test_conventions":
69814
- result = await handleValidateTestConventions(args, config2);
70888
+ result = await handleValidateTestConventions(args ?? {}, config2);
69815
70889
  break;
69816
70890
  case "suggest_test_scenarios":
69817
- result = await handleSuggestTestScenarios(args, config2);
70891
+ result = await handleSuggestTestScenarios(args ?? {}, config2);
70892
+ break;
70893
+ // Frontend Test Tools
70894
+ case "scaffold_frontend_tests":
70895
+ result = await handleScaffoldFrontendTests(args ?? {}, config2);
69818
70896
  break;
69819
70897
  // Frontend Route Tools
69820
70898
  case "scaffold_api_client":
@@ -69958,6 +71036,7 @@ var init_server3 = __esm({
69958
71036
  init_validate_frontend_routes();
69959
71037
  init_scaffold_frontend_extension();
69960
71038
  init_analyze_extension_points();
71039
+ init_scaffold_frontend_tests();
69961
71040
  init_validate_security();
69962
71041
  init_analyze_code_quality();
69963
71042
  init_analyze_hierarchy_patterns();