Skip to main content

User Test Sessions & Practice Library Commands

user_test_sessions

CommandTS wrapperSignature
get_user_test_sessionsgetUserTestSession(id, userId)Promise<UserTestSession | null>
list_user_test_sessionslistUserTestSessions(userId)Promise<UserTestSession[]>
create_user_test_sessionscreateUserTestSession(userId, { test_id, test_type, attempt_number? })Promise<string>
update_user_test_sessionsupdateUserTestSession(id, userId, input)Promise<void>
delete_user_test_sessionsdeleteUserTestSession(id, userId)Promise<void>

Partial update / COALESCE semantics

UpdateUserTestSession supports partial updates — any field left undefined/unset means "no change", implemented via SQL COALESCE(?, column) in the repository:

UPDATE user_test_sessions SET
status = COALESCE(?, status),
progress_percent = COALESCE(?, progress_percent),
score_band = COALESCE(?, score_band),
answers = COALESCE(?, answers),
feedback_data = COALESCE(?, feedback_data),
completed_at = COALESCE(?, completed_at),
last_active_at = COALESCE(?, last_active_at)
WHERE id = ? AND user_id = ?

TS input shape:

{
status?: string;
progress_percent?: number;
score_band?: number | null;
answers?: string | null;
feedback_data?: string | null;
completed_at?: string | null;
last_active_at?: string | null;
}

Passing score_band: null explicitly clears it (SQLite COALESCE(NULL, column) still evaluates the bound parameter — callers should confirm the exact null-vs-omitted behavior in src/core/src/repositories/user_test_sessions.rs before relying on "clear" semantics for nullable fields; omitting the key entirely from the JS object is the safest way to mean "no change").

Example: recording writing feedback (see Writing Module & AI Grading):

await updateUserTestSession(sessionId, userId, {
score_band: overallBand,
feedback_data: JSON.stringify(feedbackData),
});

list_practice_tests

export async function listPracticeTests(
userId: string,
module: string | undefined, // "reading" | "writing" | "listening" | undefined for "All"
page: number,
pageSize: number
): Promise<PaginatedResponse<PracticeTestCardDto>> {
return invoke<PaginatedResponse<PracticeTestCardDto>>("list_practice_tests", {
userId, module, page, pageSize,
});
}

PracticeTestCardDto:

export interface PracticeTestCardDto {
id: string;
title: string;
module: string;
difficulty: string;
duration: string;
status: string; // session status, or "not_started"
progress_percent: number;
score_band: number | null;
last_active_at: string | null;
session_id: string | null;
created_at: string;
}

PaginatedResponse<T> (src/ui/types/pagination.ts):

export interface PaginationMeta {
page: number;
page_size: number;
total_pages: number;
total_items: number;
has_next: boolean;
has_prev: boolean;
}

export interface PaginatedResponse<T> {
data: T[];
pagination: PaginationMeta;
}

When module is omitted, the backend merges all three test types by created_at DESC before paginating — see Practice Test Library for why this happens in Rust rather than SQL.