1use std::{cmp::Reverse, collections::BTreeMap};
2
3use anyhow::Result;
4use chrono::{TimeZone, Utc};
5use clap::Args;
6use serde_json::json;
7
8use crate::{
9 app::Cli,
10 commands::Command,
11 common::{
12 DIM,
13 GREEN,
14 ICONS,
15 colored,
16 day_to_timestamp,
17 parse_day,
18 resolve_tag_ids,
19 task6_note,
20 },
21 store::Task,
22 wire::{
23 task::{TaskProps, TaskStart, TaskStatus, TaskType},
24 wire_object::{EntityType, WireObject},
25 },
26};
27
28#[derive(Debug, Args)]
29#[command(about = "Create a new task")]
30pub struct NewArgs {
31 pub title: String,
33 #[arg(
34 long = "in",
35 short = 'i',
36 default_value = "inbox",
37 help = "Container: inbox, clear, project UUID/prefix, or area UUID/prefix"
38 )]
39 pub in_target: String,
40 #[arg(
41 long,
42 short = 'w',
43 help = "Schedule: anytime, someday, today, evening, or YYYY-MM-DD"
44 )]
45 pub when: Option<String>,
46 #[arg(
47 long = "before",
48 short = 'b',
49 help = "Insert before this sibling task UUID/prefix"
50 )]
51 pub before_id: Option<String>,
52 #[arg(
53 long = "after",
54 short = 'a',
55 help = "Insert after this sibling task UUID/prefix"
56 )]
57 pub after_id: Option<String>,
58 #[arg(long, short = 'n', default_value = "", help = "Task notes")]
59 pub notes: String,
60 #[arg(
61 long,
62 short = 't',
63 help = "Comma-separated tags (titles or UUID prefixes)"
64 )]
65 pub tags: Option<String>,
66 #[arg(long = "deadline", short = 'd', help = "Deadline date (YYYY-MM-DD)")]
67 pub deadline_date: Option<String>,
68}
69
70fn base_new_props(title: &str, now: f64) -> TaskProps {
71 TaskProps {
72 title: title.to_string(),
73 item_type: TaskType::Todo,
74 status: TaskStatus::Incomplete,
75 start_location: TaskStart::Inbox,
76 creation_date: Some(now),
77 modification_date: Some(now),
78 conflict_overrides: Some(json!({"_t": "oo", "sn": {}})),
79 ..Default::default()
80 }
81}
82
83fn task_bucket(task: &Task, store: &crate::store::ThingsStore) -> Vec<String> {
84 if task.is_heading() {
85 return vec![
86 "heading".to_string(),
87 task.project
88 .clone()
89 .map(|v| v.to_string())
90 .unwrap_or_default(),
91 ];
92 }
93 if task.is_project() {
94 return vec![
95 "project".to_string(),
96 task.area.clone().map(|v| v.to_string()).unwrap_or_default(),
97 ];
98 }
99 if let Some(project_uuid) = store.effective_project_uuid(task) {
100 return vec![
101 "task-project".to_string(),
102 project_uuid.to_string(),
103 task.action_group
104 .clone()
105 .map(|v| v.to_string())
106 .unwrap_or_default(),
107 ];
108 }
109 if let Some(area_uuid) = store.effective_area_uuid(task) {
110 return vec![
111 "task-area".to_string(),
112 area_uuid.to_string(),
113 i32::from(task.start).to_string(),
114 ];
115 }
116 vec!["task-root".to_string(), i32::from(task.start).to_string()]
117}
118
119fn props_bucket(props: &TaskProps) -> Vec<String> {
120 if let Some(project_uuid) = props.parent_project_ids.first() {
121 return vec![
122 "task-project".to_string(),
123 project_uuid.to_string(),
124 String::new(),
125 ];
126 }
127 if let Some(area_uuid) = props.area_ids.first() {
128 let st = i32::from(props.start_location);
129 return vec![
130 "task-area".to_string(),
131 area_uuid.to_string(),
132 st.to_string(),
133 ];
134 }
135 let st = i32::from(props.start_location);
136 vec!["task-root".to_string(), st.to_string()]
137}
138
139fn plan_ix_insert(ordered: &[Task], insert_at: usize) -> (i32, Vec<(String, i32, String)>) {
140 let prev_ix = if insert_at > 0 {
141 Some(ordered[insert_at - 1].index)
142 } else {
143 None
144 };
145 let next_ix = if insert_at < ordered.len() {
146 Some(ordered[insert_at].index)
147 } else {
148 None
149 };
150 let mut updates = Vec::new();
151
152 if prev_ix.is_none() && next_ix.is_none() {
153 return (0, updates);
154 }
155 if prev_ix.is_none() {
156 return (next_ix.unwrap_or(0) - 1, updates);
157 }
158 if next_ix.is_none() {
159 return (prev_ix.unwrap_or(0) + 1, updates);
160 }
161 if prev_ix.unwrap_or(0) + 1 < next_ix.unwrap_or(0) {
162 return ((prev_ix.unwrap_or(0) + next_ix.unwrap_or(0)) / 2, updates);
163 }
164
165 let stride = 1024;
166 let mut new_index = stride;
167 let mut idx = 1;
168 for i in 0..=ordered.len() {
169 let target_ix = idx * stride;
170 if i == insert_at {
171 new_index = target_ix;
172 idx += 1;
173 continue;
174 }
175 let source_idx = if i < insert_at { i } else { i - 1 };
176 if source_idx < ordered.len() {
177 let entry = &ordered[source_idx];
178 if entry.index != target_ix {
179 updates.push((entry.uuid.to_string(), target_ix, entry.entity.clone()));
180 }
181 idx += 1;
182 }
183 }
184 (new_index, updates)
185}
186
187#[derive(Debug, Clone)]
188struct NewPlan {
189 new_uuid: String,
190 changes: BTreeMap<String, WireObject>,
191 title: String,
192}
193
194fn build_new_plan(
195 args: &NewArgs,
196 store: &crate::store::ThingsStore,
197 now: f64,
198 today_ts: i64,
199 next_id: &mut dyn FnMut() -> String,
200) -> std::result::Result<NewPlan, String> {
201 let today = Utc
202 .timestamp_opt(today_ts, 0)
203 .single()
204 .unwrap_or_else(Utc::now)
205 .date_naive()
206 .and_hms_opt(0, 0, 0)
207 .map(|d| Utc.from_utc_datetime(&d))
208 .unwrap_or_else(Utc::now);
209 let title = args.title.trim();
210 if title.is_empty() {
211 return Err("Task title cannot be empty.".to_string());
212 }
213
214 let mut props = base_new_props(title, now);
215 if !args.notes.is_empty() {
216 props.notes = Some(task6_note(&args.notes));
217 }
218
219 let anchor_id = args.before_id.as_ref().or(args.after_id.as_ref());
220 let mut anchor: Option<Task> = None;
221 if let Some(anchor_id) = anchor_id {
222 let (task, err, _ambiguous) = store.resolve_task_identifier(anchor_id);
223 if task.is_none() {
224 return Err(err);
225 }
226 anchor = task;
227 }
228
229 let in_target = args.in_target.trim();
230 if !in_target.eq_ignore_ascii_case("inbox") {
231 let (project, _, _) = store.resolve_mark_identifier(in_target);
232 let (area, _, _) = store.resolve_area_identifier(in_target);
233 let project_uuid = project.as_ref().and_then(|p| {
234 if p.is_project() {
235 Some(p.uuid.clone())
236 } else {
237 None
238 }
239 });
240 let area_uuid = area.map(|a| a.uuid);
241
242 if project_uuid.is_some() && area_uuid.is_some() {
243 return Err(format!(
244 "Ambiguous --in target '{}' (matches project and area).",
245 in_target
246 ));
247 }
248
249 if project.is_some() && project_uuid.is_none() {
250 return Err("--in target must be inbox, a project ID, or an area ID.".to_string());
251 }
252
253 if let Some(project_uuid) = project_uuid {
254 props.parent_project_ids = vec![project_uuid.into()];
255 props.start_location = TaskStart::Anytime;
256 } else if let Some(area_uuid) = area_uuid {
257 props.area_ids = vec![area_uuid.into()];
258 props.start_location = TaskStart::Anytime;
259 } else {
260 return Err(format!("Container not found: {}", in_target));
261 }
262 }
263
264 if let Some(when_raw) = &args.when {
265 let when = when_raw.trim();
266 if when.eq_ignore_ascii_case("anytime") {
267 props.start_location = TaskStart::Anytime;
268 props.scheduled_date = None;
269 } else if when.eq_ignore_ascii_case("someday") {
270 props.start_location = TaskStart::Someday;
271 props.scheduled_date = None;
272 } else if when.eq_ignore_ascii_case("today") {
273 props.start_location = TaskStart::Anytime;
274 props.scheduled_date = Some(today_ts);
275 props.today_index_reference = Some(today_ts);
276 } else {
277 let parsed = match parse_day(Some(when), "--when") {
278 Ok(Some(day)) => day,
279 Ok(None) => {
280 return Err(
281 "--when requires anytime, someday, today, or YYYY-MM-DD".to_string()
282 );
283 }
284 Err(err) => return Err(err),
285 };
286 let day_ts = day_to_timestamp(parsed);
287 props.start_location = TaskStart::Someday;
288 props.scheduled_date = Some(day_ts);
289 props.today_index_reference = Some(day_ts);
290 }
291 }
292
293 if let Some(tags) = &args.tags {
294 let (tag_ids, tag_err) = resolve_tag_ids(store, tags);
295 if !tag_err.is_empty() {
296 return Err(tag_err);
297 }
298 props.tag_ids = tag_ids;
299 }
300
301 if let Some(deadline_date) = &args.deadline_date {
302 let parsed = match parse_day(Some(deadline_date), "--deadline") {
303 Ok(Some(day)) => day,
304 Ok(None) => return Err("--deadline requires YYYY-MM-DD".to_string()),
305 Err(err) => return Err(err),
306 };
307 props.deadline = Some(day_to_timestamp(parsed) as i64);
308 }
309
310 let anchor_is_today = anchor
311 .as_ref()
312 .map(|a| a.start == TaskStart::Anytime && (a.is_today(&today) || a.evening))
313 .unwrap_or(false);
314 let target_bucket = props_bucket(&props);
315
316 if let Some(anchor) = &anchor
317 && !anchor_is_today
318 && task_bucket(anchor, store) != target_bucket
319 {
320 return Err(
321 "Cannot place new task relative to an item in a different container/list.".to_string(),
322 );
323 }
324
325 let mut index_updates: Vec<(String, i32, String)> = Vec::new();
326 let mut siblings = store
327 .tasks_by_uuid
328 .values()
329 .filter(|t| {
330 !t.trashed
331 && t.status == TaskStatus::Incomplete
332 && task_bucket(t, store) == target_bucket
333 })
334 .cloned()
335 .collect::<Vec<_>>();
336 siblings.sort_by_key(|t| (t.index, t.uuid.clone()));
337
338 let mut structural_insert_at = 0usize;
339 if let Some(anchor) = &anchor
340 && task_bucket(anchor, store) == target_bucket
341 {
342 let anchor_pos = siblings.iter().position(|t| t.uuid == anchor.uuid);
343 let Some(anchor_pos) = anchor_pos else {
344 return Err("Anchor not found in target list.".to_string());
345 };
346 structural_insert_at = if args.before_id.is_some() {
347 anchor_pos
348 } else {
349 anchor_pos + 1
350 };
351 }
352
353 let (structural_ix, structural_updates) = plan_ix_insert(&siblings, structural_insert_at);
354 props.sort_index = structural_ix;
355 index_updates.extend(structural_updates);
356
357 let new_is_today = props.start_location == TaskStart::Anytime
358 && props.scheduled_date.map_or(false, |sr| sr <= today_ts);
359 if new_is_today && anchor_is_today {
360 let mut section_evening = if props.evening_bit != 0 { 1 } else { 0 };
361
362 if anchor_is_today && let Some(anchor) = &anchor {
363 section_evening = if anchor.evening { 1 } else { 0 };
364 props.evening_bit = section_evening;
365 }
366
367 let mut today_siblings = store
368 .tasks_by_uuid
369 .values()
370 .filter(|t| {
371 !t.trashed
372 && t.status == TaskStatus::Incomplete
373 && t.start == TaskStart::Anytime
374 && (t.is_today(&today) || t.evening)
375 && (if t.evening { 1 } else { 0 }) == section_evening
376 })
377 .cloned()
378 .collect::<Vec<_>>();
379 today_siblings.sort_by_key(|task| {
380 let tir = task.today_index_reference.unwrap_or(0);
381 (Reverse(tir), task.today_index, Reverse(task.index))
382 });
383
384 let mut today_insert_at = 0usize;
385 if anchor_is_today
386 && let Some(anchor) = &anchor
387 && (if anchor.evening { 1 } else { 0 }) == section_evening
388 && let Some(anchor_pos) = today_siblings.iter().position(|t| t.uuid == anchor.uuid)
389 {
390 today_insert_at = if args.before_id.is_some() {
391 anchor_pos
392 } else {
393 anchor_pos + 1
394 };
395 }
396
397 let prev_today = if today_insert_at > 0 {
398 today_siblings.get(today_insert_at - 1)
399 } else {
400 None
401 };
402 let next_today = today_siblings.get(today_insert_at);
403
404 if let Some(next_today) = next_today {
405 let next_tir = next_today.today_index_reference.unwrap_or(today_ts);
406 props.today_index_reference = Some(next_tir);
407 props.today_sort_index = next_today.today_index - 1;
408 } else if let Some(prev_today) = prev_today {
409 let prev_tir = prev_today.today_index_reference.unwrap_or(today_ts);
410 props.today_index_reference = Some(prev_tir);
411 props.today_sort_index = prev_today.today_index + 1;
412 } else {
413 props.today_index_reference = Some(today_ts);
414 props.today_sort_index = 0;
415 }
416 }
417
418 let new_uuid = next_id();
419
420 let mut changes = BTreeMap::new();
421 changes.insert(
422 new_uuid.clone(),
423 WireObject::create(EntityType::Task6, props.clone()),
424 );
425
426 for (task_uuid, task_index, task_entity) in index_updates {
427 use crate::wire::task::TaskPatch;
428 changes.insert(
429 task_uuid,
430 WireObject::update(
431 EntityType::from(task_entity),
432 TaskPatch {
433 sort_index: Some(task_index),
434 modification_date: Some(now),
435 ..Default::default()
436 },
437 ),
438 );
439 }
440
441 Ok(NewPlan {
442 new_uuid,
443 changes,
444 title: title.to_string(),
445 })
446}
447
448impl Command for NewArgs {
449 fn run_with_ctx(
450 &self,
451 cli: &Cli,
452 out: &mut dyn std::io::Write,
453 ctx: &mut dyn crate::cmd_ctx::CmdCtx,
454 ) -> Result<()> {
455 let store = cli.load_store()?;
456 let now = ctx.now_timestamp();
457 let today = ctx.today_timestamp();
458 let mut id_gen = || ctx.next_id();
459 let plan = match build_new_plan(self, &store, now, today, &mut id_gen) {
460 Ok(plan) => plan,
461 Err(err) => {
462 eprintln!("{err}");
463 return Ok(());
464 }
465 };
466
467 if let Err(e) = ctx.commit_changes(plan.changes, None) {
468 eprintln!("Failed to create task: {e}");
469 return Ok(());
470 }
471
472 writeln!(
473 out,
474 "{} {} {}",
475 colored(&format!("{} Created", ICONS.done), &[GREEN], cli.no_color),
476 plan.title,
477 colored(&plan.new_uuid, &[DIM], cli.no_color)
478 )?;
479 Ok(())
480 }
481}
482
483#[cfg(test)]
484mod tests {
485 use serde_json::json;
486
487 use super::*;
488 use crate::{
489 store::{ThingsStore, fold_items},
490 wire::{
491 area::AreaProps,
492 tags::TagProps,
493 task::{TaskProps, TaskStart, TaskStatus, TaskType},
494 },
495 };
496
497 const NOW: f64 = 1_700_000_000.0;
498 const NEW_UUID: &str = "MpkEei6ybkFS2n6SXvwfLf";
499 const INBOX_ANCHOR_UUID: &str = "A7h5eCi24RvAWKC3Hv3muf";
500 const INBOX_OTHER_UUID: &str = "KGvAPpMrzHAKMdgMiERP1V";
501 const PROJECT_UUID: &str = "JFdhhhp37fpryAKu8UXwzK";
502 const AREA_UUID: &str = "74rgJf6Qh9wYp2TcVk8mNB";
503 const TAG_A_UUID: &str = "By8mN2qRk5Wv7Xc9Dt3HpL";
504 const TAG_B_UUID: &str = "Cv9nP3sTk6Xw8Yd4Eu5JqM";
505 const TODAY: i64 = 1_700_000_000;
506
507 fn build_store(entries: Vec<(String, WireObject)>) -> ThingsStore {
508 let mut item = BTreeMap::new();
509 for (uuid, obj) in entries {
510 item.insert(uuid, obj);
511 }
512 ThingsStore::from_raw_state(&fold_items([item]))
513 }
514
515 fn task(
516 uuid: &str,
517 title: &str,
518 st: i32,
519 ix: i32,
520 sr: Option<i64>,
521 tir: Option<i64>,
522 ti: i32,
523 ) -> (String, WireObject) {
524 (
525 uuid.to_string(),
526 WireObject::create(
527 EntityType::Task6,
528 TaskProps {
529 title: title.to_string(),
530 item_type: TaskType::Todo,
531 status: TaskStatus::Incomplete,
532 start_location: TaskStart::from(st),
533 sort_index: ix,
534 scheduled_date: sr,
535 today_index_reference: tir,
536 today_sort_index: ti,
537 creation_date: Some(1.0),
538 modification_date: Some(1.0),
539 ..Default::default()
540 },
541 ),
542 )
543 }
544
545 fn project(uuid: &str, title: &str) -> (String, WireObject) {
546 (
547 uuid.to_string(),
548 WireObject::create(
549 EntityType::Task6,
550 TaskProps {
551 title: title.to_string(),
552 item_type: TaskType::Project,
553 status: TaskStatus::Incomplete,
554 start_location: TaskStart::Anytime,
555 sort_index: 0,
556 creation_date: Some(1.0),
557 modification_date: Some(1.0),
558 ..Default::default()
559 },
560 ),
561 )
562 }
563
564 fn area(uuid: &str, title: &str) -> (String, WireObject) {
565 (
566 uuid.to_string(),
567 WireObject::create(
568 EntityType::Area3,
569 AreaProps {
570 title: title.to_string(),
571 sort_index: 0,
572 ..Default::default()
573 },
574 ),
575 )
576 }
577
578 fn tag(uuid: &str, title: &str) -> (String, WireObject) {
579 (
580 uuid.to_string(),
581 WireObject::create(
582 EntityType::Tag4,
583 TagProps {
584 title: title.to_string(),
585 sort_index: 0,
586 ..Default::default()
587 },
588 ),
589 )
590 }
591
592 #[test]
593 fn new_payload_parity_cases() {
594 let mut id_gen = || NEW_UUID.to_string();
595
596 let bare = build_new_plan(
597 &NewArgs {
598 title: "Ship release".to_string(),
599 in_target: "inbox".to_string(),
600 when: None,
601 before_id: None,
602 after_id: None,
603 notes: String::new(),
604 tags: None,
605 deadline_date: None,
606 },
607 &build_store(vec![]),
608 NOW,
609 TODAY,
610 &mut id_gen,
611 )
612 .expect("bare");
613 let bare_json = serde_json::to_value(bare.changes).expect("to value");
614 assert_eq!(bare_json[NEW_UUID]["t"], json!(0));
615 assert_eq!(bare_json[NEW_UUID]["e"], json!("Task6"));
616 assert_eq!(bare_json[NEW_UUID]["p"]["tt"], json!("Ship release"));
617 assert_eq!(bare_json[NEW_UUID]["p"]["st"], json!(0));
618 assert_eq!(bare_json[NEW_UUID]["p"]["cd"], json!(NOW));
619 assert_eq!(bare_json[NEW_UUID]["p"]["md"], json!(NOW));
620
621 let when_today = build_new_plan(
622 &NewArgs {
623 title: "Task today".to_string(),
624 in_target: "inbox".to_string(),
625 when: Some("today".to_string()),
626 before_id: None,
627 after_id: None,
628 notes: String::new(),
629 tags: None,
630 deadline_date: None,
631 },
632 &build_store(vec![]),
633 NOW,
634 TODAY,
635 &mut id_gen,
636 )
637 .expect("today");
638 let p = &serde_json::to_value(when_today.changes).expect("to value")[NEW_UUID]["p"];
639 assert_eq!(p["st"], json!(1));
640 assert_eq!(p["sr"], json!(TODAY));
641 assert_eq!(p["tir"], json!(TODAY));
642
643 let full_store = build_store(vec![
644 project(PROJECT_UUID, "Roadmap"),
645 area(AREA_UUID, "Work"),
646 tag(TAG_A_UUID, "urgent"),
647 tag(TAG_B_UUID, "backend"),
648 ]);
649 let in_project = build_new_plan(
650 &NewArgs {
651 title: "Project task".to_string(),
652 in_target: PROJECT_UUID.to_string(),
653 when: None,
654 before_id: None,
655 after_id: None,
656 notes: "line one".to_string(),
657 tags: Some("urgent,backend".to_string()),
658 deadline_date: Some("2032-05-06".to_string()),
659 },
660 &full_store,
661 NOW,
662 TODAY,
663 &mut id_gen,
664 )
665 .expect("in project");
666 let p = &serde_json::to_value(in_project.changes).expect("to value")[NEW_UUID]["p"];
667 let deadline_ts = day_to_timestamp(
668 parse_day(Some("2032-05-06"), "--deadline")
669 .expect("parse")
670 .expect("day"),
671 );
672 assert_eq!(p["pr"], json!([PROJECT_UUID]));
673 assert_eq!(p["st"], json!(1));
674 assert_eq!(p["tg"], json!([TAG_A_UUID, TAG_B_UUID]));
675 assert_eq!(p["dd"], json!(deadline_ts));
676 }
677
678 #[test]
679 fn new_after_gap_and_rebalance() {
680 let mut id_gen = || NEW_UUID.to_string();
681 let gap_store = build_store(vec![
682 task(INBOX_ANCHOR_UUID, "Anchor", 0, 1024, None, None, 0),
683 task(INBOX_OTHER_UUID, "Other", 0, 2048, None, None, 0),
684 ]);
685 let gap = build_new_plan(
686 &NewArgs {
687 title: "Inserted".to_string(),
688 in_target: "inbox".to_string(),
689 when: None,
690 before_id: None,
691 after_id: Some(INBOX_ANCHOR_UUID.to_string()),
692 notes: String::new(),
693 tags: None,
694 deadline_date: None,
695 },
696 &gap_store,
697 NOW,
698 TODAY,
699 &mut id_gen,
700 )
701 .expect("gap");
702 assert_eq!(
703 serde_json::to_value(gap.changes).expect("to value")[NEW_UUID]["p"]["ix"],
704 json!(1536)
705 );
706
707 let rebalance_store = build_store(vec![
708 task(INBOX_ANCHOR_UUID, "Anchor", 0, 1024, None, None, 0),
709 task(INBOX_OTHER_UUID, "Other", 0, 1025, None, None, 0),
710 ]);
711 let rebalance = build_new_plan(
712 &NewArgs {
713 title: "Inserted".to_string(),
714 in_target: "inbox".to_string(),
715 when: None,
716 before_id: None,
717 after_id: Some(INBOX_ANCHOR_UUID.to_string()),
718 notes: String::new(),
719 tags: None,
720 deadline_date: None,
721 },
722 &rebalance_store,
723 NOW,
724 TODAY,
725 &mut id_gen,
726 )
727 .expect("rebalance");
728 let rb = serde_json::to_value(rebalance.changes).expect("to value");
729 assert_eq!(rb[NEW_UUID]["p"]["ix"], json!(2048));
730 assert_eq!(rb[INBOX_OTHER_UUID]["p"], json!({"ix":3072,"md":NOW}));
731 }
732
733 #[test]
734 fn new_rejections() {
735 let mut id_gen = || NEW_UUID.to_string();
736 let empty_title = build_new_plan(
737 &NewArgs {
738 title: " ".to_string(),
739 in_target: "inbox".to_string(),
740 when: None,
741 before_id: None,
742 after_id: None,
743 notes: String::new(),
744 tags: None,
745 deadline_date: None,
746 },
747 &build_store(vec![]),
748 NOW,
749 TODAY,
750 &mut id_gen,
751 )
752 .expect_err("empty title");
753 assert_eq!(empty_title, "Task title cannot be empty.");
754
755 let unknown_container = build_new_plan(
756 &NewArgs {
757 title: "Ship".to_string(),
758 in_target: "nope".to_string(),
759 when: None,
760 before_id: None,
761 after_id: None,
762 notes: String::new(),
763 tags: None,
764 deadline_date: None,
765 },
766 &build_store(vec![]),
767 NOW,
768 TODAY,
769 &mut id_gen,
770 )
771 .expect_err("unknown container");
772 assert_eq!(unknown_container, "Container not found: nope");
773 }
774}