2026-02-22 13:33:32 +01:00
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
use std::fs;
|
|
|
|
|
use std::path::PathBuf;
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
|
|
|
pub struct Settings {
|
|
|
|
|
pub anthropic_key: Option<String>,
|
|
|
|
|
pub openai_key: Option<String>,
|
|
|
|
|
pub google_key: Option<String>,
|
2026-02-22 17:40:20 +01:00
|
|
|
pub github_token: Option<String>,
|
2026-02-23 19:56:30 +01:00
|
|
|
pub github_username: Option<String>,
|
feat: auto-pull vault changes from Git (background sync + conflict handling) (#79)
* feat: add auto-pull vault sync with conflict handling
- Add git_pull, has_remote, get_conflict_files to Rust backend
- Add GitPullResult type and auto_pull_interval_minutes to Settings
- Create useAutoSync hook (pull on launch, focus, periodic interval)
- Update StatusBar with real sync status indicator
- Add pull interval setting to SettingsPanel
- Add mock handler for git_pull
- Update existing tests for new Settings field
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: add tests for git pull, settings, and useAutoSync hook
- Add Rust tests: has_remote, git_pull (no_remote, up_to_date, updated),
get_conflict_files, parse_updated_files, GitPullResult serialization
- Add frontend tests: useAutoSync (mount pull, focus pull, conflict,
error, manual trigger, concurrent prevention, no_remote)
- Fix existing settings tests for new auto_pull_interval_minutes field
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* design: add auto-pull-vault wireframes
Frames showing: sync idle, syncing, conflict indicator,
settings sync section, and conflict toast notification.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add auto_pull_interval_minutes to all Settings literals
Fix build error and test fixtures missing the new field.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: cargo fmt
* ci: re-trigger CI after flaky test
* ci: retrigger after disk space cleanup
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 01:55:16 +01:00
|
|
|
pub auto_pull_interval_minutes: Option<u32>,
|
2026-02-22 13:33:32 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn settings_path() -> Result<PathBuf, String> {
|
|
|
|
|
dirs::config_dir()
|
|
|
|
|
.map(|d| d.join("com.laputa.app").join("settings.json"))
|
|
|
|
|
.ok_or_else(|| "Could not determine config directory".to_string())
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-22 18:04:01 +01:00
|
|
|
fn get_settings_at(path: &PathBuf) -> Result<Settings, String> {
|
2026-02-22 13:33:32 +01:00
|
|
|
if !path.exists() {
|
|
|
|
|
return Ok(Settings::default());
|
|
|
|
|
}
|
feat: auto-build, GitHub Release, and in-app updater (#14)
* ci: auto-release workflow on merge to main
Rewrite .github/workflows/release.yml to trigger on every push to main
instead of manual tag pushes. The workflow now:
- Computes version as 0.YYYYMMDD.GITHUB_RUN_NUMBER
- Builds aarch64-apple-darwin and x86_64-apple-darwin in parallel
- Merges them into a universal binary using lipo
- Creates a universal .dmg and signed updater tarball
- Generates latest.json with per-arch and universal platform entries
- Publishes a GitHub Release with auto-generated release notes
- Updates a GitHub Pages release history site (gh-pages branch)
Product decisions:
- Universal binary approach: copy arm64 .app as base, lipo the main
executable, keep everything else from arm64 (shared frameworks are
architecture-independent). This is the standard Tauri pattern.
- Per-arch updater tarballs are also uploaded so the Tauri updater can
download the correct arch-specific build (smaller download).
- Release notes are auto-generated from git log since last tag.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* ci: github pages with release history
Use peaceiris/actions-gh-pages@v4 to deploy a release history site.
The page fetches releases.json (also deployed) and renders each release
with date, notes, and download links for .dmg files.
This handles the gh-pages branch creation automatically on first run.
The page is available at https://refactoringhq.github.io/laputa-app/
Product decision: used fetch() to load releases.json at runtime instead
of inlining it, which is cleaner and avoids shell escaping issues with
release note content. The releases.json is deployed alongside index.html.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: in-app update notification UI
Replace the old window.confirm updater with a proper React-based
update notification system:
- useUpdater hook now exposes state machine (idle → available →
downloading → ready) and actions (startDownload, openReleaseNotes,
dismiss)
- UpdateBanner component renders at the top of the app shell:
- "Available" state: shows version, Release Notes link, Update Now
button, dismiss X
- "Downloading" state: animated spinner, progress bar with percentage
- "Ready" state: Restart Now button to apply the update
- Silently checks on startup after 3s delay; fails silently on
network errors or 404
- Release Notes link opens the GitHub Pages release history site
Product decisions:
- Banner at top of app (not a modal) — non-intrusive, visible but
not blocking. User can dismiss and continue working.
- Progress bar shows during download so user knows it's working.
- Separate "Restart Now" state after download so user controls when
the app restarts (they may have unsaved work).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: updater component tests
Rewrite useUpdater hook tests and add UpdateBanner component tests:
Hook tests (10 cases):
- Starts in idle state
- Does nothing when not in Tauri
- Checks for updates after 3s delay
- Stays idle when no update available
- Transitions to available when update found
- Handles missing release body gracefully
- Stays idle on network error (fails silently)
- Dismiss returns to idle
- openReleaseNotes opens correct URL
- startDownload transitions through downloading to ready
Component tests (10 cases):
- Renders nothing when idle
- Renders nothing on error
- Shows version and buttons when available
- Update Now calls startDownload
- Release Notes calls openReleaseNotes
- Dismiss button works
- Shows progress bar during download
- Shows 0% at start of download
- Shows restart button when ready
- Restart button calls restartApp
All 457 tests pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* design: auto-build-release wireframes
Copy ui-design.pen as base. Frames to be added for:
1. Update notification banner (visible state) — horizontal bar at
top of app shell with version text, Release Notes link, Update
Now button, and dismiss X
2. Update download progress state — spinner icon, progress bar with
percentage, downloading text
3. "Restart to apply" state — green accent, version text, Restart
Now button
Note: Pencil editor was not available during this session. The base
design file is committed; frames will be added when the editor is
accessible. The implemented component (UpdateBanner.tsx) serves as
the source of truth for the design.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: update ARCHITECTURE.md with release/update system
Add comprehensive documentation for:
- Release pipeline (4-phase workflow: version → build → release → pages)
- Versioning scheme (0.YYYYMMDD.RUN_NUMBER)
- Universal binary strategy (lipo merge)
- Updater endpoint and latest.json manifest
- In-app update UI state machine
- GitHub Pages release history site
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: rustfmt formatting
* fix: rustfmt build.rs
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 11:50:36 +01:00
|
|
|
let content =
|
|
|
|
|
fs::read_to_string(path).map_err(|e| format!("Failed to read settings: {}", e))?;
|
|
|
|
|
serde_json::from_str(&content).map_err(|e| format!("Failed to parse settings: {}", e))
|
2026-02-22 13:33:32 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-22 18:04:01 +01:00
|
|
|
fn save_settings_at(path: &PathBuf, settings: Settings) -> Result<(), String> {
|
2026-02-22 13:33:32 +01:00
|
|
|
if let Some(parent) = path.parent() {
|
|
|
|
|
fs::create_dir_all(parent)
|
|
|
|
|
.map_err(|e| format!("Failed to create config directory: {}", e))?;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Trim whitespace and convert empty strings to None
|
|
|
|
|
let cleaned = Settings {
|
feat: auto-build, GitHub Release, and in-app updater (#14)
* ci: auto-release workflow on merge to main
Rewrite .github/workflows/release.yml to trigger on every push to main
instead of manual tag pushes. The workflow now:
- Computes version as 0.YYYYMMDD.GITHUB_RUN_NUMBER
- Builds aarch64-apple-darwin and x86_64-apple-darwin in parallel
- Merges them into a universal binary using lipo
- Creates a universal .dmg and signed updater tarball
- Generates latest.json with per-arch and universal platform entries
- Publishes a GitHub Release with auto-generated release notes
- Updates a GitHub Pages release history site (gh-pages branch)
Product decisions:
- Universal binary approach: copy arm64 .app as base, lipo the main
executable, keep everything else from arm64 (shared frameworks are
architecture-independent). This is the standard Tauri pattern.
- Per-arch updater tarballs are also uploaded so the Tauri updater can
download the correct arch-specific build (smaller download).
- Release notes are auto-generated from git log since last tag.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* ci: github pages with release history
Use peaceiris/actions-gh-pages@v4 to deploy a release history site.
The page fetches releases.json (also deployed) and renders each release
with date, notes, and download links for .dmg files.
This handles the gh-pages branch creation automatically on first run.
The page is available at https://refactoringhq.github.io/laputa-app/
Product decision: used fetch() to load releases.json at runtime instead
of inlining it, which is cleaner and avoids shell escaping issues with
release note content. The releases.json is deployed alongside index.html.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: in-app update notification UI
Replace the old window.confirm updater with a proper React-based
update notification system:
- useUpdater hook now exposes state machine (idle → available →
downloading → ready) and actions (startDownload, openReleaseNotes,
dismiss)
- UpdateBanner component renders at the top of the app shell:
- "Available" state: shows version, Release Notes link, Update Now
button, dismiss X
- "Downloading" state: animated spinner, progress bar with percentage
- "Ready" state: Restart Now button to apply the update
- Silently checks on startup after 3s delay; fails silently on
network errors or 404
- Release Notes link opens the GitHub Pages release history site
Product decisions:
- Banner at top of app (not a modal) — non-intrusive, visible but
not blocking. User can dismiss and continue working.
- Progress bar shows during download so user knows it's working.
- Separate "Restart Now" state after download so user controls when
the app restarts (they may have unsaved work).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: updater component tests
Rewrite useUpdater hook tests and add UpdateBanner component tests:
Hook tests (10 cases):
- Starts in idle state
- Does nothing when not in Tauri
- Checks for updates after 3s delay
- Stays idle when no update available
- Transitions to available when update found
- Handles missing release body gracefully
- Stays idle on network error (fails silently)
- Dismiss returns to idle
- openReleaseNotes opens correct URL
- startDownload transitions through downloading to ready
Component tests (10 cases):
- Renders nothing when idle
- Renders nothing on error
- Shows version and buttons when available
- Update Now calls startDownload
- Release Notes calls openReleaseNotes
- Dismiss button works
- Shows progress bar during download
- Shows 0% at start of download
- Shows restart button when ready
- Restart button calls restartApp
All 457 tests pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* design: auto-build-release wireframes
Copy ui-design.pen as base. Frames to be added for:
1. Update notification banner (visible state) — horizontal bar at
top of app shell with version text, Release Notes link, Update
Now button, and dismiss X
2. Update download progress state — spinner icon, progress bar with
percentage, downloading text
3. "Restart to apply" state — green accent, version text, Restart
Now button
Note: Pencil editor was not available during this session. The base
design file is committed; frames will be added when the editor is
accessible. The implemented component (UpdateBanner.tsx) serves as
the source of truth for the design.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: update ARCHITECTURE.md with release/update system
Add comprehensive documentation for:
- Release pipeline (4-phase workflow: version → build → release → pages)
- Versioning scheme (0.YYYYMMDD.RUN_NUMBER)
- Universal binary strategy (lipo merge)
- Updater endpoint and latest.json manifest
- In-app update UI state machine
- GitHub Pages release history site
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: rustfmt formatting
* fix: rustfmt build.rs
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 11:50:36 +01:00
|
|
|
anthropic_key: settings
|
|
|
|
|
.anthropic_key
|
|
|
|
|
.map(|k| k.trim().to_string())
|
|
|
|
|
.filter(|k| !k.is_empty()),
|
|
|
|
|
openai_key: settings
|
|
|
|
|
.openai_key
|
|
|
|
|
.map(|k| k.trim().to_string())
|
|
|
|
|
.filter(|k| !k.is_empty()),
|
|
|
|
|
google_key: settings
|
|
|
|
|
.google_key
|
|
|
|
|
.map(|k| k.trim().to_string())
|
|
|
|
|
.filter(|k| !k.is_empty()),
|
|
|
|
|
github_token: settings
|
|
|
|
|
.github_token
|
|
|
|
|
.map(|k| k.trim().to_string())
|
|
|
|
|
.filter(|k| !k.is_empty()),
|
2026-02-23 19:56:30 +01:00
|
|
|
github_username: settings
|
|
|
|
|
.github_username
|
|
|
|
|
.map(|k| k.trim().to_string())
|
|
|
|
|
.filter(|k| !k.is_empty()),
|
feat: auto-pull vault changes from Git (background sync + conflict handling) (#79)
* feat: add auto-pull vault sync with conflict handling
- Add git_pull, has_remote, get_conflict_files to Rust backend
- Add GitPullResult type and auto_pull_interval_minutes to Settings
- Create useAutoSync hook (pull on launch, focus, periodic interval)
- Update StatusBar with real sync status indicator
- Add pull interval setting to SettingsPanel
- Add mock handler for git_pull
- Update existing tests for new Settings field
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: add tests for git pull, settings, and useAutoSync hook
- Add Rust tests: has_remote, git_pull (no_remote, up_to_date, updated),
get_conflict_files, parse_updated_files, GitPullResult serialization
- Add frontend tests: useAutoSync (mount pull, focus pull, conflict,
error, manual trigger, concurrent prevention, no_remote)
- Fix existing settings tests for new auto_pull_interval_minutes field
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* design: add auto-pull-vault wireframes
Frames showing: sync idle, syncing, conflict indicator,
settings sync section, and conflict toast notification.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add auto_pull_interval_minutes to all Settings literals
Fix build error and test fixtures missing the new field.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: cargo fmt
* ci: re-trigger CI after flaky test
* ci: retrigger after disk space cleanup
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 01:55:16 +01:00
|
|
|
auto_pull_interval_minutes: settings.auto_pull_interval_minutes,
|
2026-02-22 13:33:32 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let json = serde_json::to_string_pretty(&cleaned)
|
|
|
|
|
.map_err(|e| format!("Failed to serialize settings: {}", e))?;
|
feat: auto-build, GitHub Release, and in-app updater (#14)
* ci: auto-release workflow on merge to main
Rewrite .github/workflows/release.yml to trigger on every push to main
instead of manual tag pushes. The workflow now:
- Computes version as 0.YYYYMMDD.GITHUB_RUN_NUMBER
- Builds aarch64-apple-darwin and x86_64-apple-darwin in parallel
- Merges them into a universal binary using lipo
- Creates a universal .dmg and signed updater tarball
- Generates latest.json with per-arch and universal platform entries
- Publishes a GitHub Release with auto-generated release notes
- Updates a GitHub Pages release history site (gh-pages branch)
Product decisions:
- Universal binary approach: copy arm64 .app as base, lipo the main
executable, keep everything else from arm64 (shared frameworks are
architecture-independent). This is the standard Tauri pattern.
- Per-arch updater tarballs are also uploaded so the Tauri updater can
download the correct arch-specific build (smaller download).
- Release notes are auto-generated from git log since last tag.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* ci: github pages with release history
Use peaceiris/actions-gh-pages@v4 to deploy a release history site.
The page fetches releases.json (also deployed) and renders each release
with date, notes, and download links for .dmg files.
This handles the gh-pages branch creation automatically on first run.
The page is available at https://refactoringhq.github.io/laputa-app/
Product decision: used fetch() to load releases.json at runtime instead
of inlining it, which is cleaner and avoids shell escaping issues with
release note content. The releases.json is deployed alongside index.html.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: in-app update notification UI
Replace the old window.confirm updater with a proper React-based
update notification system:
- useUpdater hook now exposes state machine (idle → available →
downloading → ready) and actions (startDownload, openReleaseNotes,
dismiss)
- UpdateBanner component renders at the top of the app shell:
- "Available" state: shows version, Release Notes link, Update Now
button, dismiss X
- "Downloading" state: animated spinner, progress bar with percentage
- "Ready" state: Restart Now button to apply the update
- Silently checks on startup after 3s delay; fails silently on
network errors or 404
- Release Notes link opens the GitHub Pages release history site
Product decisions:
- Banner at top of app (not a modal) — non-intrusive, visible but
not blocking. User can dismiss and continue working.
- Progress bar shows during download so user knows it's working.
- Separate "Restart Now" state after download so user controls when
the app restarts (they may have unsaved work).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: updater component tests
Rewrite useUpdater hook tests and add UpdateBanner component tests:
Hook tests (10 cases):
- Starts in idle state
- Does nothing when not in Tauri
- Checks for updates after 3s delay
- Stays idle when no update available
- Transitions to available when update found
- Handles missing release body gracefully
- Stays idle on network error (fails silently)
- Dismiss returns to idle
- openReleaseNotes opens correct URL
- startDownload transitions through downloading to ready
Component tests (10 cases):
- Renders nothing when idle
- Renders nothing on error
- Shows version and buttons when available
- Update Now calls startDownload
- Release Notes calls openReleaseNotes
- Dismiss button works
- Shows progress bar during download
- Shows 0% at start of download
- Shows restart button when ready
- Restart button calls restartApp
All 457 tests pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* design: auto-build-release wireframes
Copy ui-design.pen as base. Frames to be added for:
1. Update notification banner (visible state) — horizontal bar at
top of app shell with version text, Release Notes link, Update
Now button, and dismiss X
2. Update download progress state — spinner icon, progress bar with
percentage, downloading text
3. "Restart to apply" state — green accent, version text, Restart
Now button
Note: Pencil editor was not available during this session. The base
design file is committed; frames will be added when the editor is
accessible. The implemented component (UpdateBanner.tsx) serves as
the source of truth for the design.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: update ARCHITECTURE.md with release/update system
Add comprehensive documentation for:
- Release pipeline (4-phase workflow: version → build → release → pages)
- Versioning scheme (0.YYYYMMDD.RUN_NUMBER)
- Universal binary strategy (lipo merge)
- Updater endpoint and latest.json manifest
- In-app update UI state machine
- GitHub Pages release history site
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: rustfmt formatting
* fix: rustfmt build.rs
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 11:50:36 +01:00
|
|
|
fs::write(path, json).map_err(|e| format!("Failed to write settings: {}", e))
|
2026-02-22 13:33:32 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-22 18:04:01 +01:00
|
|
|
pub fn get_settings() -> Result<Settings, String> {
|
|
|
|
|
get_settings_at(&settings_path()?)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn save_settings(settings: Settings) -> Result<(), String> {
|
|
|
|
|
save_settings_at(&settings_path()?, settings)
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-22 13:33:32 +01:00
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
2026-02-22 18:04:01 +01:00
|
|
|
|
|
|
|
|
/// Helper: save settings to a temp file and reload them.
|
|
|
|
|
fn save_and_reload(settings: Settings) -> Settings {
|
|
|
|
|
let dir = tempfile::TempDir::new().unwrap();
|
|
|
|
|
let path = dir.path().join("settings.json");
|
|
|
|
|
save_settings_at(&path, settings).unwrap();
|
|
|
|
|
get_settings_at(&path).unwrap()
|
2026-02-22 13:33:32 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
2026-02-22 18:04:01 +01:00
|
|
|
fn test_default_settings_all_none() {
|
|
|
|
|
let s = Settings::default();
|
feat: auto-pull vault changes from Git (background sync + conflict handling) (#79)
* feat: add auto-pull vault sync with conflict handling
- Add git_pull, has_remote, get_conflict_files to Rust backend
- Add GitPullResult type and auto_pull_interval_minutes to Settings
- Create useAutoSync hook (pull on launch, focus, periodic interval)
- Update StatusBar with real sync status indicator
- Add pull interval setting to SettingsPanel
- Add mock handler for git_pull
- Update existing tests for new Settings field
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: add tests for git pull, settings, and useAutoSync hook
- Add Rust tests: has_remote, git_pull (no_remote, up_to_date, updated),
get_conflict_files, parse_updated_files, GitPullResult serialization
- Add frontend tests: useAutoSync (mount pull, focus pull, conflict,
error, manual trigger, concurrent prevention, no_remote)
- Fix existing settings tests for new auto_pull_interval_minutes field
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* design: add auto-pull-vault wireframes
Frames showing: sync idle, syncing, conflict indicator,
settings sync section, and conflict toast notification.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add auto_pull_interval_minutes to all Settings literals
Fix build error and test fixtures missing the new field.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: cargo fmt
* ci: re-trigger CI after flaky test
* ci: retrigger after disk space cleanup
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 01:55:16 +01:00
|
|
|
assert!(s.anthropic_key.is_none());
|
|
|
|
|
assert!(s.openai_key.is_none());
|
|
|
|
|
assert!(s.google_key.is_none());
|
|
|
|
|
assert!(s.github_token.is_none());
|
|
|
|
|
assert!(s.github_username.is_none());
|
|
|
|
|
assert!(s.auto_pull_interval_minutes.is_none());
|
2026-02-22 13:33:32 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
2026-02-22 18:04:01 +01:00
|
|
|
fn test_settings_json_roundtrip() {
|
2026-02-22 13:33:32 +01:00
|
|
|
let settings = Settings {
|
|
|
|
|
anthropic_key: Some("sk-ant-test123".to_string()),
|
|
|
|
|
openai_key: None,
|
|
|
|
|
google_key: Some("AIza-test".to_string()),
|
2026-02-22 17:40:20 +01:00
|
|
|
github_token: Some("gho_xyz789".to_string()),
|
2026-02-23 19:56:30 +01:00
|
|
|
github_username: Some("lucaong".to_string()),
|
feat: auto-pull vault changes from Git (background sync + conflict handling) (#79)
* feat: add auto-pull vault sync with conflict handling
- Add git_pull, has_remote, get_conflict_files to Rust backend
- Add GitPullResult type and auto_pull_interval_minutes to Settings
- Create useAutoSync hook (pull on launch, focus, periodic interval)
- Update StatusBar with real sync status indicator
- Add pull interval setting to SettingsPanel
- Add mock handler for git_pull
- Update existing tests for new Settings field
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: add tests for git pull, settings, and useAutoSync hook
- Add Rust tests: has_remote, git_pull (no_remote, up_to_date, updated),
get_conflict_files, parse_updated_files, GitPullResult serialization
- Add frontend tests: useAutoSync (mount pull, focus pull, conflict,
error, manual trigger, concurrent prevention, no_remote)
- Fix existing settings tests for new auto_pull_interval_minutes field
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* design: add auto-pull-vault wireframes
Frames showing: sync idle, syncing, conflict indicator,
settings sync section, and conflict toast notification.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add auto_pull_interval_minutes to all Settings literals
Fix build error and test fixtures missing the new field.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: cargo fmt
* ci: re-trigger CI after flaky test
* ci: retrigger after disk space cleanup
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 01:55:16 +01:00
|
|
|
..Default::default()
|
2026-02-22 13:33:32 +01:00
|
|
|
};
|
|
|
|
|
let json = serde_json::to_string(&settings).unwrap();
|
|
|
|
|
let parsed: Settings = serde_json::from_str(&json).unwrap();
|
2026-02-22 18:04:01 +01:00
|
|
|
assert_eq!(parsed.anthropic_key, settings.anthropic_key);
|
|
|
|
|
assert_eq!(parsed.google_key, settings.google_key);
|
|
|
|
|
assert_eq!(parsed.github_token, settings.github_token);
|
2026-02-23 19:56:30 +01:00
|
|
|
assert_eq!(parsed.github_username, settings.github_username);
|
2026-02-22 13:33:32 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
2026-02-22 18:04:01 +01:00
|
|
|
fn test_get_settings_returns_default_for_missing_file() {
|
|
|
|
|
let dir = tempfile::TempDir::new().unwrap();
|
|
|
|
|
let path = dir.path().join("nonexistent.json");
|
|
|
|
|
let result = get_settings_at(&path).unwrap();
|
|
|
|
|
assert!(result.anthropic_key.is_none());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_save_and_load_preserves_values() {
|
|
|
|
|
let loaded = save_and_reload(Settings {
|
|
|
|
|
anthropic_key: Some("sk-ant-key".to_string()),
|
|
|
|
|
openai_key: Some("sk-openai".to_string()),
|
|
|
|
|
google_key: None,
|
|
|
|
|
github_token: Some("gho_token123".to_string()),
|
2026-02-23 19:56:30 +01:00
|
|
|
github_username: Some("lucaong".to_string()),
|
feat: auto-pull vault changes from Git (background sync + conflict handling) (#79)
* feat: add auto-pull vault sync with conflict handling
- Add git_pull, has_remote, get_conflict_files to Rust backend
- Add GitPullResult type and auto_pull_interval_minutes to Settings
- Create useAutoSync hook (pull on launch, focus, periodic interval)
- Update StatusBar with real sync status indicator
- Add pull interval setting to SettingsPanel
- Add mock handler for git_pull
- Update existing tests for new Settings field
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: add tests for git pull, settings, and useAutoSync hook
- Add Rust tests: has_remote, git_pull (no_remote, up_to_date, updated),
get_conflict_files, parse_updated_files, GitPullResult serialization
- Add frontend tests: useAutoSync (mount pull, focus pull, conflict,
error, manual trigger, concurrent prevention, no_remote)
- Fix existing settings tests for new auto_pull_interval_minutes field
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* design: add auto-pull-vault wireframes
Frames showing: sync idle, syncing, conflict indicator,
settings sync section, and conflict toast notification.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add auto_pull_interval_minutes to all Settings literals
Fix build error and test fixtures missing the new field.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: cargo fmt
* ci: re-trigger CI after flaky test
* ci: retrigger after disk space cleanup
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 01:55:16 +01:00
|
|
|
auto_pull_interval_minutes: Some(10),
|
2026-02-22 18:04:01 +01:00
|
|
|
});
|
|
|
|
|
assert_eq!(loaded.anthropic_key.as_deref(), Some("sk-ant-key"));
|
|
|
|
|
assert_eq!(loaded.openai_key.as_deref(), Some("sk-openai"));
|
|
|
|
|
assert_eq!(loaded.github_token.as_deref(), Some("gho_token123"));
|
2026-02-23 19:56:30 +01:00
|
|
|
assert_eq!(loaded.github_username.as_deref(), Some("lucaong"));
|
feat: auto-pull vault changes from Git (background sync + conflict handling) (#79)
* feat: add auto-pull vault sync with conflict handling
- Add git_pull, has_remote, get_conflict_files to Rust backend
- Add GitPullResult type and auto_pull_interval_minutes to Settings
- Create useAutoSync hook (pull on launch, focus, periodic interval)
- Update StatusBar with real sync status indicator
- Add pull interval setting to SettingsPanel
- Add mock handler for git_pull
- Update existing tests for new Settings field
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: add tests for git pull, settings, and useAutoSync hook
- Add Rust tests: has_remote, git_pull (no_remote, up_to_date, updated),
get_conflict_files, parse_updated_files, GitPullResult serialization
- Add frontend tests: useAutoSync (mount pull, focus pull, conflict,
error, manual trigger, concurrent prevention, no_remote)
- Fix existing settings tests for new auto_pull_interval_minutes field
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* design: add auto-pull-vault wireframes
Frames showing: sync idle, syncing, conflict indicator,
settings sync section, and conflict toast notification.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add auto_pull_interval_minutes to all Settings literals
Fix build error and test fixtures missing the new field.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: cargo fmt
* ci: re-trigger CI after flaky test
* ci: retrigger after disk space cleanup
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 01:55:16 +01:00
|
|
|
assert_eq!(loaded.auto_pull_interval_minutes, Some(10));
|
2026-02-22 18:04:01 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_save_trims_whitespace() {
|
|
|
|
|
let loaded = save_and_reload(Settings {
|
2026-02-22 13:33:32 +01:00
|
|
|
anthropic_key: Some(" sk-ant-test ".to_string()),
|
2026-02-22 17:40:20 +01:00
|
|
|
github_token: Some(" gho_abc ".to_string()),
|
2026-02-23 19:56:30 +01:00
|
|
|
github_username: Some(" lucaong ".to_string()),
|
|
|
|
|
..Default::default()
|
2026-02-22 18:04:01 +01:00
|
|
|
});
|
|
|
|
|
assert_eq!(loaded.anthropic_key.as_deref(), Some("sk-ant-test"));
|
|
|
|
|
assert_eq!(loaded.github_token.as_deref(), Some("gho_abc"));
|
2026-02-23 19:56:30 +01:00
|
|
|
assert_eq!(loaded.github_username.as_deref(), Some("lucaong"));
|
2026-02-22 18:04:01 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_save_filters_empty_and_whitespace_only() {
|
|
|
|
|
let loaded = save_and_reload(Settings {
|
|
|
|
|
anthropic_key: Some("".to_string()),
|
|
|
|
|
openai_key: Some(" ".to_string()),
|
2026-02-23 19:56:30 +01:00
|
|
|
github_username: Some("".to_string()),
|
|
|
|
|
..Default::default()
|
2026-02-22 18:04:01 +01:00
|
|
|
});
|
|
|
|
|
assert!(loaded.anthropic_key.is_none());
|
|
|
|
|
assert!(loaded.openai_key.is_none());
|
2026-02-23 19:56:30 +01:00
|
|
|
assert!(loaded.github_username.is_none());
|
2026-02-22 18:04:01 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_save_creates_parent_directories() {
|
|
|
|
|
let dir = tempfile::TempDir::new().unwrap();
|
|
|
|
|
let path = dir.path().join("nested").join("dir").join("settings.json");
|
|
|
|
|
|
feat: auto-build, GitHub Release, and in-app updater (#14)
* ci: auto-release workflow on merge to main
Rewrite .github/workflows/release.yml to trigger on every push to main
instead of manual tag pushes. The workflow now:
- Computes version as 0.YYYYMMDD.GITHUB_RUN_NUMBER
- Builds aarch64-apple-darwin and x86_64-apple-darwin in parallel
- Merges them into a universal binary using lipo
- Creates a universal .dmg and signed updater tarball
- Generates latest.json with per-arch and universal platform entries
- Publishes a GitHub Release with auto-generated release notes
- Updates a GitHub Pages release history site (gh-pages branch)
Product decisions:
- Universal binary approach: copy arm64 .app as base, lipo the main
executable, keep everything else from arm64 (shared frameworks are
architecture-independent). This is the standard Tauri pattern.
- Per-arch updater tarballs are also uploaded so the Tauri updater can
download the correct arch-specific build (smaller download).
- Release notes are auto-generated from git log since last tag.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* ci: github pages with release history
Use peaceiris/actions-gh-pages@v4 to deploy a release history site.
The page fetches releases.json (also deployed) and renders each release
with date, notes, and download links for .dmg files.
This handles the gh-pages branch creation automatically on first run.
The page is available at https://refactoringhq.github.io/laputa-app/
Product decision: used fetch() to load releases.json at runtime instead
of inlining it, which is cleaner and avoids shell escaping issues with
release note content. The releases.json is deployed alongside index.html.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: in-app update notification UI
Replace the old window.confirm updater with a proper React-based
update notification system:
- useUpdater hook now exposes state machine (idle → available →
downloading → ready) and actions (startDownload, openReleaseNotes,
dismiss)
- UpdateBanner component renders at the top of the app shell:
- "Available" state: shows version, Release Notes link, Update Now
button, dismiss X
- "Downloading" state: animated spinner, progress bar with percentage
- "Ready" state: Restart Now button to apply the update
- Silently checks on startup after 3s delay; fails silently on
network errors or 404
- Release Notes link opens the GitHub Pages release history site
Product decisions:
- Banner at top of app (not a modal) — non-intrusive, visible but
not blocking. User can dismiss and continue working.
- Progress bar shows during download so user knows it's working.
- Separate "Restart Now" state after download so user controls when
the app restarts (they may have unsaved work).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: updater component tests
Rewrite useUpdater hook tests and add UpdateBanner component tests:
Hook tests (10 cases):
- Starts in idle state
- Does nothing when not in Tauri
- Checks for updates after 3s delay
- Stays idle when no update available
- Transitions to available when update found
- Handles missing release body gracefully
- Stays idle on network error (fails silently)
- Dismiss returns to idle
- openReleaseNotes opens correct URL
- startDownload transitions through downloading to ready
Component tests (10 cases):
- Renders nothing when idle
- Renders nothing on error
- Shows version and buttons when available
- Update Now calls startDownload
- Release Notes calls openReleaseNotes
- Dismiss button works
- Shows progress bar during download
- Shows 0% at start of download
- Shows restart button when ready
- Restart button calls restartApp
All 457 tests pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* design: auto-build-release wireframes
Copy ui-design.pen as base. Frames to be added for:
1. Update notification banner (visible state) — horizontal bar at
top of app shell with version text, Release Notes link, Update
Now button, and dismiss X
2. Update download progress state — spinner icon, progress bar with
percentage, downloading text
3. "Restart to apply" state — green accent, version text, Restart
Now button
Note: Pencil editor was not available during this session. The base
design file is committed; frames will be added when the editor is
accessible. The implemented component (UpdateBanner.tsx) serves as
the source of truth for the design.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: update ARCHITECTURE.md with release/update system
Add comprehensive documentation for:
- Release pipeline (4-phase workflow: version → build → release → pages)
- Versioning scheme (0.YYYYMMDD.RUN_NUMBER)
- Universal binary strategy (lipo merge)
- Updater endpoint and latest.json manifest
- In-app update UI state machine
- GitHub Pages release history site
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: rustfmt formatting
* fix: rustfmt build.rs
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 11:50:36 +01:00
|
|
|
save_settings_at(
|
|
|
|
|
&path,
|
|
|
|
|
Settings {
|
|
|
|
|
anthropic_key: Some("key".to_string()),
|
|
|
|
|
..Default::default()
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
2026-02-22 18:04:01 +01:00
|
|
|
assert!(path.exists());
|
feat: auto-build, GitHub Release, and in-app updater (#14)
* ci: auto-release workflow on merge to main
Rewrite .github/workflows/release.yml to trigger on every push to main
instead of manual tag pushes. The workflow now:
- Computes version as 0.YYYYMMDD.GITHUB_RUN_NUMBER
- Builds aarch64-apple-darwin and x86_64-apple-darwin in parallel
- Merges them into a universal binary using lipo
- Creates a universal .dmg and signed updater tarball
- Generates latest.json with per-arch and universal platform entries
- Publishes a GitHub Release with auto-generated release notes
- Updates a GitHub Pages release history site (gh-pages branch)
Product decisions:
- Universal binary approach: copy arm64 .app as base, lipo the main
executable, keep everything else from arm64 (shared frameworks are
architecture-independent). This is the standard Tauri pattern.
- Per-arch updater tarballs are also uploaded so the Tauri updater can
download the correct arch-specific build (smaller download).
- Release notes are auto-generated from git log since last tag.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* ci: github pages with release history
Use peaceiris/actions-gh-pages@v4 to deploy a release history site.
The page fetches releases.json (also deployed) and renders each release
with date, notes, and download links for .dmg files.
This handles the gh-pages branch creation automatically on first run.
The page is available at https://refactoringhq.github.io/laputa-app/
Product decision: used fetch() to load releases.json at runtime instead
of inlining it, which is cleaner and avoids shell escaping issues with
release note content. The releases.json is deployed alongside index.html.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: in-app update notification UI
Replace the old window.confirm updater with a proper React-based
update notification system:
- useUpdater hook now exposes state machine (idle → available →
downloading → ready) and actions (startDownload, openReleaseNotes,
dismiss)
- UpdateBanner component renders at the top of the app shell:
- "Available" state: shows version, Release Notes link, Update Now
button, dismiss X
- "Downloading" state: animated spinner, progress bar with percentage
- "Ready" state: Restart Now button to apply the update
- Silently checks on startup after 3s delay; fails silently on
network errors or 404
- Release Notes link opens the GitHub Pages release history site
Product decisions:
- Banner at top of app (not a modal) — non-intrusive, visible but
not blocking. User can dismiss and continue working.
- Progress bar shows during download so user knows it's working.
- Separate "Restart Now" state after download so user controls when
the app restarts (they may have unsaved work).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: updater component tests
Rewrite useUpdater hook tests and add UpdateBanner component tests:
Hook tests (10 cases):
- Starts in idle state
- Does nothing when not in Tauri
- Checks for updates after 3s delay
- Stays idle when no update available
- Transitions to available when update found
- Handles missing release body gracefully
- Stays idle on network error (fails silently)
- Dismiss returns to idle
- openReleaseNotes opens correct URL
- startDownload transitions through downloading to ready
Component tests (10 cases):
- Renders nothing when idle
- Renders nothing on error
- Shows version and buttons when available
- Update Now calls startDownload
- Release Notes calls openReleaseNotes
- Dismiss button works
- Shows progress bar during download
- Shows 0% at start of download
- Shows restart button when ready
- Restart button calls restartApp
All 457 tests pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* design: auto-build-release wireframes
Copy ui-design.pen as base. Frames to be added for:
1. Update notification banner (visible state) — horizontal bar at
top of app shell with version text, Release Notes link, Update
Now button, and dismiss X
2. Update download progress state — spinner icon, progress bar with
percentage, downloading text
3. "Restart to apply" state — green accent, version text, Restart
Now button
Note: Pencil editor was not available during this session. The base
design file is committed; frames will be added when the editor is
accessible. The implemented component (UpdateBanner.tsx) serves as
the source of truth for the design.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: update ARCHITECTURE.md with release/update system
Add comprehensive documentation for:
- Release pipeline (4-phase workflow: version → build → release → pages)
- Versioning scheme (0.YYYYMMDD.RUN_NUMBER)
- Universal binary strategy (lipo merge)
- Updater endpoint and latest.json manifest
- In-app update UI state machine
- GitHub Pages release history site
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: rustfmt formatting
* fix: rustfmt build.rs
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 11:50:36 +01:00
|
|
|
assert_eq!(
|
|
|
|
|
get_settings_at(&path).unwrap().anthropic_key.as_deref(),
|
|
|
|
|
Some("key")
|
|
|
|
|
);
|
2026-02-22 18:04:01 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_get_settings_malformed_json() {
|
|
|
|
|
let dir = tempfile::TempDir::new().unwrap();
|
|
|
|
|
let path = dir.path().join("bad.json");
|
|
|
|
|
fs::write(&path, "not valid json{{{").unwrap();
|
|
|
|
|
|
|
|
|
|
let err = get_settings_at(&path).unwrap_err();
|
|
|
|
|
assert!(err.contains("Failed to parse settings"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_settings_path_returns_ok() {
|
|
|
|
|
let result = settings_path();
|
|
|
|
|
assert!(result.is_ok());
|
|
|
|
|
assert!(result.unwrap().to_str().unwrap().contains("com.laputa.app"));
|
2026-02-22 13:33:32 +01:00
|
|
|
}
|
|
|
|
|
}
|