Serve rooms ("Räume"), which are not courses however the urls read
The account this was built against is in no rooms, so the whole space was invisible and easy to dismiss as an empty endpoint. It is not empty in general — the user had rooms until a teacher removed access — and the UI's naming actively hides the distinction: the sidebar's *Kurse* entry links to `/rooms/courses-overview` and lists courses, while *Räume* links to `/rooms` and lists rooms. A url containing `/rooms` identifies neither. list_rooms and get_room cover the latter. A room holds boards and nothing else, so get_room lists boards for get_board (which already reports "in room" from the board context) plus who else is in it. Room boards report `isVisible`, which the course-page projection does not, so a draft is named as a draft instead of being offered and then answering 403. Rooms also go through the crawl, or they would have become the next blind spot: their boards are indexed, searchable by both the index and the live-crawl path, diffed by what_changed, and mirrored by the CLI under the room's name. The board traversal and the snapshot matcher are now shared between courses and rooms rather than duplicated, which also fixed the live-crawl path silently not searching pad contents. The CLI needed no new command — it is file-centric and inherits rooms through the manifest — but `--course` now accepts a room id, and says so. `kind` gains 'room'; the column is plain TEXT, so no migration. 112 tests. Smoke: 42/42 and 44/44 local, 41/41 and 43/43 live. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -193,6 +193,12 @@ eval "$(./scripts/mcp-env.sh)" # as the demo student
|
||||
cd .. && npm run smoke # 39 checks against the local instance
|
||||
```
|
||||
|
||||
It also builds a **room** ("Raum"): rooms are a separate space from courses,
|
||||
and the naming misleads — the sidebar's *Kurse* entry points at
|
||||
`/rooms/courses-overview` while *Räume* points at `/rooms`. The fixture adds the
|
||||
demo student to one room and leaves a second room without them, so "only the
|
||||
rooms I belong to" is testable rather than assumed.
|
||||
|
||||
Two things it does **not** do, because the API does not allow them:
|
||||
|
||||
- **Teams cannot be created.** The legacy service registers
|
||||
|
||||
@@ -124,6 +124,52 @@ async function create() {
|
||||
keep('roomId', room.id);
|
||||
log(`room ${s.roomId}`);
|
||||
|
||||
step('room contents');
|
||||
// Rooms are the newer collaboration space, separate from courses: a room has
|
||||
// its own members and its own boards, and appears under "Räume" in the UI
|
||||
// while courses appear under "Kurse" at a /rooms/... url.
|
||||
await v3('PATCH', `/rooms/${s.roomId}/members/add`, { userIds: [student] });
|
||||
log('demo student added as a room member');
|
||||
|
||||
const roomBoard = await v3('POST', '/boards', {
|
||||
title: `MCP-Test Raum-Board (${stamp})`,
|
||||
parentId: s.roomId,
|
||||
parentType: 'room',
|
||||
layout: 'columns',
|
||||
});
|
||||
keep('roomBoardId', roomBoard.id);
|
||||
const roomColumn = await v3('POST', `/boards/${s.roomBoardId}/columns`);
|
||||
await v3('PATCH', `/columns/${roomColumn.id}/title`, { title: 'Projektarbeit' });
|
||||
const roomCard = await v3('POST', `/columns/${roomColumn.id}/cards`);
|
||||
await v3('PATCH', `/cards/${roomCard.id}/title`, { title: 'Aufgabenverteilung' });
|
||||
const roomText = await v3('POST', `/cards/${roomCard.id}/elements`, { type: 'richText' });
|
||||
await v3('PATCH', `/elements/${roomText.id}/content`, {
|
||||
data: {
|
||||
content: {
|
||||
text: '<p>Raum-Inhalt, nicht Kurs-Inhalt. Suchbegriff: Projektsteuerung.</p>',
|
||||
inputFormat: 'richTextCk5',
|
||||
},
|
||||
type: 'richText',
|
||||
},
|
||||
});
|
||||
// A file in a room board, so the mirror path (room name, not course name) and
|
||||
// the CLI manifest are exercised for rooms too.
|
||||
const roomFileEl = await v3('POST', `/cards/${roomCard.id}/elements`, { type: 'file' });
|
||||
keep('roomFileId', await upload(roomFileEl.id, 'projektplan.txt', 'text/plain',
|
||||
'Projektplan\n\nMeilenstein 1: Anforderungen. Stichwort: Projektsteuerung.\n'));
|
||||
|
||||
await v3('PATCH', `/boards/${s.roomBoardId}/visibility`, { isVisible: true });
|
||||
log(`room board ${s.roomBoardId} published, with one card`);
|
||||
|
||||
// A second room the student is NOT in, so "only rooms I belong to" is testable.
|
||||
const other = await v3('POST', '/rooms', {
|
||||
name: `MCP-Test Raum ohne Zugriff (${stamp})`,
|
||||
color: 'red',
|
||||
features: [],
|
||||
});
|
||||
keep('roomWithoutStudentId', other.id);
|
||||
log(`second room ${other.id} left without the student on purpose`);
|
||||
|
||||
step('team');
|
||||
// Teams cannot be created through the API: the legacy service registers
|
||||
// ['find','get','update','patch','remove'] and no 'create'
|
||||
@@ -314,6 +360,7 @@ async function remove() {
|
||||
step('deleting what was created');
|
||||
const tries = [
|
||||
['file', () => files('DELETE', `/delete/${s.folderFileId}`)],
|
||||
['room file', () => files('DELETE', `/delete/${s.roomFileId}`)],
|
||||
['file element', () => v3('DELETE', `/elements/${s.fileElementId}`)],
|
||||
['fileFolder element', () => v3('DELETE', `/elements/${s.folderId}`)],
|
||||
['etherpad element', () => v3('DELETE', `/elements/${s.padElementId}`)],
|
||||
@@ -324,7 +371,9 @@ async function remove() {
|
||||
['draft board', () => v3('DELETE', `/boards/${s.draftBoardId}`)],
|
||||
['task', () => v3('DELETE', `/tasks/${s.taskId}`)],
|
||||
['topic', () => v3('DELETE', `/lessons/${s.lessonId}`)],
|
||||
['room board', () => v3('DELETE', `/boards/${s.roomBoardId}`)],
|
||||
['room', () => v3('DELETE', `/rooms/${s.roomId}`)],
|
||||
['second room', () => v3('DELETE', `/rooms/${s.roomWithoutStudentId}`)],
|
||||
['course', () => v1('DELETE', `/courses/${s.courseId}`)],
|
||||
];
|
||||
for (const [what, fn] of tries) {
|
||||
|
||||
Reference in New Issue
Block a user