feat: implement undo/redo system with history tracking and tests

Co-authored-by: aider (deepseek/deepseek-coder) <aider@aider.chat>
This commit is contained in:
Loic Coenen
2026-05-01 20:10:45 +00:00
parent b64b0cd418
commit be3582bc13
4 changed files with 702 additions and 8 deletions

View File

@@ -1211,6 +1211,379 @@ void test_remark_existing_mark(void) {
printf("PASSED\n");
}
// Test 53: Undo single clip trigger
void test_undo_single_trigger(void) {
printf("Test 53: Undo single clip trigger... ");
Engine *engine = create_test_engine();
// Start with empty clip
int clip_idx = 10;
assert(engine->clips[clip_idx].state == CLIP_EMPTY);
// Trigger clip (empty -> recording)
engine_trigger_clip(engine, clip_idx);
engine_process_commands(engine);
assert(engine->clips[clip_idx].state == CLIP_RECORDING);
// Undo: should go back to empty
engine_undo_action(engine);
engine_process_commands(engine);
assert(engine->clips[clip_idx].state == CLIP_EMPTY);
printf("PASSED\n");
destroy_test_engine(engine);
}
// Test 54: Undo multiple clip triggers
void test_undo_multiple_triggers(void) {
printf("Test 54: Undo multiple clip triggers... ");
Engine *engine = create_test_engine();
int clip1 = 10, clip2 = 11, clip3 = 12;
// Trigger three clips
engine_trigger_clip(engine, clip1);
engine_process_commands(engine);
assert(engine->clips[clip1].state == CLIP_RECORDING);
engine_trigger_clip(engine, clip2);
engine_process_commands(engine);
assert(engine->clips[clip2].state == CLIP_RECORDING);
engine_trigger_clip(engine, clip3);
engine_process_commands(engine);
assert(engine->clips[clip3].state == CLIP_RECORDING);
// Undo last action: clip3 should go back to empty
engine_undo_action(engine);
engine_process_commands(engine);
assert(engine->clips[clip3].state == CLIP_EMPTY);
assert(engine->clips[clip2].state == CLIP_RECORDING);
assert(engine->clips[clip1].state == CLIP_RECORDING);
// Undo again: clip2 should go back to empty
engine_undo_action(engine);
engine_process_commands(engine);
assert(engine->clips[clip2].state == CLIP_EMPTY);
assert(engine->clips[clip1].state == CLIP_RECORDING);
printf("PASSED\n");
destroy_test_engine(engine);
}
// Test 55: Redo single undo
void test_redo_single_undo(void) {
printf("Test 55: Redo single undo... ");
Engine *engine = create_test_engine();
int clip_idx = 10;
// Trigger clip
engine_trigger_clip(engine, clip_idx);
engine_process_commands(engine);
assert(engine->clips[clip_idx].state == CLIP_RECORDING);
// Undo
engine_undo_action(engine);
engine_process_commands(engine);
assert(engine->clips[clip_idx].state == CLIP_EMPTY);
// Redo: should go back to recording
engine_redo_action(engine);
engine_process_commands(engine);
assert(engine->clips[clip_idx].state == CLIP_RECORDING);
printf("PASSED\n");
destroy_test_engine(engine);
}
// Test 56: Redo after multiple undos
void test_redo_multiple_undos(void) {
printf("Test 56: Redo after multiple undos... ");
Engine *engine = create_test_engine();
int clip1 = 10, clip2 = 11;
// Trigger two clips
engine_trigger_clip(engine, clip1);
engine_process_commands(engine);
engine_trigger_clip(engine, clip2);
engine_process_commands(engine);
assert(engine->clips[clip1].state == CLIP_RECORDING);
assert(engine->clips[clip2].state == CLIP_RECORDING);
// Undo twice
engine_undo_action(engine);
engine_process_commands(engine);
engine_undo_action(engine);
engine_process_commands(engine);
assert(engine->clips[clip1].state == CLIP_EMPTY);
assert(engine->clips[clip2].state == CLIP_EMPTY);
// Redo twice
engine_redo_action(engine);
engine_process_commands(engine);
assert(engine->clips[clip1].state == CLIP_RECORDING);
assert(engine->clips[clip2].state == CLIP_EMPTY);
engine_redo_action(engine);
engine_process_commands(engine);
assert(engine->clips[clip1].state == CLIP_RECORDING);
assert(engine->clips[clip2].state == CLIP_RECORDING);
printf("PASSED\n");
destroy_test_engine(engine);
}
// Test 57: Undo scene trigger
void test_undo_scene_trigger(void) {
printf("Test 57: Undo scene trigger... ");
Engine *engine = create_test_engine();
int scene_idx = 3;
// Trigger scene
engine_trigger_scene(engine, scene_idx);
engine_process_commands(engine);
// All clips in scene should be recording
for (int ch = 0; ch < MAX_CHANNELS; ch++) {
int clip_idx = CLIP_INDEX(scene_idx, ch);
assert(engine->clips[clip_idx].state == CLIP_RECORDING);
}
// Undo: all clips should go back to empty
engine_undo_action(engine);
engine_process_commands(engine);
for (int ch = 0; ch < MAX_CHANNELS; ch++) {
int clip_idx = CLIP_INDEX(scene_idx, ch);
assert(engine->clips[clip_idx].state == CLIP_EMPTY);
}
printf("PASSED\n");
destroy_test_engine(engine);
}
// Test 58: Undo clip state cycle (empty -> recording -> looping -> stopped)
void test_undo_clip_state_cycle(void) {
printf("Test 58: Undo clip state cycle... ");
Engine *engine = create_test_engine();
int clip_idx = 10;
// Cycle through all states
engine_trigger_clip(engine, clip_idx); // empty -> recording
engine_process_commands(engine);
assert(engine->clips[clip_idx].state == CLIP_RECORDING);
engine_trigger_clip(engine, clip_idx); // recording -> looping
engine_process_commands(engine);
assert(engine->clips[clip_idx].state == CLIP_LOOPING);
engine_trigger_clip(engine, clip_idx); // looping -> stopped
engine_process_commands(engine);
assert(engine->clips[clip_idx].state == CLIP_STOPPED);
// Undo three times to go back to empty
engine_undo_action(engine); // stopped -> looping
engine_process_commands(engine);
assert(engine->clips[clip_idx].state == CLIP_LOOPING);
engine_undo_action(engine); // looping -> recording
engine_process_commands(engine);
assert(engine->clips[clip_idx].state == CLIP_RECORDING);
engine_undo_action(engine); // recording -> empty
engine_process_commands(engine);
assert(engine->clips[clip_idx].state == CLIP_EMPTY);
printf("PASSED\n");
destroy_test_engine(engine);
}
// Test 59: Undo after new action clears redo history
void test_undo_clears_redo_on_new_action(void) {
printf("Test 59: Undo after new action clears redo history... ");
Engine *engine = create_test_engine();
int clip1 = 10, clip2 = 11;
// Trigger clip1
engine_trigger_clip(engine, clip1);
engine_process_commands(engine);
assert(engine->clips[clip1].state == CLIP_RECORDING);
// Undo
engine_undo_action(engine);
engine_process_commands(engine);
assert(engine->clips[clip1].state == CLIP_EMPTY);
// Redo should work
engine_redo_action(engine);
engine_process_commands(engine);
assert(engine->clips[clip1].state == CLIP_RECORDING);
// Undo again
engine_undo_action(engine);
engine_process_commands(engine);
assert(engine->clips[clip1].state == CLIP_EMPTY);
// Now do a new action (trigger clip2)
engine_trigger_clip(engine, clip2);
engine_process_commands(engine);
assert(engine->clips[clip2].state == CLIP_RECORDING);
// Redo should NOT work now (redo history cleared)
engine_redo_action(engine);
engine_process_commands(engine);
assert(engine->clips[clip1].state == CLIP_EMPTY); // Should still be empty
printf("PASSED\n");
destroy_test_engine(engine);
}
// Test 60: Undo reset clip
void test_undo_reset_clip(void) {
printf("Test 60: Undo reset clip... ");
Engine *engine = create_test_engine();
int clip_idx = 10;
// Set up a looping clip with data
engine->clips[clip_idx].state = CLIP_LOOPING;
engine->clips[clip_idx].buffer_size = 100;
engine->clips[clip_idx].write_position = 100;
engine->clips[clip_idx].read_position = 50;
// Reset the clip
engine_reset_clip(engine, clip_idx);
engine_process_commands(engine);
assert(engine->clips[clip_idx].state == CLIP_EMPTY);
assert(engine->clips[clip_idx].buffer_size == 0);
// Undo: should restore clip to previous state
engine_undo_action(engine);
engine_process_commands(engine);
assert(engine->clips[clip_idx].state == CLIP_LOOPING);
assert(engine->clips[clip_idx].buffer_size == 100);
assert(engine->clips[clip_idx].write_position == 100);
assert(engine->clips[clip_idx].read_position == 50);
printf("PASSED\n");
destroy_test_engine(engine);
}
// Test 61: Undo/redo with quantize mode change
void test_undo_quantize_change(void) {
printf("Test 61: Undo quantize mode change... ");
Engine *engine = create_test_engine();
assert(engine->quantize_mode == QUANTIZE_OFF);
// Change quantize mode
engine_set_quantize_mode(engine, QUANTIZE_BEAT);
engine_process_commands(engine);
assert(engine->quantize_mode == QUANTIZE_BEAT);
// Undo: should go back to OFF
engine_undo_action(engine);
engine_process_commands(engine);
assert(engine->quantize_mode == QUANTIZE_OFF);
// Redo: should go back to BEAT
engine_redo_action(engine);
engine_process_commands(engine);
assert(engine->quantize_mode == QUANTIZE_BEAT);
printf("PASSED\n");
destroy_test_engine(engine);
}
// Test 62: Undo/redo with threshold change
void test_undo_threshold_change(void) {
printf("Test 62: Undo threshold change... ");
Engine *engine = create_test_engine();
assert(engine->quantize_threshold == 0);
// Change threshold
engine_set_quantize_threshold(engine, 1000);
engine_process_commands(engine);
assert(engine->quantize_threshold == 1000);
// Undo: should go back to 0
engine_undo_action(engine);
engine_process_commands(engine);
assert(engine->quantize_threshold == 0);
// Redo: should go back to 1000
engine_redo_action(engine);
engine_process_commands(engine);
assert(engine->quantize_threshold == 1000);
printf("PASSED\n");
destroy_test_engine(engine);
}
// Test 63: Undo/redo with transport reset
void test_undo_transport_reset(void) {
printf("Test 63: Undo transport reset... ");
Engine *engine = create_test_engine();
// Set up transport state
engine->transport.rolling = true;
engine->transport.clock_count = 100;
engine->transport.beat_position = 2;
engine->transport.bar_position = 5;
engine->transport.sample_position = 10000;
// Reset transport
engine_reset_transport(engine);
engine_process_commands(engine);
assert(engine->transport.rolling == false);
assert(engine->transport.clock_count == 0);
// Undo: should restore transport state
engine_undo_action(engine);
engine_process_commands(engine);
assert(engine->transport.rolling == true);
assert(engine->transport.clock_count == 100);
assert(engine->transport.beat_position == 2);
assert(engine->transport.bar_position == 5);
assert(engine->transport.sample_position == 10000);
printf("PASSED\n");
destroy_test_engine(engine);
}
// Test 64: Undo/redo with paste operation
void test_undo_paste(void) {
printf("Test 64: Undo paste operation... ");
Engine *engine = create_test_engine();
int clip_idx = 27;
// Simulate paste: trigger clip three times to go empty -> recording -> looping -> stopped
engine_trigger_clip(engine, clip_idx);
engine_trigger_clip(engine, clip_idx);
engine_trigger_clip(engine, clip_idx);
engine_process_commands(engine);
assert(engine->clips[clip_idx].state == CLIP_STOPPED);
// Undo: should go back to empty
engine_undo_action(engine);
engine_process_commands(engine);
assert(engine->clips[clip_idx].state == CLIP_EMPTY);
// Redo: should go back to stopped
engine_redo_action(engine);
engine_process_commands(engine);
assert(engine->clips[clip_idx].state == CLIP_STOPPED);
printf("PASSED\n");
destroy_test_engine(engine);
}
int main(void) {
printf("Running TUI tests...\n\n");
@@ -1266,6 +1639,18 @@ int main(void) {
test_visual_yank_then_escape();
test_multiple_marks();
test_remark_existing_mark();
test_undo_single_trigger();
test_undo_multiple_triggers();
test_redo_single_undo();
test_redo_multiple_undos();
test_undo_scene_trigger();
test_undo_clip_state_cycle();
test_undo_clears_redo_on_new_action();
test_undo_reset_clip();
test_undo_quantize_change();
test_undo_threshold_change();
test_undo_transport_reset();
test_undo_paste();
printf("\nAll TUI tests passed!\n");
return 0;