sdd-full 4.6.2 → 4.8.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 (173) hide show
  1. package/bin.js +1 -1
  2. package/package.json +1 -1
  3. package/skills/.agents/skills/flutter-add-integration-test/SKILL.md +165 -0
  4. package/skills/.agents/skills/flutter-add-widget-preview/SKILL.md +147 -0
  5. package/skills/.agents/skills/flutter-add-widget-test/SKILL.md +156 -0
  6. package/skills/.agents/skills/flutter-apply-architecture-best-practices/SKILL.md +164 -0
  7. package/skills/.agents/skills/flutter-build-responsive-layout/SKILL.md +141 -0
  8. package/skills/.agents/skills/flutter-fix-layout-issues/SKILL.md +132 -0
  9. package/skills/.agents/skills/flutter-implement-json-serialization/SKILL.md +155 -0
  10. package/skills/.agents/skills/flutter-setup-declarative-routing/SKILL.md +257 -0
  11. package/skills/.agents/skills/flutter-setup-localization/SKILL.md +212 -0
  12. package/skills/.agents/skills/flutter-use-http-package/SKILL.md +177 -0
  13. package/skills/VERSION.md +176 -62
  14. package/skills/design-planning/ai-coding-rules/SKILL.md +5 -13
  15. package/skills/design-planning/design-to-code/SKILL.md +5 -14
  16. package/skills/design-planning/enterprise-spec/SKILL.md +5 -13
  17. package/skills/design-planning/flutter-av/SKILL.md +5 -16
  18. package/skills/design-planning/flutter-map/SKILL.md +5 -14
  19. package/skills/design-planning/function-sdd/SKILL.md +5 -13
  20. package/skills/design-planning/global-overlay-stack-standard/SKILL.md +73 -0
  21. package/skills/design-planning/ui-motion-interaction-standard/SKILL.md +69 -0
  22. package/skills/design-planning/ui-sdd-specialized/SKILL.md +5 -14
  23. package/skills/development-execution/flutter-errors/SKILL.md +5 -15
  24. package/skills/flutter-skills/.github/dependabot.yaml +15 -0
  25. package/skills/flutter-skills/.github/workflows/dart_skills_lint_workflow.yaml +68 -0
  26. package/skills/flutter-skills/.github/workflows/skills_tool.yaml +51 -0
  27. package/skills/flutter-skills/CODE_OF_CONDUCT.md +3 -0
  28. package/skills/flutter-skills/CONTRIBUTING.md +36 -0
  29. package/skills/flutter-skills/LICENSE +26 -0
  30. package/skills/flutter-skills/README.md +50 -0
  31. package/skills/flutter-skills/pubspec.yaml +9 -0
  32. package/skills/flutter-skills/resources/flutter_skills.yaml +434 -0
  33. package/skills/flutter-skills/skills/flutter-add-integration-test/SKILL.md +163 -0
  34. package/skills/flutter-skills/skills/flutter-add-widget-preview/SKILL.md +145 -0
  35. package/skills/flutter-skills/skills/flutter-add-widget-test/SKILL.md +154 -0
  36. package/skills/flutter-skills/skills/flutter-apply-architecture-best-practices/SKILL.md +162 -0
  37. package/skills/flutter-skills/skills/flutter-build-responsive-layout/SKILL.md +139 -0
  38. package/skills/flutter-skills/skills/flutter-fix-layout-issues/SKILL.md +130 -0
  39. package/skills/flutter-skills/skills/flutter-implement-json-serialization/SKILL.md +153 -0
  40. package/skills/flutter-skills/skills/flutter-setup-declarative-routing/SKILL.md +255 -0
  41. package/skills/flutter-skills/skills/flutter-setup-localization/SKILL.md +210 -0
  42. package/skills/flutter-skills/skills/flutter-use-http-package/SKILL.md +175 -0
  43. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/add-dart-lint-validation-rule/SKILL.md +196 -0
  44. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-best-practices/SKILL.md +65 -0
  45. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-checks-migration/SKILL.md +158 -0
  46. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-cli-app-best-practices/SKILL.md +168 -0
  47. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-doc-validation/SKILL.md +87 -0
  48. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-long-lines/SKILL.md +101 -0
  49. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-matcher-best-practices/SKILL.md +136 -0
  50. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-modern-features/SKILL.md +266 -0
  51. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-package-maintenance/SKILL.md +92 -0
  52. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-test-coverage/SKILL.md +92 -0
  53. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-test-coverage/example/lib/src/calculator.dart +7 -0
  54. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-test-coverage/example/pubspec.yaml +8 -0
  55. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-test-coverage/example/test/calculator_test.dart +11 -0
  56. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-test-coverage/scripts/interpret_coverage.dart +95 -0
  57. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-test-coverage/scripts/pubspec.yaml +6 -0
  58. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-test-coverage/scripts/test/interpret_coverage_test.dart +93 -0
  59. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-test-fundamentals/SKILL.md +173 -0
  60. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/definition-of-done/SKILL.md +27 -0
  61. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/flutter_skills_ignore.json +3 -0
  62. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/grill-me/SKILL.md +10 -0
  63. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/ignore.json +3 -0
  64. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/test-driven-development/SKILL.md +371 -0
  65. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/test-driven-development/testing-anti-patterns.md +299 -0
  66. package/skills/flutter-skills/tool/dart_skills_lint/AUTHORS +7 -0
  67. package/skills/flutter-skills/tool/dart_skills_lint/CHANGELOG.md +12 -0
  68. package/skills/flutter-skills/tool/dart_skills_lint/CONTRIBUTING.md +51 -0
  69. package/skills/flutter-skills/tool/dart_skills_lint/LICENSE +27 -0
  70. package/skills/flutter-skills/tool/dart_skills_lint/README.md +203 -0
  71. package/skills/flutter-skills/tool/dart_skills_lint/analysis_options.yaml +296 -0
  72. package/skills/flutter-skills/tool/dart_skills_lint/bench/README.md +23 -0
  73. package/skills/flutter-skills/tool/dart_skills_lint/bench/baseline_throughput.dart +230 -0
  74. package/skills/flutter-skills/tool/dart_skills_lint/bin/cli.dart +10 -0
  75. package/skills/flutter-skills/tool/dart_skills_lint/dart_skills_lint.yaml +14 -0
  76. package/skills/flutter-skills/tool/dart_skills_lint/documentation/feature_design_docs/PRODUCTION_READYNESS.md +48 -0
  77. package/skills/flutter-skills/tool/dart_skills_lint/documentation/feature_design_docs/completion_migration_plan.md +99 -0
  78. package/skills/flutter-skills/tool/dart_skills_lint/documentation/feature_design_docs/legacy_patterns_report.md +110 -0
  79. package/skills/flutter-skills/tool/dart_skills_lint/documentation/feature_design_docs/pub_vs_skill_report.md +56 -0
  80. package/skills/flutter-skills/tool/dart_skills_lint/documentation/knowledge/SPECIFICATION.md +79 -0
  81. package/skills/flutter-skills/tool/dart_skills_lint/documentation/knowledge/architecture_overview.md +64 -0
  82. package/skills/flutter-skills/tool/dart_skills_lint/lib/dart_skills_lint.dart +11 -0
  83. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/config_parser.dart +156 -0
  84. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/entry_point.dart +354 -0
  85. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/fixable_rule.dart +20 -0
  86. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/models/analysis_severity.dart +15 -0
  87. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/models/check_type.dart +17 -0
  88. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/models/ignore_entry.dart +34 -0
  89. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/models/ignore_entry.g.dart +19 -0
  90. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/models/skill_context.dart +27 -0
  91. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/models/skill_rule.dart +27 -0
  92. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/models/skills_ignores.dart +26 -0
  93. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/models/skills_ignores.g.dart +24 -0
  94. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/models/validation_error.dart +31 -0
  95. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/rule_registry.dart +79 -0
  96. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/rules/absolute_paths_rule.dart +74 -0
  97. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/rules/description_length_rule.dart +49 -0
  98. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/rules/disallowed_field_rule.dart +61 -0
  99. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/rules/name_format_rule.dart +167 -0
  100. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/rules/relative_paths_rule.dart +72 -0
  101. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/rules/trailing_whitespace_rule.dart +93 -0
  102. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/rules/valid_yaml_metadata_rule.dart +74 -0
  103. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/skills_ignores_storage.dart +36 -0
  104. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/validation_session.dart +559 -0
  105. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/validator.dart +238 -0
  106. package/skills/flutter-skills/tool/dart_skills_lint/pubspec.yaml +28 -0
  107. package/skills/flutter-skills/tool/dart_skills_lint/skills/README.md +10 -0
  108. package/skills/flutter-skills/tool/dart_skills_lint/skills/dart-skills-lint-validation/SKILL.md +195 -0
  109. package/skills/flutter-skills/tool/dart_skills_lint/skills-lock.json +75 -0
  110. package/skills/flutter-skills/tool/dart_skills_lint/test/absolute_paths_test.dart +167 -0
  111. package/skills/flutter-skills/tool/dart_skills_lint/test/cli_integration_test.dart +683 -0
  112. package/skills/flutter-skills/tool/dart_skills_lint/test/config_file_test.dart +292 -0
  113. package/skills/flutter-skills/tool/dart_skills_lint/test/custom_rule_test.dart +122 -0
  114. package/skills/flutter-skills/tool/dart_skills_lint/test/directory_structure_test.dart +163 -0
  115. package/skills/flutter-skills/tool/dart_skills_lint/test/field_constraints_test.dart +178 -0
  116. package/skills/flutter-skills/tool/dart_skills_lint/test/fixer_test.dart +172 -0
  117. package/skills/flutter-skills/tool/dart_skills_lint/test/ignore_models_test.dart +63 -0
  118. package/skills/flutter-skills/tool/dart_skills_lint/test/metadata_validation_test.dart +116 -0
  119. package/skills/flutter-skills/tool/dart_skills_lint/test/relative_path_flag_test.dart +70 -0
  120. package/skills/flutter-skills/tool/dart_skills_lint/test/relative_paths_test.dart +172 -0
  121. package/skills/flutter-skills/tool/dart_skills_lint/test/resolve_rules_test.dart +82 -0
  122. package/skills/flutter-skills/tool/dart_skills_lint/test/rule_naming_test.dart +29 -0
  123. package/skills/flutter-skills/tool/dart_skills_lint/test/skills_ignores_storage_test.dart +89 -0
  124. package/skills/flutter-skills/tool/dart_skills_lint/test/test_utils.dart +19 -0
  125. package/skills/flutter-skills/tool/dart_skills_lint/test/trailing_whitespace_test.dart +152 -0
  126. package/skills/flutter-skills/tool/generator/README.md +150 -0
  127. package/skills/flutter-skills/tool/generator/analysis_options.yaml +143 -0
  128. package/skills/flutter-skills/tool/generator/bin/skills.dart +73 -0
  129. package/skills/flutter-skills/tool/generator/lib/src/commands/base_skill_command.dart +87 -0
  130. package/skills/flutter-skills/tool/generator/lib/src/commands/base_yaml_command.dart +83 -0
  131. package/skills/flutter-skills/tool/generator/lib/src/commands/generate_skill_command.dart +92 -0
  132. package/skills/flutter-skills/tool/generator/lib/src/commands/update_readme_command.dart +150 -0
  133. package/skills/flutter-skills/tool/generator/lib/src/commands/update_skill_command.dart +97 -0
  134. package/skills/flutter-skills/tool/generator/lib/src/commands/validate_skill_command.dart +284 -0
  135. package/skills/flutter-skills/tool/generator/lib/src/models/skill_params.dart +41 -0
  136. package/skills/flutter-skills/tool/generator/lib/src/services/gemini_service.dart +310 -0
  137. package/skills/flutter-skills/tool/generator/lib/src/services/markdown_converter.dart +226 -0
  138. package/skills/flutter-skills/tool/generator/lib/src/services/prompts.dart +72 -0
  139. package/skills/flutter-skills/tool/generator/lib/src/services/resource_fetcher_service.dart +84 -0
  140. package/skills/flutter-skills/tool/generator/lib/src/services/skill_instructions.dart +30 -0
  141. package/skills/flutter-skills/tool/generator/pubspec.yaml +32 -0
  142. package/skills/flutter-skills/tool/generator/test/commands/base_skill_command_test.dart +131 -0
  143. package/skills/flutter-skills/tool/generator/test/commands/validate_skills_input_test.dart +263 -0
  144. package/skills/flutter-skills/tool/generator/test/custom_skill_rules/last_modified_rule.dart +32 -0
  145. package/skills/flutter-skills/tool/generator/test/generate_skills_retry_test.dart +105 -0
  146. package/skills/flutter-skills/tool/generator/test/generate_skills_test.dart +519 -0
  147. package/skills/flutter-skills/tool/generator/test/lint_skills_test.dart +34 -0
  148. package/skills/flutter-skills/tool/generator/test/markdown_converter_test.dart +103 -0
  149. package/skills/flutter-skills/tool/generator/test/markdown_table_test.dart +131 -0
  150. package/skills/flutter-skills/tool/generator/test/models/skill_params_test.dart +37 -0
  151. package/skills/flutter-skills/tool/generator/test/services/gemini_service_test.dart +291 -0
  152. package/skills/flutter-skills/tool/generator/test/services/markdown_converter_test.dart +156 -0
  153. package/skills/flutter-skills/tool/generator/test/services/resource_fetcher_service_test.dart +188 -0
  154. package/skills/flutter-skills/tool/generator/test/update_skills_test.dart +241 -0
  155. package/skills/flutter-skills/tool/generator/test/validate_skills_test.dart +728 -0
  156. package/skills/quality-assurance/bdd-acceptance/SKILL.md +5 -14
  157. package/skills/quality-assurance/flutter-test/SKILL.md +5 -16
  158. package/skills/rules/project_rules.md +538 -127
  159. package/skills/special-tools/env-check/SKILL.md +5 -13
  160. package/skills/special-tools/ios-full-auto-debug/SKILL.md +5 -15
  161. package/skills/writing-skills/SKILL.md +654 -0
  162. package/skills/writing-skills/anthropic-best-practices.md +1149 -0
  163. package/skills/writing-skills/examples/CLAUDE_MD_TESTING.md +189 -0
  164. package/skills/writing-skills/graphviz-conventions.dot +172 -0
  165. package/skills/writing-skills/persuasion-principles.md +187 -0
  166. package/skills/writing-skills/render-graphs.js +168 -0
  167. package/skills/writing-skills/testing-skills-with-subagents.md +384 -0
  168. package/skills/checklist.md +0 -154
  169. package/skills/rules/user_rules.md +0 -263
  170. package/skills//345/256/214/346/225/264/345/274/200/345/217/221/346/265/201/347/250/213/346/211/213/345/206/214.md +0 -454
  171. package/skills//346/212/200/350/203/275/344/275/223/347/263/273/345/256/214/345/226/204/345/273/272/350/256/256.md +0 -308
  172. package/skills//346/212/200/350/203/275/344/275/277/347/224/250/346/214/207/345/215/227.md +0 -309
  173. package/skills//346/212/200/350/203/275/345/206/263/347/255/226/346/240/221.md +0 -338
