Skip to content

Commit 1f88221

Browse files
committed
Prepare 0.8.0 release docs and tests
1 parent 91cc731 commit 1f88221

12 files changed

Lines changed: 215 additions & 45 deletions

AGENTS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ without hard-coding knowledge of any one repository.
2121
- Do not use `sleep` to resolve races. Races must be resolved with
2222
deterministic logic, explicit readiness signals, or ordered state
2323
transitions.
24+
- When tests must mutate process-global state such as environment
25+
variables, isolate the unsafe operation in a small helper, serialize
26+
access with a lock, and document the safety rationale instead of
27+
scattering raw unsafe calls through test bodies.
2428
- Run quality gates for code changes: `cargo fmt`, `cargo test`,
2529
`cargo clippy --all-targets --all-features -- -D warnings`.
2630

CHANGELOG.md

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,38 @@ All notable changes to `devloop` will be recorded in this file.
44

55
## [Unreleased]
66

7+
## [0.8.0] - 2026-04-08
8+
79
### Added
810
- Added a configurable watcher backend with non-breaking `native`
911
default behavior plus a `poll` fallback mode for environments where
1012
native filesystem notifications are unreliable.
11-
- Added a Rust repeated-edit watch flake smoke test that runs under
12-
`cargo test` and therefore in GitHub Actions alongside the existing
13-
runtime smoke test.
13+
- Added a Rust repeated-edit watch flake smoke test that can be run
14+
locally with `DEVLOOP_RUN_WATCH_FLAKE_SMOKE=1 cargo test --test
15+
watch_flake_smoke -- --nocapture`.
16+
- Added explicit trailing-slash syntax for literal directory watch
17+
targets, for example `content/`, so recursive directory intent is
18+
preserved even when the directory does not yet exist at startup.
19+
- Added a development guide under [`docs/development.md`](docs/development.md)
20+
and exposed it in the CLI as `devloop docs development`.
1421

1522
### Changed
1623
- `devloop` now derives concrete watch targets from configured watch
1724
patterns and asks the backend to watch only those files or
1825
directories instead of always watching the whole repository root.
26+
- The watch flake smoke test is now opt-in instead of running during
27+
every default `cargo test` or CI run. The existing runtime smoke test
28+
remains in CI.
1929

2030
### Fixed
2131
- Native watch registration now resolves file and directory targets at
2232
runtime, so startup no longer depends on those paths already existing
2333
when config is parsed.
34+
- Fixed a real watch flake where the debounce batch could be dropped if
35+
another `tokio::select!` branch won the race while filesystem events
36+
were already buffered.
37+
- Test-only environment mutation now lives behind locked helpers with
38+
documented safety rationale instead of scattered raw unsafe blocks.
2439

2540
## [0.7.0] - 2026-03-27
2641

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "devloop"
3-
version = "0.7.0"
3+
version = "0.8.0"
44
edition = "2024"
55

66
[dependencies]

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ Built-in reference docs are also available from the CLI:
100100
```bash
101101
devloop docs config
102102
devloop docs behavior
103+
devloop docs development
103104
devloop docs security
104105
```
105106

@@ -186,6 +187,9 @@ For the runtime behavior reference, see
186187
For the full configuration reference, see
187188
[`docs/configuration.md`](docs/configuration.md).
188189

190+
For local contributor workflow details, including the opt-in watch
191+
flake smoke test, see [`docs/development.md`](docs/development.md).
192+
189193
For the external-event trust model and push-versus-polling tradeoffs,
190194
see [`docs/security.md`](docs/security.md).
191195

docs/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
- [Behavior Reference](behavior.md)
44
- [Configuration Reference](configuration.md)
5+
- [Development Guide](development.md)
56
- [Security Notes](security.md)
67

78
This directory holds detailed reference material for `devloop`.

docs/behavior.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ watch-group patterns and watches only those files or directories.
6060
- The default backend uses native filesystem notifications. A polling
6161
backend can be selected in config as a fallback for environments
6262
where native events are unreliable.
63+
- Literal file targets are watched as narrowly as the backend allows.
64+
Use a trailing `/` in the config when you mean an explicit directory
65+
target that should be watched recursively.
6366

