feat: T4 wave 11 – bottling form with article values and confirmation, Telegram in the app
- Bottling form: jar size and best-before from the product with source, differs-hints before saving, prefill status, water-content confirmation naming the measured value, idempotency key renewed on body change; product form gets shelf life (months) and whole-gram jar weight. - Telegram frontend: link card with step-up dialog, t.me-only deep link, status polling, unlink; system Telegram card with receiver/sender state and confirmed retry; StepUpDialog shared with the backup import. - Reauth calls never refresh-and-retry on 401 (no double lockout count); German texts for expired session and 429; service worker never caches Telegram, notification settings and system responses. - Telegram backend round 2: shared link removal on every disconnect path, lease released on shutdown, connection errors not counted as poison. - ARCHITECTURE.md §2, §6 (Annahmen 56–58), §10, §11.12, §11.13; TASKS.md T4. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
fab7f7e12f
commit
9760ce154f
73 files changed
+3633
-269
No files matched your search
@@ -823,7 +823,7 @@ describe('Telegram delivery (e2e)', () => {
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.expect(200);
|
||||
expect((view.body as { telegram: object }).telegram).toMatchObject({
|
||||
configured: true,
|
||||
botNameSet: true,
|
||||
botUsername: BOT,
|
||||
linked: true,
|
||||
firstName: 'unlink',
|
||||
@@ -940,23 +940,28 @@ describe('Telegram delivery (e2e)', () => {
|
||||
const stop = jest
|
||||
.spyOn(links, 'stopIn')
|
||||
.mockRejectedValue(new Error('boom'));
|
||||
updates.retryLimits = { attempts: 2, windowMs: 600_000 };
|
||||
// A receiver with an injected limit of two attempts.
|
||||
const strict = new TelegramUpdatesService(
|
||||
prisma,
|
||||
telegram as unknown as TelegramClient,
|
||||
links,
|
||||
{ attempts: 2 },
|
||||
);
|
||||
const first = nextUpdateId;
|
||||
say(user.chatId, '/stop');
|
||||
say(other, 'hallo');
|
||||
|
||||
try {
|
||||
await expect(poll()).resolves.toBe('error');
|
||||
await expect(strict.pollOnce(NEVER_ABORTED)).resolves.toBe('error');
|
||||
const failing = await prisma.telegramPollState.findUniqueOrThrow({
|
||||
where: { id: 'singleton' },
|
||||
});
|
||||
expect(failing.updateOffset).toBe(BigInt(first));
|
||||
expect(failing.lastError).toBe('update not handled: Error');
|
||||
|
||||
await expect(poll()).resolves.toBe('polled');
|
||||
await expect(strict.pollOnce(NEVER_ABORTED)).resolves.toBe('polled');
|
||||
} finally {
|
||||
stop.mockRestore();
|
||||
updates.retryLimits = { attempts: 5, windowMs: 600_000 };
|
||||
}
|
||||
|
||||
const state = await prisma.telegramPollState.findUniqueOrThrow({
|
||||
@@ -971,13 +976,48 @@ describe('Telegram delivery (e2e)', () => {
|
||||
expect(await repliesTo(other)).toEqual(['HELP']);
|
||||
|
||||
// A clean batch afterwards clears the notice.
|
||||
await poll();
|
||||
await strict.pollOnce(NEVER_ABORTED);
|
||||
const cleared = await prisma.telegramPollState.findUniqueOrThrow({
|
||||
where: { id: 'singleton' },
|
||||
});
|
||||
expect(cleared.lastError).toBeNull();
|
||||
});
|
||||
|
||||
it('releasing the lease at shutdown lets the next receiver in at once', async () => {
|
||||
const links = app.get(TelegramLinkService);
|
||||
const client = telegram as unknown as TelegramClient;
|
||||
const first = new TelegramUpdatesService(prisma, client, links);
|
||||
const second = new TelegramUpdatesService(prisma, client, links);
|
||||
await expect(first.pollOnce(NEVER_ABORTED)).resolves.toBe('polled');
|
||||
await expect(second.pollOnce(NEVER_ABORTED)).resolves.toBe('no-lease');
|
||||
|
||||
await first.releaseLease();
|
||||
|
||||
await expect(second.pollOnce(NEVER_ABORTED)).resolves.toBe('polled');
|
||||
});
|
||||
|
||||
it('an old backlog never reaches a chat linked later', async () => {
|
||||
const user = await createUnlinkedUser('backlog');
|
||||
const waiting = await queue(user.id, { message: 'alt' });
|
||||
await prisma.telegramLink.create({
|
||||
data: { userId: user.id, chatId: user.chatId, telegramFirstName: 'x' },
|
||||
});
|
||||
await prisma.notificationPreference.update({
|
||||
where: { userId: user.id },
|
||||
data: { telegramEnabled: true },
|
||||
});
|
||||
const token = await newLinkToken(user.id);
|
||||
const newChat = chatBase + 9_400n;
|
||||
|
||||
say(newChat, `/start ${token}`);
|
||||
await poll();
|
||||
|
||||
expect((await deliveryOf(waiting.deliveryId)).status).toBe('SKIPPED');
|
||||
await drainFor(user.id);
|
||||
expect(telegram.sentTo(newChat)).toHaveLength(0);
|
||||
expect(telegram.sentTo(user.chatId)).toHaveLength(0);
|
||||
});
|
||||
|
||||
// Last on purpose: it uses up the 10-per-minute budget of the route.
|
||||
it('link-token requires step-up and is throttled', async () => {
|
||||
await createUnlinkedUser('stepup');
|
||||
|
||||
Reference in new issue
Block a user