63bb31066f
预防式闸:一条命令生成「接口 + Impl + Fake + Riverpod provider + flow-test 桩」, 外部依赖从一开始就藏接口后、可注入、自带 Fake、可测——把支柱1 变成阻力最小路径。 - scripts/new-seam.sh <PascalName> [lib子目录] - 验证:生成样例 SampleProbe → flutter analyze 无 issue + test 过 → 清理 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
78 lines
3.2 KiB
Bash
Executable File
78 lines
3.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# new-seam.sh — 闸1 脚手架:一条命令生成一个「接缝」骨架(支柱1:接缝即接口)。
|
|
#
|
|
# 让「对的写法」成为阻力最小的默认路径:外部依赖(原生/HTTP/子进程/存储/时钟/随机)
|
|
# 藏在接口后、可注入、自带 Fake,从一开始就可测(支柱1 + 支柱5)。
|
|
#
|
|
# 用法: bash scripts/new-seam.sh <PascalName> [lib 子目录(默认 seams)]
|
|
# 例: bash scripts/new-seam.sh GeoLocator services
|
|
# → client/lib/services/geo_locator.dart (接口+Impl+Fake+provider)
|
|
# → client/test/unit/geo_locator_seam_test.dart (ProviderScope override 桩)
|
|
set -euo pipefail
|
|
|
|
NAME="${1:-}"
|
|
SUBDIR="${2:-seams}"
|
|
[ -n "$NAME" ] || { echo "用法: bash scripts/new-seam.sh <PascalName> [lib子目录]" >&2; exit 2; }
|
|
case "$NAME" in [A-Z]*) ;; *) echo "✗ 名字须 PascalCase(首字母大写),如 GeoLocator" >&2; exit 2;; esac
|
|
|
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
# PascalCase → snake_case
|
|
SNAKE="$(python3 -c 'import re,sys;print(re.sub(r"(?<!^)(?=[A-Z])","_",sys.argv[1]).lower())' "$NAME")"
|
|
LOWER="$(python3 -c 'print(sys.argv[1][0].lower()+sys.argv[1][1:])' "$NAME" 2>/dev/null || python3 -c 'import sys;s=sys.argv[1];print(s[0].lower()+s[1:])' "$NAME")"
|
|
|
|
LIB="$ROOT/client/lib/$SUBDIR/$SNAKE.dart"
|
|
TEST="$ROOT/client/test/unit/${SNAKE}_seam_test.dart"
|
|
[ -e "$LIB" ] && { echo "✗ 已存在: $LIB" >&2; exit 1; }
|
|
[ -e "$TEST" ] && { echo "✗ 已存在: $TEST" >&2; exit 1; }
|
|
mkdir -p "$(dirname "$LIB")"
|
|
|
|
cat > "$LIB" <<EOF
|
|
// $SNAKE.dart — $NAME 接缝(支柱1:接缝即接口)。
|
|
//
|
|
// 外部依赖藏在接口 [$NAME] 后:业务层只依赖接口、经 [${LOWER}Provider] 注入,
|
|
// 禁止直接 new 真实现或调静态单例。测试用 [${NAME}Fake] 经 ProviderScope override 注入。
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
/// $NAME 接缝接口。TODO: 按需补方法(保持窄、可断言)。
|
|
abstract class $NAME {
|
|
// TODO: e.g. Future<String> doThing();
|
|
}
|
|
|
|
/// 真实现:连真实外部依赖。
|
|
class ${NAME}Impl implements $NAME {
|
|
// TODO: 实现接口方法。
|
|
}
|
|
|
|
/// 测试 Fake:无副作用、可控,供 ProviderScope override 注入(支柱1)。
|
|
class ${NAME}Fake implements $NAME {
|
|
// TODO: 返回可控的假数据/记录调用。
|
|
}
|
|
|
|
/// 注入点:默认真实现;测试 override 为 ${NAME}Fake。
|
|
final ${LOWER}Provider = Provider<$NAME>((ref) => ${NAME}Impl());
|
|
EOF
|
|
|
|
cat > "$TEST" <<EOF
|
|
// ${SNAKE}_seam_test.dart — $NAME 接缝的 flow 测试桩(支柱1)。
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:pangolin_vpn/$SUBDIR/$SNAKE.dart';
|
|
|
|
void main() {
|
|
test('$NAME 经 ProviderScope override 注入 Fake', () {
|
|
final c = ProviderContainer(overrides: [
|
|
${LOWER}Provider.overrideWithValue(${NAME}Fake()),
|
|
]);
|
|
addTearDown(c.dispose);
|
|
|
|
expect(c.read(${LOWER}Provider), isA<${NAME}Fake>());
|
|
// TODO: 驱动接口方法、断言 Fake 行为(支柱5:可观测即可断言)。
|
|
});
|
|
}
|
|
EOF
|
|
|
|
echo "✓ 已生成接缝骨架:"
|
|
echo " lib : client/lib/$SUBDIR/$SNAKE.dart (接口 + Impl + Fake + provider)"
|
|
echo " test: client/test/unit/${SNAKE}_seam_test.dart (override 注入 Fake)"
|
|
echo "下一步:补接口方法 + Impl/Fake + 用例;flutter analyze && flutter test。"
|