160 lines
7.5 KiB
TypeScript
160 lines
7.5 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { generateTutorResponse } from "../src/agent/respond.js";
|
|
import { deleteLocalLearningData } from "../src/server/data-management.js";
|
|
import { designDiagnosticQuestion } from "../src/server/diagnostic-designer.js";
|
|
import { getLatestCatalogRun } from "../src/server/course-catalog.js";
|
|
import { getNextDiagnosticQuestion } from "../src/server/diagnostics.js";
|
|
import { rankProgressRecommendations } from "../src/server/recommendations.js";
|
|
import { createSession, getProgressSummary } from "../src/server/services.js";
|
|
import { prepareModelContext } from "../src/server/context.js";
|
|
import { selectExercise } from "../src/tools/exercise-tools.js";
|
|
import { createProjectPlan } from "../src/tools/project-tools.js";
|
|
import { createId, nowIso } from "../src/security/ids.js";
|
|
import { createTestRuntime } from "./utils/runtime.js";
|
|
|
|
describe("eliminating learning presets", () => {
|
|
it("does not seed mastery priors or preset goals during bootstrap, catalog sync, or reset", async () => {
|
|
const runtime = await createTestRuntime();
|
|
|
|
expect(countMasteryRows(runtime)).toBe(0);
|
|
expect(profile(runtime).current_goal ?? null).toBeNull();
|
|
expect(getProgressSummary(runtime).current_goal).toBeNull();
|
|
|
|
const deleted = deleteLocalLearningData(runtime, { confirm: "DELETE_LOCAL_LEARNING_DATA" });
|
|
|
|
expect(deleted.deleted.sessions).toBe(0);
|
|
expect(countMasteryRows(runtime)).toBe(0);
|
|
expect(profile(runtime).current_goal ?? null).toBeNull();
|
|
expect(getProgressSummary(runtime).current_goal).toBeNull();
|
|
});
|
|
|
|
it("keeps projectionless active concepts unknown instead of weak mastery defaults", async () => {
|
|
const runtime = await createTestRuntime();
|
|
|
|
const progress = getProgressSummary(runtime);
|
|
const recommendations = rankProgressRecommendations(runtime, { limit: 3 });
|
|
|
|
expect(progress.course_progress_percent).toBe(0);
|
|
expect(progress.mastery).toEqual([]);
|
|
expect(progress.weak_concepts).toEqual([]);
|
|
expect(recommendations.length).toBeGreaterThan(0);
|
|
expect(recommendations[0]?.reason).toContain("unknown");
|
|
expect(JSON.stringify(recommendations)).not.toContain("0.3");
|
|
});
|
|
|
|
it("fails closed with a retryable diagnostic interruption when no authorized generated item exists", async () => {
|
|
const runtime = await createTestRuntime();
|
|
const session = createSession(runtime, { resume: false });
|
|
|
|
await expect(designDiagnosticQuestion(runtime, "loop")).rejects.toMatchObject({
|
|
code: "DIAGNOSTIC_GENERATION_UNAVAILABLE",
|
|
});
|
|
|
|
const next = await getNextDiagnosticQuestion(runtime, session.session_id);
|
|
const diagnostic = runtime.db.query<{ status: string; stop_reason: string | null }>(
|
|
"SELECT status, stop_reason FROM diagnostic_sessions WHERE session_id = ? ORDER BY started_at DESC LIMIT 1",
|
|
).get([session.session_id]);
|
|
|
|
expect(next.completed).toBe(false);
|
|
expect(next.question).toBeUndefined();
|
|
expect(next.progress.diagnostic_status).toBe("technical_unavailable");
|
|
expect(diagnostic).toMatchObject({
|
|
status: "active",
|
|
stop_reason: "diagnostic_generation_unavailable",
|
|
});
|
|
});
|
|
|
|
it("does not create generated exercises from deterministic local templates", async () => {
|
|
const runtime = await createTestRuntime();
|
|
const session = createSession(runtime, { resume: false });
|
|
completeInitialDiagnostic(runtime, session.session_id);
|
|
markGuidanceStarted(runtime, session.session_id);
|
|
|
|
const selected = await selectExercise(runtime, { concept_ids: ["loop"], difficulty: 2, mode: "practice" }, { sessionId: session.session_id });
|
|
|
|
expect(selected).toMatchObject({
|
|
ok: false,
|
|
code: "EXERCISE_CONTENT_UNAVAILABLE",
|
|
});
|
|
expect(runtime.db.query<{ count: number }>("SELECT COUNT(*) AS count FROM generated_exercises").get()?.count).toBe(0);
|
|
expect(runtime.db.query<{ count: number }>("SELECT COUNT(*) AS count FROM exercises WHERE status = 'generated_private'").get()?.count).toBe(0);
|
|
});
|
|
|
|
it("does not create project plans from a fixed local project template", async () => {
|
|
const runtime = await createTestRuntime();
|
|
|
|
const created = await createProjectPlan(runtime, { project_goal: "做一个猜数字游戏", preferred_difficulty: 2 });
|
|
|
|
expect(created).toMatchObject({
|
|
ok: false,
|
|
code: "PROJECT_CONTENT_UNAVAILABLE",
|
|
});
|
|
expect(runtime.db.query<{ count: number }>("SELECT COUNT(*) AS count FROM project_plans").get()?.count).toBe(0);
|
|
});
|
|
|
|
it("does not inject generic context topics, mistake labels, goals, or canned tutor lessons", async () => {
|
|
const runtime = await createTestRuntime();
|
|
const session = createSession(runtime, { resume: false });
|
|
seedBlankTurns(runtime, session.session_id, 5);
|
|
|
|
const context = prepareModelContext(runtime, session.session_id, { message: "继续" });
|
|
const summary = context.summary ?? "";
|
|
|
|
expect(summary).toContain("context_compaction");
|
|
expect(summary).not.toContain("Python 基础练习");
|
|
expect(summary).not.toContain("继续观察常见错因");
|
|
expect(summary).not.toContain("提高 Python 编程实践能力");
|
|
await expect(generateTutorResponse(runtime, "解释 for 循环")).rejects.toMatchObject({
|
|
code: "MODEL_UNAVAILABLE",
|
|
});
|
|
});
|
|
});
|
|
|
|
function countMasteryRows(runtime: Awaited<ReturnType<typeof createTestRuntime>>): number {
|
|
return runtime.db.query<{ count: number }>("SELECT COUNT(*) AS count FROM concept_mastery").get()?.count ?? 0;
|
|
}
|
|
|
|
function profile(runtime: Awaited<ReturnType<typeof createTestRuntime>>): Record<string, unknown> {
|
|
const row = runtime.db.query<{ profile_json: string }>("SELECT profile_json FROM local_profile WHERE id = 'local'").get();
|
|
return JSON.parse(row?.profile_json ?? "{}") as Record<string, unknown>;
|
|
}
|
|
|
|
function completeInitialDiagnostic(runtime: Awaited<ReturnType<typeof createTestRuntime>>, sessionId: string): void {
|
|
const now = nowIso();
|
|
const catalogRun = getLatestCatalogRun(runtime);
|
|
runtime.db.query(
|
|
"INSERT INTO diagnostic_sessions(id, session_id, status, target_concepts_json, stop_reason, catalog_version, catalog_run_id, started_at, ended_at) VALUES (?, ?, 'completed', ?, 'test_complete', ?, ?, ?, ?)",
|
|
).run([createId("diag"), sessionId, JSON.stringify(["loop"]), catalogRun?.kb_version ?? runtime.config.kbVersion, catalogRun?.id ?? null, now, now]);
|
|
}
|
|
|
|
function markGuidanceStarted(runtime: Awaited<ReturnType<typeof createTestRuntime>>, sessionId: string): void {
|
|
const now = nowIso();
|
|
const turnId = createId("turn");
|
|
runtime.db.transaction(() => {
|
|
runtime.db.query("INSERT INTO session_turns(id, session_id, status, user_message_summary, code_ref, assistant_message_summary, started_at, ended_at) VALUES (?, ?, 'done', ?, NULL, NULL, ?, ?)").run([
|
|
turnId,
|
|
sessionId,
|
|
"开始导师指导。",
|
|
now,
|
|
now,
|
|
]);
|
|
runtime.db.query("INSERT INTO session_messages(id, session_id, turn_id, message_id, role, content_redacted_text, created_at) VALUES (?, ?, ?, ?, 'user', ?, ?)").run([
|
|
createId("msg"),
|
|
sessionId,
|
|
turnId,
|
|
createId("msg"),
|
|
"开始导师指导。",
|
|
now,
|
|
]);
|
|
});
|
|
}
|
|
|
|
function seedBlankTurns(runtime: Awaited<ReturnType<typeof createTestRuntime>>, sessionId: string, count: number): void {
|
|
const now = nowIso();
|
|
for (let index = 0; index < count; index++) {
|
|
runtime.db.query(
|
|
"INSERT INTO session_turns(id, session_id, status, user_message_summary, assistant_message_summary, started_at, ended_at) VALUES (?, ?, 'done', '', '', ?, ?)",
|
|
).run([createId("turn"), sessionId, `${now}-${index}`, `${now}-${index}`]);
|
|
}
|
|
}
|