Make simulation block submission idempotent
This commit is contained in:
@@ -111,6 +111,16 @@ public sealed class ExamSimulationTests
|
||||
Assert.False(ExamSimulationStore.TryDecodeHistoryCursor("not-a-cursor", out _));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(ExamSimulationStates.BetweenBlocks, true)]
|
||||
[InlineData(ExamSimulationStates.Completed, true)]
|
||||
[InlineData(ExamSimulationStates.BlockActive, false)]
|
||||
[InlineData(ExamSimulationStates.Abandoned, false)]
|
||||
public void RepeatedBlockSubmissionIsIdempotentOnlyAfterSubmission(string state, bool expected)
|
||||
{
|
||||
Assert.Equal(expected, ExamSimulationStore.IsAlreadySubmittedState(state));
|
||||
}
|
||||
|
||||
private static List<ExamSimulationItem> BuildItems(int count, bool wasSeen = false) =>
|
||||
Enumerable.Range(0, count).Select(index => new ExamSimulationItem
|
||||
{
|
||||
|
||||
@@ -354,6 +354,8 @@ public class ExamSimulationStore
|
||||
|
||||
public async Task<ExamSimulation> SubmitBlockAsync(ExamSimulation simulation, bool expired = false)
|
||||
{
|
||||
if (IsAlreadySubmittedState(simulation.State))
|
||||
return simulation;
|
||||
if (simulation.State != ExamSimulationStates.BlockActive)
|
||||
throw new InvalidOperationException("There is no active block to submit.");
|
||||
if (!expired && simulation.BlockDeadlineUtc is not null)
|
||||
@@ -370,6 +372,9 @@ public class ExamSimulationStore
|
||||
return simulation;
|
||||
}
|
||||
|
||||
public static bool IsAlreadySubmittedState(string state) =>
|
||||
state is ExamSimulationStates.BetweenBlocks or ExamSimulationStates.Completed;
|
||||
|
||||
public async Task AbandonAsync(ExamSimulation simulation)
|
||||
{
|
||||
simulation.State = ExamSimulationStates.Abandoned;
|
||||
|
||||
@@ -154,6 +154,8 @@ func _on_request_completed(result: int, response_code: int, _headers: PackedStri
|
||||
if response_code == 401:
|
||||
AuthState.handle_unauthorized(request_access_token)
|
||||
if result != HTTPRequest.RESULT_SUCCESS or response_code < 200 or response_code >= 300:
|
||||
if _is_stale_submit_failure(action):
|
||||
return
|
||||
var server_message := body.get_string_from_utf8().strip_edges()
|
||||
var error_parsed: Variant = JSON.parse_string(server_message)
|
||||
if typeof(error_parsed) == TYPE_DICTIONARY:
|
||||
@@ -205,6 +207,10 @@ func _on_request_completed(result: int, response_code: int, _headers: PackedStri
|
||||
state_changed.emit()
|
||||
|
||||
|
||||
func _is_stale_submit_failure(action: String) -> bool:
|
||||
return action == "submit" and str(simulation.get("state", "")) in ["between_blocks", "completed"]
|
||||
|
||||
|
||||
func _process_request_queue() -> void:
|
||||
if is_loading or request_queue.is_empty():
|
||||
return
|
||||
|
||||
@@ -1190,6 +1190,9 @@ func _on_simulation_state_changed() -> void:
|
||||
func _on_simulation_request_failed(message: String) -> void:
|
||||
if BlockSessionService.session_kind != "simulation":
|
||||
return
|
||||
if str(ExamSimulationService.simulation.get("state", "")) in ["between_blocks", "completed"]:
|
||||
simulation_submit_pending = false
|
||||
return
|
||||
simulation_submit_pending = false
|
||||
if ExamSimulationService.last_failed_action != "submit":
|
||||
# A per-question autosave failing shouldn't interrupt a timed exam with a
|
||||
|
||||
@@ -15,6 +15,11 @@ func _run() -> void:
|
||||
service.simulation = {"id": "active", "state": "between_blocks", "currentBlock": 2, "totalBlocks": 12, "createdUtc": "2026-07-16T22:04:00Z"}
|
||||
service.history = [{"id": "completed", "state": "completed", "currentBlock": 11, "totalBlocks": 12, "createdUtc": "2026-07-15T20:35:00Z", "predictedScore": 236, "rangeLow": 221, "rangeHigh": 248, "passProbability": 0.82}]
|
||||
service.history_next_cursor = "next-page"
|
||||
_assert(service._is_stale_submit_failure("submit"), "a submit failure after reaching between-block state should be ignored")
|
||||
service.simulation["state"] = "completed"
|
||||
_assert(service._is_stale_submit_failure("submit"), "a submit failure after completion should be ignored")
|
||||
_assert(not service._is_stale_submit_failure("save"), "non-submit failures should still be surfaced")
|
||||
service.simulation["state"] = "between_blocks"
|
||||
var host := Control.new()
|
||||
host.size = Vector2(1400, 800)
|
||||
root.add_child(host)
|
||||
|
||||
Reference in New Issue
Block a user