feat(me): add lore me personal work dashboard command
Implement a personal work dashboard that shows everything relevant to the
configured GitLab user: open issues assigned to them, MRs they authored,
MRs they are reviewing, and a chronological activity feed.
Design decisions:
- Attention state computed from GitLab interaction data (comments, reviews)
with no local state tracking -- purely derived from existing synced data
- Username resolution: --user flag > config.gitlab.username > actionable error
- Project scoping: --project (fuzzy) | --all | default_project | all
- Section filtering: --issues, --mrs, --activity (combinable, default = all)
- Activity feed controlled by --since (default 30d); work item sections
always show all open items regardless of --since
Architecture (src/cli/commands/me/):
- types.rs: MeDashboard, MeSummary, AttentionState data types
- queries.rs: 4 SQL queries (open_issues, authored_mrs, reviewing_mrs,
activity) using existing issue_assignees, mr_reviewers, notes tables
- render_human.rs: colored terminal output with attention state indicators
- render_robot.rs: {ok, data, meta} JSON envelope with field selection
- mod.rs: orchestration (resolve_username, resolve_project_scope, run_me)
- me_tests.rs: comprehensive unit tests covering all query paths
Config additions:
- New optional gitlab.username field in config.json
- Tests for config with/without username
- Existing test configs updated with username: None
CLI wiring:
- MeArgs struct with section filter, since, project, all, user, fields flags
- Autocorrect support for me command flags
- LoreRenderer::try_get() for safe renderer access in me module
- Robot mode field selection presets (me_items, me_activity)
- handle_me() in main.rs command dispatch
Also fixes duplicate assertions in surgical sync tests (removed 6
duplicate assert! lines that were copy-paste artifacts).
Spec: docs/lore-me-spec.md
This commit is contained in:
@@ -16,6 +16,10 @@ pub struct GitLabConfig {
|
||||
/// Optional stored token (env var takes priority when both are set).
|
||||
#[serde(default)]
|
||||
pub token: Option<String>,
|
||||
|
||||
/// Optional GitLab username for `lore me` personal dashboard.
|
||||
#[serde(default)]
|
||||
pub username: Option<String>,
|
||||
}
|
||||
|
||||
impl GitLabConfig {
|
||||
@@ -570,6 +574,7 @@ mod tests {
|
||||
base_url: "https://gitlab.example.com".to_string(),
|
||||
token_env_var: "GITLAB_TOKEN".to_string(),
|
||||
token: None,
|
||||
username: None,
|
||||
},
|
||||
projects: vec![ProjectConfig {
|
||||
path: "group/project".to_string(),
|
||||
@@ -594,6 +599,7 @@ mod tests {
|
||||
base_url: "https://gitlab.example.com".to_string(),
|
||||
token_env_var: "GITLAB_TOKEN".to_string(),
|
||||
token: None,
|
||||
username: None,
|
||||
},
|
||||
projects: vec![ProjectConfig {
|
||||
path: "group/project".to_string(),
|
||||
@@ -615,6 +621,7 @@ mod tests {
|
||||
base_url: "https://gitlab.example.com".to_string(),
|
||||
token_env_var: "GITLAB_TOKEN".to_string(),
|
||||
token: None,
|
||||
username: None,
|
||||
},
|
||||
projects: vec![ProjectConfig {
|
||||
path: "group/project".to_string(),
|
||||
@@ -837,6 +844,7 @@ mod tests {
|
||||
base_url: "https://gitlab.example.com".to_string(),
|
||||
token_env_var: env_var.to_string(),
|
||||
token: token.map(ToString::to_string),
|
||||
username: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -907,4 +915,39 @@ mod tests {
|
||||
let cfg = gitlab_cfg_with_env(VAR, None);
|
||||
assert!(cfg.resolve_token().is_err());
|
||||
}
|
||||
|
||||
// ── gitlab.username ─────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn test_config_loads_with_username() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let path = dir.path().join("config.json");
|
||||
let config = r#"{
|
||||
"gitlab": {
|
||||
"baseUrl": "https://gitlab.example.com",
|
||||
"tokenEnvVar": "GITLAB_TOKEN",
|
||||
"username": "jdoe"
|
||||
},
|
||||
"projects": [{ "path": "group/project" }]
|
||||
}"#;
|
||||
fs::write(&path, config).unwrap();
|
||||
let cfg = Config::load_from_path(&path).unwrap();
|
||||
assert_eq!(cfg.gitlab.username.as_deref(), Some("jdoe"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_loads_without_username() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let path = dir.path().join("config.json");
|
||||
let config = r#"{
|
||||
"gitlab": {
|
||||
"baseUrl": "https://gitlab.example.com",
|
||||
"tokenEnvVar": "GITLAB_TOKEN"
|
||||
},
|
||||
"projects": [{ "path": "group/project" }]
|
||||
}"#;
|
||||
fs::write(&path, config).unwrap();
|
||||
let cfg = Config::load_from_path(&path).unwrap();
|
||||
assert_eq!(cfg.gitlab.username, None);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user