Skip to content

Commit d632480

Browse files
committed
fix(stubs): remove hardcoded paths from StubLoader and trim flaky integration tests
- Remove FLUTTER_TEST branch with hardcoded /Users/anilcan/... fallback that fails on CI (Ubuntu). Keep clean 4-strategy resolution: env var → package_config.json → Platform.script walk-up → cwd fallback - Add MAGIC_CLI_STUBS_DIR env var as first-priority override for tests - Remove stub-dependent integration tests (make:*, install, --force) that duplicate unit test coverage and fail when Directory.current changes to tempDir. Keep Kernel dispatch + key:generate tests - Add .claude/rules/ path-scoped convention files (dart, commands, tests, stubs)
1 parent fcf07e1 commit d632480

6 files changed

Lines changed: 187 additions & 212 deletions

File tree

.claude/rules/commands.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
---
2+
path: "lib/src/commands/**/*.dart"
3+
---
4+
5+
# Command Conventions
6+
7+
## GeneratorCommand Contract
8+
Every `make:*` command extends `GeneratorCommand` and overrides:
9+
- `name``'make:feature'`
10+
- `description``'Create a new feature class'`
11+
- `getDefaultNamespace()` → output directory (e.g., `'lib/app/controllers'`)
12+
- `getStub()` → stub name without `.stub` extension
13+
- `getReplacements(String name)` → custom placeholder map
14+
- `getProjectRoot()``_testRoot ?? super.getProjectRoot()`
15+
16+
## Constructor Pattern
17+
```dart
18+
final String? _testRoot;
19+
MakeXCommand({String? testRoot}) : _testRoot = testRoot;
20+
```
21+
Every command accepts optional `testRoot` for test isolation.
22+
23+
## configure() Must Call Super
24+
```dart
25+
@override
26+
void configure(ArgParser parser) {
27+
super.configure(parser); // Inherits --force flag
28+
parser.addFlag('custom', abbr: 'c', negatable: false);
29+
}
30+
```
31+
Always `super.configure(parser)` first — skipping loses `--force` inheritance.
32+
33+
## Suffix Handling
34+
Commands that auto-append suffixes (Controller, Factory, Seeder, Provider, Request, Policy):
35+
- Strip existing suffix before re-appending to prevent `UserControllerController`
36+
- Use private `_resolveClassName()` or `_stripSuffix()` + `_withSuffix()` helpers
37+
38+
## Command Chaining
39+
`MakeModelCommand` chains child generators via:
40+
```dart
41+
await MakeMigrationCommand(testRoot: _testRoot).runWith([migrationName, '--create=$tableName']);
42+
```
43+
Always pass `testRoot: _testRoot` to child commands — omitting causes tests to touch real filesystem.
44+
45+
## Non-Generator Commands
46+
`InstallCommand` and `KeyGenerateCommand` extend `Command` directly (not `GeneratorCommand`).
47+
They handle file creation manually without stubs.
48+
49+
## Nested Path Support
50+
`make:controller Admin/Dashboard``StringHelper.parseName()` returns `(directory: 'admin', className: 'Dashboard', fileName: 'dashboard')`.
51+
Directory segments are snake_cased. Class name preserves PascalCase.

.claude/rules/dart.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
path: "**/*.dart"
3+
---
4+
5+
# Dart Conventions
6+
7+
## Imports
8+
- Always `package:magic_cli/src/...` — never relative imports
9+
- Order: `dart:` stdlib → external packages (`args`, `path`, `yaml`) → internal `package:magic_cli/...`
10+
- Alias `package:path/path.dart` as `path`
11+
12+
## Naming
13+
- Classes: `PascalCase` — Files: `snake_case` — Methods: `camelCase` — Private: `_prefix`
14+
- CLI command names: `namespace:action` (e.g., `make:controller`, `key:generate`)
15+
- One class per file, filename matches class in snake_case
16+
17+
## Doc Comments
18+
- `///` on all public classes and methods
19+
- Class-level: usage examples in `## Usage` + `## Output` sections with code blocks
20+
- Method-level: single-line summary, then `@param`, `@return`, `@throws` as needed
21+
22+
## Error Handling
23+
- File operations: throw `FileSystemException('message', path)`
24+
- Missing CLI args: print error via `error()` and return — do not throw
25+
- Kernel catches `FormatException` + generic `Exception`, sets `exitCode = 1`
26+
27+
## Static Utility Classes
28+
- Constructor: `const ClassName._()` to prevent instantiation
29+
- All methods `static`, no instance state
30+
- Follows: `FileHelper`, `ConsoleStyle`, `ConfigEditor`, `JsonEditor`, `StringHelper`

