Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 18 additions & 17 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@
- **Clarity over cleverness.** Be concise, but favour explicit over terse or
obscure idioms. Prefer code that's easy to follow.
- **Use functions and composition.** Avoid repetition by extracting reusable
logic. Prefer generators or comprehensions, and declarative code to imperative
repetition when readable.
logic. Prefer generators, comprehensions, and declarative code to
imperative repetition when readable.
- **Small, meaningful functions.** Functions must be small, clear in purpose,
single responsibility, and obey command/query segregation.
- **Clear commit messages.** Commit messages should be descriptive, explaining
what was changed and why.
- **Name things precisely.** Use clear, descriptive variable and function names.
For booleans, prefer names with `is`, `has`, or `should`.
For booleans, prefer names with `is`, `has`, `should`.
- **Structure logically.** Each file should encapsulate a coherent module. Group
related code (e.g., models + utilities + fixtures) close together.
- **Group by feature, not layer.** Colocate views, logic, fixtures, and helpers
Expand All @@ -25,12 +25,13 @@
("-ize" / "-yse" / "-our") spelling and grammar, with the exception of
references to external APIs.
- **Illustrate with clear examples.** Function documentation must include clear
examples demonstrating the usage and outcome of the function. Test documentation
should omit examples where the example serves only to reiterate the test logic.
- **Keep file size managable.** No single code file may be longer than 400 lines.
Long switch statements or dispatch tables should be broken up by feature and
constituents colocated with targets. Large blocks of test data should be moved
to external data files.
examples demonstrating the usage and outcome of the function. Test
documentation should omit examples where the example serves only to reiterate
the test logic.
- **Keep file size manageable.** No single code file may be longer than 400
lines. Long switch statements or dispatch tables should be broken up by
feature and constituents colocated with targets. Large blocks of test data
should be moved to external data files.
Comment on lines +32 to +34
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Rephrase long-switch guidance for readability
Add a comma and a clarifying phrase to avoid the slightly awkward “feature and constituents colocated with targets”.

-  feature and constituents colocated with targets. Large blocks of test data
+  feature, with their constituents colocated alongside their targets. Large
+  blocks of test data
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
lines. Long switch statements or dispatch tables should be broken up by
feature and constituents colocated with targets. Large blocks of test data
should be moved to external data files.
lines. Long switch statements or dispatch tables should be broken up by
feature, with their constituents colocated alongside their targets. Large
blocks of test data should be moved to external data files.
🤖 Prompt for AI Agents
In AGENTS.md around lines 32 to 34, rephrase the guidance on long switch
statements for better readability by adding a comma after "feature" and
including a clarifying phrase to make the meaning of "constituents colocated
with targets" clearer and less awkward. Adjust the sentence structure to improve
flow and comprehension.


## Documentation Maintenance

Expand All @@ -42,8 +43,8 @@
relevant file(s) in the `docs/` directory to reflect the latest state.
**Ensure the documentation remains accurate and current.**
- Documentation must use en-GB-oxendict ("-ize" / "-yse" / "-our") spelling
and grammar. (EXCEPTION: the naming of the "LICENSE" file, which
is to be left unchanged for community consistency.)
and grammar. (EXCEPTION: the naming of the "LICENSE" file, which is to be
left unchanged for community consistency.)

## Change Quality & Committing

Expand Down Expand Up @@ -153,19 +154,19 @@ project:
specified in `Cargo.toml` must use SemVer-compatible caret requirements
(e.g., `some-crate = "1.2.3"`). This is Cargo's default and allows for safe,
non-breaking updates to minor and patch versions while preventing breaking
changes from new major versions. This approach is critical for ensuring
build stability and reproducibility.
changes from new major versions. This approach is critical for ensuring build
stability and reproducibility.
- **Prohibit unstable version specifiers.** The use of wildcard (`*`) or
open-ended inequality (`>=`) version requirements is strictly forbidden
as they introduce unacceptable risk and unpredictability. Tilde requirements
open-ended inequality (`>=`) version requirements are strictly forbidden, as
they introduce unacceptable risk and unpredictability. Tilde requirements
(`~`) should only be used where a dependency must be locked to patch-level
updates for a specific, documented reason.

### Error Handling

- **Prefer semantic error enums**. Derive `std::error::Error` (via the
`thiserror` crate) for any condition the caller might inspect, retry, or
map to an HTTP status.
`thiserror` crate) for any condition the caller might inspect, retry, or map
to an HTTP status.
- **Use an *opaque* error only at the app boundary**. Use `eyre::Report` for
human-readable logs; these should not be exposed in public APIs.
- **Never export the opaque type from a library**. Convert to domain enums at
Expand Down
25 changes: 25 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

60 changes: 56 additions & 4 deletions crates/comenqd/src/daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ use crate::config::Config;
use anyhow::Result;
use comenq_lib::CommentRequest;
use octocrab::Octocrab;
use std::fs;
use std::fs as stdfs;
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
use tokio::fs;
use tokio::io::AsyncReadExt;
use tokio::net::{UnixListener, UnixStream};
use yaque::{Receiver, Sender, channel};
Expand All @@ -23,16 +24,23 @@ fn build_octocrab(token: &str) -> Result<Octocrab> {
}

