mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-23 15:36:33 +00:00
fix(assistant): taking a proposal needs place rights, and a card's choices wait while a change runs
Plan this checks place_edit like the tour routes, the include choices of a run or group stay disabled while the chat applies a change, and a follow-up can say when it applies, so Confirm the pending bookings shows only when the card holds a pending booking of this trip.
This commit is contained in:
@@ -129,9 +129,13 @@ export class AssistantController {
|
||||
return this.foundPlaces.add(user.id, Number(tripId), idParam(threadId), body);
|
||||
}
|
||||
|
||||
/** "Plan this" on a hike or a city walk an answer proposed: a change card that creates it, no model turn. */
|
||||
/**
|
||||
* "Plan this" on a hike or a city walk an answer proposed: a change card that creates it, no model turn.
|
||||
* It holds a new tour, so it asks for the right the tour routes ask for.
|
||||
*/
|
||||
@Post('threads/:threadId/proposals')
|
||||
@HttpCode(200)
|
||||
@RequirePermission('place_edit')
|
||||
@ResponseContract(assistantAddFoundPlaceResponseSchema)
|
||||
takeProposal(
|
||||
@CurrentUser() user: User,
|
||||
|
||||
@@ -11,13 +11,16 @@
|
||||
* drawing asked for by the server and kept from the model; "Plan this" holds
|
||||
* the hike as a change card from the stored proposal, refused for an unknown
|
||||
* proposal and while it waits; applying it runs create_hike without a model
|
||||
* turn and names the tour in the receipt and on its card.
|
||||
* turn and names the tour in the receipt and on its card. Taking a proposal
|
||||
* asks for the right to edit places, like the tour routes: 403 without it,
|
||||
* 404 outside the trip.
|
||||
*/
|
||||
import { buildApp } from '../../src/bootstrap';
|
||||
import { PermissionsService } from '../../src/nest/permissions/permissions.service';
|
||||
import { HikePlanService } from '../../src/nest/tours/hike-plan.service';
|
||||
import { makePlace } from '../helpers/factories/places';
|
||||
import { setAddonEnabled } from '../helpers/factories/settings';
|
||||
import { makeTrip } from '../helpers/factories/trips';
|
||||
import { addTripMember, makeTrip } from '../helpers/factories/trips';
|
||||
import { makeUser } from '../helpers/factories/users';
|
||||
import { resetRateLimits } from '../helpers/test-db';
|
||||
import { sessionCookie } from './harness';
|
||||
@@ -319,4 +322,34 @@ describe('a proposed hike in the chat', () => {
|
||||
const [receipt] = ofType(decided.chunks, 'data-trek-card');
|
||||
expect(receipt?.data).toMatchObject({ toolCallId, card: { kind: 'tourRoute', placeId: t.placeId } });
|
||||
});
|
||||
|
||||
it('ASTE2E-TOUR-003: "Plan this" asks for the right to edit places: 403 and nothing held without it, 404 outside the trip', async () => {
|
||||
const t = await travellerWithPlanner();
|
||||
const { user: member } = await makeUser(orm);
|
||||
await addTripMember(orm, t.tripId, member.id);
|
||||
const { threadId } = await proposeTurn({ userId: member.id, base: t.base });
|
||||
const path = `${t.base}/threads/${threadId}/proposals`;
|
||||
|
||||
const permissions = vi.spyOn(nestApp.get(PermissionsService), 'checkPermission');
|
||||
permissions.mockImplementation((action: string) => Promise.resolve(action !== 'place_edit'));
|
||||
try {
|
||||
const refused = await post(path, member.id, { ...CTX, toolCallId: 'call_hike' });
|
||||
expect([refused.status, refused.json]).toEqual([403, { error: 'No permission' }]);
|
||||
expect(permissions).toHaveBeenCalledWith('place_edit', expect.any(String), t.userId, member.id, true);
|
||||
} finally {
|
||||
permissions.mockRestore();
|
||||
}
|
||||
const shown = await request(nestApp.getHttpServer())
|
||||
.get(`${t.base}/threads/${threadId}`)
|
||||
.set('Cookie', sessionCookie(member.id));
|
||||
const parts = (shown.body.messages as Array<{ parts: AssistantMessagePart[] }>).flatMap((m) => m.parts);
|
||||
expect(parts.some((part) => part.type === 'tool-call' && part.toolName === 'take_proposal')).toBe(false);
|
||||
|
||||
const { user: stranger } = await makeUser(orm);
|
||||
const outside = await post(path, stranger.id, { ...CTX, toolCallId: 'call_hike' });
|
||||
expect([outside.status, outside.json]).toEqual([404, { error: 'Trip not found' }]);
|
||||
|
||||
// With the right, the same member takes the same proposal.
|
||||
expect((await post(path, member.id, { ...CTX, toolCallId: 'call_hike' })).status).toBe(200);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -150,10 +150,11 @@ describe('AssistantController', () => {
|
||||
expect(() => controller.addFoundPlace(user, '5', 'x', body)).toThrow(DomainError);
|
||||
});
|
||||
|
||||
it('ASTCTL-008: sharing needs the right to write in the trip chat; nothing else here asks for a right', () => {
|
||||
it('ASTCTL-008: sharing asks for the trip chat right, "Plan this" for editing places; nothing else asks', () => {
|
||||
const handlers = AssistantController.prototype as unknown as Record<string, object>;
|
||||
expect(Reflect.getMetadata(TRIP_PERMISSION_KEY, handlers.share)).toBe('collab_edit');
|
||||
for (const name of ['chat', 'decide', 'undo', 'addFoundPlace', 'takeProposal']) {
|
||||
expect(Reflect.getMetadata(TRIP_PERMISSION_KEY, handlers.takeProposal)).toBe('place_edit');
|
||||
for (const name of ['chat', 'decide', 'undo', 'addFoundPlace']) {
|
||||
expect(Reflect.getMetadata(TRIP_PERMISSION_KEY, handlers[name]), name).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user