@@ -0,0 +1,728 @@
1
+ // Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
2
+ // for details. All rights reserved. Use of this source code is governed by a
3
+ // BSD-style license that can be found in the LICENSE file.
4
+
5
+ import 'dart:convert';
6
+ import 'dart:io';
7
+
8
+ import 'package:args/command_runner.dart';
9
+ import 'package:http/http.dart' as http;
10
+ import 'package:http/testing.dart';
11
+ import 'package:logging/logging.dart';
12
+ import 'package:path/path.dart' as p;
13
+ import 'package:skills/src/commands/validate_skill_command.dart';
14
+ import 'package:test/test.dart';
15
+
16
+ void main() {
17
+ group('ValidateSkillCommand', () {
18
+ late CommandRunner<void> runner;
19
+ late Directory tempDir;
20
+ late Directory skillsDir;
21
+ late Directory validationDir;
22
+ late MockClient mockClient;
23
+ final logs = <String>[];
24
+
25
+ setUp(() async {
26
+ tempDir = await Directory.systemTemp.createTemp('validate_skills_test');
27
+ skillsDir = Directory(p.join(tempDir.path, 'skills'));
28
+ await skillsDir.create();
29
+ validationDir = Directory(p.join(tempDir.path, 'validation'));
30
+ await validationDir.create();
31
+
32
+ mockClient = MockClient((request) async {
33
+ return http.Response('', 200);
34
+ });
35
+
36
+ // Capture logs
37
+ Logger.root.level = Level.INFO;
38
+ Logger.root.onRecord.listen((record) {
39
+ logs.add(record.message);
40
+ });
41
+ logs.clear();
42
+ });
43
+
44
+ tearDown(() async {
45
+ await tempDir.delete(recursive: true);
46
+ logs.clear();
47
+ });
48
+
49
+ test('validates single skill defined in config', () async {
50
+ const skillName = 'test-skill';
51
+ final skillDir = Directory(p.join(skillsDir.path, skillName));
52
+ await skillDir.create();
53
+ final skillFile = File(p.join(skillDir.path, 'SKILL.md'));
54
+ await skillFile.writeAsString('content');
55
+
56
+ final configFile = File(p.join(tempDir.path, 'config.yaml'));
57
+ await configFile.writeAsString(
58
+ jsonEncode([
59
+ {
60
+ 'name': skillName,
61
+ 'description': 'Description',
62
+ 'resources': ['https://example.com'],
63
+ },
64
+ ]),
65
+ );
66
+
67
+ runner = CommandRunner<void>('skills', 'Test runner')
68
+ ..addCommand(
69
+ ValidateSkillCommand(
70
+ environment: {'GEMINI_API_KEY': 'test-key'},
71
+ outputDir: skillsDir,
72
+ validationDir: validationDir,
73
+ httpClient: mockClient,
74
+ ),
75
+ );
76
+
77
+ await IOOverrides.runZoned(() async {
78
+ await runner.run(['validate-skill', configFile.path]);
79
+ }, getCurrentDirectory: () => tempDir);
80
+
81
+ expect(logs, contains('Validating skill: $skillName...'));
82
+ });
83
+
84
+ test('validates and grades single skill', () async {
85
+ const skillName = 'test-skill';
86
+ final skillDir = Directory(p.join(skillsDir.path, skillName));
87
+ await skillDir.create();
88
+ final skillFile = File(p.join(skillDir.path, 'SKILL.md'));
89
+ await skillFile.writeAsString('Existing content');
90
+
91
+ final configFile = File(p.join(tempDir.path, 'config.yaml'));
92
+ await configFile.writeAsString(
93
+ jsonEncode([
94
+ {
95
+ 'name': skillName,
96
+ 'description': 'Description',
97
+ 'resources': ['https://example.com/source'],
98
+ },
99
+ ]),
100
+ );
101
+
102
+ final mockClient = MockClient((request) async {
103
+ final url = request.url.toString();
104
+ if (url == 'https://example.com/source') {
105
+ return http.Response(
106
+ '<html><body><h1>Source</h1></body></html>',
107
+ 200,
108
+ );
109
+ }
110
+ if (url.contains('generativelanguage')) {
111
+ return http.Response(
112
+ jsonEncode({
113
+ 'candidates': [
114
+ {
115
+ 'content': {
116
+ 'parts': [
117
+ {
118
+ 'text':
119
+ '# Validation Report\n\n'
120
+ '- Accuracy: High\n'
121
+ '- Structure: Correct\n'
122
+ '- Completeness: Good\n\n'
123
+ 'Conclusion: Valid\n'
124
+ 'Similarity Score: 85',
125
+ },
126
+ ],
127
+ },
128
+ },
129
+ ],
130
+ }),
131
+ 200,
132
+ );
133
+ }
134
+ return http.Response('Not Found', 404);
135
+ });
136
+
137
+ runner = CommandRunner<void>('skills', 'Test runner')
138
+ ..addCommand(
139
+ ValidateSkillCommand(
140
+ environment: {'GEMINI_API_KEY': 'test-key'},
141
+ outputDir: skillsDir,
142
+ validationDir: validationDir,
143
+ httpClient: mockClient,
144
+ ),
145
+ );
146
+
147
+ await IOOverrides.runZoned(() async {
148
+ await runner.run(['validate-skill', configFile.path]);
149
+ }, getCurrentDirectory: () => tempDir);
150
+
151
+ expect(logs, contains('Validating skill: $skillName...'));
152
+ expect(logs, contains(contains('Validation report written to')));
153
+ });
154
+
155
+ test('validates all skills in config', () async {
156
+ const skill1Name = 'skill1';
157
+ final skill1Dir = Directory(p.join(skillsDir.path, skill1Name));
158
+ await skill1Dir.create();
159
+ File(p.join(skill1Dir.path, 'SKILL.md')).writeAsStringSync('content');
160
+
161
+ const skill2Name = 'skill2';
162
+ final skill2Dir = Directory(p.join(skillsDir.path, skill2Name));
163
+ await skill2Dir.create();
164
+ File(p.join(skill2Dir.path, 'SKILL.md')).writeAsStringSync('content');
165
+
166
+ final configFile = File(p.join(tempDir.path, 'config.yaml'));
167
+ await configFile.writeAsString(
168
+ jsonEncode([
169
+ {
170
+ 'name': skill1Name,
171
+ 'description': 'Desc 1',
172
+ 'resources': ['https://example.com/1'],
173
+ },
174
+ {
175
+ 'name': skill2Name,
176
+ 'description': 'Desc 2',
177
+ 'resources': ['https://example.com/2'],
178
+ },
179
+ ]),
180
+ );
181
+
182
+ runner = CommandRunner<void>('skills', 'Test runner')
183
+ ..addCommand(
184
+ ValidateSkillCommand(
185
+ environment: {'GEMINI_API_KEY': 'test-key'},
186
+ outputDir: skillsDir,
187
+ validationDir: validationDir,
188
+ httpClient: mockClient,
189
+ ),
190
+ );
191
+
192
+ await IOOverrides.runZoned(() async {
193
+ await runner.run(['validate-skill', configFile.path]);
194
+ }, getCurrentDirectory: () => tempDir);
195
+
196
+ expect(logs, contains('Validating skill: $skill1Name...'));
197
+ expect(logs, contains('Validating skill: $skill2Name...'));
198
+ });
199
+
200
+ test('logs severe error when config file not found', () async {
201
+ final path = p.join(tempDir.path, 'NON_EXISTENT.yaml');
202
+
203
+ runner = CommandRunner<void>('skills', 'Test runner')
204
+ ..addCommand(
205
+ ValidateSkillCommand(
206
+ environment: {'GEMINI_API_KEY': 'test-key'},
207
+ outputDir: skillsDir,
208
+ httpClient: mockClient,
209
+ ),
210
+ );
211
+
212
+ await IOOverrides.runZoned(() async {
213
+ await runner.run(['validate-skill', path]);
214
+ }, getCurrentDirectory: () => tempDir);
215
+
216
+ expect(logs, contains('Configuration file not found: $path'));
217
+ });
218
+
219
+ test('logs warning when existing skill file not found', () async {
220
+ const skillName = 'missing-skill';
221
+ // Do NOT create skill directory or file
222
+
223
+ final configFile = File(p.join(tempDir.path, 'config.yaml'));
224
+ await configFile.writeAsString(
225
+ jsonEncode([
226
+ {
227
+ 'name': skillName,
228
+ 'description': 'Desc',
229
+ 'resources': ['https://example.com'],
230
+ },
231
+ ]),
232
+ );
233
+
234
+ final mockClient = MockClient((request) async {
235
+ final url = request.url.toString();
236
+ if (url == 'https://example.com') {
237
+ return http.Response('Content', 200);
238
+ }
239
+ if (url.contains('generativelanguage')) {
240
+ return http.Response(
241
+ '{"candidates": [{"content": {"parts": [{"text": "Generated Content"}]}}]}',
242
+ 200,
243
+ );
244
+ }
245
+ return http.Response('Not Found', 404);
246
+ });
247
+
248
+ runner = CommandRunner<void>('skills', 'Test runner')
249
+ ..addCommand(
250
+ ValidateSkillCommand(
251
+ environment: {'GEMINI_API_KEY': 'test-key'},
252
+ outputDir: skillsDir,
253
+ httpClient: mockClient,
254
+ ),
255
+ );
256
+
257
+ await IOOverrides.runZoned(() async {
258
+ await runner.run(['validate-skill', configFile.path]);
259
+ }, getCurrentDirectory: () => tempDir);
260
+
261
+ expect(logs, contains(contains('Existing skill file not found')));
262
+ });
263
+
264
+ test('logs warning when failed to fetch URL', () async {
265
+ const skillName = 'fail-skill';
266
+ final skillDir = Directory(p.join(skillsDir.path, skillName));
267
+ await skillDir.create();
268
+ File(p.join(skillDir.path, 'SKILL.md')).writeAsStringSync('content');
269
+
270
+ final configFile = File(p.join(tempDir.path, 'config.yaml'));
271
+ await configFile.writeAsString(
272
+ jsonEncode([
273
+ {
274
+ 'name': skillName,
275
+ 'description': 'Desc',
276
+ 'resources': ['https://example.com/fail'],
277
+ },
278
+ ]),
279
+ );
280
+
281
+ final mockClient = MockClient((request) async {
282
+ // Throw to trigger the 'Error fetching' catch block
283
+ throw Exception('Network Error');
284
+ });
285
+
286
+ runner = CommandRunner<void>('skills', 'Test runner')
287
+ ..addCommand(
288
+ ValidateSkillCommand(
289
+ environment: {'GEMINI_API_KEY': 'test-key'},
290
+ outputDir: skillsDir,
291
+ httpClient: mockClient,
292
+ ),
293
+ );
294
+
295
+ await runner.run(['validate-skill', configFile.path]);
296
+
297
+ expect(
298
+ logs,
299
+ contains(
300
+ contains('Error validating $skillName: Exception: Network Error'),
301
+ ),
302
+ );
303
+ });
304
+
305
+ test(
306
+ 'logs severe error when grading fails repeatedly/exceptionally',
307
+ () async {
308
+ const skillName = 'grade-fail';
309
+ final skillDir = Directory(p.join(skillsDir.path, skillName));
310
+ await skillDir.create();
311
+ File(p.join(skillDir.path, 'SKILL.md')).writeAsStringSync('content');
312
+
313
+ final configFile = File(p.join(tempDir.path, 'config.yaml'));
314
+ await configFile.writeAsString(
315
+ jsonEncode([
316
+ {
317
+ 'name': skillName,
318
+ 'description': 'Desc',
319
+ 'resources': ['https://example.com/source'],
320
+ },
321
+ ]),
322
+ );
323
+
324
+ final mockClient = MockClient((request) async {
325
+ if (request.url.toString() == 'https://example.com/source') {
326
+ return http.Response('# Source', 200);
327
+ }
328
+ if (request.url.toString().contains('generativelanguage')) {
329
+ // Validation (grading) fails
330
+ throw Exception('Gemini API Error');
331
+ }
332
+ return http.Response('Not Found', 404);
333
+ });
334
+
335
+ runner = CommandRunner<void>('skills', 'Test runner')
336
+ ..addCommand(
337
+ ValidateSkillCommand(
338
+ environment: {'GEMINI_API_KEY': 'test-key'},
339
+ outputDir: skillsDir,
340
+ validationDir: validationDir,
341
+ httpClient: mockClient,
342
+ ),
343
+ );
344
+
345
+ await runner.run(['validate-skill', configFile.path]);
346
+
347
+ expect(
348
+ logs,
349
+ contains(contains('Failed to generate validation report')),
350
+ );
351
+ },
352
+ );
353
+
354
+ test('logs severe error when Gemini grading throws exception', () async {
355
+ const skillName = 'gemini-fail';
356
+ final skillDir = Directory(p.join(skillsDir.path, skillName));
357
+ await skillDir.create();
358
+ // Ensure name matches to avoid name mismatch error
359
+ File(
360
+ p.join(skillDir.path, 'SKILL.md'),
361
+ ).writeAsStringSync('name: $skillName\nContent');
362
+
363
+ final configFile = File(p.join(tempDir.path, 'config.yaml'));
364
+ await configFile.writeAsString(
365
+ jsonEncode([
366
+ {
367
+ 'name': skillName,
368
+ 'description': 'Desc',
369
+ 'resources': ['https://example.com/source'],
370
+ },
371
+ ]),
372
+ );
373
+
374
+ final mockClient = MockClient((request) async {
375
+ if (request.url.toString() == 'https://example.com/source') {
376
+ return http.Response('# Source', 200);
377
+ }
378
+ if (request.url.toString().contains('generativelanguage')) {
379
+ // Validation (grading) throws
380
+ throw Exception('Gemini API Error');
381
+ }
382
+ return http.Response('Not Found', 404);
383
+ });
384
+
385
+ runner = CommandRunner<void>('skills', 'Test runner')
386
+ ..addCommand(
387
+ ValidateSkillCommand(
388
+ environment: {'GEMINI_API_KEY': 'test-key'},
389
+ outputDir: skillsDir,
390
+ validationDir: validationDir,
391
+ httpClient: mockClient,
392
+ ),
393
+ );
394
+
395
+ await runner.run(['validate-skill', configFile.path]);
396
+
397
+ expect(logs, contains(contains('Failed to generate validation report')));
398
+ });
399
+
400
+ test('logs severe error when skill name/description mismatch', () async {
401
+ const skillName = 'mismatch-skill';
402
+ final skillDir = Directory(p.join(skillsDir.path, skillName));
403
+ await skillDir.create();
404
+ // Write content WITHOUT name/description
405
+ final skillFile = File(p.join(skillDir.path, 'SKILL.md'));
406
+ await skillFile.writeAsString('Invalid content without frontmatter');
407
+
408
+ final configFile = File(p.join(tempDir.path, 'config.yaml'));
409
+ await configFile.writeAsString(
410
+ jsonEncode([
411
+ {
412
+ 'name': skillName,
413
+ 'description': 'Expected Description',
414
+ 'resources': ['https://example.com/source'],
415
+ },
416
+ ]),
417
+ );
418
+
419
+ final mockClient = MockClient((request) async {
420
+ final url = request.url.toString();
421
+ if (url == 'https://example.com/source') {
422
+ return http.Response('# Source', 200);
423
+ }
424
+ if (url.contains('generativelanguage')) {
425
+ return http.Response(
426
+ '{"candidates": [{"content": {"parts": [{"text": "Generated Content"}]}}]}',
427
+ 200,
428
+ );
429
+ }
430
+ return http.Response('Not Found', 404);
431
+ });
432
+
433
+ runner = CommandRunner<void>('skills', 'Test runner')
434
+ ..addCommand(
435
+ ValidateSkillCommand(
436
+ environment: {'GEMINI_API_KEY': 'test-key'},
437
+ outputDir: skillsDir,
438
+ validationDir: validationDir,
439
+ httpClient: mockClient,
440
+ ),
441
+ );
442
+
443
+ await IOOverrides.runZoned(() async {
444
+ await runner.run(['validate-skill', configFile.path]);
445
+ }, getCurrentDirectory: () => tempDir);
446
+
447
+ expect(
448
+ logs,
449
+ contains(contains('Validation Failed: Skill name mismatch')),
450
+ );
451
+ expect(
452
+ logs,
453
+ contains(contains('Validation Failed: Skill name mismatch')),
454
+ );
455
+ // Description check was removed from the command
456
+ expect(
457
+ logs,
458
+ isNot(
459
+ contains(contains('Validation Failed: Skill description mismatch')),
460
+ ),
461
+ );
462
+ });
463
+
464
+ test('sends source URL header to Gemini during regeneration', () async {
465
+ const skillName = 'header-skill';
466
+ final skillDir = Directory(p.join(skillsDir.path, skillName));
467
+ await skillDir.create();
468
+ final skillFile = File(p.join(skillDir.path, 'SKILL.md'));
469
+ await skillFile.writeAsString('''
470
+ name: $skillName
471
+ description: Desc
472
+ ---
473
+ Content
474
+ ''');
475
+
476
+ final configFile = File(p.join(tempDir.path, 'config.yaml'));
477
+ await configFile.writeAsString(
478
+ jsonEncode([
479
+ {
480
+ 'name': skillName,
481
+ 'description': 'Desc',
482
+ 'resources': ['https://example.com/source'],
483
+ },
484
+ ]),
485
+ );
486
+
487
+ final geminiRequests = <String>[];
488
+ final mockClient = MockClient((request) async {
489
+ final url = request.url.toString();
490
+ if (url == 'https://example.com/source') {
491
+ return http.Response('Source Content', 200);
492
+ }
493
+ if (url.contains('generativelanguage')) {
494
+ geminiRequests.add(request.body);
495
+ return http.Response(
496
+ jsonEncode({
497
+ 'candidates': [
498
+ {
499
+ 'content': {
500
+ 'parts': [
501
+ {'text': 'Generated Content\nSimilarity Score: 50'},
502
+ ],
503
+ },
504
+ },
505
+ ],
506
+ }),
507
+ 200,
508
+ );
509
+ }
510
+ return http.Response('Not Found', 404);
511
+ });
512
+
513
+ runner = CommandRunner<void>('skills', 'Test runner')
514
+ ..addCommand(
515
+ ValidateSkillCommand(
516
+ environment: {'GEMINI_API_KEY': 'test-key'},
517
+ outputDir: skillsDir,
518
+ httpClient: mockClient,
519
+ validationDir: validationDir,
520
+ ),
521
+ );
522
+
523
+ await IOOverrides.runZoned(() async {
524
+ await runner.run(['validate-skill', configFile.path]);
525
+ }, getCurrentDirectory: () => tempDir);
526
+
527
+ expect(geminiRequests, isNotEmpty);
528
+ expect(
529
+ geminiRequests.first,
530
+ contains('--- Raw content from https://example.com/source ---'),
531
+ );
532
+ });
533
+ test('fails upfront validation when resources list is empty', () async {
534
+ const skillName = 'empty-fetch-val';
535
+ final skillDir = Directory(p.join(skillsDir.path, skillName));
536
+ await skillDir.create();
537
+ File(
538
+ p.join(skillDir.path, 'SKILL.md'),
539
+ ).writeAsStringSync('name: $skillName\ncontent');
540
+
541
+ final configFile = File(p.join(tempDir.path, 'config.yaml'));
542
+ await configFile.writeAsString(
543
+ jsonEncode([
544
+ {'name': skillName, 'description': 'Desc', 'resources': <String>[]},
545
+ ]),
546
+ );
547
+
548
+ runner = CommandRunner<void>('skills', 'Test runner')
549
+ ..addCommand(
550
+ ValidateSkillCommand(
551
+ environment: {'GEMINI_API_KEY': 'test-key'},
552
+ outputDir: skillsDir,
553
+ httpClient: mockClient,
554
+ ),
555
+ );
556
+
557
+ await runner.run(['validate-skill', configFile.path]);
558
+ expect(
559
+ logs,
560
+ contains(
561
+ 'Skill "empty-fetch-val" field "resources" must not be empty.',
562
+ ),
563
+ );
564
+ expect(logs, contains('Configuration validation failed.'));
565
+ });
566
+
567
+ test('logs severe error on generic exception during validation', () async {
568
+ const skillName = 'exception-val';
569
+ final skillDir = Directory(p.join(skillsDir.path, skillName));
570
+ await skillDir.create();
571
+ File(p.join(skillDir.path, 'SKILL.md')).writeAsStringSync('content');
572
+
573
+ final configFile = File(p.join(tempDir.path, 'config.yaml'));
574
+ await configFile.writeAsString(
575
+ jsonEncode([
576
+ {
577
+ 'name': skillName,
578
+ 'description': 'Desc',
579
+ 'resources': ['https://example.com/source'],
580
+ },
581
+ ]),
582
+ );
583
+
584
+ final mockClient = MockClient(
585
+ (request) async => throw Exception('Generic Error'),
586
+ );
587
+
588
+ runner = CommandRunner<void>('skills', 'Test runner')
589
+ ..addCommand(
590
+ ValidateSkillCommand(
591
+ environment: {'GEMINI_API_KEY': 'test-key'},
592
+ outputDir: skillsDir,
593
+ httpClient: mockClient,
594
+ ),
595
+ );
596
+
597
+ await runner.run(['validate-skill', configFile.path]);
598
+ expect(
599
+ logs,
600
+ contains(
601
+ contains('Error validating $skillName: Exception: Generic Error'),
602
+ ),
603
+ );
604
+ });
605
+
606
+ test('validates with missing metadata fallbacks', () async {
607
+ const skillName = 'fallback-meta';
608
+ final skillDir = Directory(p.join(skillsDir.path, skillName));
609
+ await skillDir.create();
610
+ File(
611
+ p.join(skillDir.path, 'SKILL.md'),
612
+ ).writeAsStringSync('name: $skillName\ncontent WITHOUT metadata');
613
+
614
+ final configFile = File(p.join(tempDir.path, 'config.yaml'));
615
+ await configFile.writeAsString(
616
+ jsonEncode([
617
+ {
618
+ 'name': skillName,
619
+ 'description': 'Desc',
620
+ 'resources': ['https://example.com/source'],
621
+ },
622
+ ]),
623
+ );
624
+
625
+ final mockClient = MockClient((request) async {
626
+ if (request.url.toString() == 'https://example.com/source') {
627
+ return http.Response('# Source', 200);
628
+ }
629
+ if (request.url.toString().contains('generativelanguage')) {
630
+ return http.Response(
631
+ jsonEncode({
632
+ 'candidates': [
633
+ {
634
+ 'content': {
635
+ 'parts': [
636
+ {'text': 'Generated Content\nGrade: 100'},
637
+ ],
638
+ },
639
+ },
640
+ ],
641
+ }),
642
+ 200,
643
+ );
644
+ }
645
+ return http.Response('Not Found', 404);
646
+ });
647
+
648
+ runner = CommandRunner<void>('skills', 'Test runner')
649
+ ..addCommand(
650
+ ValidateSkillCommand(
651
+ environment: {'GEMINI_API_KEY': 'test-key'},
652
+ outputDir: skillsDir,
653
+ validationDir: validationDir,
654
+ httpClient: mockClient,
655
+ ),
656
+ );
657
+
658
+ await runner.run(['validate-skill', configFile.path]);
659
+ expect(logs, contains(contains('Validation report written to')));
660
+ });
661
+
662
+ test('dry run fetches content but skips gemini and file writes', () async {
663
+ const skillName = 'dry-validate';
664
+ final skillDir = Directory(p.join(skillsDir.path, skillName));
665
+ await skillDir.create();
666
+ File(
667
+ p.join(skillDir.path, 'SKILL.md'),
668
+ ).writeAsStringSync('name: $skillName\nExisting content');
669
+
670
+ final configFile = File(p.join(tempDir.path, 'config.yaml'));
671
+ await configFile.writeAsString(
672
+ jsonEncode([
673
+ {
674
+ 'name': skillName,
675
+ 'description': 'Desc',
676
+ 'resources': ['https://example.com/source'],
677
+ },
678
+ ]),
679
+ );
680
+
681
+ var geminiCalled = false;
682
+ final mockClient = MockClient((request) async {
683
+ if (request.url.toString() == 'https://example.com/source') {
684
+ return http.Response('# Source', 200);
685
+ }
686
+ if (request.url.toString().contains('generativelanguage')) {
687
+ geminiCalled = true;
688
+ return http.Response(
689
+ jsonEncode({
690
+ 'candidates': [
691
+ {
692
+ 'content': {
693
+ 'parts': [
694
+ {'text': 'Generated Content\nGrade: 100'},
695
+ ],
696
+ },
697
+ },
698
+ ],
699
+ }),
700
+ 200,
701
+ );
702
+ }
703
+ return http.Response('Not Found', 404);
704
+ });
705
+
706
+ runner = CommandRunner<void>('skills', 'Test runner')
707
+ ..addCommand(
708
+ ValidateSkillCommand(
709
+ environment: {'GEMINI_API_KEY': 'test-key'},
710
+ outputDir: skillsDir,
711
+ validationDir: validationDir,
712
+ httpClient: mockClient,
713
+ ),
714
+ );
715
+
716
+ await runner.run(['validate-skill', '--dry-run', configFile.path]);
717
+
718
+ expect(geminiCalled, isFalse);
719
+ expect(
720
+ logs,
721
+ contains(contains('[DRY RUN] Would validate skill: $skillName')),
722
+ );
723
+
724
+ final valDir = Directory(p.join(validationDir.path, skillName));
725
+ expect(valDir.existsSync(), isFalse);
726
+ });
727
+ });
728
+ }