.claude/rules/stubs.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
path: "assets/stubs/**/*.stub"
3+
---
4+
5+
# Stub Template Conventions
6+
7+
## Placeholder Syntax
8+
- Use `{{ key }}` with spaces inside braces — flexible whitespace (`{{key}}` also works)
9+
- Case-sensitive: `{{ className }}` only — `{{ classname }}` is a different placeholder
10+
- Unreplaced placeholders remain as raw text in output — no validation or warnings
11+
12+
## Auto-Handled Placeholders
13+
- `{{ className }}` — PascalCase class name (from last path segment)
14+
- `{{ namespace }}` — output directory path (e.g., `lib/app/controllers`)
15+
16+
## Custom Placeholders
17+
Defined per command via `getReplacements()`:
18+
- `{{ tableName }}` — plural snake_case (model)
19+
- `{{ snakeName }}` — snake_case of class name
20+
- `{{ resourceName }}` — lower-case resource name
21+
- `{{ description }}` — human-readable description
22+
23+
## Naming Convention
24+
- `{feature}.stub` — default template (e.g., `controller.stub`)
25+
- `{feature}.{variant}.stub` — variant selected by flag (e.g., `controller.resource.stub`, `view.stateful.stub`, `migration.create.stub`)
26+
27+
## Content Style
28+
- Start with `import 'package:magic/magic.dart';` (or relevant Magic imports)
29+
- Class docstring: `/// {{ className }} description.`
30+
- Private fields with `_prefix`
31+
- `// TODO:` comments for user customization points
32+
- Trailing commas on multi-line params
33+
- Match the coding style of the parent Magic framework

.claude/rules/tests.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
---
2+
path: "test/**/*_test.dart"
3+
---
4+
5+
# Test Conventions
6+
7+
## Setup Pattern
8+
```dart
9+
late Directory tempDir;
10+
late MyCommand cmd;
11+
late ArgParser parser;
12+
13+
setUp(() {
14+
tempDir = Directory.systemTemp.createTempSync('magic_test_feature_');
15+
cmd = MyCommand(testRoot: tempDir.path);
16+
parser = ArgParser();
17+
cmd.configure(parser);
18+
});
19+
20+
tearDown(() {
21+
tempDir.deleteSync(recursive: true);
22+
});
23+
```
24+
Every test group gets a fresh `tempDir`. Always clean up in `tearDown`.
25+
26+
## Test Isolation
27+
- Pass `testRoot: tempDir.path` to ALL commands — never let tests touch real project
28+
- For chained commands (`make:model -mcf`), child commands must also receive `testRoot`
29+
- Set `MAGIC_CLI_STUBS_DIR` env var to override stub resolution paths in tests
30+
31+
## Running Commands in Tests
32+
```dart
33+
cmd.arguments = parser.parse(['Monitor', '--resource']);
34+
await cmd.handle();
35+
```
36+
Always `parser.parse()` before `cmd.handle()` — populates `cmd.arguments`.
37+
38+
## Assertions
39+
- File existence: `expect(File('...').existsSync(), isTrue)`
40+
- Content check: `expect(file.readAsStringSync(), contains('class MonitorController'))`
41+
- Negative: `expect(content, isNot(contains('MonitorControllerController')))`
42+
- Count: `expect(dir.listSync().length, equals(1))`
43+
44+
## Test Structure Per Command
45+
1. Name/description getters
46+
2. Argument parsing (flags, options, positional args)
47+
3. File generation — verify file exists at correct path
48+
4. Content verification — class name, imports, placeholders replaced
49+
5. Edge cases — nested paths, duplicate suffixes, `--force` overwrite, missing args
50+
51+
## Integration Tests
52+
`Directory.current` must be changed and restored in `try`/`finally`:
53+
```dart
54+
Directory.current = tempDir;
55+
try { await kernel.handle(args); }
56+
finally { Directory.current = originalDir; }
57+
```
58+
Create a dummy `pubspec.yaml` in `tempDir``findProjectRoot()` needs it.
59+
60+
## No Mocking Libraries
61+
Use real filesystem with temp dirs. No `mockito`, no code generation — mock by constructing commands with `testRoot`.

