handoff-mcp-server 0.26.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +31 -4
  2. package/bin/handoff-mcp.js +56 -13
  3. package/bin/resolve-binary.js +122 -0
  4. package/package.json +14 -9
  5. package/Cargo.lock +0 -686
  6. package/Cargo.toml +0 -30
  7. package/scripts/cargo-env.sh +0 -29
  8. package/scripts/handoff-memory-hook.py +0 -208
  9. package/scripts/install-local.sh +0 -109
  10. package/scripts/postinstall.js +0 -50
  11. package/scripts/sync-plugin-skills.sh +0 -35
  12. package/scripts/sync-plugin-version.sh +0 -85
  13. package/scripts/sync-workflow-inline.sh +0 -138
  14. package/src/cli.rs +0 -551
  15. package/src/context/injection.rs +0 -276
  16. package/src/context/mod.rs +0 -129
  17. package/src/lib.rs +0 -5
  18. package/src/main.rs +0 -157
  19. package/src/mcp/handlers/assignees.rs +0 -254
  20. package/src/mcp/handlers/auto_schedule.rs +0 -489
  21. package/src/mcp/handlers/bulk_update.rs +0 -155
  22. package/src/mcp/handlers/calendar.rs +0 -196
  23. package/src/mcp/handlers/capacity.rs +0 -318
  24. package/src/mcp/handlers/check_criterion.rs +0 -70
  25. package/src/mcp/handlers/config.rs +0 -402
  26. package/src/mcp/handlers/config_crud.rs +0 -183
  27. package/src/mcp/handlers/dashboard.rs +0 -214
  28. package/src/mcp/handlers/docs.rs +0 -2288
  29. package/src/mcp/handlers/docs_query.rs +0 -1335
  30. package/src/mcp/handlers/fork_session.rs +0 -91
  31. package/src/mcp/handlers/get_session.rs +0 -48
  32. package/src/mcp/handlers/get_task.rs +0 -53
  33. package/src/mcp/handlers/import_context.rs +0 -470
  34. package/src/mcp/handlers/init.rs +0 -28
  35. package/src/mcp/handlers/list_sessions.rs +0 -187
  36. package/src/mcp/handlers/list_tasks.rs +0 -308
  37. package/src/mcp/handlers/load_context.rs +0 -361
  38. package/src/mcp/handlers/log_time.rs +0 -67
  39. package/src/mcp/handlers/memory.rs +0 -961
  40. package/src/mcp/handlers/merge_sessions.rs +0 -103
  41. package/src/mcp/handlers/metrics.rs +0 -196
  42. package/src/mcp/handlers/milestones.rs +0 -102
  43. package/src/mcp/handlers/mod.rs +0 -140
  44. package/src/mcp/handlers/refer.rs +0 -307
  45. package/src/mcp/handlers/referrals.rs +0 -74
  46. package/src/mcp/handlers/save_context.rs +0 -354
  47. package/src/mcp/handlers/task_checklist.rs +0 -507
  48. package/src/mcp/handlers/timer.rs +0 -529
  49. package/src/mcp/handlers/update_session.rs +0 -197
  50. package/src/mcp/handlers/update_task.rs +0 -452
  51. package/src/mcp/mod.rs +0 -6
  52. package/src/mcp/protocol.rs +0 -41
  53. package/src/mcp/resources.rs +0 -57
  54. package/src/mcp/router.rs +0 -154
  55. package/src/mcp/tools.rs +0 -1522
  56. package/src/mcp/types.rs +0 -108
  57. package/src/setup.rs +0 -1212
  58. package/src/storage/config.rs +0 -578
  59. package/src/storage/docs/frontmatter.rs +0 -509
  60. package/src/storage/docs/mod.rs +0 -835
  61. package/src/storage/docs/model.rs +0 -708
  62. package/src/storage/docs/reassemble.rs +0 -167
  63. package/src/storage/docs/split.rs +0 -377
  64. package/src/storage/git.rs +0 -47
  65. package/src/storage/memory/injected.rs +0 -340
  66. package/src/storage/memory/mod.rs +0 -236
  67. package/src/storage/memory/model.rs +0 -127
  68. package/src/storage/mod.rs +0 -96
  69. package/src/storage/referrals.rs +0 -248
  70. package/src/storage/sessions.rs +0 -859
  71. package/src/storage/tasks.rs +0 -957
  72. package/templates/claude-md-section.md +0 -12