fn prepare_listener(path: &Path) -> Result<UnixListener> {
if fs::metadata(path).is_ok() {
fs::remove_file(path)?;
if stdfs::metadata(path).is_ok() {
stdfs::remove_file(path)?;
}
let listener = UnixListener::bind(path)?;
fs::set_permissions(path, fs::Permissions::from_mode(0o660))?;
stdfs::set_permissions(path, stdfs::Permissions::from_mode(0o660))?;
Ok(listener)
}

async fn ensure_queue_dir(path: &Path) -> Result<()> {
fs::create_dir_all(path).await?;
Ok(())
}

/// Start the daemon with the provided configuration.
pub async fn run(config: Config) -> Result<()> {
ensure_queue_dir(&config.queue_path).await?;
tracing::info!(queue = %config.queue_path.display(), "Queue directory prepared");
let octocrab = Arc::new(build_octocrab(&config.github_token)?);
let (tx, rx) = channel(&config.queue_path)?;
let cfg = Arc::new(config);
Expand Down Expand Up @@ -96,3 +104,47 @@ async fn run_worker(config: Arc<Config>, mut rx: Receiver, octocrab: Arc<Octocra
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
use tokio::time::{Duration, Instant, sleep};

#[tokio::test]
async fn ensure_queue_dir_creates_directory() {
let dir = tempdir().expect("Failed to create temporary directory");
let path = dir.path().join("queue");
ensure_queue_dir(&path)
.await
.expect("Failed to ensure queue directory");
assert!(path.is_dir());
}

#[tokio::test]
async fn run_creates_queue_directory() {
let dir = tempdir().expect("Failed to create temporary directory");
let cfg = Config {
github_token: "t".into(),
socket_path: dir.path().join("sock"),
queue_path: dir.path().join("q"),
cooldown_period_seconds: 1,
};

assert!(!cfg.queue_path.exists());

let handle = tokio::spawn(run(cfg.clone()));

let start = Instant::now();
let timeout = Duration::from_secs(2);
loop {
if cfg.queue_path.is_dir() || start.elapsed() > timeout {
break;
}
sleep(Duration::from_millis(10)).await;
}

handle.abort();
assert!(cfg.queue_path.is_dir(), "queue directory not created");
}
}
76 changes: 76 additions & 0 deletions crates/comenqd/src/logging.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
//! Logging utilities for the daemon.
//!
//! Initializes structured logging using `tracing` and
//! `tracing-subscriber`, reading filter settings from the `RUST_LOG`
//! environment variable.

use tracing_subscriber::fmt::MakeWriter;
use tracing_subscriber::{EnvFilter, fmt};

/// Initialize the global tracing subscriber.
pub fn init() {
init_with_writer(fmt::writer::BoxMakeWriter::new(std::io::stdout));
}

/// Initialize logging with a custom writer.
pub fn init_with_writer<W>(writer: W)
where
W: for<'a> MakeWriter<'a> + Send + Sync + 'static,
{
fmt()
.with_env_filter(EnvFilter::from_default_env())
.with_writer(writer)
.json()
.init();
}

#[cfg(test)]
mod tests {
use super::*;
use std::sync::{Arc, Mutex};
use tracing::info;

#[derive(Clone)]
struct BufMakeWriter {
buf: Arc<Mutex<Vec<u8>>>,
}

impl<'a> MakeWriter<'a> for BufMakeWriter {
type Writer = BufWriter;

fn make_writer(&'a self) -> Self::Writer {
BufWriter {
buf: self.buf.clone(),
}
}
}

struct BufWriter {
buf: Arc<Mutex<Vec<u8>>>,
}

impl std::io::Write for BufWriter {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.buf
.lock()
.expect("Failed to lock log buffer")
.extend_from_slice(buf);
Ok(buf.len())
}

fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}

#[test]
fn init_logging() {
let buf = Arc::new(Mutex::new(Vec::new()));
std::env::set_var("RUST_LOG", "info");
init_with_writer(BufMakeWriter { buf: buf.clone() });
info!("captured");
let output = String::from_utf8(buf.lock().expect("Failed to lock log buffer").clone())
.expect("Captured output is not valid UTF-8");
assert!(output.contains("captured"));
}
}
4 changes: 3 additions & 1 deletion crates/comenqd/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@

use tracing::info;

mod logging;

mod config;
mod daemon;
use config::Config;
use daemon::run;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt::init();
logging::init();
let cfg = Config::load()?;
info!(socket = ?cfg.socket_path, queue = ?cfg.queue_path, "Comenqd daemon started");
run(cfg).await
Expand Down
5 changes: 4 additions & 1 deletion docs/comenq-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -1013,7 +1013,10 @@ async fn run_worker(config: Arc<Config>, mut rx: Receiver<CommentRequest>, octoc
The repository initialises the workspace with `comenq-lib` at the root and two
binary crates under `crates/`. `CommentRequest` resides in the library and
derives both `Serialize` and `Deserialize`. The daemon now spawns a Unix
listener and queue worker as described above.
listener and queue worker as described above. Structured logging is initialised
using `tracing_subscriber` with JSON output controlled by the `RUST_LOG`
environment variable. The queue directory is created asynchronously on start if
it does not already exist before `yaque` opens it.

## Works cited

Expand Down
6 changes: 3 additions & 3 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,13 @@
(`/etc/comenqd/config.toml`) for parameters like `github_token`,
`socket_path`, and `queue_path`.

- [ ] Set up structured logging using the `tracing` and `tracing-subscriber`
- [x] Set up structured logging using the `tracing` and `tracing-subscriber`
crates.

- [ ] Initialize the `yaque` persistent queue at the path specified in the
- [x] Initialize the `yaque` persistent queue at the path specified in the
configuration.

- [ ] Structure the daemon's `main` function to spawn the two primary,
- [x] Structure the daemon's `main` function to spawn the two primary,
long-running `tokio` tasks: the UDS listener and the queue worker.

## Milestone 4: `comenqd` Daemon — UDS Listener Task
Expand Down