@@ -1072,3 +1072,282 @@ fn test_add_reported_id_resolves_as_a_dependency_target() {
10721072 . success ( )
10731073 . stderr ( predicate:: str:: contains ( "Warning" ) . not ( ) ) ;
10741074}
1075+
1076+ // ---------------------------------------------------------------------
1077+ // Issue #53: `--before`/`--after` reject the qualified ID lash prints,
1078+ // and `--dry-run` does not resolve the position at all
1079+ // ---------------------------------------------------------------------
1080+
1081+ /// A two-task file, so there is something to position against.
1082+ fn project_with_two_tasks ( ) -> TestProject {
1083+ TestProject :: builder ( )
1084+ . with_index ( "test-project" , "Test Project" )
1085+ . with_file (
1086+ "tasks.md" ,
1087+ r#"# Tasks
1088+
1089+ @id: tasks
1090+
1091+ ## Tasks
1092+
1093+ - [ ] Alpha task
1094+ - [ ] Beta task
1095+ "# ,
1096+ )
1097+ . build ( )
1098+ }
1099+
1100+ /// The order top-level task titles appear in `tasks.md`.
1101+ fn task_titles ( project : & TestProject ) -> Vec < String > {
1102+ fs:: read_to_string ( project. file_path ( "tasks.md" ) )
1103+ . unwrap ( )
1104+ . lines ( )
1105+ . filter_map ( |line| line. trim_start ( ) . strip_prefix ( "- [ ] " ) . map ( str:: to_string) )
1106+ . collect ( )
1107+ }
1108+
1109+ #[ test]
1110+ fn test_add_before_accepts_the_qualified_id_that_show_prints ( ) {
1111+ // `lash show` reports `tasks#beta-task`; pasting that back into --before
1112+ // used to fail with "task not found" even though the task existed.
1113+ let project = project_with_two_tasks ( ) ;
1114+ index ( & project) ;
1115+
1116+ run_lash_command ( )
1117+ . arg ( "--root" )
1118+ . arg ( project. path ( ) )
1119+ . arg ( "add" )
1120+ . arg ( "Gamma" )
1121+ . arg ( "--file" )
1122+ . arg ( "tasks.md" )
1123+ . arg ( "--before" )
1124+ . arg ( "tasks#beta-task" )
1125+ . assert ( )
1126+ . success ( ) ;
1127+
1128+ assert_eq ! (
1129+ task_titles( & project) ,
1130+ vec![ "Alpha task" , "Gamma" , "Beta task" ] ,
1131+ "Gamma should sit between Alpha and Beta"
1132+ ) ;
1133+ }
1134+
1135+ #[ test]
1136+ fn test_add_after_accepts_the_qualified_id_that_show_prints ( ) {
1137+ let project = project_with_two_tasks ( ) ;
1138+ index ( & project) ;
1139+
1140+ run_lash_command ( )
1141+ . arg ( "--root" )
1142+ . arg ( project. path ( ) )
1143+ . arg ( "add" )
1144+ . arg ( "Gamma" )
1145+ . arg ( "--file" )
1146+ . arg ( "tasks.md" )
1147+ . arg ( "--after" )
1148+ . arg ( "tasks#alpha-task" )
1149+ . assert ( )
1150+ . success ( ) ;
1151+
1152+ assert_eq ! (
1153+ task_titles( & project) ,
1154+ vec![ "Alpha task" , "Gamma" , "Beta task" ]
1155+ ) ;
1156+ }
1157+
1158+ #[ test]
1159+ fn test_add_before_still_accepts_the_bare_slug ( ) {
1160+ // The form that already worked must keep working.
1161+ let project = project_with_two_tasks ( ) ;
1162+ index ( & project) ;
1163+
1164+ run_lash_command ( )
1165+ . arg ( "--root" )
1166+ . arg ( project. path ( ) )
1167+ . arg ( "add" )
1168+ . arg ( "Gamma" )
1169+ . arg ( "--file" )
1170+ . arg ( "tasks.md" )
1171+ . arg ( "--before" )
1172+ . arg ( "beta-task" )
1173+ . assert ( )
1174+ . success ( ) ;
1175+
1176+ assert_eq ! (
1177+ task_titles( & project) ,
1178+ vec![ "Alpha task" , "Gamma" , "Beta task" ]
1179+ ) ;
1180+ }
1181+
1182+ #[ test]
1183+ fn test_add_before_accepts_the_file_path_as_qualifier ( ) {
1184+ // `tasks.md#beta-task` is the other spelling a caller can have in hand,
1185+ // since `@depends-on` references are written against paths.
1186+ let project = project_with_two_tasks ( ) ;
1187+ index ( & project) ;
1188+
1189+ run_lash_command ( )
1190+ . arg ( "--root" )
1191+ . arg ( project. path ( ) )
1192+ . arg ( "add" )
1193+ . arg ( "Gamma" )
1194+ . arg ( "--file" )
1195+ . arg ( "tasks.md" )
1196+ . arg ( "--before" )
1197+ . arg ( "tasks.md#task:beta-task" )
1198+ . assert ( )
1199+ . success ( ) ;
1200+
1201+ assert_eq ! (
1202+ task_titles( & project) ,
1203+ vec![ "Alpha task" , "Gamma" , "Beta task" ]
1204+ ) ;
1205+ }
1206+
1207+ #[ test]
1208+ fn test_add_before_rejects_a_qualifier_naming_a_different_file ( ) {
1209+ // Accepting the qualifier must not mean ignoring it: a qualifier naming
1210+ // another file means the caller expected the task somewhere it is not,
1211+ // and inserting next to a same-named task here would be wrong.
1212+ let project = project_with_two_tasks ( ) ;
1213+ index ( & project) ;
1214+
1215+ run_lash_command ( )
1216+ . arg ( "--root" )
1217+ . arg ( project. path ( ) )
1218+ . arg ( "add" )
1219+ . arg ( "Gamma" )
1220+ . arg ( "--file" )
1221+ . arg ( "tasks.md" )
1222+ . arg ( "--before" )
1223+ . arg ( "index#beta-task" )
1224+ . assert ( )
1225+ . failure ( )
1226+ . stderr ( predicate:: str:: contains ( "names file 'index'" ) ) ;
1227+
1228+ assert_eq ! (
1229+ task_titles( & project) ,
1230+ vec![ "Alpha task" , "Beta task" ] ,
1231+ "nothing should have been written"
1232+ ) ;
1233+ }
1234+
1235+ #[ test]
1236+ fn test_add_dry_run_fails_on_a_position_that_does_not_exist ( ) {
1237+ // Dry run used to echo the requested position back and exit 0, so it
1238+ // passed for arguments the real add rejected.
1239+ let project = project_with_two_tasks ( ) ;
1240+ index ( & project) ;
1241+
1242+ run_lash_command ( )
1243+ . arg ( "--root" )
1244+ . arg ( project. path ( ) )
1245+ . arg ( "add" )
1246+ . arg ( "Epsilon" )
1247+ . arg ( "--file" )
1248+ . arg ( "tasks.md" )
1249+ . arg ( "--before" )
1250+ . arg ( "no-such-task-at-all" )
1251+ . arg ( "--dry-run" )
1252+ . assert ( )
1253+ . failure ( )
1254+ . stderr ( predicate:: str:: contains ( "not found" ) )
1255+ . stdout ( predicate:: str:: contains ( "Validation passed" ) . not ( ) ) ;
1256+ }
1257+
1258+ #[ test]
1259+ fn test_add_dry_run_error_names_the_ids_that_do_exist ( ) {
1260+ let project = project_with_two_tasks ( ) ;
1261+ index ( & project) ;
1262+
1263+ run_lash_command ( )
1264+ . arg ( "--root" )
1265+ . arg ( project. path ( ) )
1266+ . arg ( "add" )
1267+ . arg ( "Epsilon" )
1268+ . arg ( "--file" )
1269+ . arg ( "tasks.md" )
1270+ . arg ( "--before" )
1271+ . arg ( "no-such-task-at-all" )
1272+ . arg ( "--dry-run" )
1273+ . assert ( )
1274+ . failure ( )
1275+ . stderr ( predicate:: str:: contains ( "alpha-task" ) )
1276+ . stderr ( predicate:: str:: contains ( "beta-task" ) ) ;
1277+ }
1278+
1279+ #[ test]
1280+ fn test_add_dry_run_reports_the_resolved_insert_line ( ) {
1281+ // The point of dry run is to check placement, so it has to report the
1282+ // placement it resolved rather than the argument it was handed.
1283+ let project = project_with_two_tasks ( ) ;
1284+ index ( & project) ;
1285+
1286+ run_lash_command ( )
1287+ . arg ( "--root" )
1288+ . arg ( project. path ( ) )
1289+ . arg ( "add" )
1290+ . arg ( "Gamma" )
1291+ . arg ( "--file" )
1292+ . arg ( "tasks.md" )
1293+ . arg ( "--before" )
1294+ . arg ( "tasks#beta-task" )
1295+ . arg ( "--dry-run" )
1296+ . assert ( )
1297+ . success ( )
1298+ . stdout ( predicate:: str:: contains ( "Validation passed" ) )
1299+ // `- [ ] Beta task` is on line 8 of the fixture.
1300+ . stdout ( predicate:: str:: contains ( "Insert at: line 8" ) ) ;
1301+
1302+ assert_eq ! (
1303+ task_titles( & project) ,
1304+ vec![ "Alpha task" , "Beta task" ] ,
1305+ "dry run must not write"
1306+ ) ;
1307+ }
1308+
1309+ #[ test]
1310+ fn test_add_dry_run_still_fails_on_other_validation_errors ( ) {
1311+ // Position resolution is the new check, but dry run must keep catching
1312+ // everything it caught before it reached the file at all.
1313+ let project = project_with_two_tasks ( ) ;
1314+ index ( & project) ;
1315+
1316+ run_lash_command ( )
1317+ . arg ( "--root" )
1318+ . arg ( project. path ( ) )
1319+ . arg ( "add" )
1320+ . arg ( "Gamma" )
1321+ . arg ( "--file" )
1322+ . arg ( "tasks.md" )
1323+ . arg ( "--estimate" )
1324+ . arg ( "not-a-duration" )
1325+ . arg ( "--dry-run" )
1326+ . assert ( )
1327+ . failure ( )
1328+ . stderr ( predicate:: str:: contains ( "E_CREATE_INVALID_ESTIMATE" ) ) ;
1329+ }
1330+
1331+ #[ test]
1332+ fn test_add_dry_run_passes_for_a_plain_append ( ) {
1333+ let project = project_with_two_tasks ( ) ;
1334+ index ( & project) ;
1335+
1336+ run_lash_command ( )
1337+ . arg ( "--root" )
1338+ . arg ( project. path ( ) )
1339+ . arg ( "add" )
1340+ . arg ( "Gamma" )
1341+ . arg ( "--file" )
1342+ . arg ( "tasks.md" )
1343+ . arg ( "--dry-run" )
1344+ . assert ( )
1345+ . success ( )
1346+ . stdout ( predicate:: str:: contains ( "Validation passed" ) ) ;
1347+
1348+ assert_eq ! (
1349+ task_titles( & project) ,
1350+ vec![ "Alpha task" , "Beta task" ] ,
1351+ "dry run must not write"
1352+ ) ;
1353+ }
0 commit comments