fix(P06): defend page keys defense+grading on the REAL variant task_id (verifier P0)
The defend page fabricated taskId = `task-${competencyId}`, but variant
task_ids are `task-<seed[:16]>` (D-029) — so in the real browser flow the
defense ran against an empty trace (no digest grounding) and Grade My Work
always returned UNGRADABLE_EMPTY_TRACE. The E2E test masked this by passing
the real task_id directly.
Fix: DefenseSession resolves the learner's stored variant by competency
via GET /v1/variants?learner_id (new listVariants client), defends + grades
under the variant's real task_id, and shows an honest empty state when no
build session exists for the competency yet.
---ci---
phase: 6
milestone: v0.3
status: verify
requirements:
covered: [REQ-3-008]
partial: []
lessons:
- A green E2E test can still mask a broken UI wiring when the test hand-picks
the join key the UI is supposed to derive; verify browser flows against the
ids the pages actually construct, not the engine contract alone.
---/ci---
This commit is contained in:
@@ -38,7 +38,7 @@ export default async function DefensePage({
|
||||
then request your process-trace grade.
|
||||
</p>
|
||||
</header>
|
||||
<DefenseSession taskId={`task-${competencyId}`} />
|
||||
<DefenseSession competencyId={competencyId} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
answerDefense,
|
||||
finishDefense,
|
||||
getDefense,
|
||||
listVariants,
|
||||
requestGrade,
|
||||
startDefense,
|
||||
} from '../../lib/engine-client';
|
||||
@@ -25,8 +26,16 @@ type MicState = 'idle' | 'recording' | 'denied' | 'unsupported';
|
||||
* MediaRecorder when permitted (multipart POST), browser-SR fallback per
|
||||
* the server's voice descriptor, examiner follow-ups, and the final verdict
|
||||
* rendered with integrity signals. No mock defense data anywhere.
|
||||
*
|
||||
* Task identity: the defense + grading engines key on the learner's REAL
|
||||
* variant task_id (task-<seed16>, derived server-side from the variant
|
||||
* seed), NOT a fabricated per-competency id — the defense must question
|
||||
* THIS build session's trace digest and the grade must read THIS build
|
||||
* session's trace. `competencyId` resolves the stored variant; the engine
|
||||
* client fabricates nothing.
|
||||
*/
|
||||
export function DefenseSession({ taskId }: { taskId: string }) {
|
||||
export function DefenseSession({ competencyId }: { competencyId: string }) {
|
||||
const [taskId, setTaskId] = useState<string | null>(null);
|
||||
const [defenseId, setDefenseId] = useState<string | null>(null);
|
||||
const [turns, setTurns] = useState<DefenseTurn[]>([]);
|
||||
const [answerText, setAnswerText] = useState('');
|
||||
@@ -42,7 +51,28 @@ export function DefenseSession({ taskId }: { taskId: string }) {
|
||||
setTurns(session.turns ?? []);
|
||||
}, []);
|
||||
|
||||
// Resolve the learner's stored variant for this competency — the real
|
||||
// task key the defense and grading engines trace-key on. A learner who
|
||||
// has not built yet has no variant: the panel says so (an honest empty
|
||||
// state) instead of defending a phantom task with no trace.
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
try {
|
||||
const variants = await listVariants(MOCK_LEARNER_ID);
|
||||
const mine = variants.find((v) => v.competency_id === competencyId);
|
||||
if (!cancelled) setTaskId(mine?.task_id ?? null);
|
||||
} catch {
|
||||
if (!cancelled) setTaskId(null);
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [competencyId]);
|
||||
|
||||
const start = useCallback(async () => {
|
||||
if (!taskId) return;
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
@@ -118,6 +148,7 @@ export function DefenseSession({ taskId }: { taskId: string }) {
|
||||
}, [defenseId, refresh]);
|
||||
|
||||
const gradeWork = useCallback(async () => {
|
||||
if (!taskId) return;
|
||||
setBusy(true);
|
||||
try {
|
||||
const record = await requestGrade(MOCK_LEARNER_ID, taskId);
|
||||
@@ -131,6 +162,19 @@ export function DefenseSession({ taskId }: { taskId: string }) {
|
||||
|
||||
useEffect(() => () => recorderRef.current?.stop(), []);
|
||||
|
||||
if (!taskId) {
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-3 rounded-lg border border-slate-200 bg-white px-6 py-10 text-center dark:border-slate-700 dark:bg-slate-900">
|
||||
<p className="text-sm text-slate-600 dark:text-slate-300">
|
||||
No build session found for this competency yet — start the task on
|
||||
the build page first. The defense questions you about YOUR build
|
||||
session's trace.
|
||||
</p>
|
||||
{error && <p className="text-xs text-amber-600 dark:text-amber-400">{error}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!defenseId) {
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-3 rounded-lg border border-slate-200 bg-white px-6 py-10 text-center dark:border-slate-700 dark:bg-slate-900">
|
||||
|
||||
@@ -181,6 +181,18 @@ export async function generateVariant(
|
||||
});
|
||||
}
|
||||
|
||||
/** The learner's stored variants, chronological (D-029 — one per template). */
|
||||
export async function listVariants(
|
||||
learnerId: string,
|
||||
signal?: AbortSignal,
|
||||
): Promise<TaskVariant[]> {
|
||||
const body = await jsonFetch<{ variants: TaskVariant[] }>(
|
||||
`/v1/variants?learner_id=${encodeURIComponent(learnerId)}`,
|
||||
{ signal },
|
||||
);
|
||||
return body.variants ?? [];
|
||||
}
|
||||
|
||||
// -- grading (REQ-3-004) -------------------------------------------------------
|
||||
|
||||
export async function requestGrade(
|
||||
|
||||
Reference in New Issue
Block a user