@@ -1,578 +0,0 @@
1
- use std::collections::HashMap;
2
- use std::path::Path;
3
-
4
- use anyhow::{Context, Result};
5
- use serde::de::{self, SeqAccess};
6
- use serde::{Deserialize, Deserializer, Serialize};
7
-
8
- /// Convert a weekday name to its number (0=Sun..6=Sat).
9
- pub fn weekday_to_num(s: &str) -> Option<u32> {
10
- match s.to_lowercase().as_str() {
11
- "sun" | "sunday" => Some(0),
12
- "mon" | "monday" => Some(1),
13
- "tue" | "tuesday" => Some(2),
14
- "wed" | "wednesday" => Some(3),
15
- "thu" | "thursday" => Some(4),
16
- "fri" | "friday" => Some(5),
17
- "sat" | "saturday" => Some(6),
18
- _ => None,
19
- }
20
- }
21
-
22
- fn deserialize_weekdays<'de, D>(deserializer: D) -> std::result::Result<Vec<u32>, D::Error>
23
- where
24
- D: Deserializer<'de>,
25
- {
26
- struct WeekdayVisitor;
27
-
28
- impl<'de> de::Visitor<'de> for WeekdayVisitor {
29
- type Value = Vec<u32>;
30
-
31
- fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
32
- f.write_str("an array of weekday numbers (0-6) or names (\"sun\"..\"sat\")")
33
- }
34
-
35
- fn visit_seq<A>(self, mut seq: A) -> std::result::Result<Vec<u32>, A::Error>
36
- where
37
- A: SeqAccess<'de>,
38
- {
39
- let mut vals = Vec::new();
40
- while let Some(elem) = seq.next_element::<toml::Value>()? {
41
- match &elem {
42
- toml::Value::Integer(n) => {
43
- let n = *n as u32;
44
- if n > 6 {
45
- return Err(de::Error::custom(format!(
46
- "weekday number {n} out of range 0-6"
47
- )));
48
- }
49
- vals.push(n);
50
- }
51
- toml::Value::String(s) => {
52
- let n = weekday_to_num(s).ok_or_else(|| {
53
- de::Error::custom(format!("unknown weekday name: \"{s}\""))
54
- })?;
55
- vals.push(n);
56
- }
57
- _ => {
58
- return Err(de::Error::custom(
59
- "closed_weekdays elements must be integers or strings",
60
- ));
61
- }
62
- }
63
- }
64
- Ok(vals)
65
- }
66
- }
67
-
68
- deserializer.deserialize_seq(WeekdayVisitor)
69
- }
70
-
71
- #[derive(Debug, Clone, Serialize, Deserialize)]
72
- pub struct Config {
73
- pub project: ProjectConfig,
74
- #[serde(default)]
75
- pub settings: SettingsConfig,
76
- #[serde(default)]
77
- pub dashboard: DashboardConfig,
78
- /// Project-level start timestamp (pre-start mode). RFC3339 string.
79
- #[serde(default, skip_serializing_if = "Option::is_none")]
80
- pub started_at: Option<String>,
81
- /// Scheduling mode: "manual" or "auto".
82
- #[serde(default, skip_serializing_if = "Option::is_none")]
83
- pub schedule_mode: Option<String>,
84
- /// Project-level label vocabulary.
85
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
86
- pub labels: Vec<String>,
87
- #[serde(default, skip_serializing_if = "CalendarConfig::is_empty")]
88
- pub calendar: CalendarConfig,
89
- /// Team members keyed by stable assignee key.
90
- #[serde(default, skip_serializing_if = "HashMap::is_empty")]
91
- pub assignees: HashMap<String, AssigneeConfig>,
92
- /// Milestones keyed by milestone name.
93
- #[serde(default, skip_serializing_if = "HashMap::is_empty")]
94
- pub milestones: HashMap<String, MilestoneConfig>,
95
- #[serde(default, skip_serializing_if = "GanttViewConfig::is_empty")]
96
- pub gantt_view: GanttViewConfig,
97
- #[serde(default, skip_serializing_if = "EffortBudgetConfig::is_empty")]
98
- pub effort_budget: EffortBudgetConfig,
99
- }
100
-
101
- #[derive(Debug, Clone, Serialize, Deserialize)]
102
- pub struct ProjectConfig {
103
- pub name: String,
104
- #[serde(default, skip_serializing_if = "Option::is_none")]
105
- pub description: Option<String>,
106
- }
107
-
108
- #[derive(Debug, Clone, Serialize, Deserialize)]
109
- pub struct SettingsConfig {
110
- #[serde(default = "default_history_limit")]
111
- pub history_limit: u32,
112
- #[serde(default = "default_done_task_limit")]
113
- pub done_task_limit: u32,
114
- #[serde(default = "default_auto_git_summary")]
115
- pub auto_git_summary: bool,
116
- /// Require `estimate_hours` when creating/updating leaf tasks. Default true.
117
- #[serde(default = "default_require_estimate_hours")]
118
- pub require_estimate_hours: bool,
119
- /// Multiplier applied to AI-entered `estimate_hours` to derive the
120
- /// adjusted (AI-effort) estimate at aggregation time. Default 0.2.
121
- #[serde(default = "default_ai_estimate_multiplier")]
122
- pub ai_estimate_multiplier: f64,
123
- #[serde(default)]
124
- pub context_files: Vec<String>,
125
- /// Master switch for the memory feature (save/query/cleanup). Default true.
126
- #[serde(default = "default_memory_enabled")]
127
- pub memory_enabled: bool,
128
- /// Jaccard threshold above which `memory_save` treats a save as a
129
- /// near-duplicate `conflict` for the AI to merge. Default 0.72.
130
- #[serde(default = "default_memory_dup_threshold")]
131
- pub memory_dup_threshold: f64,
132
- /// BM25 relevance floor for `memory_query`; scores below are not returned.
133
- /// Default 2.0.
134
- #[serde(default = "default_memory_query_min_score")]
135
- pub memory_query_min_score: f64,
136
- /// Relative threshold (0.0–1.0) for `memory_query`: after the absolute
137
- /// `min_score` floor, a candidate is dropped unless its score is at least
138
- /// `top_score × relative_threshold`. Prevents low-relevance "tail" matches
139
- /// from riding a strong top hit. 0.0 disables (keep everything above
140
- /// `min_score`). Default 0.3.
141
- #[serde(default = "default_memory_query_relative_threshold")]
142
- pub memory_query_relative_threshold: f64,
143
- /// Maximum number of memories `memory_query` returns per call. Default 5.
144
- #[serde(default = "default_memory_query_limit")]
145
- pub memory_query_limit: u32,
146
- /// Days after which an un(re)referenced memory is flagged `stale` by
147
- /// `memory_cleanup`. Default 60.
148
- #[serde(default = "default_memory_stale_days")]
149
- pub memory_stale_days: i64,
150
- /// Age (days) past which `memory_cleanup` garbage-collects a per-session
151
- /// `injected/` sidecar. Default 14.
152
- #[serde(default = "default_memory_injected_gc_days")]
153
- pub memory_injected_gc_days: i64,
154
- /// Timer provider mode: "auto" (authority-based), "vscode" (always delegate),
155
- /// "mcp" (always internal), "off" (disabled). Default "auto".
156
- #[serde(default = "default_timer_provider")]
157
- pub timer_provider: String,
158
- /// Heartbeat staleness threshold in seconds for authority.json. Default 30.
159
- #[serde(default = "default_timer_authority_ttl_secs")]
160
- pub timer_authority_ttl_secs: u64,
161
- /// Idle timeout in minutes for MCP fallback timer. Default 10.
162
- #[serde(default = "default_timer_idle_timeout_minutes")]
163
- pub timer_idle_timeout_minutes: u64,
164
- /// Allow multiple active sessions simultaneously. Default false (single-active).
165
- #[serde(default)]
166
- pub multi_session: bool,
167
- #[serde(default)]
168
- pub custom_fields: HashMap<String, toml::Value>,
169
- }
170
-
171
- #[derive(Debug, Clone, Serialize, Deserialize)]
172
- pub struct DashboardConfig {
173
- #[serde(default = "default_scan_dirs")]
174
- pub scan_dirs: Vec<String>,
175
- #[serde(default)]
176
- pub exclude_patterns: Vec<String>,
177
- /// Maximum directory depth for recursive scanning. Default 5 (mirrors VSCode side).
178
- #[serde(default = "default_max_depth")]
179
- pub max_depth: usize,
180
- }
181
-
182
- /// Project-wide working calendar. Mirrors VSCode `CalendarConfig`.
183
- #[derive(Debug, Clone, Default, Serialize, Deserialize)]
184
- pub struct CalendarConfig {
185
- #[serde(default, skip_serializing_if = "Option::is_none")]
186
- pub work_hours_per_day: Option<f64>,
187
- /// Weekday numbers (0=Sun..6=Sat) that are non-working.
188
- /// Accepts integers or weekday names ("sun", "sat", etc.) in TOML.
189
- #[serde(
190
- default,
191
- skip_serializing_if = "Vec::is_empty",
192
- deserialize_with = "deserialize_weekdays"
193
- )]
194
- pub closed_weekdays: Vec<u32>,
195
- /// Specific YYYY-MM-DD dates that are non-working (override weekdays).
196
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
197
- pub closed_dates: Vec<String>,
198
- /// Specific YYYY-MM-DD dates that are working even if normally closed.
199
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
200
- pub open_dates: Vec<String>,
201
- /// Per-weekday / per-date working-hour overrides. Key = weekday name or YYYY-MM-DD.
202
- #[serde(default, skip_serializing_if = "HashMap::is_empty")]
203
- pub day_hours: HashMap<String, f64>,
204
- #[serde(default, skip_serializing_if = "Option::is_none")]
205
- pub schedule_mode: Option<String>,
206
- #[serde(default, skip_serializing_if = "Option::is_none")]
207
- pub overwork_limit_percent: Option<f64>,
208
- #[serde(default, skip_serializing_if = "Option::is_none")]
209
- pub max_utilization: Option<f64>,
210
- }
211
-
212
- impl CalendarConfig {
213
- pub fn is_empty(&self) -> bool {
214
- self.work_hours_per_day.is_none()
215
- && self.closed_weekdays.is_empty()
216
- && self.closed_dates.is_empty()
217
- && self.open_dates.is_empty()
218
- && self.day_hours.is_empty()
219
- && self.schedule_mode.is_none()
220
- && self.overwork_limit_percent.is_none()
221
- && self.max_utilization.is_none()
222
- }
223
- }
224
-
225
- /// A single team member's configuration. Mirrors VSCode `AssigneeConfig`.
226
- #[derive(Debug, Clone, Default, Serialize, Deserialize)]
227
- pub struct AssigneeConfig {
228
- #[serde(default, skip_serializing_if = "Option::is_none")]
229
- pub display_name: Option<String>,
230
- #[serde(default, skip_serializing_if = "Option::is_none")]
231
- pub color: Option<String>,
232
- #[serde(default, skip_serializing_if = "Option::is_none")]
233
- pub work_hours_per_day: Option<f64>,
234
- #[serde(
235
- default,
236
- skip_serializing_if = "Vec::is_empty",
237
- deserialize_with = "deserialize_weekdays"
238
- )]
239
- pub closed_weekdays: Vec<u32>,
240
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
241
- pub closed_dates: Vec<String>,
242
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
243
- pub open_dates: Vec<String>,
244
- #[serde(default, skip_serializing_if = "HashMap::is_empty")]
245
- pub day_hours: HashMap<String, f64>,
246
- }
247
-
248
- /// A milestone definition. Mirrors VSCode `MilestoneConfig`.
249
- #[derive(Debug, Clone, Default, Serialize, Deserialize)]
250
- pub struct MilestoneConfig {
251
- #[serde(default, skip_serializing_if = "Option::is_none")]
252
- pub date: Option<String>,
253
- #[serde(default, skip_serializing_if = "Option::is_none")]
254
- pub color: Option<String>,
255
- #[serde(default, skip_serializing_if = "Option::is_none")]
256
- pub description: Option<String>,
257
- }
258
-
259
- /// Gantt view UI settings. Mirrors VSCode `GanttViewSettings`.
260
- #[derive(Debug, Clone, Default, Serialize, Deserialize)]
261
- pub struct GanttViewConfig {
262
- #[serde(default, skip_serializing_if = "Option::is_none")]
263
- pub sort: Option<String>,
264
- #[serde(default, skip_serializing_if = "Option::is_none")]
265
- pub zoom: Option<String>,
266
- #[serde(default, skip_serializing_if = "Option::is_none")]
267
- pub mode: Option<String>,
268
- #[serde(default, skip_serializing_if = "Option::is_none")]
269
- pub group_by_milestone: Option<bool>,
270
- #[serde(default, skip_serializing_if = "Option::is_none")]
271
- pub group_by_assignee: Option<bool>,
272
- #[serde(default, skip_serializing_if = "Option::is_none")]
273
- pub show_workload: Option<bool>,
274
- #[serde(default, skip_serializing_if = "Option::is_none")]
275
- pub filter_assignee: Option<String>,
276
- #[serde(default, skip_serializing_if = "Option::is_none")]
277
- pub workload_view: Option<String>,
278
- }
279
-
280
- impl GanttViewConfig {
281
- pub fn is_empty(&self) -> bool {
282
- self.sort.is_none()
283
- && self.zoom.is_none()
284
- && self.mode.is_none()
285
- && self.group_by_milestone.is_none()
286
- && self.group_by_assignee.is_none()
287
- && self.show_workload.is_none()
288
- && self.filter_assignee.is_none()
289
- && self.workload_view.is_none()
290
- }
291
- }
292
-
293
- /// Effort budget. Mirrors VSCode `budgetTotalHours`.
294
- #[derive(Debug, Clone, Default, Serialize, Deserialize)]
295
- pub struct EffortBudgetConfig {
296
- #[serde(default, skip_serializing_if = "Option::is_none")]
297
- pub total_hours: Option<f64>,
298
- }
299
-
300
- impl EffortBudgetConfig {
301
- pub fn is_empty(&self) -> bool {
302
- self.total_hours.is_none()
303
- }
304
- }
305
-
306
- fn default_history_limit() -> u32 {
307
- 20
308
- }
309
-
310
- fn default_done_task_limit() -> u32 {
311
- 10
312
- }
313
-
314
- fn default_auto_git_summary() -> bool {
315
- true
316
- }
317
-
318
- fn default_require_estimate_hours() -> bool {
319
- true
320
- }
321
-
322
- fn default_ai_estimate_multiplier() -> f64 {
323
- 0.2
324
- }
325
-
326
- fn default_memory_enabled() -> bool {
327
- true
328
- }
329
-
330
- fn default_memory_dup_threshold() -> f64 {
331
- 0.72
332
- }
333
-
334
- fn default_memory_query_min_score() -> f64 {
335
- 2.0
336
- }
337
-
338
- fn default_memory_query_relative_threshold() -> f64 {
339
- 0.3
340
- }
341
-
342
- fn default_memory_query_limit() -> u32 {
343
- 5
344
- }
345
-
346
- fn default_memory_stale_days() -> i64 {
347
- 60
348
- }
349
-
350
- fn default_memory_injected_gc_days() -> i64 {
351
- 14
352
- }
353
-
354
- fn default_timer_provider() -> String {
355
- "auto".to_string()
356
- }
357
-
358
- fn default_timer_authority_ttl_secs() -> u64 {
359
- 30
360
- }
361
-
362
- fn default_timer_idle_timeout_minutes() -> u64 {
363
- 10
364
- }
365
-
366
- fn default_scan_dirs() -> Vec<String> {
367
- vec!["~/pro/".to_string()]
368
- }
369
-
370
- fn default_max_depth() -> usize {
371
- 5
372
- }
373
-
374
- impl Default for SettingsConfig {
375
- fn default() -> Self {
376
- Self {
377
- history_limit: default_history_limit(),
378
- done_task_limit: default_done_task_limit(),
379
- auto_git_summary: default_auto_git_summary(),
380
- require_estimate_hours: default_require_estimate_hours(),
381
- ai_estimate_multiplier: default_ai_estimate_multiplier(),
382
- context_files: Vec::new(),
383
- memory_enabled: default_memory_enabled(),
384
- memory_dup_threshold: default_memory_dup_threshold(),
385
- memory_query_min_score: default_memory_query_min_score(),
386
- memory_query_relative_threshold: default_memory_query_relative_threshold(),
387
- memory_query_limit: default_memory_query_limit(),
388
- memory_stale_days: default_memory_stale_days(),
389
- memory_injected_gc_days: default_memory_injected_gc_days(),
390
- timer_provider: default_timer_provider(),
391
- timer_authority_ttl_secs: default_timer_authority_ttl_secs(),
392
- timer_idle_timeout_minutes: default_timer_idle_timeout_minutes(),
393
- multi_session: false,
394
- custom_fields: HashMap::new(),
395
- }
396
- }
397
- }
398
-
399
- impl Default for DashboardConfig {
400
- fn default() -> Self {
401
- Self {
402
- scan_dirs: default_scan_dirs(),
403
- exclude_patterns: Vec::new(),
404
- max_depth: default_max_depth(),
405
- }
406
- }
407
- }
408
-
409
- impl Config {
410
- pub fn new(name: &str, description: &str) -> Self {
411
- let settings = SettingsConfig {
412
- multi_session: true,
413
- ..SettingsConfig::default()
414
- };
415
- Self {
416
- project: ProjectConfig {
417
- name: name.to_string(),
418
- description: if description.is_empty() {
419
- None
420
- } else {
421
- Some(description.to_string())
422
- },
423
- },
424
- settings,
425
- dashboard: DashboardConfig::default(),
426
- started_at: None,
427
- schedule_mode: None,
428
- labels: Vec::new(),
429
- calendar: CalendarConfig::default(),
430
- assignees: HashMap::new(),
431
- milestones: HashMap::new(),
432
- gantt_view: GanttViewConfig::default(),
433
- effort_budget: EffortBudgetConfig::default(),
434
- }
435
- }
436
- }
437
-
438
- pub fn read_config(path: &Path) -> Result<Config> {
439
- let content = std::fs::read_to_string(path)
440
- .with_context(|| format!("Failed to read config: {}", path.display()))?;
441
- let config: Config = toml::from_str(&content)
442
- .with_context(|| format!("Failed to parse config: {}", path.display()))?;
443
- Ok(config)
444
- }
445
-
446
- pub fn write_config(path: &Path, config: &Config) -> Result<()> {
447
- let content = toml::to_string_pretty(config).context("Failed to serialize config")?;
448
- crate::storage::atomic_write(path, content.as_bytes())
449
- .with_context(|| format!("Failed to write config: {}", path.display()))?;
450
- Ok(())
451
- }
452
-
453
- #[cfg(test)]
454
- mod tests {
455
- use super::*;
456
-
457
- fn parse_config(toml_str: &str) -> Config {
458
- toml::from_str(toml_str).unwrap()
459
- }
460
-
461
- #[test]
462
- fn closed_weekdays_string_names() {
463
- let cfg = parse_config(
464
- r#"
465
- [project]
466
- name = "test"
467
- [calendar]
468
- closed_weekdays = ["sun", "sat"]
469
- "#,
470
- );
471
- assert_eq!(cfg.calendar.closed_weekdays, vec![0, 6]);
472
- }
473
-
474
- #[test]
475
- fn closed_weekdays_integer_values() {
476
- let cfg = parse_config(
477
- r#"
478
- [project]
479
- name = "test"
480
- [calendar]
481
- closed_weekdays = [0, 6]
482
- "#,
483
- );
484
- assert_eq!(cfg.calendar.closed_weekdays, vec![0, 6]);
485
- }
486
-
487
- #[test]
488
- fn closed_weekdays_mixed() {
489
- let cfg = parse_config(
490
- r#"
491
- [project]
492
- name = "test"
493
- [calendar]
494
- closed_weekdays = ["sun", 6]
495
- "#,
496
- );
497
- assert_eq!(cfg.calendar.closed_weekdays, vec![0, 6]);
498
- }
499
-
500
- #[test]
501
- fn closed_weekdays_empty() {
502
- let cfg = parse_config(
503
- r#"
504
- [project]
505
- name = "test"
506
- "#,
507
- );
508
- assert!(cfg.calendar.closed_weekdays.is_empty());
509
- }
510
-
511
- #[test]
512
- fn closed_weekdays_full_names() {
513
- let cfg = parse_config(
514
- r#"
515
- [project]
516
- name = "test"
517
- [calendar]
518
- closed_weekdays = ["sunday", "saturday"]
519
- "#,
520
- );
521
- assert_eq!(cfg.calendar.closed_weekdays, vec![0, 6]);
522
- }
523
-
524
- #[test]
525
- fn assignee_closed_weekdays_strings() {
526
- let cfg = parse_config(
527
- r#"
528
- [project]
529
- name = "test"
530
- [assignees.alice]
531
- closed_weekdays = ["mon", "fri"]
532
- "#,
533
- );
534
- let alice = cfg.assignees.get("alice").unwrap();
535
- assert_eq!(alice.closed_weekdays, vec![1, 5]);
536
- }
537
-
538
- #[test]
539
- fn closed_weekdays_invalid_name() {
540
- let result = toml::from_str::<Config>(
541
- r#"
542
- [project]
543
- name = "test"
544
- [calendar]
545
- closed_weekdays = ["funday"]
546
- "#,
547
- );
548
- assert!(result.is_err());
549
- }
550
-
551
- #[test]
552
- fn closed_weekdays_out_of_range() {
553
- let result = toml::from_str::<Config>(
554
- r#"
555
- [project]
556
- name = "test"
557
- [calendar]
558
- closed_weekdays = [7]
559
- "#,
560
- );
561
- assert!(result.is_err());
562
- }
563
-
564
- #[test]
565
- fn round_trip_preserves_integer_format() {
566
- let cfg = parse_config(
567
- r#"
568
- [project]
569
- name = "test"
570
- [calendar]
571
- closed_weekdays = ["sun", "sat"]
572
- "#,
573
- );
574
- let serialized = toml::to_string_pretty(&cfg).unwrap();
575
- let re_parsed = parse_config(&serialized);
576
- assert_eq!(re_parsed.calendar.closed_weekdays, vec![0, 6]);
577
- }
578
- }