6467
If multiple watch groups map to the same workflow, their matched paths
6568
are merged for that workflow run.

docs/configuration.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,21 +56,25 @@ bind = "127.0.0.1:0"
5656
Watch groups map file patterns to workflows.
5757

5858
```toml
59-
[watch.rust]
60-
paths = ["src/**/*.rs", "Cargo.toml"]
61-
workflow = "rust"
59+
[watch.content]
60+
paths = ["content/", "templates/**/*.html"]
61+
workflow = "content"
6262
```
6363

6464
- Table name: the watch-group name.
6565
- `paths`: glob patterns evaluated relative to `root`.
6666
Use a trailing `/` for a literal directory target that should be
67-
watched recursively even before it exists.
67+
watched recursively, including when the directory may not exist yet
68+
at startup. Without the trailing slash, a literal path is treated as
69+
a file target.
6870
- `workflow`: workflow to run when a matching file changes. If omitted,
6971
the watch-group name is used as the workflow name.
7072

7173
`devloop` derives concrete watch targets from these patterns and asks
7274
the backend to watch only those literal files or directories instead of
73-
always watching the whole repository root recursively.
75+
always watching the whole repository root recursively. `native` remains
76+
the default backend; `poll` exists as a fallback for environments where
77+
filesystem notifications are unreliable.
7478

7579
## Processes
7680

docs/development.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Development Guide
2+
3+
This guide covers local development workflows for `devloop` itself.
4+
5+
## Quality gates
6+
7+
Run the standard checks before committing:
8+
9+
```bash
10+
cargo fmt
11+
cargo test
12+
cargo clippy --all-targets --all-features -- -D warnings
13+
./scripts/ci-smoke.sh
14+
```
15+
16+
`./scripts/ci-smoke.sh` is the fast runtime smoke test used in CI. It
17+
checks that `devloop run` can start, begin watching, react to one file
18+
change, and shut down cleanly.
19+
20+
## Opt-in watch flake smoke
21+
22+
The repeated-edit watch flake smoke test is intentionally opt-in. It is
23+
useful when changing watch registration, debounce logic, or event
24+
delivery, but it is slower and more environment-sensitive than the
25+
standard test suite.
26+
27+
Run it locally with:
28+
29+
```bash
30+
DEVLOOP_RUN_WATCH_FLAKE_SMOKE=1 cargo test --test watch_flake_smoke -- --nocapture
31+
```
32+
33+
Without that environment variable, the test exits early so normal
34+
`cargo test` and CI runs stay fast.
35+
36+
## Test policy
37+
38+
When a test must mutate process-global state such as environment
39+
variables:
40+
41+
- serialize access with a test-local lock
42+
- keep `unsafe` in a narrow helper
43+
- document the safety rationale at the helper
44+
45+
Do not scatter raw `unsafe { std::env::set_var(...) }` calls across test
46+
bodies.

src/main.rs

Lines changed: 69 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ enum Command {
6262
enum DocsTopic {
6363
Config,
6464
Behavior,
65+
Development,
6566
Security,
6667
}
6768

@@ -151,6 +152,7 @@ fn docs_text(topic: DocsTopic) -> &'static str {
151152
match topic {
152153
DocsTopic::Config => include_str!("../docs/configuration.md"),
153154
DocsTopic::Behavior => include_str!("../docs/behavior.md"),
155+
DocsTopic::Development => include_str!("../docs/development.md"),
154156
DocsTopic::Security => include_str!("../docs/security.md"),
155157
}
156158
}
@@ -345,32 +347,69 @@ mod tests {
345347
format_output_prefix, normalize_internal_log_label, normalize_source_label,
346348
};
347349
use clap::Parser;
348-
use std::sync::{Mutex, OnceLock};
350+
use std::ffi::OsString;
351+
use std::sync::{Mutex, MutexGuard, OnceLock};
349352

