Skip to content

Commit 17100d1

Browse files
committed
feat: add --dry-run flag and test harness with CI
- Add --dry-run flag that outputs what would happen without side effects - Shows toast XML, title, message, icon path, ntfy config - Install/uninstall shows target files and hook types - No toast shown, no files written, no network calls - Add PowerShell test suite (tests/test-toasty.ps1) with 25 tests: - Argument parsing (--version, --help, error cases) - All 5 presets (claude, copilot, gemini, codex, cursor) - Toast XML structure and special char escaping - Install/uninstall for each agent - ntfy configuration (unconfigured, topic, custom server) - Add CI workflow (.github/workflows/ci.yml) for push/PR on main - Update README with --dry-run flag and Testing section
1 parent 2fc023a commit 17100d1

File tree

4 files changed

+419
-4
lines changed

4 files changed

+419
-4
lines changed

.github/workflows/ci.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
build-and-test:
11+
runs-on: windows-latest
12+
13+
steps:
14+
- uses: actions/checkout@v4
15+
16+
- name: Build
17+
run: |
18+
cmake -S . -B build -G "Visual Studio 17 2022" -A x64
19+
cmake --build build --config Release
20+
21+
- name: Run tests
22+
run: .\tests\test-toasty.ps1 -ExePath .\build\Release\toasty.exe

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ Options:
2828
--install [agent] Install hooks for AI CLI agents (claude, gemini, copilot, or all)
2929
--uninstall Remove hooks from all AI CLI agents
3030
--status Show installation status
31+
--dry-run Show what would happen without executing side effects
3132
```
3233

3334
## AI CLI Auto-Detection
@@ -241,6 +242,16 @@ cmake --build build --config Release
241242

242243
Output: `build\Release\toasty.exe`
243244

245+
## Testing
246+
247+
Run the test suite after building:
248+
249+
```cmd
250+
.\tests\test-toasty.ps1 -ExePath .\build\Release\toasty.exe
251+
```
252+
253+
Tests use `--dry-run` to validate argument parsing, preset icons, toast XML generation, install/uninstall logic, and ntfy configuration without showing actual notifications or modifying any config files.
254+
244255
## License
245256

246257
MIT

main.cpp

Lines changed: 78 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ const wchar_t* APP_NAME = L"Toasty";
3434
const wchar_t* PROTOCOL_NAME = L"toasty";
3535
const wchar_t* TOASTY_VERSION = L"0.3";
3636

37+
// Global flags
38+
bool g_dryRun = false;
39+
3740
// RAII wrapper for Windows handles
3841
struct HandleGuard {
3942
HANDLE h;
@@ -298,7 +301,8 @@ void print_usage() {
298301
<< L" --install [agent] Install hooks for AI CLI agents (claude, gemini, copilot, or all)\n"
299302
<< L" --uninstall Remove hooks from all AI CLI agents\n"
300303
<< L" --status Show installation status\n"
301-
<< L" --register Re-register app for notifications (troubleshooting)\n\n"
304+
<< L" --register Re-register app for notifications (troubleshooting)\n"
305+
<< L" --dry-run Show what would happen without executing side effects\n\n"
302306
<< L"Push Notifications:\n"
303307
<< L" Set TOASTY_NTFY_TOPIC to send push notifications to your phone via ntfy.sh.\n"
304308
<< L" Set TOASTY_NTFY_SERVER to use a self-hosted ntfy server (default: ntfy.sh).\n\n"
@@ -1402,6 +1406,35 @@ void handle_install(const std::wstring& agent) {
14021406
bool installGemini = installAll || agent == L"gemini";
14031407
bool installCopilot = installAll || agent == L"copilot";
14041408

1409+
if (g_dryRun) {
1410+
std::wcout << L"[dry-run] Install targets:";
1411+
if (installClaude) std::wcout << L" claude";
1412+
if (installGemini) std::wcout << L" gemini";
1413+
if (installCopilot) std::wcout << L" copilot";
1414+
std::wcout << L"\n";
1415+
1416+
if (installClaude) {
1417+
std::wstring configPath = expand_env(L"%USERPROFILE%\\.claude\\settings.json");
1418+
std::wcout << L"[dry-run] Would write: " << configPath << L"\n";
1419+
std::wstring escapedPath = escape_json_string(exePath);
1420+
std::wcout << L"[dry-run] Hook command: " << escapedPath << L" \"Task complete\" -t \"Claude Code\"\n";
1421+
std::wcout << L"[dry-run] Hook type: Stop\n";
1422+
}
1423+
if (installGemini) {
1424+
std::wstring configPath = expand_env(L"%USERPROFILE%\\.gemini\\settings.json");
1425+
std::wcout << L"[dry-run] Would write: " << configPath << L"\n";
1426+
std::wstring escapedPath = escape_json_string(exePath);
1427+
std::wcout << L"[dry-run] Hook command: " << escapedPath << L" \"Gemini finished\" -t \"Gemini\"\n";
1428+
std::wcout << L"[dry-run] Hook type: AfterAgent\n";
1429+
}
1430+
if (installCopilot) {
1431+
std::wcout << L"[dry-run] Would write: .github\\hooks\\toasty.json\n";
1432+
std::wcout << L"[dry-run] Hook command: toasty 'Copilot finished' -t 'GitHub Copilot'\n";
1433+
std::wcout << L"[dry-run] Hook type: sessionEnd\n";
1434+
}
1435+
return;
1436+
}
1437+
14051438
std::wcout << L"Detecting AI CLI agents...\n";
14061439

14071440
bool claudeDetected = detect_claude();
@@ -1455,6 +1488,16 @@ void handle_install(const std::wstring& agent) {
14551488

14561489
// Handle --uninstall command
14571490
void handle_uninstall() {
1491+
if (g_dryRun) {
1492+
std::wcout << L"[dry-run] Would check and remove hooks from:\n";
1493+
std::wstring claudePath = expand_env(L"%USERPROFILE%\\.claude\\settings.json");
1494+
std::wstring geminiPath = expand_env(L"%USERPROFILE%\\.gemini\\settings.json");
1495+
std::wcout << L"[dry-run] Claude: " << claudePath << L"\n";
1496+
std::wcout << L"[dry-run] Gemini: " << geminiPath << L"\n";
1497+
std::wcout << L"[dry-run] Copilot: .github\\hooks\\toasty.json\n";
1498+
return;
1499+
}
1500+
14581501
std::wcout << L"Removing toasty hooks...\n";
14591502

14601503
bool anyUninstalled = false;
@@ -1639,11 +1682,13 @@ int wmain(int argc, wchar_t* argv[]) {
16391682
bool explicitTitle = false; // Track if user explicitly set -t
16401683
bool debug = false;
16411684

1642-
// Quick scan for --debug flag
1685+
// Quick scan for --debug and --dry-run flags
16431686
for (int i = 1; i < argc; i++) {
1644-
if (std::wstring(argv[i]) == L"--debug") {
1687+
std::wstring flag(argv[i]);
1688+
if (flag == L"--debug") {
16451689
debug = true;
1646-
break;
1690+
} else if (flag == L"--dry-run") {
1691+
g_dryRun = true;
16471692
}
16481693
}
16491694

@@ -1739,6 +1784,12 @@ int wmain(int argc, wchar_t* argv[]) {
17391784
return 1;
17401785
}
17411786
}
1787+
else if (arg == L"--debug") {
1788+
// Already handled in pre-scan
1789+
}
1790+
else if (arg == L"--dry-run") {
1791+
// Already handled in pre-scan
1792+
}
17421793
else if (arg[0] != L'-' && message.empty()) {
17431794
message = arg;
17441795
}
@@ -1866,6 +1917,29 @@ int wmain(int argc, wchar_t* argv[]) {
18661917
L"<text>" + escape_xml(message) + L"</text>"
18671918
L"</binding></visual></toast>";
18681919

1920+
if (g_dryRun) {
1921+
std::wcout << L"[dry-run] Title: " << title << L"\n";
1922+
std::wcout << L"[dry-run] Message: " << message << L"\n";
1923+
std::wcout << L"[dry-run] Icon: " << (iconPath.empty() ? L"(none)" : iconPath) << L"\n";
1924+
std::wcout << L"[dry-run] Toast XML:\n" << xml << L"\n";
1925+
1926+
// Show ntfy status
1927+
wchar_t topicBuf[256] = {};
1928+
if (GetEnvironmentVariableW(L"TOASTY_NTFY_TOPIC", topicBuf, 256) && topicBuf[0] != L'\0') {
1929+
wchar_t serverBuf[256] = {};
1930+
std::wstring server = L"ntfy.sh";
1931+
if (GetEnvironmentVariableW(L"TOASTY_NTFY_SERVER", serverBuf, 256) && serverBuf[0] != L'\0') {
1932+
server = serverBuf;
1933+
}
1934+
std::wcout << L"[dry-run] ntfy: would POST to https://" << server << L"/" << topicBuf << L"\n";
1935+
} else {
1936+
std::wcout << L"[dry-run] ntfy: not configured\n";
1937+
}
1938+
1939+
std::wcout << L"[dry-run] Update check: skipped\n";
1940+
return 0;
1941+
}
1942+
18691943
XmlDocument doc;
18701944
doc.LoadXml(xml);
18711945

0 commit comments

Comments
 (0)