fix(question-types): realign registry ids with the question_type table
The registry hardcoded numeric ids that drifted from the database `question_type` table: MATCHING and HOTSPOT were inserted mid-list (ids 9 and 11) but never seeded, shifting ORDERING/LINK/WORD_BANK/ STAR/VIDEO_RECORDING one or two ids past their real rows. Every id emitter inherited the wrong values — the exercise editor, the AI question generator, and the grading/scoring gate — so e.g. an ORDERING question was stored as id 10 (LINK in the DB), came back as a manual type, and never auto-graded. - Move MATCHING/HOTSPOT to the end (ids 14/15) and flag them `disabled`, restoring the append-only id convention and realigning ORDERING=9, LINK=10, WORD_BANK=11, STAR=12, VIDEO_RECORDING=13 with the DB. - Add a registry test asserting every enabled type's id matches the canonical `question_type` rows and disabled types stay past the live range, so ids can't silently drift again. - Constrain the agent tool schema to enabled, DB-backed ids so the model can't emit a disabled id with no row. - Drop disabled types from the teacher prompt's type list and guidance. - Centralize the dashboard question-type picker on the `disabled` flag. This also fixes video-recording detection, which compared against the stale id 15 instead of the real 13.
This commit is contained in:
@@ -74,29 +74,11 @@ export const QUESTION_TYPE_REGISTRY: readonly QuestionTypeMetadata[] = [
|
||||
supportsPartialCredit: false,
|
||||
manualGradingRequired: true
|
||||
},
|
||||
{
|
||||
key: QUESTION_TYPE_KEY.MATCHING,
|
||||
typename: 'MATCHING',
|
||||
label: 'Matching',
|
||||
id: 9,
|
||||
autoGradable: true,
|
||||
supportsPartialCredit: true,
|
||||
manualGradingRequired: false
|
||||
},
|
||||
{
|
||||
key: QUESTION_TYPE_KEY.ORDERING,
|
||||
typename: 'ORDERING',
|
||||
label: 'Ordering',
|
||||
id: 10,
|
||||
autoGradable: true,
|
||||
supportsPartialCredit: true,
|
||||
manualGradingRequired: false
|
||||
},
|
||||
{
|
||||
key: QUESTION_TYPE_KEY.HOTSPOT,
|
||||
typename: 'HOTSPOT',
|
||||
label: 'Hotspot',
|
||||
id: 11,
|
||||
id: 9,
|
||||
autoGradable: true,
|
||||
supportsPartialCredit: true,
|
||||
manualGradingRequired: false
|
||||
@@ -105,7 +87,7 @@ export const QUESTION_TYPE_REGISTRY: readonly QuestionTypeMetadata[] = [
|
||||
key: QUESTION_TYPE_KEY.LINK,
|
||||
typename: 'LINK',
|
||||
label: 'Links',
|
||||
id: 12,
|
||||
id: 10,
|
||||
autoGradable: false,
|
||||
supportsPartialCredit: false,
|
||||
manualGradingRequired: true
|
||||
@@ -114,7 +96,7 @@ export const QUESTION_TYPE_REGISTRY: readonly QuestionTypeMetadata[] = [
|
||||
key: QUESTION_TYPE_KEY.WORD_BANK,
|
||||
typename: 'WORD_BANK',
|
||||
label: 'Word bank',
|
||||
id: 13,
|
||||
id: 11,
|
||||
autoGradable: true,
|
||||
supportsPartialCredit: true,
|
||||
manualGradingRequired: false
|
||||
@@ -123,7 +105,7 @@ export const QUESTION_TYPE_REGISTRY: readonly QuestionTypeMetadata[] = [
|
||||
key: QUESTION_TYPE_KEY.STAR,
|
||||
typename: 'STAR',
|
||||
label: 'Star rating',
|
||||
id: 14,
|
||||
id: 12,
|
||||
autoGradable: true,
|
||||
supportsPartialCredit: false,
|
||||
manualGradingRequired: false
|
||||
@@ -132,13 +114,40 @@ export const QUESTION_TYPE_REGISTRY: readonly QuestionTypeMetadata[] = [
|
||||
key: QUESTION_TYPE_KEY.VIDEO_RECORDING,
|
||||
typename: 'VIDEO_RECORDING',
|
||||
label: 'Video recording',
|
||||
id: 15,
|
||||
id: 13,
|
||||
autoGradable: false,
|
||||
supportsPartialCredit: false,
|
||||
manualGradingRequired: true
|
||||
},
|
||||
// Built but not offered in the product. Kept at the END of the id sequence
|
||||
// (append-only) so they never shift the ids of types that exist in the DB.
|
||||
// They have no `question_type` row, so `disabled` keeps them out of pickers,
|
||||
// the AI tool enum, and validation.
|
||||
{
|
||||
key: QUESTION_TYPE_KEY.MATCHING,
|
||||
typename: 'MATCHING',
|
||||
label: 'Matching',
|
||||
id: 14,
|
||||
autoGradable: true,
|
||||
supportsPartialCredit: true,
|
||||
manualGradingRequired: false,
|
||||
disabled: true
|
||||
},
|
||||
{
|
||||
key: QUESTION_TYPE_KEY.HOTSPOT,
|
||||
typename: 'HOTSPOT',
|
||||
label: 'Hotspot',
|
||||
id: 15,
|
||||
autoGradable: true,
|
||||
supportsPartialCredit: true,
|
||||
manualGradingRequired: false,
|
||||
disabled: true
|
||||
}
|
||||
] as const;
|
||||
|
||||
/** Types actually offered in the product (excludes `disabled` ones with no DB row). */
|
||||
export const ENABLED_QUESTION_TYPE_REGISTRY = QUESTION_TYPE_REGISTRY.filter((type) => !type.disabled);
|
||||
|
||||
export const QUESTION_TYPE_BY_KEY = Object.freeze(
|
||||
QUESTION_TYPE_REGISTRY.reduce(
|
||||
(acc, type) => {
|
||||
|
||||
@@ -8,4 +8,6 @@ export interface QuestionTypeMetadata {
|
||||
autoGradable: boolean;
|
||||
supportsPartialCredit: boolean;
|
||||
manualGradingRequired: boolean;
|
||||
/** Built in code but not offered in the product (no `question_type` row). */
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
@@ -82,26 +82,10 @@ const EXPECTED_REGISTRY = [
|
||||
supportsPartialCredit: false,
|
||||
manualGradingRequired: true
|
||||
},
|
||||
{
|
||||
key: QUESTION_TYPE_KEY.MATCHING,
|
||||
typename: 'MATCHING',
|
||||
id: 9,
|
||||
autoGradable: true,
|
||||
supportsPartialCredit: true,
|
||||
manualGradingRequired: false
|
||||
},
|
||||
{
|
||||
key: QUESTION_TYPE_KEY.ORDERING,
|
||||
typename: 'ORDERING',
|
||||
id: 10,
|
||||
autoGradable: true,
|
||||
supportsPartialCredit: true,
|
||||
manualGradingRequired: false
|
||||
},
|
||||
{
|
||||
key: QUESTION_TYPE_KEY.HOTSPOT,
|
||||
typename: 'HOTSPOT',
|
||||
id: 11,
|
||||
id: 9,
|
||||
autoGradable: true,
|
||||
supportsPartialCredit: true,
|
||||
manualGradingRequired: false
|
||||
@@ -109,7 +93,7 @@ const EXPECTED_REGISTRY = [
|
||||
{
|
||||
key: QUESTION_TYPE_KEY.LINK,
|
||||
typename: 'LINK',
|
||||
id: 12,
|
||||
id: 10,
|
||||
autoGradable: false,
|
||||
supportsPartialCredit: false,
|
||||
manualGradingRequired: true
|
||||
@@ -117,7 +101,7 @@ const EXPECTED_REGISTRY = [
|
||||
{
|
||||
key: QUESTION_TYPE_KEY.WORD_BANK,
|
||||
typename: 'WORD_BANK',
|
||||
id: 13,
|
||||
id: 11,
|
||||
autoGradable: true,
|
||||
supportsPartialCredit: true,
|
||||
manualGradingRequired: false
|
||||
@@ -125,7 +109,7 @@ const EXPECTED_REGISTRY = [
|
||||
{
|
||||
key: QUESTION_TYPE_KEY.STAR,
|
||||
typename: 'STAR',
|
||||
id: 14,
|
||||
id: 12,
|
||||
autoGradable: true,
|
||||
supportsPartialCredit: false,
|
||||
manualGradingRequired: false
|
||||
@@ -133,13 +117,51 @@ const EXPECTED_REGISTRY = [
|
||||
{
|
||||
key: QUESTION_TYPE_KEY.VIDEO_RECORDING,
|
||||
typename: 'VIDEO_RECORDING',
|
||||
id: 15,
|
||||
id: 13,
|
||||
autoGradable: false,
|
||||
supportsPartialCredit: false,
|
||||
manualGradingRequired: true
|
||||
},
|
||||
{
|
||||
key: QUESTION_TYPE_KEY.MATCHING,
|
||||
typename: 'MATCHING',
|
||||
id: 14,
|
||||
autoGradable: true,
|
||||
supportsPartialCredit: true,
|
||||
manualGradingRequired: false,
|
||||
disabled: true
|
||||
},
|
||||
{
|
||||
key: QUESTION_TYPE_KEY.HOTSPOT,
|
||||
typename: 'HOTSPOT',
|
||||
id: 15,
|
||||
autoGradable: true,
|
||||
supportsPartialCredit: true,
|
||||
manualGradingRequired: false,
|
||||
disabled: true
|
||||
}
|
||||
] as const;
|
||||
|
||||
// Canonical `question_type` rows in the database (the source of truth ids are
|
||||
// resolved against). Ids 1-13 are the live rows in insertion order; MATCHING and
|
||||
// HOTSPOT are intentionally absent (built-but-disabled). This list guards against
|
||||
// the registry id ever drifting from the DB again.
|
||||
const CANONICAL_DB_QUESTION_TYPES: ReadonlyArray<{ id: number; typename: string }> = [
|
||||
{ id: 1, typename: 'RADIO' },
|
||||
{ id: 2, typename: 'CHECKBOX' },
|
||||
{ id: 3, typename: 'TEXTAREA' },
|
||||
{ id: 4, typename: 'TRUE_FALSE' },
|
||||
{ id: 5, typename: 'SHORT_ANSWER' },
|
||||
{ id: 6, typename: 'NUMERIC' },
|
||||
{ id: 7, typename: 'FILL_BLANK' },
|
||||
{ id: 8, typename: 'FILE_UPLOAD' },
|
||||
{ id: 9, typename: 'ORDERING' },
|
||||
{ id: 10, typename: 'LINK' },
|
||||
{ id: 11, typename: 'WORD_BANK' },
|
||||
{ id: 12, typename: 'STAR' },
|
||||
{ id: 13, typename: 'VIDEO_RECORDING' }
|
||||
];
|
||||
|
||||
describe('question type registry', () => {
|
||||
it('contains exactly the supported question type keys', () => {
|
||||
expect(new Set(QUESTION_TYPE_REGISTRY.map((entry) => entry.key))).toEqual(
|
||||
@@ -176,6 +198,29 @@ describe('question type registry', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('registry ids match the canonical question_type DB rows (no drift)', () => {
|
||||
for (const row of CANONICAL_DB_QUESTION_TYPES) {
|
||||
const key = QUESTION_TYPE_TYPENAME_TO_KEY[row.typename];
|
||||
expect(key, `typename ${row.typename} missing from registry`).toBeDefined();
|
||||
expect(QUESTION_TYPE_IDS[key], `id mismatch for ${row.typename}`).toBe(row.id);
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps disabled types appended after every DB-backed type', () => {
|
||||
const dbIds = new Set(CANONICAL_DB_QUESTION_TYPES.map((row) => row.id));
|
||||
const maxDbId = Math.max(...CANONICAL_DB_QUESTION_TYPES.map((row) => row.id));
|
||||
|
||||
for (const entry of QUESTION_TYPE_REGISTRY) {
|
||||
if (entry.disabled) {
|
||||
// Disabled types have no DB row and must sit past the live id range.
|
||||
expect(dbIds.has(entry.id), `disabled ${entry.typename} must not reuse a DB id`).toBe(false);
|
||||
expect(entry.id).toBeGreaterThan(maxDbId);
|
||||
} else {
|
||||
expect(dbIds.has(entry.id), `enabled ${entry.typename} must map to a DB row`).toBe(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('returns undefined for unknown typename and id lookups', () => {
|
||||
expect(getQuestionTypeByTypename('UNKNOWN')).toBeUndefined();
|
||||
expect(getQuestionTypeById(999)).toBeUndefined();
|
||||
|
||||
Reference in New Issue
Block a user