350353
fn rust_log_lock() -> &'static Mutex<()> {
351354
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
352355
LOCK.get_or_init(|| Mutex::new(()))
353356
}
354357

355-
#[test]
356-
fn default_rust_log_uses_info_when_unset() {
357-
let _guard = rust_log_lock().lock().expect("lock RUST_LOG test mutex");
358+
struct RustLogGuard {
359+
_lock: MutexGuard<'static, ()>,
360+
original: Option<OsString>,
361+
}
362+
363+
impl RustLogGuard {
364+
fn set(value: Option<&str>) -> Self {
365+
let lock = rust_log_lock().lock().expect("lock RUST_LOG test mutex");
366+
let original = std::env::var_os("RUST_LOG");
367+
match value {
368+
Some(value) => set_test_env_var("RUST_LOG", value),
369+
None => remove_test_env_var("RUST_LOG"),
370+
}
371+
Self {
372+
_lock: lock,
373+
original,
374+
}
375+
}
376+
}
377+
378+
impl Drop for RustLogGuard {
379+
fn drop(&mut self) {
380+
match &self.original {
381+
Some(value) => set_test_env_var("RUST_LOG", value),
382+
None => remove_test_env_var("RUST_LOG"),
383+
}
384+
}
385+
}
386+
387+
fn set_test_env_var(key: &str, value: impl AsRef<std::ffi::OsStr>) {
388+
// SAFETY: tests serialize all RUST_LOG mutation through `rust_log_lock`,
389+
// so no concurrent test can observe partially updated process-global state.
358390
unsafe {
359-
std::env::remove_var("RUST_LOG");
391+
std::env::set_var(key, value);
360392
}
393+
}
394+
395+
fn remove_test_env_var(key: &str) {
396+
// SAFETY: tests serialize all RUST_LOG mutation through `rust_log_lock`,
397+
// so removing the variable cannot race with other tests here.
398+
unsafe {
399+
std::env::remove_var(key);
400+
}
401+
}
402+
403+
#[test]
404+
fn default_rust_log_uses_info_when_unset() {
405+
let _guard = RustLogGuard::set(None);
361406
assert_eq!(default_rust_log(), "info");
362407
}
363408

364409
#[test]
365410
fn default_rust_log_respects_environment_override() {
366-
let _guard = rust_log_lock().lock().expect("lock RUST_LOG test mutex");
367-
unsafe {
368-
std::env::set_var("RUST_LOG", "debug,devloop=trace");
369-
}
411+
let _guard = RustLogGuard::set(Some("debug,devloop=trace"));
370412
assert_eq!(default_rust_log(), "debug,devloop=trace");
371-
unsafe {
372-
std::env::remove_var("RUST_LOG");
373-
}
374413
}
375414

376415
#[test]
@@ -418,6 +457,14 @@ mod tests {
418457
assert!(rendered.contains("startup_workflows"));
419458
}
420459

460+
#[test]
461+
fn docs_text_uses_embedded_development_reference() {
462+
let rendered = docs_text(DocsTopic::Development);
463+
464+
assert!(rendered.starts_with("# Development Guide"));
465+
assert!(rendered.contains("DEVLOOP_RUN_WATCH_FLAKE_SMOKE"));
466+
}
467+
421468
#[test]
422469
fn rendered_docs_drop_markdown_heading_markers() {
423470
let rendered = render_docs_text(DocsTopic::Config);
@@ -460,4 +507,14 @@ mod tests {
460507
_ => panic!("expected docs subcommand"),
461508
}
462509
}
510+
511+
#[test]
512+
fn cli_parses_development_docs_subcommand() {
513+
let cli = Cli::try_parse_from(["devloop", "docs", "development"]).expect("parse cli");
514+
515+
match cli.command {
516+
super::Command::Docs { topic } => assert!(matches!(topic, DocsTopic::Development)),
517+
_ => panic!("expected docs subcommand"),
518+
}
519+
}
463520
}

0 commit comments

Comments
 (0)