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:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user