lib/src/stubs/stub_loader.dart

Lines changed: 10 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -76,35 +76,17 @@ class StubLoader {
7676
/// `name: magic_cli` (works for direct script execution).
7777
/// 3. Check common development paths as fallback.
7878
static List<String> _defaultSearchPaths() {
79-
// First, check if we are in a test environment. If we are running tests,
80-
// we should use the current working directory's assets/stubs if it exists.
81-
// In tests, the script path is often a temp file generated by the test runner.
82-
if (Platform.environment.containsKey('FLUTTER_TEST') ||
83-
Platform.script.path.endsWith('.dart') &&
84-
Platform.script.path.contains('test')) {
85-
final possibleRoots = [
86-
Directory.current.path,
87-
// When running test from magic root directory
88-
path.join(Directory.current.path, 'plugins', 'magic_cli'),
89-
// When running tests from IDE, Directory.current might be the project root
90-
// but magic_cli is inside plugins
91-
path.join(
92-
Directory.current.path, 'plugins', 'magic', 'plugins', 'magic_cli'),
93-
// The actual absolute path where we know it exists during development
94-
'/Users/anilcan/StudioProjects/uptizm/plugins/magic/plugins/magic_cli'
95-
];
96-
97-
for (final root in possibleRoots) {
98-
final stubDir = path.join(root, 'assets', 'stubs');
99-
if (Directory(stubDir).existsSync()) {
100-
return [stubDir];
101-
}
102-
}
79+
// 1. Environment variable override — highest priority.
80+
// Used in tests: set MAGIC_CLI_STUBS_DIR to point at a custom stubs dir.
81+
final envDir = Platform.environment['MAGIC_CLI_STUBS_DIR'];
82+
if (envDir != null && Directory(envDir).existsSync()) {
83+
return [envDir];
10384
}
10485

105-
// 1. Parse .dart_tool/package_config.json — the most reliable strategy.
86+
// 2. Parse .dart_tool/package_config.json — the most reliable strategy.
10687
// This file is generated by `dart pub get` and contains the rootUri
10788
// for every dependency, including path dependencies.
89+
// Works in all environments: local dev, CI, test, consumer projects.
10890
final packageConfigRoot = _resolveFromPackageConfig();
10991
if (packageConfigRoot != null) {
11092
final stubDir = path.join(packageConfigRoot, 'assets', 'stubs');
@@ -113,7 +95,7 @@ class StubLoader {
11395
}
11496
}
11597

116-
// 2. Walk up from Platform.script to find the magic_cli pubspec.
98+
// 3. Walk up from Platform.script to find the magic_cli pubspec.
11799
// Works when running the CLI directly from its own directory.
118100
final scriptPath = Platform.script.toFilePath();
119101
var current = Directory(path.dirname(scriptPath));
@@ -131,12 +113,10 @@ class StubLoader {
131113
current = parent;
132114
}
133115

134-
// 3. Fallback: check common development paths.
116+
// 4. Fallback: check current directory and common dev paths.
135117
final possibleRoots = [
136-
path.join(Directory.current.path, 'plugins', 'magic_cli'),
137118
Directory.current.path,
138-
// For testing, since tests run in temp directories
139-
'/Users/anilcan/StudioProjects/uptizm/plugins/magic/plugins/magic_cli'
119+
path.join(Directory.current.path, 'plugins', 'magic_cli'),
140120
];
141121

142122
for (final root in possibleRoots) {

0 commit comments

Comments
 (0)