Prepare for a live account: separate the indexes, and probe new routes live

With .env pointing at the real account, testing against the local instance
became dangerous: process env beats --env-file, so a local smoke run would
crawl fixtures straight into the live index, where a per-course refresh
carries them forward indefinitely. mcp-env.sh now pins its own database
(schulcloud_local) and mirror as well as the instance.

The override publishes the server on MCP_HOST_PORT and takes
CRAWL_INTERVAL_MS from .env, since what_changed can only report what
happened between crawls. The build context leaves out tmp/, which holds a
mirror of the account's files, and local-instance/.

probe checks live what the gap fixes established locally: the /api/v1 course
and user routes, classes, room allowedOperations as an object, and the
preview enums on a real file. A failure there means a feature degrades
rather than breaks.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-09-16 20:19:16 +02:00
parent 5ae2210459
commit 10c6544579
8 changed files with 135 additions and 5 deletions

View File

@@ -124,6 +124,84 @@ if (me) {
const lesson = await client.getLesson(lessonId);
console.log(` ok lesson ${lessonId} ("${lesson.name}", ${lesson.contents?.length ?? 0} section(s))`);
}
// --- assumptions first established against the local 33.40 instance ----
// Each of these was verified locally and against the deployment's ingress
// table, not against this instance. A FAIL here means the matching feature
// degrades (to bare ids, to no timetable, to no preview) rather than breaks.
console.log('\nlegacy routes, classes, rooms:');
const firstCourse = courses[0];
if (firstCourse) {
await expect('GET /api/v1/courses/{id} (description, teachers, timetable)', async () => {
const legacy = await client.getLegacyCourse(firstCourse.id);
const parts = [
legacy.description ? 'description' : null,
legacy.teacherIds?.length ? `${legacy.teacherIds.length} teacher id(s)` : null,
legacy.times?.length ? `${legacy.times.length} timetable slot(s)` : null,
].filter(Boolean);
return parts.length > 0 ? parts.join(', ') : 'responds, but carries none of the fields';
});
}
await expect('GET /api/v1/users/{me} (the only id -> name route)', async () => {
const user = await client.getLegacyUser(me.user.id);
return user.firstName || user.fullName ? 'resolves your own name' : 'responds without a name';
});
const teacherId = firstCourse ? (await client.getLegacyCourse(firstCourse.id).catch(() => undefined))?.teacherIds?.[0] : undefined;
if (teacherId) {
// Expected to be refused for a student: names then degrade to "not
// visible to this account". Reported, not failed, either way.
const seen = await client.getLegacyUser(teacherId).then(() => 'readable', (error) => `refused (${error.status ?? error.message})`);
console.log(` — GET /api/v1/users/{teacher}: ${seen} — a student is expected to be refused`);
}
await expect('GET /api/v3/groups/class', async () => {
const classes = await client.listClasses();
const named = classes.filter((entry) => entry.teacherNames?.length).length;
return `${classes.length} class(es), ${named} with teacher names`;
});
const rooms = await client.listRooms().catch(() => []);
if (rooms[0]) {
await expect('room allowedOperations is an object, not a list', async () => {
const room = await client.getRoom(rooms[0].id);
const ops = room.allowedOperations;
if (Array.isArray(ops)) throw new Error('it is an array here — fix RoomItem.allowedOperations and rooms.ts');
return `${Object.values(ops ?? {}).filter(Boolean).length} operation(s) granted`;
});
} else {
console.log(' — no room to check allowedOperations against (in none is normal)');
}
// The preview route is the answer for image-only PDFs, and it has two
// undocumented enums. Only testable with a file whose preview is possible.
if (boardId) {
const skeleton = await client.getBoardSkeleton(boardId);
const cards = await client.getCards(skeleton.columns.flatMap((c) => c.cards.map((x) => x.cardId)).slice(0, 20));
const pdfElement = cards.flatMap((c) => c.elements).filter((e) => e.type === 'file' || e.type === 'fileFolder');
let previewed = false;
for (const element of pdfElement) {
const page = await client
.listFiles({ storageLocationId: me.school.id, parentType: 'boardnodes', parentId: element.id })
.catch(() => undefined);
const record = page?.data.find((file) => file.previewStatus === 'preview_possible');
if (!record) continue;
await expect(`GET /api/v3/file/preview (width=500, outputFormat=image/webp) on ${record.name}`, async () => {
const preview = await client.getFilePreview(record, 500);
return `${preview.mimeType}, ${preview.bytes.length} bytes`;
});
previewed = true;
break;
}
if (!previewed) console.log(' — no previewable file among the sampled cards');
}
}
async function expect(label, run) {
try {
const detail = await run();
console.log(` ok ${label}${detail ? ` (${detail})` : ''}`);
} catch (error) {
const status = error instanceof SchulcloudApiError ? error.status : '—';
console.log(` FAIL ${label}${status} ${String(error.message).slice(0, 120)}`);
}
}
function summarize(result) {