@@ -1099,6 +1099,244 @@ fi
10991099assert_eq " test_phase_migrate_disables_compaction" " true" " $_HAS_COMPACT_DISABLED "
11001100assert_pass_if_clean " test_phase_migrate_disables_compaction"
11011101
1102+ # =============================================================================
1103+ # Test 19: test_cutover_snapshot_and_migrate_pipeline_end_to_end
1104+ #
1105+ # Integration test: verifies the full snapshot + migrate pipeline runs
1106+ # end-to-end on a populated fixture.
1107+ #
1108+ # Setup:
1109+ # - Temp git repo with initial commit
1110+ # - 3 .tickets/*.md files with different types, statuses, and one with a note
1111+ # containing special characters (dollar sign, ampersand, angle brackets)
1112+ # - One ticket has a dependency (deps frontmatter field) on another
1113+ # - CUTOVER_SNAPSHOT_FILE, CUTOVER_TICKETS_DIR, CUTOVER_TRACKER_DIR all
1114+ # pointed at fixture paths
1115+ #
1116+ # Run: bash cutover-tickets-migration.sh --repo-root=FIXTURE (all phases)
1117+ #
1118+ # Assertions:
1119+ # 1. Exit 0
1120+ # 2. Snapshot file exists and contains ticket_count=3
1121+ # 3. CREATE event exists in tracker for each of the 3 tickets (by old ticket ID)
1122+ # 4. Ticket with non-open status: STATUS event exists
1123+ # 5. Ticket with note: COMMENT event exists and body matches note content
1124+ # 6. Ticket with dep: LINK event exists (or CREATE data contains deps)
1125+ # 7. Idempotency: run again, exit 0, still exactly 1 CREATE event per ticket
1126+ # =============================================================================
1127+ _setup_fixture
1128+
1129+ # Ticket 1: type=epic, status=open (no STATUS event expected), has a dep on ticket 2
1130+ cat > " $_FIXTURE_DIR /.tickets/dso-e2e-t1.md" << 'TICKET_EOF '
1131+ ---
1132+ id: dso-e2e-t1
1133+ title: E2E Epic Ticket One
1134+ status: open
1135+ type: epic
1136+ priority: 1
1137+ deps: [dso-e2e-t2]
1138+ ---
1139+ # E2E Epic Ticket One
1140+
1141+ This is the epic ticket for the e2e integration test.
1142+ TICKET_EOF
1143+
1144+ # Ticket 2: type=story, status=in_progress (STATUS event expected)
1145+ cat > " $_FIXTURE_DIR /.tickets/dso-e2e-t2.md" << 'TICKET_EOF '
1146+ ---
1147+ id: dso-e2e-t2
1148+ title: E2E Story Ticket Two
1149+ status: in_progress
1150+ type: story
1151+ priority: 2
1152+ ---
1153+ # E2E Story Ticket Two
1154+
1155+ This story is in progress during the e2e test.
1156+ TICKET_EOF
1157+
1158+ # Ticket 3: type=task, status=open, has a note with special characters
1159+ cat > " $_FIXTURE_DIR /.tickets/dso-e2e-t3.md" << 'TICKET_EOF '
1160+ ---
1161+ id: dso-e2e-t3
1162+ title: E2E Task Ticket Three
1163+ status: open
1164+ type: task
1165+ priority: 3
1166+ ---
1167+ # E2E Task Ticket Three
1168+
1169+ Body content.
1170+
1171+ ## Notes
1172+
1173+ [2026-01-15T10:00:00Z] Fixed issue with $VAR & <template> processing.
1174+ TICKET_EOF
1175+
1176+ git -C " $_FIXTURE_DIR " add .tickets/
1177+ git -C " $_FIXTURE_DIR " commit -q -m " initial commit with 3 e2e tickets"
1178+
1179+ _E2E_SNAPSHOT_FILE=" $_FIXTURE_DIR /e2e-snapshot.json"
1180+ _E2E_TRACKER_DIR=" $_FIXTURE_DIR /.tickets-tracker"
1181+ _E2E_STATE_FILE=" $_FIXTURE_DIR /.cutover-state-e2e.json"
1182+ mkdir -p " $_E2E_TRACKER_DIR "
1183+ _E2E_RC=0
1184+
1185+ CUTOVER_LOG_DIR=" $_FIXTURE_LOG_DIR " \
1186+ CUTOVER_STATE_FILE=" $_E2E_STATE_FILE " \
1187+ CUTOVER_SNAPSHOT_FILE=" $_E2E_SNAPSHOT_FILE " \
1188+ CUTOVER_TICKETS_DIR=" $_FIXTURE_DIR /.tickets" \
1189+ CUTOVER_TRACKER_DIR=" $_E2E_TRACKER_DIR " \
1190+ bash " $CUTOVER_SCRIPT " --repo-root=" $_FIXTURE_DIR " 2>&1 > /dev/null || _E2E_RC=$?
1191+
1192+ _snapshot_fail
1193+
1194+ # Assertion 1: exit 0
1195+ assert_eq " test_cutover_snapshot_and_migrate_pipeline_end_to_end_exit_0" " 0" " $_E2E_RC "
1196+
1197+ # Assertion 2a: snapshot file exists
1198+ if [[ -f " $_E2E_SNAPSHOT_FILE " ]]; then
1199+ _E2E_SNAP_EXISTS=" true"
1200+ else
1201+ _E2E_SNAP_EXISTS=" false"
1202+ fi
1203+ assert_eq " test_cutover_snapshot_and_migrate_pipeline_end_to_end_snapshot_exists" " true" " $_E2E_SNAP_EXISTS "
1204+
1205+ # Assertion 2b: snapshot file contains ticket_count=3
1206+ _E2E_TICKET_COUNT=" not_found"
1207+ if [[ -f " $_E2E_SNAPSHOT_FILE " ]]; then
1208+ _E2E_TICKET_COUNT=$( python3 -c "
1209+ import json, sys
1210+ try:
1211+ with open('$_E2E_SNAPSHOT_FILE ') as fh:
1212+ data = json.load(fh)
1213+ print(data.get('ticket_count', 'missing'))
1214+ except Exception as e:
1215+ print('error:' + str(e))
1216+ " 2> /dev/null || echo " error" )
1217+ fi
1218+ assert_eq " test_cutover_snapshot_and_migrate_pipeline_end_to_end_ticket_count" " 3" " $_E2E_TICKET_COUNT "
1219+
1220+ # Assertion 3: CREATE event exists for each of the 3 tickets
1221+ for _e2e_id in dso-e2e-t1 dso-e2e-t2 dso-e2e-t3; do
1222+ _E2E_CREATE_FILE=$( find " $_E2E_TRACKER_DIR " -path " */${_e2e_id} /*" -name " *-CREATE.json" 2> /dev/null | head -1)
1223+ if [[ -n " $_E2E_CREATE_FILE " ]]; then
1224+ _E2E_HAS_CREATE=" true"
1225+ else
1226+ _E2E_HAS_CREATE=" false"
1227+ fi
1228+ assert_eq " test_cutover_snapshot_and_migrate_pipeline_end_to_end_create_${_e2e_id} " " true" " $_E2E_HAS_CREATE "
1229+ done
1230+
1231+ # Assertion 4: STATUS event exists for dso-e2e-t2 (status=in_progress, not open)
1232+ _E2E_STATUS_FILE=$( find " $_E2E_TRACKER_DIR " -path " */dso-e2e-t2/*" -name " *-STATUS.json" 2> /dev/null | head -1)
1233+ if [[ -n " $_E2E_STATUS_FILE " ]]; then
1234+ _E2E_HAS_STATUS=" true"
1235+ else
1236+ _E2E_HAS_STATUS=" false"
1237+ fi
1238+ assert_eq " test_cutover_snapshot_and_migrate_pipeline_end_to_end_status_event" " true" " $_E2E_HAS_STATUS "
1239+
1240+ # Assertion 5: COMMENT event exists for dso-e2e-t3 (has a note) and body contains the note text
1241+ _E2E_COMMENT_FILE=$( find " $_E2E_TRACKER_DIR " -path " */dso-e2e-t3/*" -name " *-COMMENT.json" 2> /dev/null | head -1)
1242+ if [[ -n " $_E2E_COMMENT_FILE " ]]; then
1243+ _E2E_HAS_COMMENT=" true"
1244+ else
1245+ _E2E_HAS_COMMENT=" false"
1246+ fi
1247+ assert_eq " test_cutover_snapshot_and_migrate_pipeline_end_to_end_comment_event" " true" " $_E2E_HAS_COMMENT "
1248+
1249+ _E2E_COMMENT_BODY_OK=" false"
1250+ if [[ -n " $_E2E_COMMENT_FILE " ]]; then
1251+ if python3 -c "
1252+ import json, sys
1253+ try:
1254+ with open('$_E2E_COMMENT_FILE ') as fh:
1255+ d = json.load(fh)
1256+ body = d.get('body', d.get('data', {}).get('body', ''))
1257+ if '\$ VAR' in body and '&' in body and '<template>' in body:
1258+ sys.exit(0)
1259+ except Exception:
1260+ pass
1261+ sys.exit(1)
1262+ " 2> /dev/null; then
1263+ _E2E_COMMENT_BODY_OK=" true"
1264+ fi
1265+ fi
1266+ assert_eq " test_cutover_snapshot_and_migrate_pipeline_end_to_end_comment_body" " true" " $_E2E_COMMENT_BODY_OK "
1267+
1268+ # Assertion 6: LINK event exists for dso-e2e-t1 (has deps on dso-e2e-t2)
1269+ # The migrate phase writes a LINK event with relation=depends_on when deps are present.
1270+ # If no separate LINK event is found, also accept the dep stored in the CREATE data.
1271+ _E2E_LINK_FILE=$( find " $_E2E_TRACKER_DIR " -path " */dso-e2e-t1/*" -name " *-LINK.json" 2> /dev/null | head -1)
1272+ _E2E_HAS_DEP=" false"
1273+ if [[ -n " $_E2E_LINK_FILE " ]]; then
1274+ # LINK event found: verify it references dso-e2e-t2
1275+ if python3 -c "
1276+ import json, sys
1277+ try:
1278+ with open('$_E2E_LINK_FILE ') as fh:
1279+ d = json.load(fh)
1280+ data = d.get('data', {})
1281+ target = data.get('target', data.get('target_id', ''))
1282+ relation = data.get('relation', data.get('link_type', ''))
1283+ if 'dso-e2e-t2' in target and 'depends' in relation.lower():
1284+ sys.exit(0)
1285+ except Exception:
1286+ pass
1287+ sys.exit(1)
1288+ " 2> /dev/null; then
1289+ _E2E_HAS_DEP=" true"
1290+ fi
1291+ fi
1292+ # Fallback: check CREATE event data.deps for the dependency
1293+ if [[ " $_E2E_HAS_DEP " == " false" ]]; then
1294+ _E2E_CREATE_T1=$( find " $_E2E_TRACKER_DIR " -path " */dso-e2e-t1/*" -name " *-CREATE.json" 2> /dev/null | head -1)
1295+ if [[ -n " $_E2E_CREATE_T1 " ]]; then
1296+ if python3 -c "
1297+ import json, sys
1298+ try:
1299+ with open('$_E2E_CREATE_T1 ') as fh:
1300+ d = json.load(fh)
1301+ deps = d.get('data', {}).get('deps', [])
1302+ if isinstance(deps, list) and 'dso-e2e-t2' in deps:
1303+ sys.exit(0)
1304+ # Also check top-level deps field
1305+ deps2 = d.get('deps', [])
1306+ if isinstance(deps2, list) and 'dso-e2e-t2' in deps2:
1307+ sys.exit(0)
1308+ except Exception:
1309+ pass
1310+ sys.exit(1)
1311+ " 2> /dev/null; then
1312+ _E2E_HAS_DEP=" true"
1313+ fi
1314+ fi
1315+ fi
1316+ assert_eq " test_cutover_snapshot_and_migrate_pipeline_end_to_end_link_event" " true" " $_E2E_HAS_DEP "
1317+
1318+ # Assertion 7: Idempotency — run again, exit 0, still exactly 1 CREATE event per ticket
1319+ rm -f " $_E2E_STATE_FILE "
1320+ _E2E_RC2=0
1321+ CUTOVER_LOG_DIR=" $_FIXTURE_LOG_DIR " \
1322+ CUTOVER_STATE_FILE=" $_E2E_STATE_FILE " \
1323+ CUTOVER_SNAPSHOT_FILE=" $_E2E_SNAPSHOT_FILE " \
1324+ CUTOVER_TICKETS_DIR=" $_FIXTURE_DIR /.tickets" \
1325+ CUTOVER_TRACKER_DIR=" $_E2E_TRACKER_DIR " \
1326+ bash " $CUTOVER_SCRIPT " --repo-root=" $_FIXTURE_DIR " 2>&1 > /dev/null || _E2E_RC2=$?
1327+
1328+ assert_eq " test_cutover_snapshot_and_migrate_pipeline_end_to_end_idempotent_exit_0" " 0" " $_E2E_RC2 "
1329+
1330+ for _e2e_id in dso-e2e-t1 dso-e2e-t2 dso-e2e-t3; do
1331+ _E2E_CREATE_COUNT=$( find " $_E2E_TRACKER_DIR " -path " */${_e2e_id} /*" -name " *-CREATE.json" 2> /dev/null | wc -l | tr -d ' ' )
1332+ assert_eq " test_cutover_snapshot_and_migrate_pipeline_end_to_end_idempotent_create_count_${_e2e_id} " " 1" " $_E2E_CREATE_COUNT "
1333+ done
1334+
1335+ assert_pass_if_clean " test_cutover_snapshot_and_migrate_pipeline_end_to_end"
1336+
1337+ rm -rf " $_FIXTURE_DIR "
1338+ unset _FIXTURE_DIR _FIXTURE_LOG_DIR _E2E_SNAPSHOT_FILE _E2E_TRACKER_DIR _E2E_STATE_FILE _E2E_COMMENT_FILE _E2E_LINK_FILE _E2E_CREATE_T1 _e2e_id
1339+
11021340# =============================================================================
11031341# =============================================================================
11041342# Test 5: test_cutover_rollback_committed_uses_revert
0 commit comments